diff options
471 files changed, 11730 insertions, 4733 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index a988acf1f4a9..a352d9d2ea06 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3415,6 +3415,10 @@ package android.telecom { method public void onBindClient(@Nullable android.content.Intent); } + public class TelecomManager { + method @FlaggedApi("com.android.server.telecom.flags.voip_call_monitor_refactor") public boolean hasForegroundServiceDelegation(@Nullable android.telecom.PhoneAccountHandle); + } + } package android.telephony { diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 999db18a1229..6151b8e2ef0a 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -142,6 +142,15 @@ public abstract class ActivityManagerInternal { String processName, String abiOverride, int uid, Runnable crashHandler); /** + * Called when a user is being deleted. This can happen during normal device usage + * or just at startup, when partially removed users are purged. Any state persisted by the + * ActivityManager should be purged now. + * + * @param userId The user being cleaned up. + */ + public abstract void onUserRemoving(@UserIdInt int userId); + + /** * Called when a user has been deleted. This can happen during normal device usage * or just at startup, when partially removed users are purged. Any state persisted by the * ActivityManager should be purged now. diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 1f3e6559a695..717a2acb4b4a 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -105,7 +105,6 @@ import android.content.pm.ServiceInfo; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; -import android.content.res.ResourceTimer; import android.content.res.Resources; import android.content.res.ResourcesImpl; import android.content.res.loader.ResourcesLoader; @@ -5254,7 +5253,6 @@ public final class ActivityThread extends ClientTransactionHandler Resources.dumpHistory(pw, ""); pw.flush(); - ResourceTimer.dumpTimers(info.fd.getFileDescriptor(), "-refresh"); if (info.finishCallback != null) { info.finishCallback.sendResult(null); } diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index 67f7bee4028e..b5ac4e78c7ad 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -70,7 +70,6 @@ import com.android.internal.widget.VerifyCredentialResponse; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -1064,7 +1063,7 @@ public class KeyguardManager { Log.e(TAG, "Save lock exception", e); success = false; } finally { - Arrays.fill(password, (byte) 0); + LockPatternUtils.zeroize(password); } return success; } diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 3d85ea6a1fca..ffd235f91e09 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -1129,6 +1129,10 @@ public final class LoadedApk { @UnsupportedAppUsage public ClassLoader getClassLoader() { + ClassLoader ret = mClassLoader; + if (ret != null) { + return ret; + } synchronized (mLock) { if (mClassLoader == null) { createOrUpdateClassLoaderLocked(null /*addedPaths*/); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 5176aee9051f..c2ce7d511681 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1968,6 +1968,13 @@ public class Notification implements Parcelable @SystemApi public static final int SEMANTIC_ACTION_CONVERSATION_IS_PHISHING = 12; + /** + * {@link #extras} key to a boolean defining if this action requires special visual + * treatment. + * @hide + */ + public static final String EXTRA_IS_MAGIC = "android.extra.IS_MAGIC"; + private final Bundle mExtras; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Icon mIcon; @@ -5984,6 +5991,15 @@ public class Notification implements Parcelable } setHeaderlessVerticalMargins(contentView, p, hasSecondLine); + // Update margins to leave space for the top line (but not for headerless views like + // HUNS, which use a different layout that already accounts for that). + if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) { + int margin = getContentMarginTop(mContext, + R.dimen.notification_2025_content_margin_top); + contentView.setViewLayoutMargin(R.id.notification_main_column, + RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX); + } + return contentView; } @@ -6207,7 +6223,7 @@ public class Notification implements Parcelable int textColor = Colors.flattenAlpha(getPrimaryTextColor(p), pillColor); contentView.setInt(R.id.expand_button, "setDefaultTextColor", textColor); contentView.setInt(R.id.expand_button, "setDefaultPillColor", pillColor); - // Use different highlighted colors for e.g. unopened groups + // Use different highlighted colors for conversations' unread count if (p.mHighlightExpander) { pillColor = Colors.flattenAlpha( getColors(p).getTertiaryFixedDimAccentColor(), bgColor); @@ -6456,16 +6472,6 @@ public class Notification implements Parcelable big.setColorStateList(R.id.snooze_button, "setImageTintList", actionColor); big.setColorStateList(R.id.bubble_button, "setImageTintList", actionColor); - // Update margins to leave space for the top line (but not for HUNs, which use a - // different layout that already accounts for that). - if (Flags.notificationsRedesignTemplates() - && p.mViewType != StandardTemplateParams.VIEW_TYPE_HEADS_UP) { - int margin = getContentMarginTop(mContext, - R.dimen.notification_2025_content_margin_top); - big.setViewLayoutMargin(R.id.notification_main_column, RemoteViews.MARGIN_TOP, - margin, TypedValue.COMPLEX_UNIT_PX); - } - boolean validRemoteInput = false; // In the UI, contextual actions appear separately from the standard actions, so we @@ -6807,8 +6813,6 @@ public class Notification implements Parcelable public RemoteViews makeNotificationGroupHeader() { return makeNotificationHeader(mParams.reset() .viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER) - // Highlight group expander until the group is first opened - .highlightExpander(Flags.notificationsRedesignTemplates()) .fillTextsFrom(this)); } @@ -6984,14 +6988,12 @@ public class Notification implements Parcelable * @param useRegularSubtext uses the normal subtext set if there is one available. Otherwise * a new subtext is created consisting of the content of the * notification. - * @param highlightExpander whether the expander should use the highlighted colors * @hide */ - public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext, - boolean highlightExpander) { + public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) { StandardTemplateParams p = mParams.reset() .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED) - .highlightExpander(highlightExpander) + .highlightExpander(false) .fillTextsFrom(this); if (!useRegularSubtext || TextUtils.isEmpty(p.mSubText)) { p.summaryText(createSummaryText()); diff --git a/core/java/android/app/backup/BackupManagerInternal.java b/core/java/android/app/backup/BackupManagerInternal.java new file mode 100644 index 000000000000..ceb5ae01f177 --- /dev/null +++ b/core/java/android/app/backup/BackupManagerInternal.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.annotation.UserIdInt; +import android.os.IBinder; + +/** + * Local system service interface for {@link com.android.server.backup.BackupManagerService}. + * + * @hide Only for use within the system server. + */ +public interface BackupManagerInternal { + + /** + * Notifies the Backup Manager Service that an agent has become available. This + * method is only invoked by the Activity Manager. + */ + void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent); + + /** + * Notify the Backup Manager Service that an agent has unexpectedly gone away. + * This method is only invoked by the Activity Manager. + */ + void agentDisconnectedForUser(String packageName, @UserIdInt int userId); +} diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 041c2a7c09f4..5d01d72c35a0 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -93,38 +93,6 @@ interface IBackupManager { IBackupObserver observer); /** - * Notifies the Backup Manager Service that an agent has become available. This - * method is only invoked by the Activity Manager. - * - * If {@code userId} is different from the calling user id, then the caller must hold the - * android.permission.INTERACT_ACROSS_USERS_FULL permission. - * - * @param userId User id for which an agent has become available. - */ - void agentConnectedForUser(int userId, String packageName, IBinder agent); - - /** - * {@link android.app.backup.IBackupManager.agentConnected} for the calling user id. - */ - void agentConnected(String packageName, IBinder agent); - - /** - * Notify the Backup Manager Service that an agent has unexpectedly gone away. - * This method is only invoked by the Activity Manager. - * - * If {@code userId} is different from the calling user id, then the caller must hold the - * android.permission.INTERACT_ACROSS_USERS_FULL permission. - * - * @param userId User id for which an agent has unexpectedly gone away. - */ - void agentDisconnectedForUser(int userId, String packageName); - - /** - * {@link android.app.backup.IBackupManager.agentDisconnected} for the calling user id. - */ - void agentDisconnected(String packageName); - - /** * Notify the Backup Manager Service that an application being installed will * need a data-restore pass. This method is only invoked by the Package Manager. * diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java index 316d129bd6b9..971d402569c0 100644 --- a/core/java/android/companion/CompanionDeviceService.java +++ b/core/java/android/companion/CompanionDeviceService.java @@ -62,10 +62,11 @@ import java.util.concurrent.Executor; * * <p> * If the companion application has requested observing device presence (see - * {@link CompanionDeviceManager#startObservingDevicePresence(String)}) the system will - * <a href="https://developer.android.com/guide/components/bound-services"> bind the service</a> - * when it detects the device nearby (for BLE devices) or when the device is connected - * (for Bluetooth devices). + * {@link CompanionDeviceManager#stopObservingDevicePresence(ObservingDevicePresenceRequest)}) + * the system will <a href="https://developer.android.com/guide/components/bound-services"> + * bind the service</a> when one of the {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified. * * <p> * The system binding {@link CompanionDeviceService} elevates the priority of the process that @@ -102,15 +103,25 @@ public abstract class CompanionDeviceService extends Service { /** * An intent action for a service to be bound whenever this app's companion device(s) - * are nearby. + * are nearby or self-managed device(s) report app appeared. * - * <p>The app will be kept alive for as long as the device is nearby or companion app reports - * appeared. - * If the app is not running at the time device gets connected, the app will be woken up.</p> + * <p>The app will be kept bound by the system when one of the + * {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified. * - * <p>Shortly after the device goes out of range or the companion app reports disappeared, - * the service will be unbound, and the app will be eligible for cleanup, unless any other - * user-visible components are running.</p> + * If the app is not running when one of the + * {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified, the app will be + * kept bound by the system.</p> + * + * <p>Shortly, the service will be unbound if both + * {@link DevicePresenceEvent#EVENT_BLE_DISAPPEARED} and + * {@link DevicePresenceEvent#EVENT_BT_DISCONNECTED} are notified, or + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_DISAPPEARED} event is notified. + * The app will be eligible for cleanup, unless any other user-visible components are + * running.</p> * * If running in background is not essential for the devices that this app can manage, * app should avoid declaring this service.</p> diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java index 42c74414ecd9..311e24ba6254 100644 --- a/core/java/android/companion/virtual/VirtualDeviceInternal.java +++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java @@ -83,7 +83,6 @@ import java.util.function.IntConsumer; public class VirtualDeviceInternal { private final Context mContext; - private final IVirtualDeviceManager mService; private final IVirtualDevice mVirtualDevice; private final Object mActivityListenersLock = new Object(); @GuardedBy("mActivityListenersLock") @@ -206,7 +205,6 @@ public class VirtualDeviceInternal { Context context, int associationId, VirtualDeviceParams params) throws RemoteException { - mService = service; mContext = context.getApplicationContext(); mVirtualDevice = service.createVirtualDevice( new Binder(), @@ -217,11 +215,7 @@ public class VirtualDeviceInternal { mSoundEffectListener); } - VirtualDeviceInternal( - IVirtualDeviceManager service, - Context context, - IVirtualDevice virtualDevice) { - mService = service; + VirtualDeviceInternal(Context context, IVirtualDevice virtualDevice) { mContext = context.getApplicationContext(); mVirtualDevice = virtualDevice; try { diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index ed2fd99c55c5..73ea9f0462d5 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -577,9 +577,8 @@ public final class VirtualDeviceManager { } /** @hide */ - public VirtualDevice(IVirtualDeviceManager service, Context context, - IVirtualDevice virtualDevice) { - mVirtualDeviceInternal = new VirtualDeviceInternal(service, context, virtualDevice); + public VirtualDevice(Context context, IVirtualDevice virtualDevice) { + mVirtualDeviceInternal = new VirtualDeviceInternal(context, virtualDevice); } /** diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index e3e10388754c..350048df3112 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -12394,14 +12394,30 @@ public class Intent implements Parcelable, Cloneable { * @hide */ public void collectExtraIntentKeys() { + collectExtraIntentKeys(false); + } + + /** + * Collects keys in the extra bundle whose value are intents. + * With these keys collected on the client side, the system server would only unparcel values + * of these keys and create IntentCreatorToken for them. + * This method could also be called from the system server side as a catch all safty net in case + * these keys are not collected on the client side. In that case, call it with forceUnparcel set + * to true since everything is parceled on the system server side. + * + * @param forceUnparcel if it is true, unparcel everything to determine if an object is an + * intent. Otherwise, do not unparcel anything. + * @hide + */ + public void collectExtraIntentKeys(boolean forceUnparcel) { if (preventIntentRedirect()) { - collectNestedIntentKeysRecur(new ArraySet<>()); + collectNestedIntentKeysRecur(new ArraySet<>(), forceUnparcel); } } - private void collectNestedIntentKeysRecur(Set<Intent> visited) { + private void collectNestedIntentKeysRecur(Set<Intent> visited, boolean forceUnparcel) { addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED); - if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) { + if (mExtras != null && (forceUnparcel || !mExtras.isParcelled()) && !mExtras.isEmpty()) { for (String key : mExtras.keySet()) { Object value; try { @@ -12410,23 +12426,25 @@ public class Intent implements Parcelable, Cloneable { // It is okay to not collect a parceled intent since it would have been // coming from another process and collected by its containing intent already // in that process. - if (!mExtras.isValueParceled(key)) { + if (forceUnparcel || !mExtras.isValueParceled(key)) { value = mExtras.get(key); } else { value = null; } } catch (BadParcelableException e) { - // This probably would never happen. But just in case, simply ignore it since - // it is not an intent anyway. + // This may still happen if the keys are collected on the system server side, in + // which case, we will try to unparcel everything. If this happens, simply + // ignore it since it is not an intent anyway. value = null; } if (value instanceof Intent intent) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0), + forceUnparcel); } else if (value instanceof Parcelable[] parcelables) { - handleParcelableArray(parcelables, key, visited); + handleParcelableArray(parcelables, key, visited, forceUnparcel); } else if (value instanceof ArrayList<?> parcelables) { - handleParcelableList(parcelables, key, visited); + handleParcelableList(parcelables, key, visited, forceUnparcel); } } } @@ -12436,13 +12454,15 @@ public class Intent implements Parcelable, Cloneable { Intent intent = mClipData.getItemAt(i).mIntent; if (intent != null && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i), + forceUnparcel); } } } } - private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key) { + private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key, + boolean forceUnparcel) { if (mCreatorTokenInfo == null) { mCreatorTokenInfo = new CreatorTokenInfo(); } @@ -12452,24 +12472,28 @@ public class Intent implements Parcelable, Cloneable { mCreatorTokenInfo.mNestedIntentKeys.add(key); if (!visited.contains(intent)) { visited.add(intent); - intent.collectNestedIntentKeysRecur(visited); + intent.collectNestedIntentKeysRecur(visited, forceUnparcel); } } - private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited) { + private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited, + boolean forceUnparcel) { for (int i = 0; i < parcelables.length; i++) { if (parcelables[i] instanceof Intent intent && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i), + forceUnparcel); } } } - private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited) { + private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited, + boolean forceUnparcel) { for (int i = 0; i < parcelables.size(); i++) { if (parcelables.get(i) instanceof Intent intent && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i), + forceUnparcel); } } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index c16582f19c9b..8c7e93a834b7 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -4649,6 +4649,7 @@ public abstract class PackageManager { * the Android Keystore backed by an isolated execution environment. The version indicates * which features are implemented in the isolated execution environment: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: Hardware support for Curve 25519 (including both Ed25519 signature generation and @@ -4682,6 +4683,7 @@ public abstract class PackageManager { * StrongBox</a>. If this feature has a version, the version number indicates which features are * implemented in StrongBox: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: No new features for StrongBox (the Android Keystore environment backed by an diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index b938aac811fd..075457885586 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -25,7 +25,6 @@ import android.content.res.loader.ResourcesProvider; import android.ravenwood.annotation.RavenwoodClassLoadHook; import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.text.TextUtils; -import android.util.Log; import com.android.internal.annotations.GuardedBy; @@ -51,7 +50,6 @@ import java.util.Objects; @RavenwoodKeepWholeClass @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class ApkAssets { - private static final boolean DEBUG = false; /** * The apk assets contains framework resource values specified by the system. @@ -136,17 +134,6 @@ public final class ApkAssets { @Nullable private final AssetsProvider mAssets; - @NonNull - private String mName; - - private static final int UPTODATE_FALSE = 0; - private static final int UPTODATE_TRUE = 1; - private static final int UPTODATE_ALWAYS_TRUE = 2; - - // Start with the only value that may change later and would force a native call to - // double check it. - private int mPreviousUpToDateResult = UPTODATE_TRUE; - /** * Creates a new ApkAssets instance from the given path on disk. * @@ -317,7 +304,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets, path); + this(format, flags, assets); Objects.requireNonNull(path, "path"); mNativePtr = nativeLoad(format, path, flags, assets); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); @@ -326,7 +313,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets, friendlyName); + this(format, flags, assets); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets); @@ -336,7 +323,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, long offset, long length, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets, friendlyName); + this(format, flags, assets); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets); @@ -344,17 +331,16 @@ public final class ApkAssets { } private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) { - this(FORMAT_APK, flags, assets, "empty"); + this(FORMAT_APK, flags, assets); mNativePtr = nativeLoadEmpty(flags, assets); mStringBlock = null; } private ApkAssets(@FormatType int format, @PropertyFlags int flags, - @Nullable AssetsProvider assets, @NonNull String name) { + @Nullable AssetsProvider assets) { mFlags = flags; mAssets = assets; mIsOverlay = format == FORMAT_IDMAP; - if (DEBUG) mName = name; } @UnsupportedAppUsage @@ -367,7 +353,7 @@ public final class ApkAssets { /** @hide */ public @NonNull String getDebugName() { synchronized (this) { - return nativeGetDebugName(mNativePtr); + return mNativePtr == 0 ? "<destroyed>" : nativeGetDebugName(mNativePtr); } } @@ -435,41 +421,13 @@ public final class ApkAssets { } } - private static double intervalMs(long beginNs, long endNs) { - return (endNs - beginNs) / 1000000.0; - } - /** * Returns false if the underlying APK was changed since this ApkAssets was loaded. */ public boolean isUpToDate() { - // This function is performance-critical - it's called multiple times on every Resources - // object creation, and on few other cache accesses - so it's important to avoid the native - // call when we know for sure what it will return (which is the case for both ALWAYS_TRUE - // and FALSE). - if (mPreviousUpToDateResult != UPTODATE_TRUE) { - return mPreviousUpToDateResult == UPTODATE_ALWAYS_TRUE; - } - final long beforeTs, afterLockTs, afterNativeTs, afterUnlockTs; - if (DEBUG) beforeTs = System.nanoTime(); - final int res; synchronized (this) { - if (DEBUG) afterLockTs = System.nanoTime(); - res = nativeIsUpToDate(mNativePtr); - if (DEBUG) afterNativeTs = System.nanoTime(); - } - if (DEBUG) { - afterUnlockTs = System.nanoTime(); - if (afterUnlockTs - beforeTs >= 10L * 1000000) { - Log.d("ApkAssets", "isUpToDate(" + mName + ") took " - + intervalMs(beforeTs, afterUnlockTs) - + " ms: " + intervalMs(beforeTs, afterLockTs) - + " / " + intervalMs(afterLockTs, afterNativeTs) - + " / " + intervalMs(afterNativeTs, afterUnlockTs)); - } + return nativeIsUpToDate(mNativePtr); } - mPreviousUpToDateResult = res; - return res != UPTODATE_FALSE; } public boolean isSystem() { @@ -529,7 +487,7 @@ public final class ApkAssets { private static native @NonNull String nativeGetAssetPath(long ptr); private static native @NonNull String nativeGetDebugName(long ptr); private static native long nativeGetStringBlock(long ptr); - @CriticalNative private static native int nativeIsUpToDate(long ptr); + @CriticalNative private static native boolean nativeIsUpToDate(long ptr); private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException; private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr, String overlayableName) throws IOException; diff --git a/core/java/android/content/res/ResourceTimer.java b/core/java/android/content/res/ResourceTimer.java index 2d1bf4d9d296..d51f64ce8106 100644 --- a/core/java/android/content/res/ResourceTimer.java +++ b/core/java/android/content/res/ResourceTimer.java @@ -17,10 +17,13 @@ package android.content.res; import android.annotation.NonNull; +import android.annotation.Nullable; + import android.app.AppProtoEnums; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; @@ -30,7 +33,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; -import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.Arrays; @@ -275,40 +277,38 @@ public final class ResourceTimer { * Update the metrics information and dump it. * @hide */ - public static void dumpTimers(@NonNull FileDescriptor fd, String... args) { - try (PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd))) { - pw.println("\nDumping ResourceTimers"); - - final boolean enabled; - synchronized (sLock) { - enabled = sEnabled && sConfig != null; - } - if (!enabled) { + public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) { + FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor()); + PrintWriter pw = new FastPrintWriter(fout); + synchronized (sLock) { + if (!sEnabled || (sConfig == null)) { pw.println(" Timers are not enabled in this process"); + pw.flush(); return; } + } - // Look for the --refresh switch. If the switch is present, then sTimers is updated. - // Otherwise, the current value of sTimers is displayed. - boolean refresh = Arrays.asList(args).contains("-refresh"); - - synchronized (sLock) { - update(refresh); - long runtime = sLastUpdated - sProcessStart; - pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); - for (int i = 0; i < sTimers.length; i++) { - Timer t = sTimers[i]; - if (t.count != 0) { - String name = sConfig.timers[i]; - pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " - + "largest=%s\n", - name, t.count, t.total / t.count, t.mintime, t.maxtime, - packedString(t.percentile), - packedString(t.largest)); - } + // Look for the --refresh switch. If the switch is present, then sTimers is updated. + // Otherwise, the current value of sTimers is displayed. + boolean refresh = Arrays.asList(args).contains("-refresh"); + + synchronized (sLock) { + update(refresh); + long runtime = sLastUpdated - sProcessStart; + pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); + for (int i = 0; i < sTimers.length; i++) { + Timer t = sTimers[i]; + if (t.count != 0) { + String name = sConfig.timers[i]; + pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " + + "largest=%s\n", + name, t.count, t.total / t.count, t.mintime, t.maxtime, + packedString(t.percentile), + packedString(t.largest)); } } } + pw.flush(); } // Enable (or disabled) the runtime timers. Note that timers are disabled by default. This diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java index 71702d996883..25cdc508fdce 100644 --- a/core/java/android/hardware/contexthub/HubEndpoint.java +++ b/core/java/android/hardware/contexthub/HubEndpoint.java @@ -137,6 +137,8 @@ public class HubEndpoint { serviceDescriptor, mLifecycleCallback.onSessionOpenRequest( initiator, serviceDescriptor))); + } else { + invokeCallbackFinished(); } } @@ -163,6 +165,8 @@ public class HubEndpoint { + result.getReason()); rejectSession(sessionId); } + + invokeCallbackFinished(); } private void acceptSession( @@ -249,7 +253,12 @@ public class HubEndpoint { activeSession.setOpened(); if (mLifecycleCallback != null) { mLifecycleCallbackExecutor.execute( - () -> mLifecycleCallback.onSessionOpened(activeSession)); + () -> { + mLifecycleCallback.onSessionOpened(activeSession); + invokeCallbackFinished(); + }); + } else { + invokeCallbackFinished(); } } @@ -278,7 +287,10 @@ public class HubEndpoint { synchronized (mLock) { mActiveSessions.remove(sessionId); } + invokeCallbackFinished(); }); + } else { + invokeCallbackFinished(); } } @@ -323,8 +335,17 @@ public class HubEndpoint { e.rethrowFromSystemServer(); } } + invokeCallbackFinished(); }); } + + private void invokeCallbackFinished() { + try { + mServiceToken.onCallbackFinished(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } }; /** Binder returned from system service, non-null while registered. */ diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl index 44f80c819e83..eb1255c06094 100644 --- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl +++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl @@ -94,4 +94,10 @@ interface IContextHubEndpoint { */ @EnforcePermission("ACCESS_CONTEXT_HUB") void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode); + + /** + * Invoked when a callback from IContextHubEndpointCallback finishes. + */ + @EnforcePermission("ACCESS_CONTEXT_HUB") + void onCallbackFinished(); } diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java index d84d29292ed5..8fbe05c4e9eb 100644 --- a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java +++ b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java @@ -121,15 +121,20 @@ public class FingerprintSensorPropertiesInternal extends SensorPropertiesInterna /** * Returns if sensor type is ultrasonic Udfps - * @return true if sensor is ultrasonic Udfps, false otherwise */ public boolean isUltrasonicUdfps() { return sensorType == TYPE_UDFPS_ULTRASONIC; } /** + * Returns if sensor type is optical Udfps + */ + public boolean isOpticalUdfps() { + return sensorType == TYPE_UDFPS_OPTICAL; + } + + /** * Returns if sensor type is side-FPS - * @return true if sensor is side-fps, false otherwise */ public boolean isAnySidefpsType() { switch (sensorType) { diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index ed510e467f82..2bb28a1b6b0b 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -266,6 +266,11 @@ interface IInputManager { @PermissionManuallyEnforced @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.MANAGE_KEY_GESTURES)") + AidlInputGestureData getInputGesture(int userId, in AidlInputGestureData.Trigger trigger); + + @PermissionManuallyEnforced + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MANAGE_KEY_GESTURES)") int addCustomInputGesture(int userId, in AidlInputGestureData data); @PermissionManuallyEnforced diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java index f41550f6061e..75c652c973e4 100644 --- a/core/java/android/hardware/input/InputGestureData.java +++ b/core/java/android/hardware/input/InputGestureData.java @@ -48,27 +48,7 @@ public final class InputGestureData { /** Returns the trigger information for this input gesture */ public Trigger getTrigger() { - switch (mInputGestureData.trigger.getTag()) { - case AidlInputGestureData.Trigger.Tag.key: { - AidlInputGestureData.KeyTrigger trigger = mInputGestureData.trigger.getKey(); - if (trigger == null) { - throw new RuntimeException("InputGestureData is corrupted, null key trigger!"); - } - return createKeyTrigger(trigger.keycode, trigger.modifierState); - } - case AidlInputGestureData.Trigger.Tag.touchpadGesture: { - AidlInputGestureData.TouchpadGestureTrigger trigger = - mInputGestureData.trigger.getTouchpadGesture(); - if (trigger == null) { - throw new RuntimeException( - "InputGestureData is corrupted, null touchpad trigger!"); - } - return createTouchpadTrigger(trigger.gestureType); - } - default: - throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!"); - - } + return createTriggerFromAidlTrigger(mInputGestureData.trigger); } /** Returns the action to perform for this input gesture */ @@ -147,18 +127,7 @@ public final class InputGestureData { "No app launch data for system action launch application"); } AidlInputGestureData data = new AidlInputGestureData(); - data.trigger = new AidlInputGestureData.Trigger(); - if (mTrigger instanceof KeyTrigger keyTrigger) { - data.trigger.setKey(new AidlInputGestureData.KeyTrigger()); - data.trigger.getKey().keycode = keyTrigger.getKeycode(); - data.trigger.getKey().modifierState = keyTrigger.getModifierState(); - } else if (mTrigger instanceof TouchpadTrigger touchpadTrigger) { - data.trigger.setTouchpadGesture(new AidlInputGestureData.TouchpadGestureTrigger()); - data.trigger.getTouchpadGesture().gestureType = - touchpadTrigger.getTouchpadGestureType(); - } else { - throw new IllegalArgumentException("Invalid trigger type!"); - } + data.trigger = mTrigger.getAidlTrigger(); data.gestureType = mKeyGestureType; if (mAppLaunchData != null) { if (mAppLaunchData instanceof AppLaunchData.CategoryData categoryData) { @@ -198,6 +167,7 @@ public final class InputGestureData { } public interface Trigger { + AidlInputGestureData.Trigger getAidlTrigger(); } /** Creates a input gesture trigger based on a key press */ @@ -210,85 +180,128 @@ public final class InputGestureData { return new TouchpadTrigger(touchpadGestureType); } + public static Trigger createTriggerFromAidlTrigger(AidlInputGestureData.Trigger aidlTrigger) { + switch (aidlTrigger.getTag()) { + case AidlInputGestureData.Trigger.Tag.key: { + AidlInputGestureData.KeyTrigger trigger = aidlTrigger.getKey(); + if (trigger == null) { + throw new RuntimeException("aidlTrigger is corrupted, null key trigger!"); + } + return new KeyTrigger(trigger); + } + case AidlInputGestureData.Trigger.Tag.touchpadGesture: { + AidlInputGestureData.TouchpadGestureTrigger trigger = + aidlTrigger.getTouchpadGesture(); + if (trigger == null) { + throw new RuntimeException( + "aidlTrigger is corrupted, null touchpad trigger!"); + } + return new TouchpadTrigger(trigger); + } + default: + throw new RuntimeException("aidlTrigger is corrupted, invalid trigger type!"); + + } + } + /** Key based input gesture trigger */ public static class KeyTrigger implements Trigger { - private static final int SHORTCUT_META_MASK = - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON - | KeyEvent.META_SHIFT_ON; - private final int mKeycode; - private final int mModifierState; + + AidlInputGestureData.KeyTrigger mAidlKeyTrigger; + + private KeyTrigger(@NonNull AidlInputGestureData.KeyTrigger aidlKeyTrigger) { + mAidlKeyTrigger = aidlKeyTrigger; + } private KeyTrigger(int keycode, int modifierState) { if (keycode <= KeyEvent.KEYCODE_UNKNOWN || keycode > KeyEvent.getMaxKeyCode()) { throw new IllegalArgumentException("Invalid keycode = " + keycode); } - mKeycode = keycode; - mModifierState = modifierState; + mAidlKeyTrigger = new AidlInputGestureData.KeyTrigger(); + mAidlKeyTrigger.keycode = keycode; + mAidlKeyTrigger.modifierState = modifierState; } public int getKeycode() { - return mKeycode; + return mAidlKeyTrigger.keycode; } public int getModifierState() { - return mModifierState; + return mAidlKeyTrigger.modifierState; + } + + public AidlInputGestureData.Trigger getAidlTrigger() { + AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger(); + trigger.setKey(mAidlKeyTrigger); + return trigger; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof KeyTrigger that)) return false; - return mKeycode == that.mKeycode && mModifierState == that.mModifierState; + return Objects.equals(mAidlKeyTrigger, that.mAidlKeyTrigger); } @Override public int hashCode() { - return Objects.hash(mKeycode, mModifierState); + return mAidlKeyTrigger.hashCode(); } @Override public String toString() { return "KeyTrigger{" + - "mKeycode=" + KeyEvent.keyCodeToString(mKeycode) + - ", mModifierState=" + mModifierState + + "mKeycode=" + KeyEvent.keyCodeToString(mAidlKeyTrigger.keycode) + + ", mModifierState=" + mAidlKeyTrigger.modifierState + '}'; } } /** Touchpad based input gesture trigger */ public static class TouchpadTrigger implements Trigger { - private final int mTouchpadGestureType; + AidlInputGestureData.TouchpadGestureTrigger mAidlTouchpadTrigger; + + private TouchpadTrigger( + @NonNull AidlInputGestureData.TouchpadGestureTrigger aidlTouchpadTrigger) { + mAidlTouchpadTrigger = aidlTouchpadTrigger; + } private TouchpadTrigger(int touchpadGestureType) { if (touchpadGestureType != TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP) { throw new IllegalArgumentException( "Invalid touchpadGestureType = " + touchpadGestureType); } - mTouchpadGestureType = touchpadGestureType; + mAidlTouchpadTrigger = new AidlInputGestureData.TouchpadGestureTrigger(); + mAidlTouchpadTrigger.gestureType = touchpadGestureType; } public int getTouchpadGestureType() { - return mTouchpadGestureType; + return mAidlTouchpadTrigger.gestureType; + } + + public AidlInputGestureData.Trigger getAidlTrigger() { + AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger(); + trigger.setTouchpadGesture(mAidlTouchpadTrigger); + return trigger; } @Override public String toString() { return "TouchpadTrigger{" + - "mTouchpadGestureType=" + mTouchpadGestureType + + "mTouchpadGestureType=" + mAidlTouchpadTrigger.gestureType + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TouchpadTrigger that = (TouchpadTrigger) o; - return mTouchpadGestureType == that.mTouchpadGestureType; + if (!(o instanceof TouchpadTrigger that)) return false; + return Objects.equals(mAidlTouchpadTrigger, that.mAidlTouchpadTrigger); } @Override public int hashCode() { - return Objects.hashCode(mTouchpadGestureType); + return mAidlTouchpadTrigger.hashCode(); } } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 10224c1be788..cf41e138047a 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1480,6 +1480,30 @@ public final class InputManager { mGlobal.unregisterKeyGestureEventHandler(handler); } + /** + * Find an input gesture mapped to a particular trigger. + * + * @param trigger to find the input gesture for + * @return input gesture mapped to the provided trigger, {@code null} if none found + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) + @UserHandleAware + @Nullable + public InputGestureData getInputGesture(@NonNull InputGestureData.Trigger trigger) { + try { + AidlInputGestureData result = mIm.getInputGesture(mContext.getUserId(), + trigger.getAidlTrigger()); + if (result == null) { + return null; + } + return new InputGestureData(result); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** Adds a new custom input gesture * * @param inputGestureData gesture data to add as custom gesture diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index cb1e0161441f..4025242fd208 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -43,6 +43,9 @@ public final class KeyGestureEvent { private static final int LOG_EVENT_UNSPECIFIED = FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED; + // Used as a placeholder to identify if a gesture is reserved for system + public static final int KEY_GESTURE_TYPE_SYSTEM_RESERVED = -1; + // These values should not change and values should not be re-used as this data is persisted to // long term storage and must be kept backwards compatible. public static final int KEY_GESTURE_TYPE_UNSPECIFIED = 0; @@ -144,6 +147,7 @@ public final class KeyGestureEvent { public static final int ACTION_GESTURE_COMPLETE = 2; @IntDef(prefix = "KEY_GESTURE_TYPE_", value = { + KEY_GESTURE_TYPE_SYSTEM_RESERVED, KEY_GESTURE_TYPE_UNSPECIFIED, KEY_GESTURE_TYPE_HOME, KEY_GESTURE_TYPE_RECENT_APPS, @@ -232,6 +236,26 @@ public final class KeyGestureEvent { public @interface KeyGestureType { } + /** + * Returns whether the key gesture type passed as argument is allowed for visible background + * users. + * + * @hide + */ + public static boolean isVisibleBackgrounduserAllowedGesture(int keyGestureType) { + switch (keyGestureType) { + case KEY_GESTURE_TYPE_SLEEP: + case KEY_GESTURE_TYPE_WAKEUP: + case KEY_GESTURE_TYPE_LAUNCH_ASSISTANT: + case KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT: + case KEY_GESTURE_TYPE_VOLUME_MUTE: + case KEY_GESTURE_TYPE_RECENT_APPS: + case KEY_GESTURE_TYPE_APP_SWITCH: + return false; + } + return true; + } + public KeyGestureEvent(@NonNull AidlKeyGestureEvent keyGestureEvent) { this.mKeyGestureEvent = keyGestureEvent; } @@ -645,6 +669,8 @@ public final class KeyGestureEvent { private static String keyGestureTypeToString(@KeyGestureType int value) { switch (value) { + case KEY_GESTURE_TYPE_SYSTEM_RESERVED: + return "KEY_GESTURE_TYPE_SYSTEM_RESERVED"; case KEY_GESTURE_TYPE_UNSPECIFIED: return "KEY_GESTURE_TYPE_UNSPECIFIED"; case KEY_GESTURE_TYPE_HOME: diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 2bc6ab5a18e9..d5640221e6bd 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -2783,4 +2783,12 @@ public final class Debug */ public static native boolean logAllocatorStats(); + /** + * Return the amount of memory (in kB) allocated by kernel drivers through CMA. + * @return a non-negative value or -1 on error. + * + * @hide + */ + public static native long getKernelCmaUsageKb(); + } diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index 12080ca511b2..45a7afa014a1 100644 --- a/core/java/android/os/GraphicsEnvironment.java +++ b/core/java/android/os/GraphicsEnvironment.java @@ -706,11 +706,11 @@ public class GraphicsEnvironment { * @param context */ public void showAngleInUseDialogBox(Context context) { - if (!shouldShowAngleInUseDialogBox(context)) { + if (!mShouldUseAngle) { return; } - if (!mShouldUseAngle) { + if (!shouldShowAngleInUseDialogBox(context)) { return; } diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 0879118ff856..4aa74621bd62 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -593,11 +593,11 @@ public final class Parcel { */ public final void recycle() { if (mRecycled) { - Log.wtf(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: " + String error = "Recycle called on unowned Parcel. (recycle twice?) Here: " + Log.getStackTraceString(new Throwable()) - + " Original recycle call (if DEBUG_RECYCLE): ", mStack); - - return; + + " Original recycle call (if DEBUG_RECYCLE): "; + Log.wtf(TAG, error, mStack); + throw new IllegalStateException(error, mStack); } mRecycled = true; diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java index 9085fe09bdaa..a58fea891851 100644 --- a/core/java/android/os/ServiceManager.java +++ b/core/java/android/os/ServiceManager.java @@ -278,7 +278,7 @@ public final class ServiceManager { return service; } else { return Binder.allowBlocking( - getIServiceManager().checkService(name).getServiceWithMetadata().service); + getIServiceManager().checkService2(name).getServiceWithMetadata().service); } } catch (RemoteException e) { Log.e(TAG, "error in checkService", e); diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java index 7ea521ec5dd4..a5aa1b3efcd2 100644 --- a/core/java/android/os/ServiceManagerNative.java +++ b/core/java/android/os/ServiceManagerNative.java @@ -62,16 +62,23 @@ class ServiceManagerProxy implements IServiceManager { @UnsupportedAppUsage public IBinder getService(String name) throws RemoteException { // Same as checkService (old versions of servicemanager had both methods). - return checkService(name).getServiceWithMetadata().service; + return checkService2(name).getServiceWithMetadata().service; } public Service getService2(String name) throws RemoteException { // Same as checkService (old versions of servicemanager had both methods). - return checkService(name); + return checkService2(name); } - public Service checkService(String name) throws RemoteException { - return mServiceManager.checkService(name); + // TODO(b/355394904): This function has been deprecated, please use checkService2 instead. + @UnsupportedAppUsage + public IBinder checkService(String name) throws RemoteException { + // Same as checkService (old versions of servicemanager had both methods). + return checkService2(name).getServiceWithMetadata().service; + } + + public Service checkService2(String name) throws RemoteException { + return mServiceManager.checkService2(name); } public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 11dddfb24ad5..baaaa464a4cf 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10525,6 +10525,15 @@ public final class Settings { public static final String SCREENSAVER_ACTIVATE_ON_SLEEP = "screensaver_activate_on_sleep"; /** + * If screensavers are enabled, whether the screensaver should be + * automatically launched when the device is stationary and upright. + * @hide + */ + @Readable + public static final String SCREENSAVER_ACTIVATE_ON_POSTURED = + "screensaver_activate_on_postured"; + + /** * If screensavers are enabled, the default screensaver component. * @hide */ diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index ebb6fb451699..4a9e945e62a9 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -42,6 +42,16 @@ flag { } flag { + name: "secure_array_zeroization" + namespace: "platform_security" + description: "Enable secure array zeroization" + bug: "320392352" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "deprecate_fsv_sig" namespace: "hardware_backed_security" description: "Feature flag for deprecating .fsv_sig" diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig index dfc11dcb5427..d3a230d1335d 100644 --- a/core/java/android/service/dreams/flags.aconfig +++ b/core/java/android/service/dreams/flags.aconfig @@ -77,3 +77,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "allow_dream_when_postured" + namespace: "systemui" + description: "Allow dreaming when device is stationary and upright" + bug: "383208131" +} diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java index b21e85aeeb6a..da3a817f0341 100644 --- a/core/java/android/view/PointerIcon.java +++ b/core/java/android/view/PointerIcon.java @@ -514,10 +514,14 @@ public final class PointerIcon implements Parcelable { final TypedArray a = resources.obtainAttributes( parser, com.android.internal.R.styleable.PointerIcon); bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0); - hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0) - * pointerScale; - hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0) - * pointerScale; + // Cast the hotspot dimensions to int before scaling to match the scaling logic of + // the bitmap, whose intrinsic size is also an int before it is scaled. + final int unscaledHotSpotX = + (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0); + final int unscaledHotSpotY = + (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0); + hotSpotX = unscaledHotSpotX * pointerScale; + hotSpotY = unscaledHotSpotY * pointerScale; a.recycle(); } catch (Exception ex) { throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 71a832d84f08..99fe0cbdca25 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -18,7 +18,6 @@ package android.widget; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.graphics.Paint.NEW_FONT_VARIATION_MANAGEMENT; import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT; import static android.view.ContentInfo.SOURCE_AUTOFILL; import static android.view.ContentInfo.SOURCE_CLIPBOARD; @@ -5544,13 +5543,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; } - final boolean useFontVariationStore = Flags.typefaceRedesignReadonly() - && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT); boolean effective; - if (useFontVariationStore) { + if (Flags.typefaceRedesignReadonly()) { if (mFontWeightAdjustment != 0 && mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) { - mTextPaint.setFontVariationSettings(fontVariationSettings, mFontWeightAdjustment); + List<FontVariationAxis> axes = FontVariationAxis.fromFontVariationSettingsForList( + fontVariationSettings); + if (axes == null) { + return false; // invalid format of the font variation settings. + } + boolean wghtAdjusted = false; + for (int i = 0; i < axes.size(); ++i) { + FontVariationAxis axis = axes.get(i); + if (axis.getOpenTypeTagValue() == 0x77676874 /* wght */) { + axes.set(i, new FontVariationAxis("wght", + Math.clamp(axis.getStyleValue() + mFontWeightAdjustment, + FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX))); + wghtAdjusted = true; + } + } + if (!wghtAdjusted) { + axes.add(new FontVariationAxis("wght", + Math.clamp(400 + mFontWeightAdjustment, + FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX))); + } + mTextPaint.setFontVariationSettings( + FontVariationAxis.toFontVariationSettings(axes)); } else { mTextPaint.setFontVariationSettings(fontVariationSettings); } diff --git a/core/java/android/window/DisplayAreaOrganizer.java b/core/java/android/window/DisplayAreaOrganizer.java index 84ce247264f6..bd711fc79083 100644 --- a/core/java/android/window/DisplayAreaOrganizer.java +++ b/core/java/android/window/DisplayAreaOrganizer.java @@ -121,6 +121,14 @@ public class DisplayAreaOrganizer extends WindowOrganizer { public static final int FEATURE_WINDOWING_LAYER = FEATURE_SYSTEM_FIRST + 9; /** + * Display area for rendering app zoom out. When there are multiple layers on the screen, + * we want to render these layers based on a depth model. Here we zoom out the layer behind, + * whether it's an app or the homescreen. + * @hide + */ + public static final int FEATURE_APP_ZOOM_OUT = FEATURE_SYSTEM_FIRST + 10; + + /** * The last boundary of display area for system features */ public static final int FEATURE_SYSTEM_LAST = 10_000; diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index de3e0d3faf43..7a1078f8718f 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -415,6 +415,17 @@ flag { } flag { + name: "keep_app_window_hide_while_locked" + namespace: "windowing_frontend" + description: "Do not let app window visible while device is locked" + is_fixed_read_only: true + bug: "378088391" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "port_window_size_animation" namespace: "windowing_frontend" description: "Port window-resize animation from legacy to shell" diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index c9c4be1e2c93..dc440e36ca0d 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -19,6 +19,7 @@ package com.android.internal.os; import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_FINISH; import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_START; import static android.os.BatteryStats.HistoryItem.EVENT_STATE_CHANGE; +import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; import android.annotation.NonNull; import android.annotation.Nullable; @@ -215,6 +216,7 @@ public class BatteryStatsHistory { private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>(); private byte mLastHistoryStepLevel = 0; private boolean mMutable = true; + private int mIteratorCookie; private final BatteryStatsHistory mWritableHistory; private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> { @@ -289,6 +291,7 @@ public class BatteryStatsHistory { } void load() { + Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); mDirectory.mkdirs(); if (!mDirectory.exists()) { Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath()); @@ -325,8 +328,11 @@ public class BatteryStatsHistory { } } finally { unlock(); + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); } }); + } else { + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); } } @@ -418,6 +424,7 @@ public class BatteryStatsHistory { } void writeToParcel(Parcel out, boolean useBlobs) { + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel"); lock(); try { final long start = SystemClock.uptimeMillis(); @@ -443,6 +450,7 @@ public class BatteryStatsHistory { } } finally { unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } @@ -482,34 +490,39 @@ public class BatteryStatsHistory { } private void cleanup() { - if (mDirectory == null) { - return; - } - - if (!tryLock()) { - mCleanupNeeded = true; - return; - } - - mCleanupNeeded = false; + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup"); try { - // if free disk space is less than 100MB, delete oldest history file. - if (!hasFreeDiskSpace(mDirectory)) { - BatteryHistoryFile oldest = mHistoryFiles.remove(0); - oldest.atomicFile.delete(); + if (mDirectory == null) { + return; + } + + if (!tryLock()) { + mCleanupNeeded = true; + return; } - // if there is more history stored than allowed, delete oldest history files. - int size = getSize(); - while (size > mMaxHistorySize) { - BatteryHistoryFile oldest = mHistoryFiles.get(0); - int length = (int) oldest.atomicFile.getBaseFile().length(); - oldest.atomicFile.delete(); - mHistoryFiles.remove(0); - size -= length; + mCleanupNeeded = false; + try { + // if free disk space is less than 100MB, delete oldest history file. + if (!hasFreeDiskSpace(mDirectory)) { + BatteryHistoryFile oldest = mHistoryFiles.remove(0); + oldest.atomicFile.delete(); + } + + // if there is more history stored than allowed, delete oldest history files. + int size = getSize(); + while (size > mMaxHistorySize) { + BatteryHistoryFile oldest = mHistoryFiles.get(0); + int length = (int) oldest.atomicFile.getBaseFile().length(); + oldest.atomicFile.delete(); + mHistoryFiles.remove(0); + size -= length; + } + } finally { + unlock(); } } finally { - unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } } @@ -710,13 +723,18 @@ public class BatteryStatsHistory { * in the system directory, so it is not safe while actively writing history. */ public BatteryStatsHistory copy() { - synchronized (this) { - // Make a copy of battery history to avoid concurrent modification. - Parcel historyBufferCopy = Parcel.obtain(); - historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.copy"); + try { + synchronized (this) { + // Make a copy of battery history to avoid concurrent modification. + Parcel historyBufferCopy = Parcel.obtain(); + historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); - return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null, - null, mEventLogger, this); + return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, + null, null, mEventLogger, this); + } + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } @@ -826,7 +844,7 @@ public class BatteryStatsHistory { */ @NonNull public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) { - if (mMutable) { + if (mMutable || mIteratorCookie != 0) { return copy().iterate(startTimeMs, endTimeMs); } @@ -837,7 +855,12 @@ public class BatteryStatsHistory { mCurrentParcel = null; mCurrentParcelEnd = 0; mParcelIndex = 0; - return new BatteryStatsHistoryIterator(this, startTimeMs, endTimeMs); + BatteryStatsHistoryIterator iterator = new BatteryStatsHistoryIterator( + this, startTimeMs, endTimeMs); + mIteratorCookie = System.identityHashCode(iterator); + Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate", + mIteratorCookie); + return iterator; } /** @@ -848,6 +871,9 @@ public class BatteryStatsHistory { if (mHistoryDir != null) { mHistoryDir.unlock(); } + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate", + mIteratorCookie); + mIteratorCookie = 0; } /** @@ -949,28 +975,33 @@ public class BatteryStatsHistory { * @return true if success, false otherwise. */ public boolean readFileToParcel(Parcel out, AtomicFile file) { - byte[] raw = null; + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read"); try { - final long start = SystemClock.uptimeMillis(); - raw = file.readFully(); - if (DEBUG) { - Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() - + " duration ms:" + (SystemClock.uptimeMillis() - start)); + byte[] raw = null; + try { + final long start = SystemClock.uptimeMillis(); + raw = file.readFully(); + if (DEBUG) { + Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() + + " duration ms:" + (SystemClock.uptimeMillis() - start)); + } + } catch (Exception e) { + Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); + return false; } - } catch (Exception e) { - Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); - return false; - } - out.unmarshall(raw, 0, raw.length); - out.setDataPosition(0); - if (!verifyVersion(out)) { - return false; + out.unmarshall(raw, 0, raw.length); + out.setDataPosition(0); + if (!verifyVersion(out)) { + return false; + } + // skip monotonic time field. + out.readLong(); + // skip monotonic size field + out.readLong(); + return true; + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } - // skip monotonic time field. - out.readLong(); - // skip monotonic size field - out.readLong(); - return true; } /** diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java index 7f7ea8b28546..37500766a4ac 100644 --- a/core/java/com/android/internal/security/VerityUtils.java +++ b/core/java/com/android/internal/security/VerityUtils.java @@ -36,7 +36,6 @@ import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier; import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import com.android.internal.org.bouncycastle.operator.OperatorCreationException; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -53,12 +52,6 @@ import java.security.cert.X509Certificate; public abstract class VerityUtils { private static final String TAG = "VerityUtils"; - /** - * File extension of the signature file. For example, foo.apk.fsv_sig is the signature file of - * foo.apk. - */ - public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig"; - /** SHA256 hash size. */ private static final int HASH_SIZE_BYTES = 32; @@ -67,16 +60,6 @@ public abstract class VerityUtils { || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2; } - /** Returns true if the given file looks like containing an fs-verity signature. */ - public static boolean isFsveritySignatureFile(File file) { - return file.getName().endsWith(FSVERITY_SIGNATURE_FILE_EXTENSION); - } - - /** Returns the fs-verity signature file path of the given file. */ - public static String getFsveritySignatureFilePath(String filePath) { - return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION; - } - /** Enables fs-verity for the file without signature. */ public static void setUpFsverity(@NonNull String filePath) throws IOException { int errno = enableFsverityNative(filePath); diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index f14e1f63cdf6..ec0954d5590a 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -239,4 +239,7 @@ interface IStatusBarService /** Unbundle a categorized notification */ void unbundleNotification(String key); + + /** Rebundle an (un)categorized notification */ + void rebundleNotification(String key); } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 39ddea614ee4..74707703f5f2 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -65,6 +65,7 @@ import android.util.SparseLongArray; import android.view.InputDevice; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.google.android.collect.Lists; @@ -75,6 +76,7 @@ import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -292,6 +294,56 @@ public class LockPatternUtils { } + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static byte[] newNonMovableByteArray(int length) { + if (!android.security.Flags.secureArrayZeroization()) { + return new byte[length]; + } + return ArrayUtils.newNonMovableByteArray(length); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static char[] newNonMovableCharArray(int length) { + if (!android.security.Flags.secureArrayZeroization()) { + return new char[length]; + } + return ArrayUtils.newNonMovableCharArray(length); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static void zeroize(byte[] array) { + if (!android.security.Flags.secureArrayZeroization()) { + if (array != null) { + Arrays.fill(array, (byte) 0); + } + return; + } + ArrayUtils.zeroize(array); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static void zeroize(char[] array) { + if (!android.security.Flags.secureArrayZeroization()) { + if (array != null) { + Arrays.fill(array, (char) 0); + } + return; + } + ArrayUtils.zeroize(array); + } + @UnsupportedAppUsage public DevicePolicyManager getDevicePolicyManager() { if (mDevicePolicyManager == null) { diff --git a/core/java/com/android/internal/widget/LockscreenCredential.java b/core/java/com/android/internal/widget/LockscreenCredential.java index 54b9a225f944..92ce990c67df 100644 --- a/core/java/com/android/internal/widget/LockscreenCredential.java +++ b/core/java/com/android/internal/widget/LockscreenCredential.java @@ -246,7 +246,7 @@ public class LockscreenCredential implements Parcelable, AutoCloseable { */ public void zeroize() { if (mCredential != null) { - Arrays.fill(mCredential, (byte) 0); + LockPatternUtils.zeroize(mCredential); mCredential = null; } } @@ -346,7 +346,7 @@ public class LockscreenCredential implements Parcelable, AutoCloseable { byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword); byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword); - Arrays.fill(saltedPassword, (byte) 0); + LockPatternUtils.zeroize(saltedPassword); return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5)); } catch (NoSuchAlgorithmException e) { throw new AssertionError("Missing digest algorithm: ", e); diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java index 80bc4fd89c8d..dd12f69a56fb 100644 --- a/core/java/com/android/internal/widget/NotificationExpandButton.java +++ b/core/java/com/android/internal/widget/NotificationExpandButton.java @@ -56,8 +56,6 @@ public class NotificationExpandButton extends FrameLayout { private int mDefaultTextColor; private int mHighlightPillColor; private int mHighlightTextColor; - // Track whether this ever had mExpanded = true, so that we don't highlight it anymore. - private boolean mWasExpanded = false; public NotificationExpandButton(Context context) { this(context, null, 0, 0); @@ -136,7 +134,6 @@ public class NotificationExpandButton extends FrameLayout { int contentDescriptionId; if (mExpanded) { if (notificationsRedesignTemplates()) { - mWasExpanded = true; drawableId = R.drawable.ic_notification_2025_collapse; } else { drawableId = R.drawable.ic_collapse_notification; @@ -156,8 +153,6 @@ public class NotificationExpandButton extends FrameLayout { if (!notificationsRedesignTemplates()) { // changing the expanded state can affect the number display updateNumber(); - } else { - updateColors(); } } @@ -197,43 +192,22 @@ public class NotificationExpandButton extends FrameLayout { ); } - /** - * Use highlight colors for the expander for groups (when the number is showing) that haven't - * been opened before, as long as the colors are available. - */ - private boolean shouldBeHighlighted() { - return !mWasExpanded && shouldShowNumber() - && mHighlightPillColor != 0 && mHighlightTextColor != 0; - } - private void updateColors() { - if (notificationsRedesignTemplates()) { - if (shouldBeHighlighted()) { + if (shouldShowNumber()) { + if (mHighlightPillColor != 0) { mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor)); - mIconView.setColorFilter(mHighlightTextColor); + } + mIconView.setColorFilter(mHighlightTextColor); + if (mHighlightTextColor != 0) { mNumberView.setTextColor(mHighlightTextColor); - } else { - mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); - mIconView.setColorFilter(mDefaultTextColor); - mNumberView.setTextColor(mDefaultTextColor); } } else { - if (shouldShowNumber()) { - if (mHighlightPillColor != 0) { - mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor)); - } - mIconView.setColorFilter(mHighlightTextColor); - if (mHighlightTextColor != 0) { - mNumberView.setTextColor(mHighlightTextColor); - } - } else { - if (mDefaultPillColor != 0) { - mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); - } - mIconView.setColorFilter(mDefaultTextColor); - if (mDefaultTextColor != 0) { - mNumberView.setTextColor(mDefaultTextColor); - } + if (mDefaultPillColor != 0) { + mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); + } + mIconView.setColorFilter(mDefaultTextColor); + if (mDefaultTextColor != 0) { + mNumberView.setTextColor(mDefaultTextColor); } } } diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index 8cd7843fe1d9..904b73f41e70 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -31,6 +32,7 @@ import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.RemotableViewMethod; import android.widget.ProgressBar; import android.widget.RemoteViews; @@ -40,14 +42,15 @@ import androidx.annotation.ColorInt; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; +import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -56,18 +59,26 @@ import java.util.TreeSet; * represent Notification ProgressStyle progress, such as for ridesharing and navigation. */ @RemoteViews.RemoteView -public final class NotificationProgressBar extends ProgressBar { +public final class NotificationProgressBar extends ProgressBar implements + NotificationProgressDrawable.BoundsChangeListener { private static final String TAG = "NotificationProgressBar"; + private static final boolean DEBUG = false; private NotificationProgressDrawable mNotificationProgressDrawable; + private final Rect mProgressDrawableBounds = new Rect(); private NotificationProgressModel mProgressModel; @Nullable - private List<Part> mProgressDrawableParts = null; + private List<Part> mParts = null; + + // List of drawable parts before segment splitting by process. + @Nullable + private List<DrawablePart> mProgressDrawableParts = null; @Nullable private Drawable mTracker = null; + private boolean mHasTrackerIcon = false; /** @see R.styleable#NotificationProgressBar_trackerHeight */ private final int mTrackerHeight; @@ -76,7 +87,13 @@ public final class NotificationProgressBar extends ProgressBar { private final Matrix mMatrix = new Matrix(); private Matrix mTrackerDrawMatrix = null; - private float mScale = 0; + private float mProgressFraction = 0; + /** + * The location of progress on the stretched and rescaled progress bar, in fraction. Used for + * calculating the tracker position. If stretching and rescaling is not needed, == + * mProgressFraction. + */ + private float mAdjustedProgressFraction = 0; /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */ private boolean mTrackerPosIsDirty = false; @@ -96,20 +113,21 @@ public final class NotificationProgressBar extends ProgressBar { int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a, defStyleAttr, defStyleRes); try { mNotificationProgressDrawable = getNotificationProgressDrawable(); + mNotificationProgressDrawable.setBoundsChangeListener(this); } catch (IllegalStateException ex) { Log.e(TAG, "Can't get NotificationProgressDrawable", ex); } // Supports setting the tracker in xml, but ProgressStyle notifications set/override it - // via {@code setProgressTrackerIcon}. + // via {@code #setProgressTrackerIcon}. final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker); setTracker(tracker); @@ -126,8 +144,7 @@ public final class NotificationProgressBar extends ProgressBar { */ @RemotableViewMethod public void setProgressModel(@Nullable Bundle bundle) { - Preconditions.checkArgument(bundle != null, - "Bundle shouldn't be null"); + Preconditions.checkArgument(bundle != null, "Bundle shouldn't be null"); mProgressModel = NotificationProgressModel.fromBundle(bundle); final boolean isIndeterminate = mProgressModel.isIndeterminate(); @@ -137,20 +154,25 @@ public final class NotificationProgressBar extends ProgressBar { final int indeterminateColor = mProgressModel.getIndeterminateColor(); setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor)); } else { + // TODO: b/372908709 - maybe don't rerun the entire calculation every time the + // progress model is updated? For example, if the segments and parts aren't changed, + // there is no need to call `processAndConvertToViewParts` again. + final int progress = mProgressModel.getProgress(); final int progressMax = mProgressModel.getProgressMax(); - mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(), + + mParts = processAndConvertToViewParts(mProgressModel.getSegments(), mProgressModel.getPoints(), progress, - progressMax, - mProgressModel.isStyledByProgress()); - - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setParts(mProgressDrawableParts); - } + progressMax); setMax(progressMax); setProgress(progress); + + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0) { + updateDrawableParts(); + } } } @@ -200,9 +222,7 @@ public final class NotificationProgressBar extends ProgressBar { } else { progressTrackerDrawable = null; } - return () -> { - setTracker(progressTrackerDrawable); - }; + return () -> setTracker(progressTrackerDrawable); } private void setTracker(@Nullable Drawable tracker) { @@ -226,8 +246,14 @@ public final class NotificationProgressBar extends ProgressBar { final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker); mTracker = tracker; - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setHasTrackerIcon(mTracker != null); + final boolean hasTrackerIcon = (mTracker != null); + if (mHasTrackerIcon != hasTrackerIcon) { + mHasTrackerIcon = hasTrackerIcon; + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0 + && mProgressModel.isStyledByProgress()) { + updateDrawableParts(); + } } configureTrackerBounds(); @@ -293,6 +319,8 @@ public final class NotificationProgressBar extends ProgressBar { mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setProgress(int progress) { super.setProgress(progress); @@ -300,6 +328,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public void setProgress(int progress, boolean animate) { // Animation isn't supported by NotificationProgressBar. @@ -308,6 +338,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMin(int min) { super.setMin(min); @@ -315,6 +347,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMax(int max) { super.setMax(max); @@ -323,10 +357,10 @@ public final class NotificationProgressBar extends ProgressBar { } private void onMaybeVisualProgressChanged() { - float scale = getScale(); - if (mScale == scale) return; + float progressFraction = getProgressFraction(); + if (mProgressFraction == progressFraction) return; - mScale = scale; + mProgressFraction = progressFraction; mTrackerPosIsDirty = true; invalidate(); } @@ -350,8 +384,7 @@ public final class NotificationProgressBar extends ProgressBar { super.drawableStateChanged(); final Drawable tracker = mTracker; - if (tracker != null && tracker.isStateful() - && tracker.setState(getDrawableState())) { + if (tracker != null && tracker.isStateful() && tracker.setState(getDrawableState())) { invalidateDrawable(tracker); } } @@ -372,6 +405,65 @@ public final class NotificationProgressBar extends ProgressBar { updateTrackerAndBarPos(w, h); } + @Override + public void onDrawableBoundsChanged() { + final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds(); + + if (mProgressDrawableBounds.equals(progressDrawableBounds)) return; + + if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) { + updateDrawableParts(); + } + + mProgressDrawableBounds.set(progressDrawableBounds); + } + + private void updateDrawableParts() { + if (DEBUG) { + Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = " + + mNotificationProgressDrawable + ", mParts = " + mParts); + } + + if (mNotificationProgressDrawable == null) return; + if (mParts == null) return; + + final float width = mNotificationProgressDrawable.getBounds().width(); + if (width == 0) { + if (mProgressDrawableParts != null) { + if (DEBUG) { + Log.d(TAG, "Clearing mProgressDrawableParts"); + } + mProgressDrawableParts.clear(); + mNotificationProgressDrawable.setParts(mProgressDrawableParts); + } + return; + } + + mProgressDrawableParts = processAndConvertToDrawableParts( + mParts, + width, + mNotificationProgressDrawable.getSegSegGap(), + mNotificationProgressDrawable.getSegPointGap(), + mNotificationProgressDrawable.getPointRadius(), + mHasTrackerIcon + ); + Pair<List<DrawablePart>, Float> p = maybeStretchAndRescaleSegments( + mParts, + mProgressDrawableParts, + mNotificationProgressDrawable.getSegmentMinWidth(), + mNotificationProgressDrawable.getPointRadius(), + getProgressFraction(), + width, + mProgressModel.isStyledByProgress(), + mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap()); + + if (DEBUG) { + Log.d(TAG, "Updating NotificationProgressDrawable parts"); + } + mNotificationProgressDrawable.setParts(p.first); + mAdjustedProgressFraction = p.second / width; + } + private void updateTrackerAndBarPos(int w, int h) { final int paddedHeight = h - mPaddingTop - mPaddingBottom; final Drawable bar = getCurrentDrawable(); @@ -402,11 +494,11 @@ public final class NotificationProgressBar extends ProgressBar { } if (tracker != null) { - setTrackerPos(w, tracker, mScale, trackerOffsetY); + setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY); } } - private float getScale() { + private float getProgressFraction() { int min = getMin(); int max = getMax(); int range = max - min; @@ -416,19 +508,19 @@ public final class NotificationProgressBar extends ProgressBar { /** * Updates the tracker drawable bounds. * - * @param w Width of the view, including padding - * @param tracker Drawable used for the tracker - * @param scale Current progress between 0 and 1 - * @param offsetY Vertical offset for centering. If set to - * {@link Integer#MIN_VALUE}, the current offset will be used. + * @param w Width of the view, including padding + * @param tracker Drawable used for the tracker + * @param progressFraction Current progress between 0 and 1 + * @param offsetY Vertical offset for centering. If set to + * {@link Integer#MIN_VALUE}, the current offset will be used. */ - private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) { + private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) { int available = w - mPaddingLeft - mPaddingRight; final int trackerWidth = tracker.getIntrinsicWidth(); final int trackerHeight = tracker.getIntrinsicHeight(); available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth); - final int trackerPos = (int) (scale * available + 0.5f); + final int trackerPos = (int) (progressFraction * available + 0.5f); final int top, bottom; if (offsetY == Integer.MIN_VALUE) { @@ -448,8 +540,8 @@ public final class NotificationProgressBar extends ProgressBar { if (background != null) { final int bkgOffsetX = mPaddingLeft; final int bkgOffsetY = mPaddingTop; - background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, - right + bkgOffsetX, bottom + bkgOffsetY); + background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, right + bkgOffsetX, + bottom + bkgOffsetY); } // Canvas will be translated, so 0,0 is where we start drawing @@ -482,7 +574,7 @@ public final class NotificationProgressBar extends ProgressBar { if (mTracker == null) return; if (mTrackerPosIsDirty) { - setTrackerPos(getWidth(), mTracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE); } final int saveCount = canvas.save(); @@ -531,7 +623,7 @@ public final class NotificationProgressBar extends ProgressBar { final Drawable tracker = mTracker; if (tracker != null) { - setTrackerPos(getWidth(), tracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE); // Since we draw translated, the drawable's bounds that it signals // for invalidation won't be the actual bounds we want invalidated, @@ -541,16 +633,14 @@ public final class NotificationProgressBar extends ProgressBar { } /** - * Processes the ProgressStyle data and convert to list of {@code - * NotificationProgressDrawable.Part}. + * Processes the ProgressStyle data and convert to a list of {@code Part}. */ @VisibleForTesting - public static List<Part> processAndConvertToDrawableParts( + public static List<Part> processAndConvertToViewParts( List<ProgressStyle.Segment> segments, List<ProgressStyle.Point> points, int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { if (segments.isEmpty()) { throw new IllegalArgumentException("List of segments shouldn't be empty"); @@ -571,6 +661,7 @@ public final class NotificationProgressBar extends ProgressBar { if (progress < 0 || progress > progressMax) { throw new IllegalArgumentException("Invalid progress : " + progress); } + for (ProgressStyle.Point point : points) { final int pos = point.getPosition(); if (pos < 0 || pos > progressMax) { @@ -583,23 +674,21 @@ public final class NotificationProgressBar extends ProgressBar { final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap( points); final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap, - positionToPointMap, progress, isStyledByProgress); + positionToPointMap); - final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = - splitSegmentsByPointsAndProgress( - startToSegmentMap, sortedPos, progressMax); + final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = splitSegmentsByPoints( + startToSegmentMap, sortedPos, progressMax); - return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos, - progress, progressMax, - isStyledByProgress); + return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos, + progressMax); } // Any segment with a point on it gets split by the point. - // If isStyledByProgress is true, also split the segment with the progress value in its range. - private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress( + private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints( Map<Integer, ProgressStyle.Segment> startToSegmentMap, SortedSet<Integer> sortedPos, - int progressMax) { + int progressMax + ) { int prevSegStart = 0; for (Integer pos : sortedPos) { if (pos == 0 || pos == progressMax) continue; @@ -610,8 +699,7 @@ public final class NotificationProgressBar extends ProgressBar { final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart); final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment( - pos - prevSegStart).setColor( - prevSeg.getColor()); + pos - prevSegStart).setColor(prevSeg.getColor()); final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment( prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor()); @@ -624,32 +712,21 @@ public final class NotificationProgressBar extends ProgressBar { return startToSegmentMap; } - private static List<Part> convertToDrawableParts( + private static List<Part> convertToViewParts( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap, SortedSet<Integer> sortedPos, - int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { List<Part> parts = new ArrayList<>(); - boolean styleRemainingParts = false; for (Integer pos : sortedPos) { if (positionToPointMap.containsKey(pos)) { final ProgressStyle.Point point = positionToPointMap.get(pos); - final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts); - parts.add(new Point(null, color, styleRemainingParts)); - } - // We want the Point at the current progress to be filled (not faded), but a Segment - // starting at this progress to be faded. - if (isStyledByProgress && !styleRemainingParts && pos == progress) { - styleRemainingParts = true; + parts.add(new Point(point.getColor())); } if (startToSegmentMap.containsKey(pos)) { final ProgressStyle.Segment seg = startToSegmentMap.get(pos); - final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts); - parts.add(new Segment( - (float) seg.getLength() / progressMax, color, styleRemainingParts)); + parts.add(new Segment((float) seg.getLength() / progressMax, seg.getColor())); } } @@ -660,11 +737,24 @@ public final class NotificationProgressBar extends ProgressBar { private static int maybeGetFadedColor(@ColorInt int color, boolean fade) { if (!fade) return color; - return NotificationProgressDrawable.getFadedColor(color); + return getFadedColor(color); + } + + /** + * Get a color with an opacity that's 40% of the input color. + */ + @ColorInt + static int getFadedColor(@ColorInt int color) { + return Color.argb( + (int) (Color.alpha(color) * 0.4f + 0.5f), + Color.red(color), + Color.green(color), + Color.blue(color)); } private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap( - List<ProgressStyle.Segment> segments) { + List<ProgressStyle.Segment> segments + ) { final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>(); int currentStart = 0; // Initial start position is 0 @@ -681,7 +771,8 @@ public final class NotificationProgressBar extends ProgressBar { } private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap( - List<ProgressStyle.Point> points) { + List<ProgressStyle.Point> points + ) { final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>(); for (ProgressStyle.Point point : points) { @@ -693,14 +784,392 @@ public final class NotificationProgressBar extends ProgressBar { private static SortedSet<Integer> generateSortedPositionSet( Map<Integer, ProgressStyle.Segment> startToSegmentMap, - Map<Integer, ProgressStyle.Point> positionToPointMap, int progress, - boolean isStyledByProgress) { + Map<Integer, ProgressStyle.Point> positionToPointMap + ) { final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet()); sortedPos.addAll(positionToPointMap.keySet()); - if (isStyledByProgress) { - sortedPos.add(progress); - } return sortedPos; } + + /** + * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}. + */ + @VisibleForTesting + public static List<DrawablePart> processAndConvertToDrawableParts( + List<Part> parts, + float totalWidth, + float segSegGap, + float segPointGap, + float pointRadius, + boolean hasTrackerIcon + ) { + List<DrawablePart> drawableParts = new ArrayList<>(); + + // generally, we will start drawing at (x, y) and end at (x+w, y) + float x = (float) 0; + + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1); + final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1); + if (part instanceof Segment segment) { + final float segWidth = segment.mFraction * totalWidth; + // Advance the start position to account for a point immediately prior. + final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); + final float start = x + startOffset; + // Retract the end position to account for the padding and a point immediately + // after. + final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, + segSegGap, x + segWidth, totalWidth, hasTrackerIcon); + final float end = x + segWidth - endOffset; + + drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded)); + + segment.mStart = x; + segment.mEnd = x + segWidth; + + // Advance the current position to account for the segment's fraction of the total + // width (ignoring offset and padding) + x += segWidth; + } else if (part instanceof Point point) { + final float pointWidth = 2 * pointRadius; + float start = x - pointRadius; + if (start < 0) start = 0; + float end = start + pointWidth; + if (end > totalWidth) { + end = totalWidth; + if (totalWidth > pointWidth) start = totalWidth - pointWidth; + } + + drawableParts.add(new DrawablePoint(start, end, point.mColor)); + } + } + + return drawableParts; + } + + private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, + float startX) { + if (!(prevPart instanceof Point)) return 0F; + final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; + return pointOffset + pointRadius + segPointGap; + } + + private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, + float segPointGap, float segSegGap, float endX, float totalWidth, + boolean hasTrackerIcon) { + if (nextPart == null) return 0F; + if (nextPart instanceof Segment nextSeg) { + if (!seg.mFaded && nextSeg.mFaded) { + // @see Segment#mFaded + return hasTrackerIcon ? 0F : segSegGap; + } + return segSegGap; + } + + final float pointWidth = 2 * pointRadius; + final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) + ? (endX + pointRadius - totalWidth) : 0; + return segPointGap + pointRadius + pointOffset; + } + + /** + * Processes the list of {@code DrawablePart} data and convert to a pair of: + * - list of processed {@code DrawablePart}. + * - location of progress on the stretched and rescaled progress bar. + */ + @VisibleForTesting + public static Pair<List<DrawablePart>, Float> maybeStretchAndRescaleSegments( + List<Part> parts, + List<DrawablePart> drawableParts, + float segmentMinWidth, + float pointRadius, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + final List<DrawableSegment> drawableSegments = drawableParts + .stream() + .filter(DrawableSegment.class::isInstance) + .map(DrawableSegment.class::cast) + .toList(); + float totalExcessWidth = 0; + float totalPositiveExcessWidth = 0; + for (DrawableSegment drawableSegment : drawableSegments) { + final float excessWidth = drawableSegment.getWidth() - segmentMinWidth; + totalExcessWidth += excessWidth; + if (excessWidth > 0) totalPositiveExcessWidth += excessWidth; + } + + // All drawable segments are above minimum width. No need to stretch and rescale. + if (totalExcessWidth == totalPositiveExcessWidth) { + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + if (totalExcessWidth < 0) { + // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback + // option. (instead of return.) + Log.w(TAG, "Not enough width to satisfy the minimum width for segments."); + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + final int nParts = drawableParts.size(); + float startOffset = 0; + for (int iPart = 0; iPart < nParts; iPart++) { + final DrawablePart drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof DrawableSegment drawableSegment) { + final float origDrawableSegmentWidth = drawableSegment.getWidth(); + + float drawableSegmentWidth = segmentMinWidth; + // Allocate the totalExcessWidth to the segments above minimum, proportionally to + // their initial excessWidth. + if (origDrawableSegmentWidth > segmentMinWidth) { + drawableSegmentWidth += + totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth) + / totalPositiveExcessWidth; + } + + final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth(); + + // Adjust drawable segments to new widths + drawableSegment.setStart(drawableSegment.getStart() + startOffset); + drawableSegment.setEnd( + drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff); + + // Also adjust view segments to new width. (For view segments, only start is + // needed?) + // Check that segments and drawableSegments are of the same size? + final Segment segment = (Segment) parts.get(iPart); + final float origSegmentWidth = segment.getWidth(); + segment.mStart = segment.mStart + startOffset; + segment.mEnd = segment.mStart + origSegmentWidth + widthDiff; + + // Increase startOffset for the subsequent segments. + startOffset += widthDiff; + } else if (drawablePart instanceof DrawablePoint drawablePoint) { + drawablePoint.setStart(drawablePoint.getStart() + startOffset); + drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius); + } + } + + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + /** + * Find the location of progress on the stretched and rescaled progress bar. + * If isStyledByProgress is true, also split the drawable segment with the progress value in its + * range. Style the drawable parts after process with reduced opacity and segment height. + */ + private static Pair<List<DrawablePart>, Float> maybeSplitDrawableSegmentsByProgress( + // Needed to get the original segment start and end positions in pixels. + List<Part> parts, + List<DrawablePart> drawableParts, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth); + + int iPartFirstSegmentToStyle = -1; + int iPartSegmentToSplit = -1; + float rescaledProgressX = 0; + float startFraction = 0; + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + if (!(part instanceof Segment)) continue; + final Segment segment = (Segment) part; + if (startFraction == progressFraction) { + iPartFirstSegmentToStyle = iPart; + rescaledProgressX = segment.mStart; + break; + } else if (startFraction < progressFraction + && progressFraction < startFraction + segment.mFraction) { + iPartSegmentToSplit = iPart; + rescaledProgressX = segment.mStart + + (progressFraction - startFraction) / segment.mFraction + * segment.getWidth(); + break; + } + startFraction += segment.mFraction; + } + + if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX); + + List<DrawablePart> splitDrawableParts = new ArrayList<>(); + boolean styleRemainingParts = false; + for (int iPart = 0; iPart < nParts; iPart++) { + final DrawablePart drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof DrawablePoint drawablePoint) { + final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts); + splitDrawableParts.add( + new DrawablePoint(drawablePoint.getStart(), drawablePoint.getEnd(), color)); + } + if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true; + if (drawablePart instanceof DrawableSegment drawableSegment) { + if (iPart == iPartSegmentToSplit) { + if (rescaledProgressX <= drawableSegment.getStart()) { + styleRemainingParts = true; + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), color, true)); + } else if (drawableSegment.getStart() < rescaledProgressX + && rescaledProgressX < drawableSegment.getEnd()) { + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + rescaledProgressX - progressGap, drawableSegment.getColor())); + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add( + new DrawableSegment(rescaledProgressX, drawableSegment.getEnd(), + color, true)); + styleRemainingParts = true; + } else { + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), drawableSegment.getColor())); + styleRemainingParts = true; + } + } else { + final int color = maybeGetFadedColor(drawableSegment.getColor(), + styleRemainingParts); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), color, styleRemainingParts)); + } + } + } + + return new Pair<>(splitDrawableParts, rescaledProgressX); + } + + /** + * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a + * {@link Point} with zero length. + */ + // TODO: b/372908709 - maybe this should be made private? Only test the final + // NotificationDrawable.Parts. + public interface Part { + } + + /** + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + */ + public static final class Segment implements Part { + private final float mFraction; + @ColorInt + private final int mColor; + /** + * Whether the segment is faded or not. + * <p> + * <pre> + * When mFaded is set to true, a combination of the following is done to the segment: + * 1. The drawing color is mColor with opacity updated to 40%. + * 2. The gap between faded and non-faded segments is: + * - the segment-segment gap, when there is no tracker icon + * - 0, when there is tracker icon + * </pre> + * </p> + */ + private final boolean mFaded; + + /** Start position (in pixels) */ + private float mStart; + /** End position (in pixels */ + private float mEnd; + + public Segment(float fraction, @ColorInt int color) { + this(fraction, color, false); + } + + public Segment(float fraction, @ColorInt int color, boolean faded) { + mFraction = fraction; + mColor = color; + mFaded = faded; + } + + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; + } + + @Override + public String toString() { + return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" + + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd; + } + + // Needed for unit tests + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Segment that = (Segment) other; + if (Float.compare(this.mFraction, that.mFraction) != 0) return false; + if (this.mColor != that.mColor) return false; + return this.mFaded == that.mFaded; + } + + @Override + public int hashCode() { + return Objects.hash(mFraction, mColor, mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class Point implements Part { + @ColorInt + private final int mColor; + + public Point(@ColorInt int color) { + mColor = color; + } + + @Override + public String toString() { + return "Point(color=" + this.mColor + ")"; + } + + // Needed for unit tests. + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Point that = (Point) other; + + return this.mColor == that.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mColor); + } + } } diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java index 8629a1c95202..4ece81c24edc 100644 --- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java +++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java @@ -21,7 +21,6 @@ import android.content.res.Resources; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -49,22 +48,24 @@ import java.util.Objects; /** * This is used by NotificationProgressBar for displaying a custom background. It composes of - * segments, which have non-zero length, and points, which have zero length. + * segments, which have non-zero length varying drawing width, and points, which have zero length + * and fixed size for drawing. * - * @see Segment - * @see Point + * @see DrawableSegment + * @see DrawablePoint */ public final class NotificationProgressDrawable extends Drawable { private static final String TAG = "NotifProgressDrawable"; + @Nullable + private BoundsChangeListener mBoundsChangeListener = null; + private State mState; private boolean mMutated; - private final ArrayList<Part> mParts = new ArrayList<>(); - private boolean mHasTrackerIcon; + private final ArrayList<DrawablePart> mParts = new ArrayList<>(); private final RectF mSegRectF = new RectF(); - private final Rect mPointRect = new Rect(); private final RectF mPointRectF = new RectF(); private final Paint mFillPaint = new Paint(); @@ -80,33 +81,37 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * <p>Set the segment default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the stroke - * @see #mutate() + * Returns the gap between two segments. */ - public void setSegmentDefaultColor(@ColorInt int color) { - mState.setSegmentColor(color); + public float getSegSegGap() { + return mState.mSegSegGap; } /** - * <p>Set the point rect default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the point rect - * @see #mutate() + * Returns the gap between a segment and a point. + */ + public float getSegPointGap() { + return mState.mSegPointGap; + } + + /** + * Returns the gap between a segment and a point. */ - public void setPointRectDefaultColor(@ColorInt int color) { - mState.setPointRectColor(color); + public float getSegmentMinWidth() { + return mState.mSegmentMinWidth; + } + + /** + * Returns the radius for the points. + */ + public float getPointRadius() { + return mState.mPointRadius; } /** * Set the segments and points that constitute the drawable. */ - public void setParts(List<Part> parts) { + public void setParts(List<DrawablePart> parts) { mParts.clear(); mParts.addAll(parts); @@ -116,51 +121,22 @@ public final class NotificationProgressDrawable extends Drawable { /** * Set the segments and points that constitute the drawable. */ - public void setParts(@NonNull Part... parts) { + public void setParts(@NonNull DrawablePart... parts) { setParts(Arrays.asList(parts)); } - /** - * Set whether a tracker is drawn on top of this NotificationProgressDrawable. - */ - public void setHasTrackerIcon(boolean hasTrackerIcon) { - if (mHasTrackerIcon != hasTrackerIcon) { - mHasTrackerIcon = hasTrackerIcon; - invalidateSelf(); - } - } - @Override public void draw(@NonNull Canvas canvas) { - final float pointRadius = - mState.mPointRadius; // how big the point icon will be, halved - - // generally, we will start drawing at (x, y) and end at (x+w, y) - float x = (float) getBounds().left; + final float pointRadius = mState.mPointRadius; + final float left = (float) getBounds().left; final float centerY = (float) getBounds().centerY(); - final float totalWidth = (float) getBounds().width(); - float segPointGap = mState.mSegPointGap; final int numParts = mParts.size(); for (int iPart = 0; iPart < numParts; iPart++) { - final Part part = mParts.get(iPart); - final Part prevPart = iPart == 0 ? null : mParts.get(iPart - 1); - final Part nextPart = iPart + 1 == numParts ? null : mParts.get(iPart + 1); - if (part instanceof Segment segment) { - final float segWidth = segment.mFraction * totalWidth; - // Advance the start position to account for a point immediately prior. - final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); - final float start = x + startOffset; - // Retract the end position to account for the padding and a point immediately - // after. - final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, - mState.mSegSegGap, x + segWidth, totalWidth, mHasTrackerIcon); - final float end = x + segWidth - endOffset; - - // Advance the current position to account for the segment's fraction of the total - // width (ignoring offset and padding) - x += segWidth; - + final DrawablePart part = mParts.get(iPart); + final float start = left + part.mStart; + final float end = left + part.mEnd; + if (part instanceof DrawableSegment segment) { // No space left to draw the segment if (start > end) continue; @@ -168,67 +144,23 @@ public final class NotificationProgressDrawable extends Drawable { : mState.mSegmentHeight / 2F; final float cornerRadius = mState.mSegmentCornerRadius; - mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor - : (segment.mFaded ? mState.mFadedSegmentColor : mState.mSegmentColor)); + mFillPaint.setColor(segment.mColor); mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY); canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint); - } else if (part instanceof Point point) { - final float pointWidth = 2 * pointRadius; - float start = x - pointRadius; - if (start < 0) start = 0; - float end = start + pointWidth; - if (end > totalWidth) { - end = totalWidth; - if (totalWidth > pointWidth) start = totalWidth - pointWidth; - } - mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end, - (int) (centerY + pointRadius)); - - if (point.mIcon != null) { - point.mIcon.setBounds(mPointRect); - point.mIcon.draw(canvas); - } else { - // TODO: b/367804171 - actually use a vector asset for the default point - // rather than drawing it as a box? - mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); - final float inset = mState.mPointRectInset; - final float cornerRadius = mState.mPointRectCornerRadius; - mPointRectF.inset(inset, inset); - - mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor - : (point.mFaded ? mState.mFadedPointRectColor - : mState.mPointRectColor)); - - canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); - } - } - } - } + } else if (part instanceof DrawablePoint point) { + // TODO: b/367804171 - actually use a vector asset for the default point + // rather than drawing it as a box? + mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); + final float inset = mState.mPointRectInset; + final float cornerRadius = mState.mPointRectCornerRadius; + mPointRectF.inset(inset, inset); - private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, - float startX) { - if (!(prevPart instanceof Point)) return 0F; - final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; - return pointOffset + pointRadius + segPointGap; - } + mFillPaint.setColor(point.mColor); - private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, - float segPointGap, - float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) { - if (nextPart == null) return 0F; - if (nextPart instanceof Segment nextSeg) { - if (!seg.mFaded && nextSeg.mFaded) { - // @see Segment#mFaded - return hasTrackerIcon ? 0F : segSegGap; + canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); } - return segSegGap; } - - final float pointWidth = 2 * pointRadius; - final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) - ? (endX + pointRadius - totalWidth) : 0; - return segPointGap + pointRadius + pointOffset; } @Override @@ -260,6 +192,19 @@ public final class NotificationProgressDrawable extends Drawable { return PixelFormat.UNKNOWN; } + public void setBoundsChangeListener(BoundsChangeListener listener) { + mBoundsChangeListener = listener; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + if (mBoundsChangeListener != null) { + mBoundsChangeListener.onDrawableBoundsChanged(); + } + } + @Override public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) @@ -384,6 +329,8 @@ public final class NotificationProgressDrawable extends Drawable { // Extract the theme attributes, if any. state.mThemeAttrsSegments = a.extractThemeAttrs(); + state.mSegmentMinWidth = a.getDimension( + R.styleable.NotificationProgressDrawableSegments_minWidth, state.mSegmentMinWidth); state.mSegmentHeight = a.getDimension( R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight); state.mFadedSegmentHeight = a.getDimension( @@ -392,9 +339,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mSegmentCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawableSegments_cornerRadius, state.mSegmentCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawableSegments_color, - state.mSegmentColor); - setSegmentDefaultColor(color); } private void updatePointsFromTypedArray(TypedArray a) { @@ -413,9 +357,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mPointRectCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawablePoints_cornerRadius, state.mPointRectCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color, - state.mPointRectColor); - setPointRectDefaultColor(color); } static int resolveDensity(@Nullable Resources r, int parentDensity) { @@ -464,63 +405,57 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * A part of the progress bar, which is either a S{@link Segment} with non-zero length, or a - * {@link Point} with zero length. + * Listener to receive updates about drawable bounds changing */ - public interface Part { + public interface BoundsChangeListener { + /** Called when bounds have changed */ + void onDrawableBoundsChanged(); } /** - * A segment is a part of the progress bar with non-zero length. For example, it can - * represent a portion in a navigation journey with certain traffic condition. - * + * A part of the progress drawable, which is either a {@link DrawableSegment} with non-zero + * length and varying drawing width, or a {@link DrawablePoint} with zero length and fixed size + * for drawing. */ - public static final class Segment implements Part { - private final float mFraction; - @ColorInt private final int mColor; - /** Whether the segment is faded or not. - * <p> - * <pre> - * When mFaded is set to true, a combination of the following is done to the segment: - * 1. The drawing color is mColor with opacity updated to 40%. - * 2. The gap between faded and non-faded segments is: - * - the segment-segment gap, when there is no tracker icon - * - 0, when there is tracker icon - * </pre> - * </p> - */ - private final boolean mFaded; - - public Segment(float fraction) { - this(fraction, Color.TRANSPARENT); + public abstract static class DrawablePart { + // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the + // bounds rect. + /** Start position for drawing (in pixels) */ + protected float mStart; + /** End position for drawing (in pixels) */ + protected float mEnd; + /** Drawing color. */ + @ColorInt protected final int mColor; + + protected DrawablePart(float start, float end, @ColorInt int color) { + mStart = start; + mEnd = end; + mColor = color; } - public Segment(float fraction, @ColorInt int color) { - this(fraction, color, false); + public float getStart() { + return this.mStart; } - public Segment(float fraction, @ColorInt int color, boolean faded) { - mFraction = fraction; - mColor = color; - mFaded = faded; + public void setStart(float start) { + mStart = start; } - public float getFraction() { - return this.mFraction; + public float getEnd() { + return this.mEnd; } - public int getColor() { - return this.mColor; + public void setEnd(float end) { + mEnd = end; } - public boolean getFaded() { - return this.mFaded; + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; } - @Override - public String toString() { - return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" - + this.mFaded + ')'; + public int getColor() { + return this.mColor; } // Needed for unit tests @@ -530,80 +465,79 @@ public final class NotificationProgressDrawable extends Drawable { if (other == null || getClass() != other.getClass()) return false; - Segment that = (Segment) other; - if (Float.compare(this.mFraction, that.mFraction) != 0) return false; - if (this.mColor != that.mColor) return false; - return this.mFaded == that.mFaded; + DrawablePart that = (DrawablePart) other; + if (Float.compare(this.mStart, that.mStart) != 0) return false; + if (Float.compare(this.mEnd, that.mEnd) != 0) return false; + return this.mColor == that.mColor; } @Override public int hashCode() { - return Objects.hash(mFraction, mColor, mFaded); + return Objects.hash(mStart, mEnd, mColor); } } /** - * A point is a part of the progress bar with zero length. Points are designated points within a - * progressbar to visualize distinct stages or milestones. For example, a stop in a multi-stop - * ride-share journey. + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + * <p> + * The start and end positions for drawing a segment are assumed to have been adjusted for + * the Points and gaps neighboring the segment. + * </p> */ - public static final class Point implements Part { - @Nullable - private final Drawable mIcon; - @ColorInt private final int mColor; + public static final class DrawableSegment extends DrawablePart { + /** + * Whether the segment is faded or not. + * <p> + * Faded segments and non-faded segments are drawn with different heights. + * </p> + */ private final boolean mFaded; - public Point(@Nullable Drawable icon) { - this(icon, Color.TRANSPARENT, false); - } - - public Point(@Nullable Drawable icon, @ColorInt int color) { - this(icon, color, false); - + public DrawableSegment(float start, float end, int color) { + this(start, end, color, false); } - public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) { - mIcon = icon; - mColor = color; + public DrawableSegment(float start, float end, int color, boolean faded) { + super(start, end, color); mFaded = faded; } - @Nullable - public Drawable getIcon() { - return this.mIcon; - } - - public int getColor() { - return this.mColor; - } - - public boolean getFaded() { - return this.mFaded; - } - @Override public String toString() { - return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded - + ")"; + return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ", faded=" + this.mFaded + ')'; } // Needed for unit tests. @Override public boolean equals(@Nullable Object other) { - if (this == other) return true; - - if (other == null || getClass() != other.getClass()) return false; + if (!super.equals(other)) return false; - Point that = (Point) other; - - if (!Objects.equals(this.mIcon, that.mIcon)) return false; - if (this.mColor != that.mColor) return false; + DrawableSegment that = (DrawableSegment) other; return this.mFaded == that.mFaded; } @Override public int hashCode() { - return Objects.hash(mIcon, mColor, mFaded); + return Objects.hash(super.hashCode(), mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class DrawablePoint extends DrawablePart { + public DrawablePoint(float start, float end, int color) { + super(start, end, color); + } + + @Override + public String toString() { + return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ")"; } } @@ -628,16 +562,14 @@ public final class NotificationProgressDrawable extends Drawable { int mChangingConfigurations; float mSegSegGap = 0.0f; float mSegPointGap = 0.0f; + float mSegmentMinWidth = 0.0f; float mSegmentHeight; float mFadedSegmentHeight; float mSegmentCornerRadius; - int mSegmentColor; - int mFadedSegmentColor; + // how big the point icon will be, halved float mPointRadius; float mPointRectInset; float mPointRectCornerRadius; - int mPointRectColor; - int mFadedPointRectColor; int[] mThemeAttrs; int[] mThemeAttrsSegments; @@ -652,16 +584,13 @@ public final class NotificationProgressDrawable extends Drawable { mChangingConfigurations = orig.mChangingConfigurations; mSegSegGap = orig.mSegSegGap; mSegPointGap = orig.mSegPointGap; + mSegmentMinWidth = orig.mSegmentMinWidth; mSegmentHeight = orig.mSegmentHeight; mFadedSegmentHeight = orig.mFadedSegmentHeight; mSegmentCornerRadius = orig.mSegmentCornerRadius; - mSegmentColor = orig.mSegmentColor; - mFadedSegmentColor = orig.mFadedSegmentColor; mPointRadius = orig.mPointRadius; mPointRectInset = orig.mPointRectInset; mPointRectCornerRadius = orig.mPointRectCornerRadius; - mPointRectColor = orig.mPointRectColor; - mFadedPointRectColor = orig.mFadedPointRectColor; mThemeAttrs = orig.mThemeAttrs; mThemeAttrsSegments = orig.mThemeAttrsSegments; @@ -674,6 +603,18 @@ public final class NotificationProgressDrawable extends Drawable { } private void applyDensityScaling(int sourceDensity, int targetDensity) { + if (mSegSegGap > 0) { + mSegSegGap = scaleFromDensity( + mSegSegGap, sourceDensity, targetDensity); + } + if (mSegPointGap > 0) { + mSegPointGap = scaleFromDensity( + mSegPointGap, sourceDensity, targetDensity); + } + if (mSegmentMinWidth > 0) { + mSegmentMinWidth = scaleFromDensity( + mSegmentMinWidth, sourceDensity, targetDensity); + } if (mSegmentHeight > 0) { mSegmentHeight = scaleFromDensity( mSegmentHeight, sourceDensity, targetDensity); @@ -740,28 +681,6 @@ public final class NotificationProgressDrawable extends Drawable { applyDensityScaling(sourceDensity, targetDensity); } } - - public void setSegmentColor(int color) { - mSegmentColor = color; - mFadedSegmentColor = getFadedColor(color); - } - - public void setPointRectColor(int color) { - mPointRectColor = color; - mFadedPointRectColor = getFadedColor(color); - } - } - - /** - * Get a color with an opacity that's 25% of the input color. - */ - @ColorInt - static int getFadedColor(@ColorInt int color) { - return Color.argb( - (int) (Color.alpha(color) * 0.4f + 0.5f), - Color.red(color), - Color.green(color), - Color.blue(color)); } @Override diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp index e6364a96bd9f..1e7bfe32ba79 100644 --- a/core/jni/android_content_res_ApkAssets.cpp +++ b/core/jni/android_content_res_ApkAssets.cpp @@ -111,8 +111,9 @@ static void DeleteGuardedApkAssets(Guarded<AssetManager2::ApkAssetsPtr>& apk_ass class LoaderAssetsProvider : public AssetsProvider { public: static std::unique_ptr<AssetsProvider> Create(JNIEnv* env, jobject assets_provider) { - return std::unique_ptr<AssetsProvider>{ - assets_provider ? new LoaderAssetsProvider(env, assets_provider) : nullptr}; + return (!assets_provider) ? EmptyAssetsProvider::Create() + : std::unique_ptr<AssetsProvider>(new LoaderAssetsProvider( + env, assets_provider)); } bool ForEachFile(const std::string& /* root_path */, @@ -128,8 +129,8 @@ class LoaderAssetsProvider : public AssetsProvider { return debug_name_; } - UpToDate IsUpToDate() const override { - return UpToDate::Always; + bool IsUpToDate() const override { + return true; } ~LoaderAssetsProvider() override { @@ -211,7 +212,7 @@ class LoaderAssetsProvider : public AssetsProvider { auto string_result = static_cast<jstring>(env->CallObjectMethod( assets_provider_, gAssetsProviderOffsets.toString)); ScopedUtfChars str(env, string_result); - debug_name_ = std::string(str.c_str()); + debug_name_ = std::string(str.c_str(), str.size()); } // The global reference to the AssetsProvider @@ -242,10 +243,10 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma apk_assets = ApkAssets::LoadOverlay(path.c_str(), property_flags); break; case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), - MultiAssetsProvider::Create(std::move(loader_assets)), - property_flags); - break; + apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), + std::move(loader_assets), + property_flags); + break; case FORMAT_DIRECTORY: { auto assets = MultiAssetsProvider::Create(std::move(loader_assets), DirectoryAssetsProvider::Create(path.c_str())); @@ -315,11 +316,10 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t break; } case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), - nullptr /* path */), - MultiAssetsProvider::Create(std::move(loader_assets)), - property_flags); - break; + apk_assets = ApkAssets::LoadTable( + AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */), + std::move(loader_assets), property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -386,15 +386,12 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ break; } case FORMAT_ARSC: - apk_assets = - ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), - nullptr /* path */, - static_cast<off64_t>(offset), - static_cast<off64_t>( - length)), - MultiAssetsProvider::Create(std::move(loader_assets)), - property_flags); - break; + apk_assets = ApkAssets::LoadTable( + AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */, + static_cast<off64_t>(offset), + static_cast<off64_t>(length)), + std::move(loader_assets), property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -411,16 +408,13 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ } static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jint flags, jobject assets_provider) { - auto apk_assets = ApkAssets::Load(MultiAssetsProvider::Create( - LoaderAssetsProvider::Create(env, assets_provider)), - flags); - if (apk_assets == nullptr) { - const std::string error_msg = - base::StringPrintf("Failed to load empty assets with provider %p", - (void*)assets_provider); - jniThrowException(env, "java/io/IOException", error_msg.c_str()); - return 0; - } + auto apk_assets = ApkAssets::Load(LoaderAssetsProvider::Create(env, assets_provider), flags); + if (apk_assets == nullptr) { + const std::string error_msg = + base::StringPrintf("Failed to load empty assets with provider %p", (void*)assets_provider); + jniThrowException(env, "java/io/IOException", error_msg.c_str()); + return 0; + } return CreateGuardedApkAssets(std::move(apk_assets)); } @@ -449,10 +443,10 @@ static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool()); } -static jint NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { +static jboolean NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { auto scoped_apk_assets = ScopedLock(ApkAssetsFromLong(ptr)); auto apk_assets = scoped_apk_assets->get(); - return (jint)apk_assets->IsUpToDate(); + return apk_assets->IsUpToDate() ? JNI_TRUE : JNI_FALSE; } static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) { @@ -564,7 +558,7 @@ static const JNINativeMethod gApkAssetsMethods[] = { {"nativeGetDebugName", "(J)Ljava/lang/String;", (void*)NativeGetDebugName}, {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock}, // @CriticalNative - {"nativeIsUpToDate", "(J)I", (void*)NativeIsUpToDate}, + {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate}, {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml}, {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;", (void*)NativeGetOverlayableInfo}, diff --git a/core/jni/android_hardware_UsbDeviceConnection.cpp b/core/jni/android_hardware_UsbDeviceConnection.cpp index b1221ee38db3..68ef3d424d12 100644 --- a/core/jni/android_hardware_UsbDeviceConnection.cpp +++ b/core/jni/android_hardware_UsbDeviceConnection.cpp @@ -165,19 +165,25 @@ android_hardware_UsbDeviceConnection_control_request(JNIEnv *env, jobject thiz, return -1; } - jbyte* bufferBytes = NULL; - if (buffer) { - bufferBytes = (jbyte*)env->GetPrimitiveArrayCritical(buffer, NULL); + bool is_dir_in = (requestType & USB_ENDPOINT_DIR_MASK) == USB_DIR_IN; + std::unique_ptr<jbyte[]> bufferBytes(new (std::nothrow) jbyte[length]); + if (!bufferBytes) { + jniThrowException(env, "java/lang/OutOfMemoryError", NULL); + return -1; + } + + if (!is_dir_in && buffer) { + env->GetByteArrayRegion(buffer, start, length, bufferBytes.get()); } - jint result = usb_device_control_transfer(device, requestType, request, - value, index, bufferBytes + start, length, timeout); + jint bytes_transferred = usb_device_control_transfer(device, requestType, request, + value, index, bufferBytes.get(), length, timeout); - if (bufferBytes) { - env->ReleasePrimitiveArrayCritical(buffer, bufferBytes, 0); + if (bytes_transferred > 0 && is_dir_in) { + env->SetByteArrayRegion(buffer, start, bytes_transferred, bufferBytes.get()); } - return result; + return bytes_transferred; } static jint diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index 3c2dccd451d4..9ef17e82c38e 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -729,6 +729,17 @@ static jlong android_os_Debug_getGpuPrivateMemoryKb(JNIEnv* env, jobject clazz) return gpuPrivateMem / 1024; } +static jlong android_os_Debug_getKernelCmaUsageKb(JNIEnv* env, jobject clazz) { + jlong totalKernelCmaUsageKb = -1; + uint64_t size; + + if (meminfo::ReadKernelCmaUsageKb(&size)) { + totalKernelCmaUsageKb = size; + } + + return totalKernelCmaUsageKb; +} + static jlong android_os_Debug_getDmabufMappedSizeKb(JNIEnv* env, jobject clazz) { jlong dmabufPss = 0; std::vector<dmabufinfo::DmaBuffer> dmabufs; @@ -836,6 +847,7 @@ static const JNINativeMethod gMethods[] = { {"getGpuTotalUsageKb", "()J", (void*)android_os_Debug_getGpuTotalUsageKb}, {"isVmapStack", "()Z", (void*)android_os_Debug_isVmapStack}, {"logAllocatorStats", "()Z", (void*)android_os_Debug_logAllocatorStats}, + {"getKernelCmaUsageKb", "()J", (void*)android_os_Debug_getKernelCmaUsageKb}, }; int register_android_os_Debug(JNIEnv *env) diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 4f7ba9388a1d..5d0b340ac839 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -580,6 +580,7 @@ message SecureSettingsProto { optional SettingProto activate_on_dock = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto activate_on_sleep = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto default_component = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto activate_on_postured = 6 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Screensaver screensaver = 47; diff --git a/core/res/res/drawable/notification_progress.xml b/core/res/res/drawable/notification_progress.xml index 5d272fb00e34..ff5450ee106f 100644 --- a/core/res/res/drawable/notification_progress.xml +++ b/core/res/res/drawable/notification_progress.xml @@ -24,6 +24,7 @@ android:segPointGap="@dimen/notification_progress_segPoint_gap"> <segments android:color="?attr/colorProgressBackgroundNormal" + android:minWidth="@dimen/notification_progress_segments_min_width" android:height="@dimen/notification_progress_segments_height" android:fadedHeight="@dimen/notification_progress_segments_faded_height" android:cornerRadius="@dimen/notification_progress_segments_corner_radius"/> diff --git a/core/res/res/values-round-watch/dimens.xml b/core/res/res/values-round-watch/dimens.xml index f288b41fb556..59ee554798bc 100644 --- a/core/res/res/values-round-watch/dimens.xml +++ b/core/res/res/values-round-watch/dimens.xml @@ -26,6 +26,6 @@ <item name="input_extract_action_button_height" type="dimen">32dp</item> <item name="input_extract_action_icon_padding" type="dimen">5dp</item> - <item name="global_actions_vertical_padding_percentage" type="fraction">20.8%</item> + <item name="global_actions_vertical_padding_percentage" type="fraction">21.8%</item> <item name="global_actions_horizontal_padding_percentage" type="fraction">5.2%</item> </resources> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 728c856f5855..8372aecf0d27 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -7572,25 +7572,31 @@ <!-- NotificationProgressDrawable class --> <!-- ================================== --> - <!-- Drawable used to render a segmented bar, with segments and points. --> + <!-- Drawable used to render a notification progress bar, with segments and points. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawable"> - <!-- Default color for the parts. --> + <!-- The gap between two segments. --> <attr name="segSegGap" format="dimension" /> + <!-- The gap between a segment and a point. --> <attr name="segPointGap" format="dimension" /> </declare-styleable> <!-- Used to config the segments of a NotificationProgressDrawable. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawableSegments"> - <!-- Height of the solid segments --> + <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only + place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap + above. --> + <!-- Minimum required drawing width. The drawing width refers to the width after + the original segments have been adjusted for the neighboring Points and gaps. This is + enforced by stretching the segments that are too short. --> + <attr name="minWidth" format="dimension" /> + <!-- Height of the solid segments. --> <attr name="height" /> - <!-- Height of the faded segments --> - <attr name="fadedHeight" format="dimension"/> + <!-- Height of the faded segments. --> + <attr name="fadedHeight" format="dimension" /> <!-- Corner radius of the segment rect. --> <attr name="cornerRadius" format="dimension" /> - <!-- Default color of the segment. --> - <attr name="color" /> </declare-styleable> <!-- Used to config the points of a NotificationProgressDrawable. --> @@ -7602,8 +7608,6 @@ <attr name="inset" /> <!-- Corner radius of the point rect. --> <attr name="cornerRadius"/> - <!-- Default color of the point rect. --> - <attr name="color" /> </declare-styleable> <!-- ========================== --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index ce9a0c636d62..e14cffd72b0c 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2766,6 +2766,9 @@ <bool name="config_dreamsActivatedOnDockByDefault">true</bool> <!-- If supported and enabled, are dreams activated when asleep and charging? (by default) --> <bool name="config_dreamsActivatedOnSleepByDefault">false</bool> + <!-- If supported and enabled, are dreams enabled while device is stationary and upright? + (by default) --> + <bool name="config_dreamsActivatedOnPosturedByDefault">false</bool> <!-- ComponentName of the default dream (Settings.Secure.DEFAULT_SCREENSAVER_COMPONENT) --> <string name="config_dreamsDefaultComponent" translatable="false">com.android.deskclock/com.android.deskclock.Screensaver</string> <!-- ComponentNames of the dreams that we should hide --> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 4f7351c7cc4d..d6b8704a978b 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -899,6 +899,8 @@ <dimen name="notification_progress_segSeg_gap">4dp</dimen> <!-- The gap between a segment and a point in the notification progress bar --> <dimen name="notification_progress_segPoint_gap">4dp</dimen> + <!-- The minimum required drawing width of the notification progress bar segments --> + <dimen name="notification_progress_segments_min_width">16dp</dimen> <!-- The height of the notification progress bar segments --> <dimen name="notification_progress_segments_height">6dp</dimen> <!-- The height of the notification progress bar faded segments --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f89ca44cce30..5f6619d4e4cc 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2330,6 +2330,7 @@ <java-symbol type="bool" name="config_dreamsEnabledOnBattery" /> <java-symbol type="bool" name="config_dreamsActivatedOnDockByDefault" /> <java-symbol type="bool" name="config_dreamsActivatedOnSleepByDefault" /> + <java-symbol type="bool" name="config_dreamsActivatedOnPosturedByDefault" /> <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenNotPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" /> @@ -3950,6 +3951,7 @@ <java-symbol type="dimen" name="notification_progress_tracker_height" /> <java-symbol type="dimen" name="notification_progress_segSeg_gap" /> <java-symbol type="dimen" name="notification_progress_segPoint_gap" /> + <java-symbol type="dimen" name="notification_progress_segments_min_width" /> <java-symbol type="dimen" name="notification_progress_segments_height" /> <java-symbol type="dimen" name="notification_progress_segments_faded_height" /> <java-symbol type="dimen" name="notification_progress_segments_corner_radius" /> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index ca6ad6fae46e..7be6950fb613 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -2504,6 +2504,21 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressSegments() { + final List<Notification.ProgressStyle.Segment> segments = List.of( + new Notification.ProgressStyle.Segment(100).setColor(Color.WHITE), + new Notification.ProgressStyle.Segment(50).setColor(Color.RED), + new Notification.ProgressStyle.Segment(50).setColor(Color.BLUE) + ); + + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressSegments(segments); + + assertThat(progressStyle1.getProgressSegments()).isEqualTo(segments); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_addProgressPoint_dropsNegativePoints() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2532,6 +2547,21 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressPoints() { + final List<Notification.ProgressStyle.Point> points = List.of( + new Notification.ProgressStyle.Point(0).setColor(Color.WHITE), + new Notification.ProgressStyle.Point(50).setColor(Color.RED), + new Notification.ProgressStyle.Point(100).setColor(Color.BLUE) + ); + + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressPoints(points); + + assertThat(progressStyle1.getProgressPoints()).isEqualTo(points); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_createProgressModel_ignoresPointsExceedingMax() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2673,11 +2703,58 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressIndeterminate() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressIndeterminate(true); + assertThat(progressStyle1.isProgressIndeterminate()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_styledByProgress_defaultValueTrue() { final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); assertThat(progressStyle1.isStyledByProgress()).isTrue(); } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setStyledByProgress() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setStyledByProgress(false); + assertThat(progressStyle1.isStyledByProgress()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_point() { + final int id = 1; + final int position = 10; + final int color = Color.RED; + + final Notification.ProgressStyle.Point point = + new Notification.ProgressStyle.Point(position).setId(id).setColor(color); + + assertEquals(id, point.getId()); + assertEquals(position, point.getPosition()); + assertEquals(color, point.getColor()); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_segment() { + final int id = 1; + final int length = 100; + final int color = Color.RED; + + final Notification.ProgressStyle.Segment segment = + new Notification.ProgressStyle.Segment(length).setId(id).setColor(color); + + assertEquals(id, segment.getId()); + assertEquals(length, segment.getLength()); + assertEquals(color, segment.getColor()); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS index c45080fb5e26..5fd4ffc7329a 100644 --- a/core/tests/coretests/src/android/os/OWNERS +++ b/core/tests/coretests/src/android/os/OWNERS @@ -10,6 +10,9 @@ per-file PowerManager*.java = file:/services/core/java/com/android/server/power/ # PerformanceHintManager per-file PerformanceHintManagerTest.java = file:/ADPF_OWNERS +# SystemHealthManager +per-file SystemHealthManagerUnitTest.java = file:/ADPF_OWNERS + # Caching per-file IpcDataCache* = file:/PERFORMANCE_OWNERS diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index da9d687ee2b0..3e6520106ab0 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -361,7 +361,11 @@ public class ParcelTest { p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie"); p.recycle(); - assertThat(p.getClassCookie(ParcelTest.class)).isNull(); + + // cannot access Parcel after it's recycled! + // this test is equivalent to checking hasClassCookie false + // after obtaining above + // assertThat(p.getClassCookie(ParcelTest.class)).isNull(); } @Test diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java index d26bb35e5481..5df2c1279eb8 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java @@ -20,12 +20,16 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Notification.ProgressStyle; import android.graphics.Color; +import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; +import com.android.internal.widget.NotificationProgressBar.Part; +import com.android.internal.widget.NotificationProgressBar.Point; +import com.android.internal.widget.NotificationProgressBar.Segment; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; +import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,183 +41,287 @@ import java.util.List; public class NotificationProgressBarTest { @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsIsEmpty() { + public void processAndConvertToParts_segmentsIsEmpty() { List<ProgressStyle.Segment> segments = new ArrayList<>(); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() { + public void processAndConvertToParts_segmentsLengthNotMatchingProgressMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsNegative() { + public void processAndConvertToParts_segmentLengthIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(-50)); segments.add(new ProgressStyle.Segment(150)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsZero() { + public void processAndConvertToParts_segmentLengthIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(0)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressIsNegative() { + public void processAndConvertToParts_progressIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = -50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_progressIsZero() { + public void processAndConvertToParts_progressIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 0; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, Color.RED))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedRed = 0x66FF0000; + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, fadedRed, true))); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true))); - - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(0); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_progressAtMax() { + public void processAndConvertToParts_progressAtMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 100; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, Color.RED))); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + assertThat(drawableParts).isEqualTo(expectedDrawableParts); - assertThat(parts).isEqualTo(expected); + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + assertThat(p.second).isEqualTo(300); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressAboveMax() { + public void processAndConvertToParts_progressAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 150; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionIsNegative() { + public void processAndConvertToParts_pointPositionIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(-50).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionAboveMax() { + public void processAndConvertToParts_pointPositionAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(150).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() { + public void processAndConvertToParts_multipleSegmentsWithoutPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 180, Color.GREEN), + new DrawableSegment(180, 300, fadedGreen, true))); + + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void processAndConvertToParts_multipleSegmentsWithoutPoints_noTracker() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); + segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); + List<ProgressStyle.Point> points = new ArrayList<>(); + int progress = 60; + int progressMax = 100; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN))); - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.50f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Segment(0.40f, fadedGreen, true))); + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = false; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 176, Color.GREEN), + new DrawableSegment(180, 300, fadedGreen, true))); + + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_singleSegmentWithPoints() { + public void processAndConvertToParts_singleSegmentWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); @@ -223,31 +331,68 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.BLUE), new Point(Color.RED), + new Segment(0.10f, Color.BLUE), new Point(Color.BLUE), + new Segment(0.35f, Color.BLUE), new Point(Color.BLUE), + new Segment(0.15f, Color.BLUE), new Point(Color.YELLOW), + new Segment(0.25f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.BLUE), + new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.BLUE), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 170, Color.BLUE), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 215, Color.BLUE), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + // Colors with 40% opacity int fadedBlue = 0x660000FF; int fadedYellow = 0x66FFFF00; - - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.BLUE), - new Point(null, Color.RED), - new Segment(0.10f, Color.BLUE), - new Point(null, Color.BLUE), - new Segment(0.35f, Color.BLUE), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedBlue, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedBlue, true))); - - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); - - assertThat(parts).isEqualTo(expected); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.219177F, Color.BLUE), + new DrawablePoint(38.219177F, 50.219177F, Color.RED), + new DrawableSegment(54.219177F, 70.21918F, Color.BLUE), + new DrawablePoint(74.21918F, 86.21918F, Color.BLUE), + new DrawableSegment(90.21918F, 172.38356F, Color.BLUE), + new DrawablePoint(176.38356F, 188.38356F, Color.BLUE), + new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true), + new DrawablePoint(221.0137F, 233.0137F, fadedYellow), + new DrawableSegment(237.0137F, 300F, fadedBlue, true))); + + assertThat(p.second).isEqualTo(182.38356F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() { + public void processAndConvertToParts_multipleSegmentsWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -258,32 +403,68 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), + new Segment(0.10f, Color.RED), new Point(Color.BLUE), + new Segment(0.25f, Color.RED), new Segment(0.10f, Color.GREEN), + new Point(Color.BLUE), new Segment(0.15f, Color.GREEN), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 170, Color.GREEN), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 215, Color.GREEN), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; int fadedYellow = 0x66FFFF00; - - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.RED), - new Point(null, Color.RED), - new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedGreen, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedGreen, true))); - - assertThat(parts).isEqualTo(expected); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.095238F, Color.RED), + new DrawablePoint(38.095238F, 50.095238F, Color.RED), + new DrawableSegment(54.095238F, 70.09524F, Color.RED), + new DrawablePoint(74.09524F, 86.09524F, Color.BLUE), + new DrawableSegment(90.09524F, 148.9524F, Color.RED), + new DrawableSegment(152.95238F, 172.7619F, Color.GREEN), + new DrawablePoint(176.7619F, 188.7619F, Color.BLUE), + new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true), + new DrawablePoint(221.33333F, 233.33333F, fadedYellow), + new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true))); + + assertThat(p.second).isEqualTo(182.7619F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() { + public void processAndConvertToParts_multipleSegmentsWithPoints_notStyledByProgress() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -293,21 +474,223 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), + new Segment(0.10f, Color.RED), new Point(Color.BLUE), + new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 215, Color.GREEN), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = false; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.296295F, Color.RED), + new DrawablePoint(38.296295F, 50.296295F, Color.RED), + new DrawableSegment(54.296295F, 70.296295F, Color.RED), + new DrawablePoint(74.296295F, 86.296295F, Color.BLUE), + new DrawableSegment(90.296295F, 149.62962F, Color.RED), + new DrawableSegment(153.62962F, 216.8148F, Color.GREEN), + new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW), + new DrawableSegment(236.81482F, 300, Color.GREEN))); + + assertThat(p.second).isEqualTo(182.9037F); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `zeroWidthDrawableSegment` test below is the longer + // segmentMinWidth (= 16dp). + @Test + public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 16, Color.BLUE), + new DrawableSegment(20, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 32, Color.BLUE), + new DrawableSegment(36, 69.41936F, Color.BLUE), + new DrawableSegment(73.41936F, 124.25807F, Color.BLUE), + new DrawableSegment(128.25807F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `negativeWidthDrawableSegment` test above is the shorter + // segmentMinWidth (= 10dp). + @Test + public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 16, Color.BLUE), + new DrawableSegment(20, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 26, Color.BLUE), + new DrawableSegment(30, 64.169014F, Color.BLUE), + new DrawableSegment(68.169014F, 120.92958F, Color.BLUE), + new DrawableSegment(124.92958F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void maybeStretchAndRescaleSegments_noStretchingNecessary() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE), + new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 36, Color.BLUE), + new DrawableSegment(40, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.RED), - new Point(null, Color.RED), - new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.25f, Color.GREEN), - new Point(null, Color.YELLOW), - new Segment(0.25f, Color.GREEN))); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); } } diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 50c95a9fa882..3378cc11d565 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -16,9 +16,9 @@ package android.graphics; +import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; -import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; import static com.android.text.flags.Flags.FLAG_VERTICAL_TEXT_LAYOUT; import android.annotation.ColorInt; @@ -34,7 +34,6 @@ import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; -import android.graphics.fonts.FontStyle; import android.graphics.fonts.FontVariationAxis; import android.graphics.text.TextRunShaper; import android.os.Build; @@ -2100,14 +2099,6 @@ public class Paint { } /** - * A change ID for new font variation settings management. - * @hide - */ - @ChangeId - @EnabledSince(targetSdkVersion = 36) - public static final long NEW_FONT_VARIATION_MANAGEMENT = 361260253L; - - /** * Sets TrueType or OpenType font variation settings. The settings string is constructed from * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that @@ -2136,16 +2127,12 @@ public class Paint { * </li> * </ul> * - * <p>Note: If the application that targets API 35 or before, this function mutates the - * underlying typeface instance. - * * @param fontVariationSettings font variation settings. You can pass null or empty string as * no variation settings. * - * @return If the application that targets API 36 or later and is running on devices API 36 or - * later, this function always returns true. Otherwise, this function returns true if - * the given settings is effective to at least one font file underlying this typeface. - * This function also returns true for empty settings string. Otherwise returns false. + * @return true if the given settings is effective to at least one font file underlying this + * typeface. This function also returns true for empty settings string. Otherwise + * returns false * * @throws IllegalArgumentException If given string is not a valid font variation settings * format @@ -2154,39 +2141,6 @@ public class Paint { * @see FontVariationAxis */ public boolean setFontVariationSettings(String fontVariationSettings) { - return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */); - } - - /** - * Set font variation settings with weight adjustment - * @hide - */ - public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) { - final boolean useFontVariationStore = Flags.typefaceRedesignReadonly() - && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT); - if (useFontVariationStore) { - FontVariationAxis[] axes = - FontVariationAxis.fromFontVariationSettings(fontVariationSettings); - if (axes == null) { - nSetFontVariationOverride(mNativePaint, 0); - mFontVariationSettings = null; - return true; - } - - long builderPtr = nCreateFontVariationBuilder(axes.length); - for (int i = 0; i < axes.length; ++i) { - int tag = axes[i].getOpenTypeTagValue(); - float value = axes[i].getStyleValue(); - if (tag == 0x77676874 /* wght */) { - value = Math.clamp(value + wghtAdjust, - FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX); - } - nAddFontVariationToBuilder(builderPtr, tag, value); - } - nSetFontVariationOverride(mNativePaint, builderPtr); - mFontVariationSettings = fontVariationSettings; - return true; - } final String settings = TextUtils.nullIfEmpty(fontVariationSettings); if (settings == mFontVariationSettings || (settings != null && settings.equals(mFontVariationSettings))) { diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java index d1fe2cdbcd77..30a248bb3e0e 100644 --- a/graphics/java/android/graphics/fonts/FontVariationAxis.java +++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java @@ -23,6 +23,7 @@ import android.os.Build; import android.text.TextUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; @@ -139,9 +140,19 @@ public final class FontVariationAxis { */ public static @Nullable FontVariationAxis[] fromFontVariationSettings( @Nullable String settings) { - if (settings == null || settings.isEmpty()) { + List<FontVariationAxis> result = fromFontVariationSettingsForList(settings); + if (result.isEmpty()) { return null; } + return result.toArray(new FontVariationAxis[0]); + } + + /** @hide */ + public static @NonNull List<FontVariationAxis> fromFontVariationSettingsForList( + @Nullable String settings) { + if (settings == null || settings.isEmpty()) { + return Collections.emptyList(); + } final ArrayList<FontVariationAxis> axisList = new ArrayList<>(); final int length = settings.length(); for (int i = 0; i < length; i++) { @@ -172,9 +183,9 @@ public final class FontVariationAxis { i = endOfValueString; } if (axisList.isEmpty()) { - return null; + return Collections.emptyList(); } - return axisList.toArray(new FontVariationAxis[0]); + return axisList; } /** diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java index 740ccb53a691..13f1a72469c2 100644 --- a/keystore/java/android/security/keystore/KeyStoreManager.java +++ b/keystore/java/android/security/keystore/KeyStoreManager.java @@ -312,9 +312,11 @@ public final class KeyStoreManager { * When passed into getSupplementaryAttestationInfo, getSupplementaryAttestationInfo returns the * DER-encoded structure corresponding to the `Modules` schema described in the KeyMint HAL's * KeyCreationResult.aidl. The SHA-256 hash of this encoded structure is what's included with - * the tag in attestations. + * the tag in attestations. To ensure the returned encoded structure is the one attested to, + * clients should verify its SHA-256 hash matches the one in the attestation. Note that the + * returned structure can vary between boots. */ - // TODO(b/369375199): Replace with Tag.MODULE_HASH when flagging is removed. + // TODO(b/380020528): Replace with Tag.MODULE_HASH when KeyMint V4 is frozen. public static final int MODULE_HASH = TagType.BYTES | 724; /** diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 755f472ee22e..2fed1380b635 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -233,6 +233,16 @@ public class DesktopModeStatus { } /** + * Returns whether the multiple desktops feature is enabled for this device (both backend and + * frontend implementations). + */ + public static boolean enableMultipleDesktops(@NonNull Context context) { + return Flags.enableMultipleDesktopsBackend() + && Flags.enableMultipleDesktopsFrontend() + && canEnterDesktopMode(context); + } + + /** * @return {@code true} if this device is requesting to show the app handle despite non * necessarily enabling desktop mode */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java new file mode 100644 index 000000000000..5018fdb615da --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.animation; + +import static com.android.wm.shell.transition.DefaultSurfaceAnimator.setupValueAnimator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.ClipRectAnimation; +import android.view.animation.ScaleAnimation; +import android.view.animation.Transformation; +import android.view.animation.TranslateAnimation; + +import java.util.function.Consumer; + +/** + * Animation implementation for size-changing window container animations. Ported from + * {@link com.android.server.wm.WindowChangeAnimationSpec}. + * <p> + * This animation behaves slightly differently depending on whether the window is growing + * or shrinking: + * <ul> + * <li>If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old) + * snapshot. + * <li>If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker + * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into + * place. + * </ul> + */ +public class SizeChangeAnimation { + private final Rect mTmpRect = new Rect(); + final Transformation mTmpTransform = new Transformation(); + final Matrix mTmpMatrix = new Matrix(); + final float[] mTmpFloats = new float[9]; + final float[] mTmpVecs = new float[4]; + + private final Animation mAnimation; + private final Animation mSnapshotAnim; + + private final ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f); + + /** + * The maximum of stretching applied to any surface during interpolation (since the animation + * is a combination of stretching/cropping/fading). + */ + private static final float SCALE_FACTOR = 0.7f; + + /** + * Since this animation is made of several sub-animations, we want to pre-arrange the + * sub-animations on a "virtual timeline" and then drive the overall progress in lock-step. + * + * To do this, we have a single value-animator which animates progress from 0-1 with an + * arbitrary duration and interpolator. Then we convert the progress to a frame in our virtual + * timeline to get the interpolated transforms. + * + * The APIs for arranging the sub-animations use integral frame numbers, so we need to pick + * an integral "duration" for our virtual timeline. That's what this constant specifies. It + * is effectively an animation "resolution" since it divides-up the 0-1 interpolation-space. + */ + private static final int ANIMATION_RESOLUTION = 1000; + + public SizeChangeAnimation(Rect startBounds, Rect endBounds) { + mAnimation = buildContainerAnimation(startBounds, endBounds); + mSnapshotAnim = buildSnapshotAnimation(startBounds, endBounds); + } + + /** + * Initialize a size-change animation for a container leash. + */ + public void initialize(SurfaceControl leash, SurfaceControl snapshot, + SurfaceControl.Transaction startT) { + startT.reparent(snapshot, leash); + startT.setPosition(snapshot, 0, 0); + startT.show(snapshot); + startT.show(leash); + apply(startT, leash, snapshot, 0.f); + } + + /** + * Initialize a size-change animation for a view containing the leash surface(s). + * + * Note that this **will** apply {@param startToApply}! + */ + public void initialize(View view, SurfaceControl leash, SurfaceControl snapshot, + SurfaceControl.Transaction startToApply) { + startToApply.reparent(snapshot, leash); + startToApply.setPosition(snapshot, 0, 0); + startToApply.show(snapshot); + startToApply.show(leash); + apply(view, startToApply, leash, snapshot, 0.f); + } + + private ValueAnimator buildAnimatorInner(ValueAnimator.AnimatorUpdateListener updater, + SurfaceControl leash, SurfaceControl snapshot, Consumer<Animator> onFinish, + SurfaceControl.Transaction transaction, @Nullable View view) { + return setupValueAnimator(mAnimator, updater, (anim) -> { + transaction.reparent(snapshot, null); + if (view != null) { + view.setClipBounds(null); + view.setAnimationMatrix(null); + transaction.setCrop(leash, null); + } + transaction.apply(); + transaction.close(); + onFinish.accept(anim); + }); + } + + /** + * Build an animator which works on a pair of surface controls (where the snapshot is assumed + * to be a child of the main leash). + * + * @param onFinish Called when animation finishes. This is called on the anim thread! + */ + public ValueAnimator buildAnimator(SurfaceControl leash, SurfaceControl snapshot, + Consumer<Animator> onFinish) { + final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + Choreographer choreographer = Choreographer.getInstance(); + return buildAnimatorInner(animator -> { + // The finish callback in buildSurfaceAnimation will ensure that the animation ends + // with fraction 1. + final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f); + apply(transaction, leash, snapshot, progress); + transaction.setFrameTimelineVsync(choreographer.getVsyncId()); + transaction.apply(); + }, leash, snapshot, onFinish, transaction, null /* view */); + } + + /** + * Build an animator which works on a view that contains a pair of surface controls (where + * the snapshot is assumed to be a child of the main leash). + * + * @param onFinish Called when animation finishes. This is called on the anim thread! + */ + public ValueAnimator buildViewAnimator(View view, SurfaceControl leash, + SurfaceControl snapshot, Consumer<Animator> onFinish) { + final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + return buildAnimatorInner(animator -> { + // The finish callback in buildSurfaceAnimation will ensure that the animation ends + // with fraction 1. + final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f); + apply(view, transaction, leash, snapshot, progress); + }, leash, snapshot, onFinish, transaction, view); + } + + /** Animation for the whole container (snapshot is inside this container). */ + private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds) { + final long duration = ANIMATION_RESOLUTION; + boolean growing = endBounds.width() - startBounds.width() + + endBounds.height() - startBounds.height() >= 0; + long scalePeriod = (long) (duration * SCALE_FACTOR); + float startScaleX = SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width() + + (1.f - SCALE_FACTOR); + float startScaleY = SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height() + + (1.f - SCALE_FACTOR); + final AnimationSet animSet = new AnimationSet(true); + + final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1); + scaleAnim.setDuration(scalePeriod); + if (!growing) { + scaleAnim.setStartOffset(duration - scalePeriod); + } + animSet.addAnimation(scaleAnim); + final Animation translateAnim = new TranslateAnimation(startBounds.left, + endBounds.left, startBounds.top, endBounds.top); + translateAnim.setDuration(duration); + animSet.addAnimation(translateAnim); + Rect startClip = new Rect(startBounds); + Rect endClip = new Rect(endBounds); + startClip.offsetTo(0, 0); + endClip.offsetTo(0, 0); + final Animation clipAnim = new ClipRectAnimation(startClip, endClip); + clipAnim.setDuration(duration); + animSet.addAnimation(clipAnim); + animSet.initialize(startBounds.width(), startBounds.height(), + endBounds.width(), endBounds.height()); + return animSet; + } + + /** The snapshot surface is assumed to be a child of the container surface. */ + private static AnimationSet buildSnapshotAnimation(Rect startBounds, Rect endBounds) { + final long duration = ANIMATION_RESOLUTION; + boolean growing = endBounds.width() - startBounds.width() + + endBounds.height() - startBounds.height() >= 0; + long scalePeriod = (long) (duration * SCALE_FACTOR); + float endScaleX = 1.f / (SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width() + + (1.f - SCALE_FACTOR)); + float endScaleY = 1.f / (SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height() + + (1.f - SCALE_FACTOR)); + + AnimationSet snapAnimSet = new AnimationSet(true); + // Animation for the "old-state" snapshot that is atop the task. + final Animation snapAlphaAnim = new AlphaAnimation(1.f, 0.f); + snapAlphaAnim.setDuration(scalePeriod); + if (!growing) { + snapAlphaAnim.setStartOffset(duration - scalePeriod); + } + snapAnimSet.addAnimation(snapAlphaAnim); + final Animation snapScaleAnim = + new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY); + snapScaleAnim.setDuration(duration); + snapAnimSet.addAnimation(snapScaleAnim); + snapAnimSet.initialize(startBounds.width(), startBounds.height(), + endBounds.width(), endBounds.height()); + return snapAnimSet; + } + + private void calcCurrentClipBounds(Rect outClip, Transformation fromTransform) { + // The following applies an inverse scale to the clip-rect so that it crops "after" the + // scale instead of before. + mTmpVecs[1] = mTmpVecs[2] = 0; + mTmpVecs[0] = mTmpVecs[3] = 1; + fromTransform.getMatrix().mapVectors(mTmpVecs); + + mTmpVecs[0] = 1.f / mTmpVecs[0]; + mTmpVecs[3] = 1.f / mTmpVecs[3]; + final Rect clipRect = fromTransform.getClipRect(); + outClip.left = (int) (clipRect.left * mTmpVecs[0] + 0.5f); + outClip.right = (int) (clipRect.right * mTmpVecs[0] + 0.5f); + outClip.top = (int) (clipRect.top * mTmpVecs[3] + 0.5f); + outClip.bottom = (int) (clipRect.bottom * mTmpVecs[3] + 0.5f); + } + + private void apply(SurfaceControl.Transaction t, SurfaceControl leash, SurfaceControl snapshot, + float progress) { + long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress); + // update thumbnail surface + mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform); + t.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats); + t.setAlpha(snapshot, mTmpTransform.getAlpha()); + + // update container surface + mAnimation.getTransformation(currentPlayTime, mTmpTransform); + final Matrix matrix = mTmpTransform.getMatrix(); + t.setMatrix(leash, matrix, mTmpFloats); + + calcCurrentClipBounds(mTmpRect, mTmpTransform); + t.setCrop(leash, mTmpRect); + } + + private void apply(View view, SurfaceControl.Transaction tmpT, SurfaceControl leash, + SurfaceControl snapshot, float progress) { + long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress); + // update thumbnail surface + mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform); + tmpT.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats); + tmpT.setAlpha(snapshot, mTmpTransform.getAlpha()); + + // update container surface + mAnimation.getTransformation(currentPlayTime, mTmpTransform); + final Matrix matrix = mTmpTransform.getMatrix(); + mTmpMatrix.set(matrix); + // animationMatrix is applied after getTranslation, so "move" the translate to the end. + mTmpMatrix.preTranslate(-view.getTranslationX(), -view.getTranslationY()); + mTmpMatrix.postTranslate(view.getTranslationX(), view.getTranslationY()); + view.setAnimationMatrix(mTmpMatrix); + + calcCurrentClipBounds(mTmpRect, mTmpTransform); + tmpT.setCrop(leash, mTmpRect); + view.setClipBounds(mTmpRect); + + // this takes stuff out of mTmpT so mTmpT can be re-used immediately + view.getViewRootImpl().applyTransactionOnDraw(tmpT); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java new file mode 100644 index 000000000000..9451374befe0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import com.android.wm.shell.shared.annotations.ExternalThread; + +/** + * Interface to engage with the app zoom out feature. + */ +@ExternalThread +public interface AppZoomOut { + + /** + * Called when the zoom out progress is updated, which is used to scale down the current app + * surface from fullscreen to the max pushback level we want to apply. {@param progress} ranges + * between [0,1], 0 when fullscreen, 1 when it's at the max pushback level. + */ + void setProgress(float progress); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java new file mode 100644 index 000000000000..8cd7b0f48003 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Configuration; +import android.util.Slog; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; + +/** Class that manages the app zoom out UI and states. */ +public class AppZoomOutController implements RemoteCallable<AppZoomOutController>, + ShellTaskOrganizer.FocusListener, DisplayChangeController.OnDisplayChangingListener { + + private static final String TAG = "AppZoomOutController"; + + private final Context mContext; + private final ShellTaskOrganizer mTaskOrganizer; + private final DisplayController mDisplayController; + private final AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer; + private final ShellExecutor mMainExecutor; + private final AppZoomOutImpl mImpl = new AppZoomOutImpl(); + + private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + updateDisplayLayout(displayId); + } + + @Override + public void onDisplayAdded(int displayId) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + updateDisplayLayout(displayId); + } + }; + + + public static AppZoomOutController create(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, + DisplayLayout displayLayout, @ShellMainThread ShellExecutor mainExecutor) { + AppZoomOutDisplayAreaOrganizer displayAreaOrganizer = new AppZoomOutDisplayAreaOrganizer( + context, displayLayout, mainExecutor); + return new AppZoomOutController(context, shellInit, shellTaskOrganizer, displayController, + displayAreaOrganizer, mainExecutor); + } + + @VisibleForTesting + AppZoomOutController(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, + AppZoomOutDisplayAreaOrganizer displayAreaOrganizer, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mTaskOrganizer = shellTaskOrganizer; + mDisplayController = displayController; + mDisplayAreaOrganizer = displayAreaOrganizer; + mMainExecutor = mainExecutor; + + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mTaskOrganizer.addFocusListener(this); + + mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); + mDisplayController.addDisplayChangingController(this); + + mDisplayAreaOrganizer.registerOrganizer(); + } + + public AppZoomOut asAppZoomOut() { + return mImpl; + } + + public void setProgress(float progress) { + mDisplayAreaOrganizer.setProgress(progress); + } + + void updateDisplayLayout(int displayId) { + final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId); + if (newDisplayLayout == null) { + Slog.w(TAG, "Failed to get new DisplayLayout."); + return; + } + mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout); + } + + @Override + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo == null) { + return; + } + if (taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME) { + mDisplayAreaOrganizer.setIsHomeTaskFocused(taskInfo.isFocused); + } + } + + @Override + public void onDisplayChange(int displayId, int fromRotation, int toRotation, + @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { + // TODO: verify if there is synchronization issues. + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + @ExternalThread + private class AppZoomOutImpl implements AppZoomOut { + @Override + public void setProgress(float progress) { + mMainExecutor.execute(() -> AppZoomOutController.this.setProgress(progress)); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java new file mode 100644 index 000000000000..1c37461b2d2b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; +import android.window.WindowContainerToken; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayLayout; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** Display area organizer that manages the app zoom out UI and states. */ +public class AppZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer { + + private static final float PUSHBACK_SCALE_FOR_LAUNCHER = 0.05f; + private static final float PUSHBACK_SCALE_FOR_APP = 0.025f; + private static final float INVALID_PROGRESS = -1; + + private final DisplayLayout mDisplayLayout = new DisplayLayout(); + private final Context mContext; + private final float mCornerRadius; + private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap = + new ArrayMap<>(); + + private float mProgress = INVALID_PROGRESS; + // Denote whether the home task is focused, null when it's not yet initialized. + @Nullable private Boolean mIsHomeTaskFocused; + + public AppZoomOutDisplayAreaOrganizer(Context context, + DisplayLayout displayLayout, Executor mainExecutor) { + super(mainExecutor); + mContext = context; + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + setDisplayLayout(displayLayout); + } + + @Override + public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) { + leash.setUnreleasedWarningCallSite( + "AppZoomOutDisplayAreaOrganizer.onDisplayAreaAppeared"); + mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); + } + + @Override + public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { + final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token); + if (leash != null) { + leash.release(); + } + mDisplayAreaTokenMap.remove(displayAreaInfo.token); + } + + public void registerOrganizer() { + final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer( + AppZoomOutDisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT); + for (int i = 0; i < displayAreaInfos.size(); i++) { + final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); + onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); + } + } + + @Override + public void unregisterOrganizer() { + super.unregisterOrganizer(); + reset(); + } + + void setProgress(float progress) { + if (mProgress == progress) { + return; + } + + mProgress = progress; + apply(); + } + + void setIsHomeTaskFocused(boolean isHomeTaskFocused) { + if (mIsHomeTaskFocused != null && mIsHomeTaskFocused == isHomeTaskFocused) { + return; + } + + mIsHomeTaskFocused = isHomeTaskFocused; + apply(); + } + + private void apply() { + if (mIsHomeTaskFocused == null || mProgress == INVALID_PROGRESS) { + return; + } + + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + float scale = mProgress * (mIsHomeTaskFocused + ? PUSHBACK_SCALE_FOR_LAUNCHER : PUSHBACK_SCALE_FOR_APP); + mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, scale)); + tx.apply(); + } + + void setDisplayLayout(DisplayLayout displayLayout) { + mDisplayLayout.set(displayLayout); + } + + private void reset() { + setProgress(0); + mProgress = INVALID_PROGRESS; + mIsHomeTaskFocused = null; + } + + private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) { + if (scale == 0) { + // Reset when scale is set back to 0. + tx + .setCrop(leash, null) + .setScale(leash, 1, 1) + .setPosition(leash, 0, 0) + .setCornerRadius(leash, 0); + return; + } + + tx + // Rounded corner can only be applied if a crop is set. + .setCrop(leash, 0, 0, mDisplayLayout.width(), mDisplayLayout.height()) + .setScale(leash, 1 - scale, 1 - scale) + .setPosition(leash, scale * mDisplayLayout.width() * 0.5f, + scale * mDisplayLayout.height() * 0.5f) + .setCornerRadius(leash, mCornerRadius * (1 - scale)); + } + + void onRotateDisplay(Context context, int toRotation) { + if (mDisplayLayout.rotation() == toRotation) { + return; + } + mDisplayLayout.rotateTo(context.getResources(), toRotation); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java index c493aadd57b0..151dc438702d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java @@ -20,6 +20,7 @@ import android.os.HandlerThread; import androidx.annotation.Nullable; +import com.android.wm.shell.appzoomout.AppZoomOut; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -112,4 +113,7 @@ public interface WMComponent { */ @WMSingleton Optional<DesktopMode> getDesktopMode(); + + @WMSingleton + Optional<AppZoomOut> getAppZoomOut(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 23a0f4adb6d2..ab3c33ec7e43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -91,6 +91,7 @@ import com.android.wm.shell.compatui.impl.DefaultComponentIdGenerator; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; import com.android.wm.shell.displayareahelper.DisplayAreaHelperController; import com.android.wm.shell.freeform.FreeformComponents; @@ -111,6 +112,8 @@ import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.appzoomout.AppZoomOut; +import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.startingsurface.StartingSurface; @@ -1031,6 +1034,38 @@ public abstract class WMShellBaseModule { }); } + @WMSingleton + @Provides + static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() { + return new DesktopWallpaperActivityTokenProvider(); + } + + @WMSingleton + @Provides + static Optional<DesktopWallpaperActivityTokenProvider> + provideOptionalDesktopWallpaperActivityTokenProvider( + Context context, + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.of(desktopWallpaperActivityTokenProvider); + } + return Optional.empty(); + } + + // + // App zoom out (optional feature) + // + + @WMSingleton + @Provides + static Optional<AppZoomOut> provideAppZoomOut( + Optional<AppZoomOutController> appZoomOutController) { + return appZoomOutController.map((controller) -> controller.asAppZoomOut()); + } + + @BindsOptionalOf + abstract AppZoomOutController optionalAppZoomOutController(); + // // Task Stack // @@ -1075,6 +1110,7 @@ public abstract class WMShellBaseModule { Optional<RecentTasksController> recentTasksOptional, Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional, Optional<OneHandedController> oneHandedControllerOptional, + Optional<AppZoomOutController> appZoomOutControllerOptional, Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional, Optional<ActivityEmbeddingController> activityEmbeddingOptional, Optional<MixedTransitionHandler> mixedTransitionHandler, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 1916215dea74..e8add56619c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -50,6 +50,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; import com.android.wm.shell.apptoweb.AssistContentRequester; +import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.back.BackAnimationController; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -945,7 +946,8 @@ public abstract class WMShellModule { FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -961,7 +963,7 @@ public abstract class WMShellModule { desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader)); + taskResourceLoader, recentsTransitionHandler)); } @WMSingleton @@ -1312,10 +1314,21 @@ public abstract class WMShellModule { return new DesktopModeUiEventLogger(uiEventLogger, packageManager); } + // + // App zoom out + // + @WMSingleton @Provides - static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() { - return new DesktopWallpaperActivityTokenProvider(); + static AppZoomOutController provideAppZoomOutController( + Context context, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + DisplayController displayController, + DisplayLayout displayLayout, + @ShellMainThread ShellExecutor mainExecutor) { + return AppZoomOutController.create(context, shellInit, shellTaskOrganizer, + displayController, displayLayout, mainExecutor); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 9e2b9b20be16..c8d0dab39837 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -41,6 +41,7 @@ import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipMotionHelper; @@ -82,11 +83,14 @@ public abstract class Pip2Module { @NonNull PipTransitionState pipStackListenerController, @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipUiStateChangeController pipUiStateChangeController, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) { + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, pipScheduler, pipStackListenerController, pipDisplayLayoutState, - pipUiStateChangeController, desktopUserRepositoriesOptional); + pipUiStateChangeController, desktopUserRepositoriesOptional, + desktopWallpaperActivityTokenProviderOptional); } @WMSingleton @@ -138,9 +142,12 @@ public abstract class Pip2Module { @ShellMainThread ShellExecutor mainExecutor, PipTransitionState pipTransitionState, Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState, - desktopUserRepositoriesOptional, rootTaskDisplayAreaOrganizer); + desktopUserRepositoriesOptional, desktopWallpaperActivityTokenProviderOptional, + rootTaskDisplayAreaOrganizer); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index d3066645f32e..1a58363dab81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -27,6 +27,7 @@ import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread @@ -56,6 +57,10 @@ class DesktopRepository( * @property topTransparentFullscreenTaskId the task id of any current top transparent * fullscreen task launched on top of Desktop Mode. Cleared when the transparent task is * closed or sent to back. (top is at index 0). + * @property pipTaskId the task id of PiP task entered while in Desktop Mode. + * @property pipShouldKeepDesktopActive whether an active PiP window should keep the Desktop + * Mode session active. Only false when we are explicitly exiting Desktop Mode (via user + * action) while there is an active PiP window. */ private data class DesktopTaskData( val activeTasks: ArraySet<Int> = ArraySet(), @@ -66,6 +71,8 @@ class DesktopRepository( val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), var fullImmersiveTaskId: Int? = null, var topTransparentFullscreenTaskId: Int? = null, + var pipTaskId: Int? = null, + var pipShouldKeepDesktopActive: Boolean = true, ) { fun deepCopy(): DesktopTaskData = DesktopTaskData( @@ -76,6 +83,8 @@ class DesktopRepository( freeformTasksInZOrder = ArrayList(freeformTasksInZOrder), fullImmersiveTaskId = fullImmersiveTaskId, topTransparentFullscreenTaskId = topTransparentFullscreenTaskId, + pipTaskId = pipTaskId, + pipShouldKeepDesktopActive = pipShouldKeepDesktopActive, ) fun clear() { @@ -86,6 +95,8 @@ class DesktopRepository( freeformTasksInZOrder.clear() fullImmersiveTaskId = null topTransparentFullscreenTaskId = null + pipTaskId = null + pipShouldKeepDesktopActive = true } } @@ -104,6 +115,9 @@ class DesktopRepository( /* Tracks last bounds of task before toggled to immersive state. */ private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>() + /* Callback for when a pending PiP transition has been aborted. */ + private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null + private var desktopGestureExclusionListener: Consumer<Region>? = null private var desktopGestureExclusionExecutor: Executor? = null @@ -302,6 +316,54 @@ class DesktopRepository( } } + /** Set whether the given task is the Desktop-entered PiP task in this display. */ + fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) { + val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId) + if (enterPip) { + desktopData.pipTaskId = taskId + desktopData.pipShouldKeepDesktopActive = true + } else { + desktopData.pipTaskId = + if (desktopData.pipTaskId == taskId) null + else { + logW( + "setTaskInPip: taskId=$taskId did not match saved taskId=${desktopData.pipTaskId}" + ) + desktopData.pipTaskId + } + } + notifyVisibleTaskListeners(displayId, getVisibleTaskCount(displayId)) + } + + /** Returns whether there is a PiP that was entered/minimized from Desktop in this display. */ + fun isMinimizedPipPresentInDisplay(displayId: Int): Boolean = + desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId != null + + /** Returns whether the given task is the Desktop-entered PiP task in this display. */ + fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean = + desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId == taskId + + /** Returns whether Desktop session should be active in this display due to active PiP. */ + fun shouldDesktopBeActiveForPip(displayId: Int): Boolean = + Flags.enableDesktopWindowingPip() && + isMinimizedPipPresentInDisplay(displayId) && + desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive + + /** Saves whether a PiP window should keep Desktop session active in this display. */ + fun setPipShouldKeepDesktopActive(displayId: Int, keepActive: Boolean) { + desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive = keepActive + } + + /** Saves callback to handle a pending PiP transition being aborted. */ + fun setOnPipAbortedCallback(callbackIfPipAborted: ((Int, Int) -> Unit)?) { + onPipAbortedCallback = callbackIfPipAborted + } + + /** Invokes callback to handle a pending PiP transition with the given task id being aborted. */ + fun onPipAborted(displayId: Int, pipTaskId: Int) { + onPipAbortedCallback?.invoke(displayId, pipTaskId) + } + /** Set whether the given task is the full-immersive task in this display. */ fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) { val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId) @@ -338,8 +400,12 @@ class DesktopRepository( } private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) { + val visibleAndPipTasksCount = + if (shouldDesktopBeActiveForPip(displayId)) visibleTasksCount + 1 else visibleTasksCount visibleTasksListeners.forEach { (listener, executor) -> - executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) } + executor.execute { + listener.onTasksVisibilityChanged(displayId, visibleAndPipTasksCount) + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 050dfb6f562c..6013648c9806 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -225,7 +225,6 @@ class DesktopTasksController( // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun. // Used to prevent handleRequest from moving the new fullscreen task to freeform. private var dragAndDropFullscreenCookie: Binder? = null - private var pendingPipTransitionAndTask: Pair<IBinder, Int>? = null init { desktopMode = DesktopModeImpl() @@ -321,18 +320,29 @@ class DesktopTasksController( fun visibleTaskCount(displayId: Int): Int = taskRepository.getVisibleTaskCount(displayId) /** - * Returns true if any freeform tasks are visible or if a transparent fullscreen task exists on - * top in Desktop Mode. + * Returns true if any of the following is true: + * - Any freeform tasks are visible + * - A transparent fullscreen task exists on top in Desktop Mode + * - PiP on Desktop Windowing is enabled, there is an active PiP window and the desktop + * wallpaper is visible. */ fun isDesktopModeShowing(displayId: Int): Boolean { + val hasVisibleTasks = visibleTaskCount(displayId) > 0 + val hasTopTransparentFullscreenTask = + taskRepository.getTopTransparentFullscreenTaskId(displayId) != null + val hasMinimizedPip = + Flags.enableDesktopWindowingPip() && + taskRepository.isMinimizedPipPresentInDisplay(displayId) && + desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(displayId) if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() ) { - return visibleTaskCount(displayId) > 0 || - taskRepository.getTopTransparentFullscreenTaskId(displayId) != null + return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip + } else if (Flags.enableDesktopWindowingPip()) { + return hasVisibleTasks || hasMinimizedPip } - return visibleTaskCount(displayId) > 0 + return hasVisibleTasks } /** Moves focused task to desktop mode for given [displayId]. */ @@ -592,7 +602,7 @@ class DesktopTasksController( ): ((IBinder) -> Unit)? { val taskId = taskInfo.taskId desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId) - performDesktopExitCleanupIfNeeded(taskId, displayId, wct) + performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) taskRepository.addClosingTask(displayId, taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId, taskId) @@ -624,8 +634,12 @@ class DesktopTasksController( ) val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null) wct.merge(requestRes.second, true) - pendingPipTransitionAndTask = - freeformTaskTransitionStarter.startPipTransition(wct) to taskInfo.taskId + freeformTaskTransitionStarter.startPipTransition(wct) + taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true) + taskRepository.setOnPipAbortedCallback { displayId, taskId -> + minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!) + taskRepository.setTaskInPip(displayId, taskId, enterPip = false) + } return } @@ -636,7 +650,7 @@ class DesktopTasksController( val taskId = taskInfo.taskId val displayId = taskInfo.displayId val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded(taskId, displayId, wct) + performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) // Notify immersive handler as it might need to exit immersive state. val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( @@ -898,7 +912,12 @@ class DesktopTasksController( } if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct) + performDesktopExitCleanupIfNeeded( + task.taskId, + task.displayId, + wct, + forceToFullscreen = false, + ) } transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) @@ -1414,7 +1433,9 @@ class DesktopTasksController( taskId: Int, displayId: Int, wct: WindowContainerTransaction, + forceToFullscreen: Boolean, ) { + taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen) if (Flags.enablePerDisplayDesktopWallpaperActivity()) { if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) { return @@ -1422,6 +1443,12 @@ class DesktopTasksController( if (displayId != DEFAULT_DISPLAY) { return } + } else if ( + Flags.enableDesktopWindowingPip() && + taskRepository.isMinimizedPipPresentInDisplay(displayId) && + !forceToFullscreen + ) { + return } else { if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) { return @@ -1462,21 +1489,6 @@ class DesktopTasksController( return false } - override fun onTransitionConsumed( - transition: IBinder, - aborted: Boolean, - finishT: Transaction?, - ) { - pendingPipTransitionAndTask?.let { (pipTransition, taskId) -> - if (transition == pipTransition) { - if (aborted) { - shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { minimizeTaskInner(it) } - } - pendingPipTransitionAndTask = null - } - } - } - override fun handleRequest( transition: IBinder, request: TransitionRequestInfo, @@ -1926,7 +1938,12 @@ class DesktopTasksController( if (!isDesktopModeShowing(task.displayId)) return null val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct) + performDesktopExitCleanupIfNeeded( + task.taskId, + task.displayId, + wct, + forceToFullscreen = false, + ) if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { taskRepository.addClosingTask(task.displayId, task.taskId) @@ -2053,7 +2070,12 @@ class DesktopTasksController( wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } - performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct) + performDesktopExitCleanupIfNeeded( + taskInfo.taskId, + taskInfo.displayId, + wct, + forceToFullscreen = true, + ) } private fun cascadeWindow(bounds: Rect, displayLayout: DisplayLayout, displayId: Int) { @@ -2087,7 +2109,12 @@ class DesktopTasksController( // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) - performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct) + performDesktopExitCleanupIfNeeded( + taskInfo.taskId, + taskInfo.displayId, + wct, + forceToFullscreen = false, + ) } /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 9bf5555fc194..d61ffdaf5cf8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -21,9 +21,11 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder import android.view.SurfaceControl -import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.DesktopModeFlags import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import android.window.TransitionInfo @@ -39,6 +41,8 @@ import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP +import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP /** * A [Transitions.TransitionObserver] that observes shell transitions and updates the @@ -57,6 +61,8 @@ class DesktopTasksTransitionObserver( ) : Transitions.TransitionObserver { private var transitionToCloseWallpaper: IBinder? = null + /* Pending PiP transition and its associated display id and task id. */ + private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null private var currentProfileId: Int init { @@ -90,6 +96,33 @@ class DesktopTasksTransitionObserver( removeTaskIfNeeded(info) } removeWallpaperOnLastTaskClosingIfNeeded(transition, info) + + val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) + info.changes.forEach { change -> + change.taskInfo?.let { taskInfo -> + if ( + Flags.enableDesktopWindowingPip() && + desktopRepository.isTaskMinimizedPipInDisplay( + taskInfo.displayId, + taskInfo.taskId, + ) + ) { + when (info.type) { + TRANSIT_PIP -> + pendingPipTransitionAndPipTask = + Triple(transition, taskInfo.displayId, taskInfo.taskId) + + TRANSIT_EXIT_PIP, + TRANSIT_REMOVE_PIP -> + desktopRepository.setTaskInPip( + taskInfo.displayId, + taskInfo.taskId, + enterPip = false, + ) + } + } + } + } } private fun removeTaskIfNeeded(info: TransitionInfo) { @@ -252,6 +285,18 @@ class DesktopTasksTransitionObserver( } } transitionToCloseWallpaper = null + } else if (pendingPipTransitionAndPipTask?.first == transition) { + val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) + if (aborted) { + pendingPipTransitionAndPipTask?.let { + desktopRepository.onPipAborted( + /*displayId=*/ it.second, + /* taskId=*/ it.third, + ) + } + } + desktopRepository.setOnPipAbortedCallback(null) + pendingPipTransitionAndPipTask = null } } @@ -263,11 +308,15 @@ class DesktopTasksTransitionObserver( change.taskInfo?.let { taskInfo -> if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) { when (change.mode) { - WindowManager.TRANSIT_OPEN -> { + TRANSIT_OPEN -> { desktopWallpaperActivityTokenProvider.setToken( taskInfo.token, taskInfo.displayId, ) + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = true, + taskInfo.displayId, + ) // After the task for the wallpaper is created, set it non-trimmable. // This is important to prevent recents from trimming and removing the // task. @@ -278,6 +327,16 @@ class DesktopTasksTransitionObserver( } TRANSIT_CLOSE -> desktopWallpaperActivityTokenProvider.removeToken(taskInfo.displayId) + TRANSIT_TO_FRONT -> + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = true, + taskInfo.displayId, + ) + TRANSIT_TO_BACK -> + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = false, + taskInfo.displayId, + ) else -> {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt index a87004c07d43..2bd7a9873a5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode.desktopwallpaperactivity import android.util.SparseArray +import android.util.SparseBooleanArray import android.view.Display.DEFAULT_DISPLAY import android.window.WindowContainerToken @@ -24,6 +25,7 @@ import android.window.WindowContainerToken class DesktopWallpaperActivityTokenProvider { private val wallpaperActivityTokenByDisplayId = SparseArray<WindowContainerToken>() + private val wallpaperActivityVisByDisplayId = SparseBooleanArray() fun setToken(token: WindowContainerToken, displayId: Int = DEFAULT_DISPLAY) { wallpaperActivityTokenByDisplayId[displayId] = token @@ -36,4 +38,16 @@ class DesktopWallpaperActivityTokenProvider { fun removeToken(displayId: Int = DEFAULT_DISPLAY) { wallpaperActivityTokenByDisplayId.delete(displayId) } + + fun setWallpaperActivityIsVisible( + isVisible: Boolean = false, + displayId: Int = DEFAULT_DISPLAY, + ) { + wallpaperActivityVisByDisplayId.put(displayId, isVisible) + } + + fun isWallpaperActivityVisible(displayId: Int = DEFAULT_DISPLAY): Boolean { + return wallpaperActivityTokenByDisplayId[displayId] != null && + wallpaperActivityVisByDisplayId.get(displayId, false) + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java index a62dd1c83520..4df9ae4b2ee3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java @@ -107,8 +107,11 @@ public class DragUtils { /** * Returns a list of the mime types provided in the clip description. */ - public static String getMimeTypesConcatenated(ClipDescription description) { + public static String getMimeTypesConcatenated(@Nullable ClipDescription description) { String mimeTypes = ""; + if (description == null) { + return mimeTypes; + } for (int i = 0; i < description.getMimeTypeCount(); i++) { if (i > 0) { mimeTypes += ", "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 7f673d2efc68..ea8dac982703 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -40,6 +40,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; @@ -59,6 +60,8 @@ public class PipScheduler { private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; + private final Optional<DesktopWallpaperActivityTokenProvider> + mDesktopWallpaperActivityTokenProviderOptional; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private PipTransitionController mPipTransitionController; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory @@ -73,12 +76,16 @@ public class PipScheduler { ShellExecutor mainExecutor, PipTransitionState pipTransitionState, Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; + mDesktopWallpaperActivityTokenProviderOptional = + desktopWallpaperActivityTokenProviderOptional; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mSurfaceControlTransactionFactory = @@ -260,10 +267,18 @@ public class PipScheduler { /** Returns whether PiP is exiting while we're in desktop mode. */ private boolean isPipExitingToDesktopMode() { - return Flags.enableDesktopWindowingPip() && mDesktopUserRepositoriesOptional.isPresent() - && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( - Objects.requireNonNull(mPipTransitionState.getPipTaskInfo()).displayId) > 0 - || isDisplayInFreeform()); + // Early return if PiP in Desktop Windowing is not supported. + if (!Flags.enableDesktopWindowingPip() || mDesktopUserRepositoriesOptional.isEmpty() + || mDesktopWallpaperActivityTokenProviderOptional.isEmpty()) { + return false; + } + final int displayId = Objects.requireNonNull( + mPipTransitionState.getPipTaskInfo()).displayId; + return mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(displayId) + > 0 + || mDesktopWallpaperActivityTokenProviderOptional.get().isWallpaperActivityVisible( + displayId) + || isDisplayInFreeform(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 8061ee9090b6..38015ca6d45f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -63,7 +63,9 @@ import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.split.SplitScreenUtils; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; @@ -110,6 +112,8 @@ public class PipTransition extends PipTransitionController implements private final PipTransitionState mPipTransitionState; private final PipDisplayLayoutState mPipDisplayLayoutState; private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; + private final Optional<DesktopWallpaperActivityTokenProvider> + mDesktopWallpaperActivityTokenProviderOptional; // // Transition caches @@ -145,7 +149,9 @@ public class PipTransition extends PipTransitionController implements PipTransitionState pipTransitionState, PipDisplayLayoutState pipDisplayLayoutState, PipUiStateChangeController pipUiStateChangeController, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) { + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -157,6 +163,8 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.addPipTransitionStateChangedListener(this); mPipDisplayLayoutState = pipDisplayLayoutState; mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; + mDesktopWallpaperActivityTokenProviderOptional = + desktopWallpaperActivityTokenProviderOptional; } @Override @@ -826,13 +834,14 @@ public class PipTransition extends PipTransitionController implements return false; } - // Since opening a new task while in Desktop Mode always first open in Fullscreen // until DesktopMode Shell code resolves it to Freeform, PipTransition will get a // possibility to handle it also. In this case return false to not have it enter PiP. final boolean isInDesktopSession = !mDesktopUserRepositoriesOptional.isEmpty() - && mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( - pipTask.displayId) > 0; + && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( + pipTask.displayId) > 0 + || mDesktopUserRepositoriesOptional.get().getCurrent() + .isMinimizedPipPresentInDisplay(pipTask.displayId)); if (isInDesktopSession) { return false; } @@ -968,6 +977,27 @@ public class PipTransition extends PipTransitionController implements "Unexpected bundle for " + mPipTransitionState); break; case PipTransitionState.EXITED_PIP: + final TaskInfo pipTask = mPipTransitionState.getPipTaskInfo(); + final boolean desktopPipEnabled = Flags.enableDesktopWindowingPip() + && mDesktopUserRepositoriesOptional.isPresent() + && mDesktopWallpaperActivityTokenProviderOptional.isPresent(); + if (desktopPipEnabled && pipTask != null) { + final DesktopRepository desktopRepository = + mDesktopUserRepositoriesOptional.get().getCurrent(); + final boolean wallpaperIsVisible = + mDesktopWallpaperActivityTokenProviderOptional.get() + .isWallpaperActivityVisible(pipTask.displayId); + if (desktopRepository.getVisibleTaskCount(pipTask.displayId) == 0 + && wallpaperIsVisible) { + mTransitions.startTransition( + TRANSIT_TO_BACK, + new WindowContainerTransaction().reorder( + mDesktopWallpaperActivityTokenProviderOptional.get() + .getToken(pipTask.displayId), /* onTop= */ false), + null + ); + } + } mPipTransitionState.setPinnedTaskLeash(null); mPipTransitionState.setPipTaskInfo(null); break; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl index 32c79a2d02de..8cdb8c4512a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl @@ -17,9 +17,10 @@ package com.android.wm.shell.recents; import android.graphics.Rect; +import android.os.Bundle; import android.view.RemoteAnimationTarget; import android.window.TaskSnapshot; -import android.os.Bundle; +import android.window.TransitionInfo; import com.android.wm.shell.recents.IRecentsAnimationController; @@ -57,7 +58,8 @@ oneway interface IRecentsAnimationRunner { */ void onAnimationStart(in IRecentsAnimationController controller, in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers, - in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras) = 2; + in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras, + in TransitionInfo info) = 2; /** * Called when the task of an activity that has been started while the recents animation diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 76496b06a4dd..aeccd86e122c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -411,10 +411,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mInstanceId = System.identityHashCode(this); mListener = listener; mDeathHandler = () -> { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - "[%d] RecentsController.DeathRecipient: binder died", mInstanceId); - finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */, - "deathRecipient"); + mExecutor.execute(() -> { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.DeathRecipient: binder died", mInstanceId); + finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */, + "deathRecipient"); + }); }; try { mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */); @@ -585,7 +587,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mListener.onAnimationStart(this, apps.toArray(new RemoteAnimationTarget[apps.size()]), new RemoteAnimationTarget[0], - new Rect(0, 0, 0, 0), new Rect(), new Bundle()); + new Rect(0, 0, 0, 0), new Rect(), new Bundle(), + null); for (int i = 0; i < mStateListeners.size(); i++) { mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING); } @@ -816,7 +819,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mListener.onAnimationStart(this, apps.toArray(new RemoteAnimationTarget[apps.size()]), wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]), - new Rect(0, 0, 0, 0), new Rect(), b); + new Rect(0, 0, 0, 0), new Rect(), b, info); for (int i = 0; i < mStateListeners.size(); i++) { mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING); } @@ -1273,6 +1276,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "requested")); } + /** + * @param runnerFinishCb The remote finish callback to run after finish is complete, this is + * not the same as mFinishCb which reports the transition is finished + * to WM. + */ private void finishInner(boolean toHome, boolean sendUserLeaveHint, IResultReceiver runnerFinishCb, String reason) { if (finishSyntheticTransition(runnerFinishCb, reason)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 5aa329108596..b6bd879c75eb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2974,9 +2974,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int transitType = info.getType(); TransitionInfo.Change pipChange = null; int closingSplitTaskId = -1; - // This array tracks if we are sending stages TO_BACK in this transition. - // TODO (b/349828130): Update for n apps - boolean[] stagesSentToBack = new boolean[2]; + // This array tracks where we are sending stages (TO_BACK/TO_FRONT) in this transition. + // TODO (b/349828130): Update for n apps (needs to handle different indices than 0/1). + // Also make sure having multiple changes per stage (2+ tasks in one stage) is being + // handled properly. + int[] stageChanges = new int[2]; for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); @@ -3040,18 +3042,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " with " + taskId + " before startAnimation()."); } } - if (isClosingType(change.getMode()) && - getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED) { - - // Record which stages are getting sent to back - if (change.getMode() == TRANSIT_TO_BACK) { - stagesSentToBack[getStageOfTask(taskId)] = true; - } + final int stageOfTaskId = getStageOfTask(taskId); + if (stageOfTaskId == STAGE_TYPE_UNDEFINED) { + continue; + } + if (isClosingType(change.getMode())) { // (For PiP transitions) If either one of the 2 stages is closing we're assuming // we'll break split closingSplitTaskId = taskId; } + if (transitType == WindowManager.TRANSIT_WAKE) { + // Record which stages are receiving which changes + if ((change.getMode() == TRANSIT_TO_BACK + || change.getMode() == TRANSIT_TO_FRONT) + && (stageOfTaskId == STAGE_TYPE_MAIN + || stageOfTaskId == STAGE_TYPE_SIDE)) { + stageChanges[stageOfTaskId] = change.getMode(); + } + } } if (pipChange != null) { @@ -3076,19 +3085,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return true; } - // If keyguard is active, check to see if we have our TO_BACK transitions in order. - // This array should either be all false (no split stages sent to back) or all true - // (all stages sent to back). In any other case (which can happen with SHOW_ABOVE_LOCKED - // apps) we should break split. - if (mKeyguardActive) { - boolean isFirstStageSentToBack = stagesSentToBack[0]; - for (boolean b : stagesSentToBack) { - // Compare each boolean to the first one. If any are different, break split. - if (b != isFirstStageSentToBack) { - dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); - break; - } - } + // If keyguard is active, check to see if we have all our stages showing. If one stage + // was moved but not the other (which can happen with SHOW_ABOVE_LOCKED apps), we should + // break split. + if (mKeyguardActive && stageChanges[STAGE_TYPE_MAIN] != stageChanges[STAGE_TYPE_SIDE]) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); } final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java index d8884f6d8d38..f5aaaad93229 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java @@ -33,6 +33,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.TransactionPool; import java.util.ArrayList; +import java.util.function.Consumer; public class DefaultSurfaceAnimator { @@ -58,42 +59,12 @@ public class DefaultSurfaceAnimator { // Animation length is already expected to be scaled. va.overrideDurationScale(1.0f); va.setDuration(anim.computeDurationHint()); - va.addUpdateListener(updateListener); - va.addListener(new AnimatorListenerAdapter() { - // It is possible for the end/cancel to be called more than once, which may cause - // issues if the animating surface has already been released. Track the finished - // state here to skip duplicate callbacks. See b/252872225. - private boolean mFinished; - - @Override - public void onAnimationEnd(Animator animation) { - onFinish(); - } - - @Override - public void onAnimationCancel(Animator animation) { - onFinish(); - } - - private void onFinish() { - if (mFinished) return; - mFinished = true; - // Apply transformation of end state in case the animation is canceled. - if (va.getAnimatedFraction() < 1f) { - va.setCurrentFraction(1f); - } - - pool.release(transaction); - mainExecutor.execute(() -> { - animations.remove(va); - finishCallback.run(); - }); - // The update listener can continue to be called after the animation has ended if - // end() is called manually again before the finisher removes the animation. - // Remove it manually here to prevent animating a released surface. - // See b/252872225. - va.removeUpdateListener(updateListener); - } + setupValueAnimator(va, updateListener, (vanim) -> { + pool.release(transaction); + mainExecutor.execute(() -> { + animations.remove(vanim); + finishCallback.run(); + }); }); animations.add(va); } @@ -188,4 +159,50 @@ public class DefaultSurfaceAnimator { } } } + + /** + * Setup some callback logic on a value-animator. This helper ensures that a value animator + * finishes at its final fraction (1f) and that relevant callbacks are only called once. + */ + public static ValueAnimator setupValueAnimator(ValueAnimator animator, + ValueAnimator.AnimatorUpdateListener updateListener, + Consumer<ValueAnimator> afterFinish) { + animator.addUpdateListener(updateListener); + animator.addListener(new AnimatorListenerAdapter() { + // It is possible for the end/cancel to be called more than once, which may cause + // issues if the animating surface has already been released. Track the finished + // state here to skip duplicate callbacks. See b/252872225. + private boolean mFinished; + + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + onFinish(); + } + + @Override + public void onAnimationCancel(Animator animation) { + onFinish(); + } + + private void onFinish() { + if (mFinished) return; + mFinished = true; + // Apply transformation of end state in case the animation is canceled. + if (animator.getAnimatedFraction() < 1f) { + animator.setCurrentFraction(1f); + } + afterFinish.accept(animator); + // The update listener can continue to be called after the animation has ended if + // end() is called manually again before the finisher removes the animation. + // Remove it manually here to prevent animating a released surface. + // See b/252872225. + animator.removeUpdateListener(updateListener); + } + }); + return animator; + } } 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 1689bb5778ae..36c3e9711f5c 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 @@ -55,6 +55,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; @@ -69,6 +70,7 @@ import static com.android.wm.shell.transition.TransitionAnimationHelper.isCovere import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; import android.animation.Animator; +import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -104,6 +106,7 @@ import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.animation.SizeChangeAnimation; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; @@ -422,6 +425,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish); continue; } + + if (Flags.portWindowSizeAnimation() && isTask + && TransitionInfo.isIndependent(change, info) + && change.getSnapshot() != null) { + startBoundsChangeAnimation(startTransaction, animations, change, onAnimFinish, + mMainExecutor); + continue; + } } // Hide the invisible surface directly without animating it if there is a display @@ -734,6 +745,21 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } + private void startBoundsChangeAnimation(@NonNull SurfaceControl.Transaction startT, + @NonNull ArrayList<Animator> animations, @NonNull TransitionInfo.Change change, + @NonNull Runnable finishCb, @NonNull ShellExecutor mainExecutor) { + final SizeChangeAnimation sca = + new SizeChangeAnimation(change.getStartAbsBounds(), change.getEndAbsBounds()); + sca.initialize(change.getLeash(), change.getSnapshot(), startT); + final ValueAnimator va = sca.buildAnimator(change.getLeash(), change.getSnapshot(), + (animator) -> mainExecutor.execute(() -> { + animations.remove(animator); + finishCb.run(); + })); + va.setDuration(DEFAULT_APP_TRANSITION_DURATION); + animations.add(va); + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 9fbda46bd2b7..429e0564dd2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -126,6 +126,8 @@ import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppToWebEducationController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.recents.RecentsTransitionHandler; +import com.android.wm.shell.recents.RecentsTransitionStateListener; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; @@ -157,8 +159,10 @@ import kotlinx.coroutines.MainCoroutineDispatcher; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; /** @@ -247,6 +251,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final DesktopModeEventLogger mDesktopModeEventLogger; private final DesktopModeUiEventLogger mDesktopModeUiEventLogger; private final WindowDecorTaskResourceLoader mTaskResourceLoader; + private final RecentsTransitionHandler mRecentsTransitionHandler; public DesktopModeWindowDecorViewModel( Context context, @@ -282,7 +287,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader) { + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler) { this( context, shellExecutor, @@ -323,7 +329,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader); + taskResourceLoader, + recentsTransitionHandler); } @VisibleForTesting @@ -367,7 +374,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader) { + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -436,6 +444,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopModeEventLogger = desktopModeEventLogger; mDesktopModeUiEventLogger = desktopModeUiEventLogger; mTaskResourceLoader = taskResourceLoader; + mRecentsTransitionHandler = recentsTransitionHandler; shellInit.addInitCallback(this::onInit, this); } @@ -450,6 +459,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, new DesktopModeOnTaskResizeAnimationListener()); mDesktopTasksController.setOnTaskRepositionAnimationListener( new DesktopModeOnTaskRepositionAnimationListener()); + if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + mRecentsTransitionHandler.addTransitionStateListener( + new DesktopModeRecentsTransitionStateListener()); + } mDisplayController.addDisplayChangingController(mOnDisplayChangingListener); try { mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, @@ -1859,6 +1872,38 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } } + private class DesktopModeRecentsTransitionStateListener + implements RecentsTransitionStateListener { + final Set<Integer> mAnimatingTaskIds = new HashSet<>(); + + @Override + public void onTransitionStateChanged(int state) { + switch (state) { + case RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED: + for (int n = 0; n < mWindowDecorByTaskId.size(); n++) { + int taskId = mWindowDecorByTaskId.keyAt(n); + mAnimatingTaskIds.add(taskId); + setIsRecentsTransitionRunningForTask(taskId, true); + } + return; + case RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING: + // No Recents transition running - clean up window decorations + for (int taskId : mAnimatingTaskIds) { + setIsRecentsTransitionRunningForTask(taskId, false); + } + mAnimatingTaskIds.clear(); + return; + default: + } + } + + private void setIsRecentsTransitionRunningForTask(int taskId, boolean isRecentsRunning) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) return; + decoration.setIsRecentsTransitionRunning(isRecentsRunning); + } + } + private class DragEventListenerImpl implements DragPositioningCallbackUtility.DragEventListener { @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 4ac89546c9c7..39a989ce7c7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -204,6 +204,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopUserRepositories mDesktopUserRepositories; + private boolean mIsRecentsTransitionRunning = false; private Runnable mLoadAppInfoRunnable; private Runnable mSetAppInfoRunnable; @@ -498,7 +499,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, - displayExclusionRegion); + displayExclusionRegion, mIsRecentsTransitionRunning); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -869,7 +870,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean inFullImmersiveMode, @NonNull InsetsState displayInsetsState, boolean hasGlobalFocus, - @NonNull Region displayExclusionRegion) { + @NonNull Region displayExclusionRegion, + boolean shouldIgnoreCornerRadius) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); final boolean isAppHeader = captionLayoutId == R.layout.desktop_mode_app_header; @@ -1006,13 +1008,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mWindowDecorConfig = windowDecorConfig; if (DesktopModeStatus.useRoundedCorners()) { - relayoutParams.mCornerRadius = taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - ? loadDimensionPixelSize(context.getResources(), - R.dimen.desktop_windowing_freeform_rounded_corner_radius) - : INVALID_CORNER_RADIUS; + relayoutParams.mCornerRadius = shouldIgnoreCornerRadius ? INVALID_CORNER_RADIUS : + getCornerRadius(context, relayoutParams.mLayoutResId); } } + private static int getCornerRadius(@NonNull Context context, int layoutResId) { + if (layoutResId == R.layout.desktop_mode_app_header) { + return loadDimensionPixelSize(context.getResources(), + R.dimen.desktop_windowing_freeform_rounded_corner_radius); + } + return INVALID_CORNER_RADIUS; + } + /** * If task has focused window decor, return the caption id of the fullscreen caption size * resource. Otherwise, return ID_NULL and caption width be set to task width. @@ -1740,6 +1748,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** + * Declares whether a Recents transition is currently active. + * + * <p> When a Recents transition is active we allow that transition to take ownership of the + * corner radius of its task surfaces, so each window decoration should stop updating the corner + * radius of its task surface during that time. + */ + void setIsRecentsTransitionRunning(boolean isRecentsTransitionRunning) { + mIsRecentsTransitionRunning = isRecentsTransitionRunning; + } + + /** * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button. */ void onMaximizeButtonHoverExit() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 5d1bedb85b5e..fa7183ad0fd8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -967,4 +967,4 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects), mFlags); } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt index 636549fa0662..a6f8150ffc55 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt @@ -176,7 +176,6 @@ open class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransitio * transition */ @Ignore("TODO(b/356277166): enable the tablet test") - @Postsubmit @Test open fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { assumeTrue(tapl.isTablet) diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt index 4987ab7b9344..d65f158e00d6 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt @@ -78,7 +78,6 @@ class BottomHalfEnterPipToOtherOrientation(flicker: LegacyFlickerTest) : } @Ignore("TODO(b/356277166): enable the tablet test") - @Presubmit @Test override fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { // Test app and pip app should covers the entire screen on start. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java new file mode 100644 index 000000000000..e91a1238a390 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.testing.AndroidTestingRunner; +import android.view.Display; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class AppZoomOutControllerTest extends ShellTestCase { + + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private DisplayController mDisplayController; + @Mock private AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer; + @Mock private ShellExecutor mExecutor; + @Mock private ActivityManager.RunningTaskInfo mRunningTaskInfo; + + private AppZoomOutController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Display display = mContext.getDisplay(); + DisplayLayout displayLayout = new DisplayLayout(mContext, display); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(displayLayout); + + ShellInit shellInit = spy(new ShellInit(mExecutor)); + mController = spy(new AppZoomOutController(mContext, shellInit, mTaskOrganizer, + mDisplayController, mDisplayAreaOrganizer, mExecutor)); + } + + @Test + public void isHomeTaskFocused_zoomOutForHome() { + mRunningTaskInfo.isFocused = true; + when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME); + mController.onFocusTaskChanged(mRunningTaskInfo); + + verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(true); + } + + @Test + public void isHomeTaskNotFocused_zoomOutForApp() { + mRunningTaskInfo.isFocused = false; + when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME); + mController.onFocusTaskChanged(mRunningTaskInfo); + + verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(false); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index c0ff2f0652b3..9b24c1c06cec 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -52,6 +52,7 @@ import org.junit.Test import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.whenever /** Tests for [DesktopModeEventLogger]. */ @@ -90,20 +91,12 @@ class DesktopModeEventLoggerTest : ShellTestCase() { val sessionId = desktopModeEventLogger.currentSessionId.get() assertThat(sessionId).isNotEqualTo(NO_SESSION_ID) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER), - /* enter_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER), - /* exit_reason */ - eq(0), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER, + 0, + sessionId, + ) verify { EventLogTags.writeWmShellEnterDesktopMode( eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason), @@ -122,20 +115,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { val sessionId = desktopModeEventLogger.currentSessionId.get() assertThat(sessionId).isNotEqualTo(NO_SESSION_ID) assertThat(sessionId).isNotEqualTo(previousSessionId) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER), - /* enter_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER), - /* exit_reason */ - eq(0), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER, + /* exit_reason */ + 0, + sessionId, + ) verify { EventLogTags.writeWmShellEnterDesktopMode( eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason), @@ -149,7 +135,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logSessionExit_noOngoingSession_doesNotLog() { desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -159,20 +145,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT), - /* enter_reason */ - eq(0), - /* exit_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT, + /* enter_reason */ + 0, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT, + sessionId, + ) verify { EventLogTags.writeWmShellExitDesktopMode( eq(ExitReason.DRAG_TO_EXIT.reason), @@ -187,7 +166,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskAdded_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskAdded(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -197,32 +176,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskAdded(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED), @@ -245,7 +211,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskRemoved_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskRemoved(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -255,32 +221,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskRemoved(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED), @@ -303,7 +256,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskInfoChanged_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -313,35 +266,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -371,37 +308,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskUpdate(minimizeReason = MinimizeReason.TASK_LIMIT) ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - /* minimize_reason */ - eq(MinimizeReason.TASK_LIMIT.reason), - /* unminimize_reason */ - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + MinimizeReason.TASK_LIMIT.reason, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -431,37 +350,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskUpdate(unminimizeReason = UnminimizeReason.TASKBAR_TAP) ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - /* minimize_reason */ - eq(UNSET_MINIMIZE_REASON), - /* unminimize_reason */ - eq(UnminimizeReason.TASKBAR_TAP.reason), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UnminimizeReason.TASKBAR_TAP.reason, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -491,7 +392,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskInfo(), ) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -509,39 +410,17 @@ class DesktopModeEventLoggerTest : ShellTestCase() { displayController, ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), - /* resize_trigger */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER - ), - /* resizing_stage */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE - ), - /* input_method */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD - ), - /* desktop_mode_session_id */ - eq(sessionId), - /* instance_id */ - eq(TASK_SIZE_UPDATE.instanceId), - /* uid */ - eq(TASK_SIZE_UPDATE.uid), - /* task_width */ - eq(TASK_SIZE_UPDATE.taskWidth), - /* task_height */ - eq(TASK_SIZE_UPDATE.taskHeight), - /* display_area */ - eq(DISPLAY_AREA), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskSizeUpdatedLogging( + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD, + sessionId, + TASK_SIZE_UPDATE.instanceId, + TASK_SIZE_UPDATE.uid, + TASK_SIZE_UPDATE.taskWidth, + TASK_SIZE_UPDATE.taskHeight, + DISPLAY_AREA, + ) } @Test @@ -552,7 +431,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskInfo(), ) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -568,39 +447,17 @@ class DesktopModeEventLoggerTest : ShellTestCase() { displayController = displayController, ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), - /* resize_trigger */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER - ), - /* resizing_stage */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE - ), - /* input_method */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD - ), - /* desktop_mode_session_id */ - eq(sessionId), - /* instance_id */ - eq(TASK_SIZE_UPDATE.instanceId), - /* uid */ - eq(TASK_SIZE_UPDATE.uid), - /* task_width */ - eq(TASK_SIZE_UPDATE.taskWidth), - /* task_height */ - eq(TASK_SIZE_UPDATE.taskHeight), - /* display_area */ - eq(DISPLAY_AREA), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskSizeUpdatedLogging( + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD, + sessionId, + TASK_SIZE_UPDATE.instanceId, + TASK_SIZE_UPDATE.uid, + TASK_SIZE_UPDATE.taskWidth, + TASK_SIZE_UPDATE.taskHeight, + DISPLAY_AREA, + ) } private fun startDesktopModeSession(): Int { @@ -652,6 +509,171 @@ class DesktopModeEventLoggerTest : ShellTestCase() { .build() } + private fun verifyNoLogging() { + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + } + + private fun verifyOnlyOneUiChangedLogging( + event: Int, + enterReason: Int, + exitReason: Int, + sessionId: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + eq(event), + eq(enterReason), + eq(exitReason), + eq(sessionId), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) + } + + private fun verifyOnlyOneTaskUpdateLogging( + taskEvent: Int, + instanceId: Int, + uid: Int, + taskHeight: Int, + taskWidth: Int, + taskX: Int, + taskY: Int, + sessionId: Int, + minimizeReason: Int, + unminimizeReason: Int, + visibleTaskCount: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + eq(taskEvent), + eq(instanceId), + eq(uid), + eq(taskHeight), + eq(taskWidth), + eq(taskX), + eq(taskY), + eq(sessionId), + eq(minimizeReason), + eq(unminimizeReason), + eq(visibleTaskCount), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) + } + + private fun verifyOnlyOneTaskSizeUpdatedLogging( + resizeTrigger: Int, + resizingStage: Int, + inputMethod: Int, + sessionId: Int, + instanceId: Int, + uid: Int, + taskWidth: Int, + taskHeight: Int, + displayArea: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + eq(resizeTrigger), + eq(resizingStage), + eq(inputMethod), + eq(sessionId), + eq(instanceId), + eq(uid), + eq(taskWidth), + eq(taskHeight), + eq(displayArea), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) + } + private companion object { private const val TASK_ID = 1 private const val TASK_UID = 1 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 5629127b8c54..daecccef9344 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -25,6 +25,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.ShellExecutor @@ -1067,6 +1068,67 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2) } + @Test + fun setTaskInPip_savedAsMinimizedPipInDisplay() { + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + } + + @Test + fun removeTaskInPip_removedAsMinimizedPipInDisplay() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse() + } + + @Test + fun setTaskInPip_multipleDisplays_bothAreInPip() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun setPipShouldKeepDesktopActive_shouldKeepDesktopActive() { + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = true) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun setPipShouldNotKeepDesktopActive_shouldNotKeepDesktopActive() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + + repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = false) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun removeTaskInPip_shouldNotKeepDesktopActive() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 692b50303038..4bb743079861 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -82,6 +82,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT @@ -569,6 +570,38 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_minimizedPipTask_wallpaperVisible_returnsTrue() { + val pipTask = setUpPipTask(autoEnterEnabled = true) + whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible()) + .thenReturn(true) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_minimizedPipTask_wallpaperNotVisible_returnsFalse() { + val pipTask = setUpPipTask(autoEnterEnabled = true) + whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible()) + .thenReturn(false) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_pipTaskNotMinimizedNorVisible_returnsFalse() { + setUpPipTask(autoEnterEnabled = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { val homeTask = setUpHomeTask(SECOND_DISPLAY) @@ -2039,6 +2072,41 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onDesktopWindowClose_minimizedPipPresent_doesNotExitDesktop() { + val freeformTask = setUpFreeformTask().apply { isFocused = true } + val pipTask = setUpPipTask(autoEnterEnabled = true) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask) + + verifyExitDesktopWCTNotExecuted() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onDesktopWindowClose_minimizedPipNotPresent_exitDesktop() { + val freeformTask = setUpFreeformTask() + val pipTask = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(pipTask) + verifyExitDesktopWCTNotExecuted() + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = false) + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask) + + // Remove wallpaper operation + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = false) val transition = Binder() @@ -2055,10 +2123,9 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() { + fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { val task = setUpPipTask(autoEnterEnabled = true) val handler = mock(TransitionHandler::class.java) - whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder()) whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) @@ -2069,7 +2136,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() { + fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() { val task = setUpPipTask(autoEnterEnabled = false) whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) .thenReturn(Binder()) @@ -2081,6 +2148,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun onPipTaskMinimize_doesntRemoveWallpaper() { + val task = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(task) + + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startPipTransition(captor.capture()) + captor.value.hierarchyOps.none { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = true) val transition = Binder() @@ -3125,6 +3208,31 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() { + val freeformTask = setUpFreeformTask() + val pipTask = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(pipTask) + verifyExitDesktopWCTNotExecuted() + + freeformTask.isFocused = true + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[freeformTask.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Remove wallpaper operation + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeDesktop_multipleTasks_removesAll() { val task1 = setUpFreeformTask() @@ -4851,7 +4959,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo { - return setUpFreeformTask().apply { + // active = false marks the task as non-visible; PiP window doesn't count as visible tasks + return setUpFreeformTask(active = false).apply { pictureInPictureParams = PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 89ab65a42bbf..96ed214e7f88 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.ComponentName import android.content.Context import android.content.Intent +import android.os.Binder import android.os.IBinder import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule @@ -29,7 +30,9 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.IWindowContainerToken import android.window.TransitionInfo import android.window.TransitionInfo.Change @@ -38,6 +41,7 @@ import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController @@ -47,6 +51,8 @@ import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpape import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP +import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -300,6 +306,115 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).clearTopTransparentFullscreenTaskId(topTransparentTask.displayId) } + @Test + fun transitOpenWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId) + } + + @Test + fun transitToFrontWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createToFrontTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId) + } + + @Test + fun transitToBackWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createToBackTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = false, wallpaperTask.displayId) + } + + @Test + fun transitCloseWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createCloseTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider).removeToken(wallpaperTask.displayId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun pendingPipTransitionAborted_taskRepositoryOnPipAbortedInvoked() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + val pipTransition = Binder() + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = pipTransition, + info = createOpenChangeTransition(task, TRANSIT_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + transitionObserver.onTransitionFinished(transition = pipTransition, aborted = true) + + verify(taskRepository).onPipAborted(task.displayId, task.taskId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun exitPipTransition_taskRepositoryClearTaskInPip() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(task, type = TRANSIT_EXIT_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun removePipTransition_taskRepositoryClearTaskInPip() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(task, type = TRANSIT_REMOVE_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) + } + private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, @@ -331,7 +446,7 @@ class DesktopTasksTransitionObserverTest { task: RunningTaskInfo?, type: Int = TRANSIT_OPEN, ): TransitionInfo { - return TransitionInfo(TRANSIT_OPEN, /* flags= */ 0).apply { + return TransitionInfo(type, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_OPEN @@ -369,6 +484,19 @@ class DesktopTasksTransitionObserverTest { } } + private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo { + return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_FRONT + parent = null + taskInfo = task + flags = flags + } + ) + } + } + private fun getLatestWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN, handlerClass: Class<out Transitions.TransitionHandler>? = null, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index 32bb8bbdbbe3..1b1a5a909220 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -152,17 +152,16 @@ public class DragAndDropControllerTest extends ShellTestCase { } @Test - public void testOnDragStarted_withNoClipData() { + public void testOnDragStarted_withNoClipDataOrDescription() { final View dragLayout = mock(View.class); final Display display = mock(Display.class); doReturn(display).when(dragLayout).getDisplay(); doReturn(DEFAULT_DISPLAY).when(display).getDisplayId(); - final ClipData clipData = createAppClipData(MIMETYPE_APPLICATION_SHORTCUT); final DragEvent event = mock(DragEvent.class); doReturn(ACTION_DRAG_STARTED).when(event).getAction(); doReturn(null).when(event).getClipData(); - doReturn(clipData.getDescription()).when(event).getClipDescription(); + doReturn(null).when(event).getClipDescription(); // Ensure there's a target so that onDrag will execute mController.addDisplayDropTarget(0, mContext, mock(WindowManager.class), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index a8aa25700c7e..c42f6c35bcb0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -30,6 +30,7 @@ import static org.mockito.kotlin.MatchersKt.eq; import static org.mockito.kotlin.VerificationKt.times; import static org.mockito.kotlin.VerificationKt.verify; +import android.app.TaskInfo; import android.content.Context; import android.content.res.Resources; import android.graphics.Matrix; @@ -45,7 +46,9 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; @@ -56,6 +59,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -83,7 +87,8 @@ public class PipSchedulerTest { @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; @Mock private SurfaceControl.Transaction mMockTransaction; @Mock private PipAlphaAnimator mMockAlphaAnimator; - @Mock private Optional<DesktopUserRepositories> mMockOptionalDesktopUserRepositories; + @Mock private DesktopUserRepositories mMockDesktopUserRepositories; + @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider; @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; @@ -100,9 +105,13 @@ public class PipSchedulerTest { when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) .thenReturn(mMockTransaction); + when(mMockDesktopUserRepositories.getCurrent()) + .thenReturn(Mockito.mock(DesktopRepository.class)); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(Mockito.mock(TaskInfo.class)); mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor, - mMockPipTransitionState, mMockOptionalDesktopUserRepositories, + mMockPipTransitionState, Optional.of(mMockDesktopUserRepositories), + Optional.of(mMockDesktopWallpaperActivityTokenProvider), mRootTaskDisplayAreaOrganizer); mPipScheduler.setPipTransitionController(mMockPipTransitionController); mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index 894d238b7e15..ab43119b14c0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -169,7 +169,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { final IResultReceiver finishCallback = mock(IResultReceiver.class); final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner); - verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any()); // Finish and verify no transition remains and that the provided finish callback is called mRecentsTransitionHandler.findController(transition).finish(true /* toHome */, @@ -184,7 +184,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class); final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner); - verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any()); mRecentsTransitionHandler.findController(transition).cancel("test"); mMainExecutor.flushAll(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index ffe8e7135513..79e9b9c8cd77 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -59,11 +59,12 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.window.flags.Flags import com.android.wm.shell.R -import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.DesktopImmersiveController import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition +import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction +import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.splitscreen.SplitScreenController @@ -539,7 +540,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest onLeftSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), + .snapToHalfScreen( + eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), eq(decor), @@ -616,11 +618,12 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest onRightSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), + .snapToHalfScreen( + eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), eq(decor), - ) + ) } @Test @@ -1223,6 +1226,49 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest verify(task2, never()).onExclusionRegionChanged(newRegion) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_requestedState_setsTransitionRunning() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + + verify(decoration).setIsRecentsTransitionRunning(true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_nonRunningState_setsTransitionNotRunning() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING) + + verify(decoration).setIsRecentsTransitionRunning(false) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_requestedAndAnimating_setsTransitionRunningOnce() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING) + + verify(decoration, times(1)).setIsRecentsTransitionRunning(true) + } + private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, taskSurface: SurfaceControl = SurfaceControl(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index b5e8cebc1277..8af8285d031c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -40,6 +40,7 @@ import android.view.SurfaceControl import android.view.WindowInsets.Type.statusBars import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -65,6 +66,8 @@ import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController import com.android.wm.shell.desktopmode.education.AppToWebEducationController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter +import com.android.wm.shell.recents.RecentsTransitionHandler +import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -151,6 +154,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockFocusTransitionObserver = mock<FocusTransitionObserver>() protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>() protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>() + protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>() protected val motionEvent = mock<MotionEvent>() val displayLayout = mock<DisplayLayout>() protected lateinit var spyContext: TestableContext @@ -164,6 +168,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected lateinit var mockitoSession: StaticMockitoSession protected lateinit var shellInit: ShellInit internal lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener + protected lateinit var desktopModeRecentsTransitionStateListener: RecentsTransitionStateListener protected lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener @@ -219,7 +224,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockFocusTransitionObserver, desktopModeEventLogger, mock<DesktopModeUiEventLogger>(), - mock<WindowDecorTaskResourceLoader>() + mock<WindowDecorTaskResourceLoader>(), + mockRecentsTransitionHandler, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -256,6 +262,13 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { verify(displayInsetsController) .addGlobalInsetsChangedListener(insetsChangedCaptor.capture()) desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue + val recentsTransitionStateListenerCaptor = argumentCaptor<RecentsTransitionStateListener>() + if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + verify(mockRecentsTransitionHandler) + .addTransitionStateListener(recentsTransitionStateListenerCaptor.capture()) + desktopModeRecentsTransitionStateListener = + recentsTransitionStateListenerCaptor.firstValue + } val keyguardChangedCaptor = argumentCaptor<DesktopModeKeyguardChangeListener>() verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 6b02aeffd42a..9ea5fd6e1abe 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -169,6 +169,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final boolean DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED = false; private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false; private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true; + private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @@ -396,6 +397,31 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + public void updateRelayoutParams_shouldIgnoreCornerRadius_roundedCornersNotSet() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + fillRoundedCornersResources(/* fillValue= */ 30); + RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + mMockSplitScreenController, + DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, + DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, + DEFAULT_IS_STATUSBAR_VISIBLE, + DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, + DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + new InsetsState(), + DEFAULT_HAS_GLOBAL_FOCUS, + mExclusionRegion, + /* shouldIgnoreCornerRadius= */ true); + + assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY) public void updateRelayoutParams_appHeader_usesTaskDensity() { final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources() @@ -634,7 +660,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, insetsState, DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); // Takes status bar inset as padding, ignores caption bar inset. assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50); @@ -659,7 +686,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsInsetSource).isFalse(); } @@ -683,7 +711,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); // Header is always shown because it's assumed the status bar is always visible. assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -707,7 +736,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); } @@ -730,7 +760,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -753,7 +784,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -777,7 +809,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -793,7 +826,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -817,7 +851,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -1480,7 +1515,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); } private DesktopModeWindowDecoration createWindowDecoration( diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index e693fcfd3918..dbb891455ddd 100644 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -162,13 +162,10 @@ const std::string& ApkAssets::GetDebugName() const { return assets_provider_->GetDebugName(); } -UpToDate ApkAssets::IsUpToDate() const { +bool ApkAssets::IsUpToDate() const { // Loaders are invalidated by the app, not the system, so assume they are up to date. - if (IsLoader()) { - return UpToDate::Always; - } - const auto idmap_res = loaded_idmap_ ? loaded_idmap_->IsUpToDate() : UpToDate::Always; - return combine(idmap_res, [this] { return assets_provider_->IsUpToDate(); }); + return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate()) + && assets_provider_->IsUpToDate()); } } // namespace android diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp index 11b12eb030a6..2d3c06506a1f 100644 --- a/libs/androidfw/AssetsProvider.cpp +++ b/libs/androidfw/AssetsProvider.cpp @@ -24,8 +24,9 @@ #include <ziparchive/zip_archive.h> namespace android { - -static constexpr std::string_view kEmptyDebugString = "<empty>"; +namespace { +constexpr const char* kEmptyDebugString = "<empty>"; +} // namespace std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode, bool* file_exists) const { @@ -85,9 +86,11 @@ void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const { } ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path, - package_property_t flags, ModDate last_mod_time) - : zip_handle_(handle), name_(std::move(path)), flags_(flags), last_mod_time_(last_mod_time) { -} + package_property_t flags, time_t last_mod_time) + : zip_handle_(handle), + name_(std::move(path)), + flags_(flags), + last_mod_time_(last_mod_time) {} std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, package_property_t flags, @@ -101,10 +104,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, return {}; } - ModDate mod_date = kInvalidModDate; + struct stat sb{.st_mtime = -1}; // Skip all up-to-date checks if the file won't ever change. - if (isKnownWritablePath(path.c_str()) || !isReadonlyFilesystem(GetFileDescriptor(handle))) { - if (mod_date = getFileModDate(GetFileDescriptor(handle)); mod_date == kInvalidModDate) { + if (!isReadonlyFilesystem(path.c_str())) { + if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -113,7 +116,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, } return std::unique_ptr<ZipAssetsProvider>( - new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, mod_date)); + new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime)); } std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, @@ -134,10 +137,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, return {}; } - ModDate mod_date = kInvalidModDate; + struct stat sb{.st_mtime = -1}; // Skip all up-to-date checks if the file won't ever change. if (!isReadonlyFilesystem(released_fd)) { - if (mod_date = getFileModDate(released_fd); mod_date == kInvalidModDate) { + if (fstat(released_fd, &sb) < 0) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -147,7 +150,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, } return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider( - handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, mod_date)); + handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime)); } std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, @@ -279,16 +282,21 @@ const std::string& ZipAssetsProvider::GetDebugName() const { return name_.GetDebugName(); } -UpToDate ZipAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == kInvalidModDate) { - return UpToDate::Always; +bool ZipAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == -1) { + return true; + } + struct stat sb{}; + if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) { + // If fstat fails on the zip archive, return true so the zip archive the resource system does + // attempt to refresh the ApkAsset. + return true; } - return fromBool(last_mod_time_ == getFileModDate(GetFileDescriptor(zip_handle_.get()))); + return last_mod_time_ == sb.st_mtime; } -DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time) - : dir_(std::move(path)), last_mod_time_(last_mod_time) { -} +DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time) + : dir_(std::move(path)), last_mod_time_(last_mod_time) {} std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) { struct stat sb; @@ -309,7 +317,7 @@ std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::st const bool isReadonly = isReadonlyFilesystem(path.c_str()); return std::unique_ptr<DirectoryAssetsProvider>( - new DirectoryAssetsProvider(std::move(path), isReadonly ? kInvalidModDate : getModDate(sb))); + new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime)); } std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path, @@ -338,11 +346,17 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const { return dir_; } -UpToDate DirectoryAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == kInvalidModDate) { - return UpToDate::Always; +bool DirectoryAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == -1) { + return true; + } + struct stat sb; + if (stat(dir_.c_str(), &sb) < 0) { + // If stat fails on the zip archive, return true so the zip archive the resource system does + // attempt to refresh the ApkAsset. + return true; } - return fromBool(last_mod_time_ == getFileModDate(dir_.c_str())); + return last_mod_time_ == sb.st_mtime; } MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, @@ -355,14 +369,8 @@ MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& prima std::unique_ptr<AssetsProvider> MultiAssetsProvider::Create( std::unique_ptr<AssetsProvider>&& primary, std::unique_ptr<AssetsProvider>&& secondary) { - if (primary == nullptr && secondary == nullptr) { - return EmptyAssetsProvider::Create(); - } - if (!primary) { - return secondary; - } - if (!secondary) { - return primary; + if (primary == nullptr || secondary == nullptr) { + return nullptr; } return std::unique_ptr<MultiAssetsProvider>(new MultiAssetsProvider(std::move(primary), std::move(secondary))); @@ -389,8 +397,8 @@ const std::string& MultiAssetsProvider::GetDebugName() const { return debug_name_; } -UpToDate MultiAssetsProvider::IsUpToDate() const { - return combine(primary_->IsUpToDate(), [this] { return secondary_->IsUpToDate(); }); +bool MultiAssetsProvider::IsUpToDate() const { + return primary_->IsUpToDate() && secondary_->IsUpToDate(); } EmptyAssetsProvider::EmptyAssetsProvider(std::optional<std::string>&& path) : @@ -430,12 +438,12 @@ const std::string& EmptyAssetsProvider::GetDebugName() const { if (path_.has_value()) { return *path_; } - constexpr static std::string kEmpty{kEmptyDebugString}; + const static std::string kEmpty = kEmptyDebugString; return kEmpty; } -UpToDate EmptyAssetsProvider::IsUpToDate() const { - return UpToDate::Always; +bool EmptyAssetsProvider::IsUpToDate() const { + return true; } } // namespace android diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index 262e7df185b7..3ecd82b074a1 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -22,10 +22,9 @@ #include "android-base/logging.h" #include "android-base/stringprintf.h" #include "android-base/utf8.h" -#include "androidfw/AssetManager.h" +#include "androidfw/misc.h" #include "androidfw/ResourceTypes.h" #include "androidfw/Util.h" -#include "androidfw/misc.h" #include "utils/ByteOrder.h" #include "utils/Trace.h" @@ -269,16 +268,11 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head configurations_(configs), overlay_entries_(overlay_entries), string_pool_(std::move(string_pool)), + idmap_fd_( + android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)), overlay_apk_path_(overlay_apk_path), target_apk_path_(target_apk_path), - idmap_last_mod_time_(kInvalidModDate) { - if (!isReadonlyFilesystem(std::string(overlay_apk_path_).c_str()) || - !(target_apk_path_ == AssetManager::TARGET_APK_PATH || - isReadonlyFilesystem(std::string(target_apk_path_).c_str()))) { - idmap_fd_.reset( - android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)); - idmap_last_mod_time_ = getFileModDate(idmap_fd_); - } + idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) { } std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) { @@ -387,11 +381,8 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path)); } -UpToDate LoadedIdmap::IsUpToDate() const { - if (idmap_last_mod_time_ == kInvalidModDate) { - return UpToDate::Always; - } - return fromBool(idmap_last_mod_time_ == getFileModDate(idmap_fd_.get())); +bool LoadedIdmap::IsUpToDate() const { + return idmap_last_mod_time_ == getFileModDate(idmap_fd_.get()); } } // namespace android diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index a8eb062a2ece..de9991a8be5e 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -152,11 +152,12 @@ static void fill9patchOffsets(Res_png_9patch* patch) { patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t)); } -void Res_value::copyFrom_dtoh_slow(const Res_value& src) { - size = dtohs(src.size); - res0 = src.res0; - dataType = src.dataType; - data = dtohl(src.data); +void Res_value::copyFrom_dtoh(const Res_value& src) +{ + size = dtohs(src.size); + res0 = src.res0; + dataType = src.dataType; + data = dtohl(src.data); } void Res_png_9patch::deviceToFile() @@ -2030,6 +2031,16 @@ status_t ResXMLTree::validateNode(const ResXMLTree_node* node) const // -------------------------------------------------------------------- // -------------------------------------------------------------------- +void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) { + const size_t size = dtohl(o.size); + if (size >= sizeof(ResTable_config)) { + *this = o; + } else { + memcpy(this, &o, size); + memset(((uint8_t*)this)+size, 0, sizeof(ResTable_config)-size); + } +} + /* static */ size_t unpackLanguageOrRegion(const char in[2], const char base, char out[4]) { if (in[0] & 0x80) { @@ -2094,33 +2105,34 @@ size_t ResTable_config::unpackRegion(char region[4]) const { return unpackLanguageOrRegion(this->country, '0', region); } -void ResTable_config::copyFromDtoH_slow(const ResTable_config& o) { - copyFromDeviceNoSwap(o); - size = sizeof(ResTable_config); - mcc = dtohs(mcc); - mnc = dtohs(mnc); - density = dtohs(density); - screenWidth = dtohs(screenWidth); - screenHeight = dtohs(screenHeight); - sdkVersion = dtohs(sdkVersion); - minorVersion = dtohs(minorVersion); - smallestScreenWidthDp = dtohs(smallestScreenWidthDp); - screenWidthDp = dtohs(screenWidthDp); - screenHeightDp = dtohs(screenHeightDp); -} - -void ResTable_config::swapHtoD_slow() { - size = htodl(size); - mcc = htods(mcc); - mnc = htods(mnc); - density = htods(density); - screenWidth = htods(screenWidth); - screenHeight = htods(screenHeight); - sdkVersion = htods(sdkVersion); - minorVersion = htods(minorVersion); - smallestScreenWidthDp = htods(smallestScreenWidthDp); - screenWidthDp = htods(screenWidthDp); - screenHeightDp = htods(screenHeightDp); + +void ResTable_config::copyFromDtoH(const ResTable_config& o) { + copyFromDeviceNoSwap(o); + size = sizeof(ResTable_config); + mcc = dtohs(mcc); + mnc = dtohs(mnc); + density = dtohs(density); + screenWidth = dtohs(screenWidth); + screenHeight = dtohs(screenHeight); + sdkVersion = dtohs(sdkVersion); + minorVersion = dtohs(minorVersion); + smallestScreenWidthDp = dtohs(smallestScreenWidthDp); + screenWidthDp = dtohs(screenWidthDp); + screenHeightDp = dtohs(screenHeightDp); +} + +void ResTable_config::swapHtoD() { + size = htodl(size); + mcc = htods(mcc); + mnc = htods(mnc); + density = htods(density); + screenWidth = htods(screenWidth); + screenHeight = htods(screenHeight); + sdkVersion = htods(sdkVersion); + minorVersion = htods(minorVersion); + smallestScreenWidthDp = htods(smallestScreenWidthDp); + screenWidthDp = htods(screenWidthDp); + screenHeightDp = htods(screenHeightDp); } /* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) { @@ -2133,7 +2145,7 @@ void ResTable_config::swapHtoD_slow() { // systems should happen very infrequently (if at all.) // The comparison code relies on memcmp low-level optimizations that make it // more efficient than strncmp. - static constexpr char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; + const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; const char *lScript = l.localeScriptWasComputed ? emptyScript : l.localeScript; const char *rScript = r.localeScriptWasComputed ? emptyScript : r.localeScript; diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp index 86c459fb4647..be55fe8b4bb6 100644 --- a/libs/androidfw/Util.cpp +++ b/libs/androidfw/Util.cpp @@ -32,18 +32,13 @@ namespace android { namespace util { void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out) { - static constexpr bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; - if constexpr (kDeviceEndiannessSame) { - *out = Utf16ToUtf8({(const char16_t*)src, strnlen16((const char16_t*)src, len)}); - } else { - char buf[5]; - while (*src && len != 0) { - char16_t c = static_cast<char16_t>(dtohs(*src)); - utf16_to_utf8(&c, 1, buf, sizeof(buf)); - out->append(buf, strlen(buf)); - ++src; - --len; - } + char buf[5]; + while (*src && len != 0) { + char16_t c = static_cast<char16_t>(dtohs(*src)); + utf16_to_utf8(&c, 1, buf, sizeof(buf)); + out->append(buf, strlen(buf)); + ++src; + --len; } } @@ -68,10 +63,8 @@ std::string Utf16ToUtf8(StringPiece16 utf16) { } std::string utf8; - utf8.resize_and_overwrite(utf8_length, [&utf16](char* data, size_t size) { - utf16_to_utf8(utf16.data(), utf16.length(), data, size + 1); - return size; - }); + utf8.resize(utf8_length); + utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8_length + 1); return utf8; } diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp index a1385f2cf7b1..f7f62c51a25b 100644 --- a/libs/androidfw/ZipUtils.cpp +++ b/libs/androidfw/ZipUtils.cpp @@ -87,19 +87,29 @@ class BufferReader final : public zip_archive::Reader { } bool ReadAtOffset(uint8_t* buf, size_t len, off64_t offset) const override { - if (mInputSize < len || offset > mInputSize - len) { - return false; - } - - const incfs::map_ptr<uint8_t> pos = mInput.offset(offset); - if (!pos.verify(len)) { + auto in = AccessAtOffset(buf, len, offset); + if (!in) { return false; } - - memcpy(buf, pos.unsafe_ptr(), len); + memcpy(buf, in, len); return true; } + const uint8_t* AccessAtOffset(uint8_t*, size_t len, off64_t offset) const override { + if (offset > mInputSize - len) { + return nullptr; + } + const incfs::map_ptr<uint8_t> pos = mInput.offset(offset); + if (!pos.verify(len)) { + return nullptr; + } + return pos.unsafe_ptr(); + } + + bool IsZeroCopy() const override { + return true; + } + private: const incfs::map_ptr<uint8_t> mInput; const size_t mInputSize; @@ -107,7 +117,7 @@ class BufferReader final : public zip_archive::Reader { class BufferWriter final : public zip_archive::Writer { public: - BufferWriter(void* output, size_t outputSize) : Writer(), + BufferWriter(void* output, size_t outputSize) : mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) { } @@ -121,6 +131,12 @@ class BufferWriter final : public zip_archive::Writer { return true; } + Buffer GetBuffer(size_t length) override { + const auto remaining_size = mOutputSize - mBytesWritten; + return remaining_size >= length + ? Buffer(mOutput + mBytesWritten, remaining_size) : Buffer(); + } + private: uint8_t* const mOutput; const size_t mOutputSize; diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h index 3f6f4661f2f7..231808beb718 100644 --- a/libs/androidfw/include/androidfw/ApkAssets.h +++ b/libs/androidfw/include/androidfw/ApkAssets.h @@ -116,7 +116,7 @@ class ApkAssets : public RefBase { return resources_asset_ != nullptr && resources_asset_->isAllocated(); } - UpToDate IsUpToDate() const; + bool IsUpToDate() const; // DANGER! // This is a destructive method that rips the assets provider out of ApkAssets object. diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h index e3b3ae41f7f4..d33c325ff369 100644 --- a/libs/androidfw/include/androidfw/AssetsProvider.h +++ b/libs/androidfw/include/androidfw/AssetsProvider.h @@ -14,7 +14,8 @@ * limitations under the License. */ -#pragma once +#ifndef ANDROIDFW_ASSETSPROVIDER_H +#define ANDROIDFW_ASSETSPROVIDER_H #include <memory> #include <string> @@ -57,7 +58,7 @@ struct AssetsProvider { WARN_UNUSED virtual const std::string& GetDebugName() const = 0; // Returns whether the interface provides the most recent version of its files. - WARN_UNUSED virtual UpToDate IsUpToDate() const = 0; + WARN_UNUSED virtual bool IsUpToDate() const = 0; // Creates an Asset from a file on disk. static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); @@ -94,7 +95,7 @@ struct ZipAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED UpToDate IsUpToDate() const override; + WARN_UNUSED bool IsUpToDate() const override; WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const; ~ZipAssetsProvider() override = default; @@ -105,7 +106,7 @@ struct ZipAssetsProvider : public AssetsProvider { private: struct PathOrDebugName; ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, package_property_t flags, - ModDate last_mod_time); + time_t last_mod_time); struct PathOrDebugName { static PathOrDebugName Path(std::string value) { @@ -134,7 +135,7 @@ struct ZipAssetsProvider : public AssetsProvider { std::unique_ptr<ZipArchive, ZipCloser> zip_handle_; PathOrDebugName name_; package_property_t flags_; - ModDate last_mod_time_; + time_t last_mod_time_; }; // Supplies assets from a root directory. @@ -146,7 +147,7 @@ struct DirectoryAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED UpToDate IsUpToDate() const override; + WARN_UNUSED bool IsUpToDate() const override; ~DirectoryAssetsProvider() override = default; protected: @@ -155,23 +156,23 @@ struct DirectoryAssetsProvider : public AssetsProvider { bool* file_exists) const override; private: - explicit DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time); + explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time); std::string dir_; - ModDate last_mod_time_; + time_t last_mod_time_; }; // Supplies assets from a `primary` asset provider and falls back to supplying assets from the // `secondary` asset provider if the asset cannot be found in the `primary`. struct MultiAssetsProvider : public AssetsProvider { static std::unique_ptr<AssetsProvider> Create(std::unique_ptr<AssetsProvider>&& primary, - std::unique_ptr<AssetsProvider>&& secondary = {}); + std::unique_ptr<AssetsProvider>&& secondary); bool ForEachFile(const std::string& root_path, base::function_ref<void(StringPiece, FileType)> f) const override; WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED UpToDate IsUpToDate() const override; + WARN_UNUSED bool IsUpToDate() const override; ~MultiAssetsProvider() override = default; protected: @@ -198,7 +199,7 @@ struct EmptyAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED UpToDate IsUpToDate() const override; + WARN_UNUSED bool IsUpToDate() const override; ~EmptyAssetsProvider() override = default; protected: @@ -211,3 +212,5 @@ struct EmptyAssetsProvider : public AssetsProvider { }; } // namespace android + +#endif /* ANDROIDFW_ASSETSPROVIDER_H */ diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index 87f3c9df9a91..ac75eb3bb98c 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -14,7 +14,8 @@ * limitations under the License. */ -#pragma once +#ifndef IDMAP_H_ +#define IDMAP_H_ #include <memory> #include <string> @@ -31,31 +32,6 @@ namespace android { -// An enum that tracks more states than just 'up to date' or 'not' for a resources container: -// there are several cases where we know for sure that the object can't change and won't get -// out of date. Reporting those states to the managed layer allows it to stop checking here -// completely, speeding up the cache lookups by dozens of milliseconds. -enum class UpToDate : int { False, True, Always }; - -// Combines two UpToDate values, and only accesses the second one if it matters to the result. -template <class Getter> -UpToDate combine(UpToDate first, Getter secondGetter) { - switch (first) { - case UpToDate::False: - return UpToDate::False; - case UpToDate::True: { - const auto second = secondGetter(); - return second == UpToDate::False ? UpToDate::False : UpToDate::True; - } - case UpToDate::Always: - return secondGetter(); - } -} - -inline UpToDate fromBool(bool value) { - return value ? UpToDate::True : UpToDate::False; -} - class LoadedIdmap; class IdmapResMap; struct Idmap_header; @@ -220,7 +196,7 @@ class LoadedIdmap { // Returns whether the idmap file on disk has not been modified since the construction of this // LoadedIdmap. - UpToDate IsUpToDate() const; + bool IsUpToDate() const; protected: // Exposed as protected so that tests can subclass and mock this class out. @@ -255,3 +231,5 @@ class LoadedIdmap { }; } // namespace android + +#endif // IDMAP_H_ diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 819fe4b38c87..e330410ed1a0 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -47,8 +47,6 @@ namespace android { -constexpr const bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; - constexpr const uint32_t kIdmapMagic = 0x504D4449u; constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Au; @@ -410,16 +408,7 @@ struct Res_value typedef uint32_t data_type; data_type data; - void copyFrom_dtoh(const Res_value& src) { - if constexpr (kDeviceEndiannessSame) { - *this = src; - } else { - copyFrom_dtoh_slow(src); - } - } - - private: - void copyFrom_dtoh_slow(const Res_value& src); + void copyFrom_dtoh(const Res_value& src); }; /** @@ -1265,32 +1254,11 @@ struct ResTable_config // Varies in length from 3 to 8 chars. Zero-filled value. char localeNumberingSystem[8]; - void copyFromDeviceNoSwap(const ResTable_config& o) { - const auto o_size = dtohl(o.size); - if (o_size >= sizeof(ResTable_config)) [[likely]] { - *this = o; - } else { - memcpy(this, &o, o_size); - memset(((uint8_t*)this) + o_size, 0, sizeof(ResTable_config) - o_size); - } - this->size = sizeof(*this); - } - - void copyFromDtoH(const ResTable_config& o) { - if constexpr (kDeviceEndiannessSame) { - copyFromDeviceNoSwap(o); - } else { - copyFromDtoH_slow(o); - } - } - - void swapHtoD() { - if constexpr (kDeviceEndiannessSame) { - ; // noop - } else { - swapHtoD_slow(); - } - } + void copyFromDeviceNoSwap(const ResTable_config& o); + + void copyFromDtoH(const ResTable_config& o); + + void swapHtoD(); int compare(const ResTable_config& o) const; int compareLogical(const ResTable_config& o) const; @@ -1416,10 +1384,6 @@ struct ResTable_config bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const; String8 toString() const; - - private: - void copyFromDtoH_slow(const ResTable_config& o); - void swapHtoD_slow(); }; /** diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index d8ca64a174a2..c9ba8a01a5e9 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -15,7 +15,6 @@ */ #pragma once -#include <sys/stat.h> #include <time.h> // @@ -65,15 +64,10 @@ ModDate getFileModDate(const char* fileName); /* same, but also returns -1 if the file has already been deleted */ ModDate getFileModDate(int fd); -// Extract the modification date from the stat structure. -ModDate getModDate(const struct ::stat& st); - // Check if |path| or |fd| resides on a readonly filesystem. bool isReadonlyFilesystem(const char* path); bool isReadonlyFilesystem(int fd); -bool isKnownWritablePath(const char* path); - } // namespace android // Whoever uses getFileModDate() will need this as well diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 26eb320805c9..32f3624a3aee 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -16,10 +16,10 @@ #define LOG_TAG "misc" -#include "androidfw/misc.h" - -#include <errno.h> -#include <sys/stat.h> +// +// Miscellaneous utility functions. +// +#include <androidfw/misc.h> #include "android-base/logging.h" @@ -28,7 +28,9 @@ #include <sys/vfs.h> #endif // __linux__ -#include <array> +#include <errno.h> +#include <sys/stat.h> + #include <cstdio> #include <cstring> #include <tuple> @@ -38,26 +40,28 @@ namespace android { /* * Get a file's type. */ -FileType getFileType(const char* fileName) { - struct stat sb; - if (stat(fileName, &sb) < 0) { - if (errno == ENOENT || errno == ENOTDIR) - return kFileTypeNonexistent; - else { - PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; - return kFileTypeUnknown; - } - } else { - if (S_ISREG(sb.st_mode)) - return kFileTypeRegular; - else if (S_ISDIR(sb.st_mode)) - return kFileTypeDirectory; - else if (S_ISCHR(sb.st_mode)) - return kFileTypeCharDev; - else if (S_ISBLK(sb.st_mode)) - return kFileTypeBlockDev; - else if (S_ISFIFO(sb.st_mode)) - return kFileTypeFifo; +FileType getFileType(const char* fileName) +{ + struct stat sb; + + if (stat(fileName, &sb) < 0) { + if (errno == ENOENT || errno == ENOTDIR) + return kFileTypeNonexistent; + else { + PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; + return kFileTypeUnknown; + } + } else { + if (S_ISREG(sb.st_mode)) + return kFileTypeRegular; + else if (S_ISDIR(sb.st_mode)) + return kFileTypeDirectory; + else if (S_ISCHR(sb.st_mode)) + return kFileTypeCharDev; + else if (S_ISBLK(sb.st_mode)) + return kFileTypeBlockDev; + else if (S_ISFIFO(sb.st_mode)) + return kFileTypeFifo; #if defined(S_ISLNK) else if (S_ISLNK(sb.st_mode)) return kFileTypeSymlink; @@ -71,7 +75,7 @@ FileType getFileType(const char* fileName) { } } -ModDate getModDate(const struct stat& st) { +static ModDate getModDate(const struct stat& st) { #ifdef _WIN32 return st.st_mtime; #elif defined(__APPLE__) @@ -109,14 +113,8 @@ bool isReadonlyFilesystem(const char*) { bool isReadonlyFilesystem(int) { return false; } -bool isKnownWritablePath(const char*) { - return false; -} #else // __linux__ bool isReadonlyFilesystem(const char* path) { - if (isKnownWritablePath(path)) { - return false; - } struct statfs sfs; if (::statfs(path, &sfs)) { PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed"; @@ -133,13 +131,6 @@ bool isReadonlyFilesystem(int fd) { } return (sfs.f_flags & ST_RDONLY) != 0; } - -bool isKnownWritablePath(const char* path) { - // We know that all paths in /data/ are writable. - static constexpr char kRwPrefix[] = "/data/"; - return strncmp(kRwPrefix, path, std::size(kRwPrefix) - 1) == 0; -} - #endif // __linux__ } // namespace android diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index 22b9e69500d9..cb2e56f5f5e4 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -218,11 +218,10 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { auto apk_assets = ApkAssets::LoadOverlay(temp_file.path); ASSERT_NE(nullptr, apk_assets); - ASSERT_TRUE(apk_assets->IsOverlay()); - ASSERT_EQ(UpToDate::True, apk_assets->IsUpToDate()); + ASSERT_TRUE(apk_assets->IsUpToDate()); unlink(temp_file.path); - ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); + ASSERT_FALSE(apk_assets->IsUpToDate()); const auto sleep_duration = std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull)); @@ -231,27 +230,7 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { base::WriteStringToFile("hello", temp_file.path); std::this_thread::sleep_for(sleep_duration); - ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); -} - -TEST(IdmapTestUpToDate, Combine) { - ASSERT_EQ(UpToDate::False, combine(UpToDate::False, [] { - ADD_FAILURE(); // Shouldn't get called at all. - return UpToDate::False; - })); - - ASSERT_EQ(UpToDate::False, combine(UpToDate::True, [] { return UpToDate::False; })); - - ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::True; })); - ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::Always; })); - ASSERT_EQ(UpToDate::True, combine(UpToDate::Always, [] { return UpToDate::True; })); - - ASSERT_EQ(UpToDate::Always, combine(UpToDate::Always, [] { return UpToDate::Always; })); -} - -TEST(IdmapTestUpToDate, FromBool) { - ASSERT_EQ(UpToDate::False, fromBool(false)); - ASSERT_EQ(UpToDate::True, fromBool(true)); + ASSERT_FALSE(apk_assets->IsUpToDate()); } } // namespace diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index f8fb6b7207a0..139ccfd22b0e 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -36,8 +36,7 @@ package com.android.extensions.appfunctions { public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.content.pm.SigningInfo, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); - method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); + method @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java index a09451ede4fc..81d9d81c4f58 100644 --- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java @@ -25,7 +25,6 @@ import android.annotation.Nullable; import android.annotation.SdkConstant; import android.app.Service; import android.content.Intent; -import android.content.pm.SigningInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; @@ -82,7 +81,6 @@ public abstract class AppFunctionService extends Service { SidecarConverter.getSidecarExecuteAppFunctionRequest( platformRequest), callingPackage, - callingPackageSigningInfo, cancellationSignal, new OutcomeReceiver<>() { @Override @@ -129,52 +127,10 @@ public abstract class AppFunctionService extends Service { * * @param request The function execution request. * @param callingPackage The package name of the app that is requesting the execution. - * @param callingPackageSigningInfo The signing information of the app that is requesting the - * execution. * @param cancellationSignal A signal to cancel the execution. * @param callback A callback to report back the result or error. */ @MainThread - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull String callingPackage, - @NonNull SigningInfo callingPackageSigningInfo, - @NonNull CancellationSignal cancellationSignal, - @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback) { - onExecuteFunction(request, callingPackage, cancellationSignal, callback); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is the entry point for handling all app function requests in an app. When the - * system needs your AppFunctionService to perform a function, it will invoke this method. - * - * <p>Each function you've registered is identified by a unique identifier. This identifier - * doesn't need to be globally unique, but it must be unique within your app. For example, a - * function to order food could be identified as "orderFood". In most cases, this identifier is - * automatically generated by the AppFunctions SDK. - * - * <p>You can determine which function to execute by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the - * incoming request to the appropriate logic for handling the specific function. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * - * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel - * the execution of function if requested by the system. - * - * @param request The function execution request. - * @param callingPackage The package name of the app that is requesting the execution. - * @param cancellationSignal A signal to cancel the execution. - * @param callback A callback to report back the result or error. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, SigningInfo, - * CancellationSignal, OutcomeReceiver)} instead. - */ - @MainThread - @Deprecated public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index 7b45070af312..290df997a8ed 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -36,7 +36,7 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint, const Typeface* resolvedFace = Typeface::resolveDefault(typeface); const SkFont& font = paint->getSkFont(); - minikin::MinikinPaint minikinPaint(resolvedFace->fFontCollection); + minikin::MinikinPaint minikinPaint(resolvedFace->getFontCollection()); /* Prepare minikin Paint */ minikinPaint.size = font.isLinearMetrics() ? font.getSize() : static_cast<int>(font.getSize()); @@ -46,9 +46,9 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint, minikinPaint.wordSpacing = paint->getWordSpacing(); minikinPaint.fontFlags = MinikinFontSkia::packFontFlags(font); minikinPaint.localeListId = paint->getMinikinLocaleListId(); - minikinPaint.fontStyle = resolvedFace->fStyle; + minikinPaint.fontStyle = resolvedFace->getFontStyle(); minikinPaint.fontFeatureSettings = paint->getFontFeatureSettings(); - if (!resolvedFace->fIsVariationInstance) { + if (!resolvedFace->isVariationInstance()) { // This is an optimization for direct private API use typically done by System UI. // In the public API surface, if Typeface is already configured for variation instance // (Target SDK <= 35) the font variation settings of Paint is not set. @@ -132,7 +132,7 @@ minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin:: bool MinikinUtils::hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs) { const Typeface* resolvedFace = Typeface::resolveDefault(typeface); - return resolvedFace->fFontCollection->hasVariationSelector(codepoint, vs); + return resolvedFace->getFontCollection()->hasVariationSelector(codepoint, vs); } float MinikinUtils::xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout) { diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index 4dfe05377a48..a73aac632752 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -70,74 +70,45 @@ const Typeface* Typeface::resolveDefault(const Typeface* src) { Typeface* Typeface::createRelative(Typeface* src, Typeface::Style style) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface; - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = style; - result->fStyle = computeRelativeStyle(result->fBaseWeight, style); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), + computeRelativeStyle(resolvedFace->getBaseWeight(), style), style, + resolvedFace->getBaseWeight(), resolvedFace->isVariationInstance()); } Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) { const Typeface* resolvedFace = Typeface::resolveDefault(base); - Typeface* result = new Typeface(); - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = computeAPIStyle(weight, italic); - result->fStyle = computeMinikinStyle(weight, italic); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), computeMinikinStyle(weight, italic), + computeAPIStyle(weight, italic), resolvedFace->getBaseWeight(), + resolvedFace->isVariationInstance()); } Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src, const minikin::VariationSettings& variations) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface(); - if (result != nullptr) { - result->fFontCollection = - resolvedFace->fFontCollection->createCollectionWithVariation(variations); - if (result->fFontCollection == nullptr) { + const std::shared_ptr<minikin::FontCollection>& fc = + resolvedFace->getFontCollection()->createCollectionWithVariation(variations); + return new Typeface( // None of passed axes are supported by this collection. // So we will reuse the same collection with incrementing reference count. - result->fFontCollection = resolvedFace->fFontCollection; - } - // Do not update styles. - // TODO: We may want to update base weight if the 'wght' is specified. - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = resolvedFace->fAPIStyle; - result->fStyle = resolvedFace->fStyle; - result->fIsVariationInstance = true; - } - return result; + fc ? fc : resolvedFace->getFontCollection(), + // Do not update styles. + // TODO: We may want to update base weight if the 'wght' is specified. + resolvedFace->fStyle, resolvedFace->getAPIStyle(), resolvedFace->getBaseWeight(), true); } Typeface* Typeface::createWithDifferentBaseWeight(Typeface* src, int weight) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface; - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = weight; - result->fAPIStyle = resolvedFace->fAPIStyle; - result->fStyle = computeRelativeStyle(weight, result->fAPIStyle); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), + computeRelativeStyle(weight, resolvedFace->getAPIStyle()), + resolvedFace->getAPIStyle(), weight, resolvedFace->isVariationInstance()); } Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic, const Typeface* fallback) { - Typeface* result = new Typeface; - if (fallback == nullptr) { - result->fFontCollection = minikin::FontCollection::create(std::move(families)); - } else { - result->fFontCollection = - fallback->fFontCollection->createCollectionWithFamilies(std::move(families)); - } + const std::shared_ptr<minikin::FontCollection>& fc = + fallback ? fallback->getFontCollection()->createCollectionWithFamilies( + std::move(families)) + : minikin::FontCollection::create(std::move(families)); if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) { int weightFromFont; @@ -171,11 +142,8 @@ Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::Font weight = SkFontStyle::kNormal_Weight; } - result->fBaseWeight = weight; - result->fAPIStyle = computeAPIStyle(weight, italic); - result->fStyle = computeMinikinStyle(weight, italic); - result->fIsVariationInstance = false; - return result; + return new Typeface(fc, computeMinikinStyle(weight, italic), computeAPIStyle(weight, italic), + weight, false); } void Typeface::setDefault(const Typeface* face) { @@ -205,11 +173,8 @@ void Typeface::setRobotoTypefaceForTest() { std::shared_ptr<minikin::FontCollection> collection = minikin::FontCollection::create(minikin::FontFamily::create(std::move(fonts))); - Typeface* hwTypeface = new Typeface(); - hwTypeface->fFontCollection = collection; - hwTypeface->fAPIStyle = Typeface::kNormal; - hwTypeface->fBaseWeight = SkFontStyle::kNormal_Weight; - hwTypeface->fStyle = minikin::FontStyle(); + Typeface* hwTypeface = new Typeface(collection, minikin::FontStyle(), Typeface::kNormal, + SkFontStyle::kNormal_Weight, false); Typeface::setDefault(hwTypeface); #endif diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h index 97d1bf4ef011..e8233a6bc6d8 100644 --- a/libs/hwui/hwui/Typeface.h +++ b/libs/hwui/hwui/Typeface.h @@ -32,21 +32,39 @@ constexpr int RESOLVE_BY_FONT_TABLE = -1; struct ANDROID_API Typeface { public: - std::shared_ptr<minikin::FontCollection> fFontCollection; + enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 }; + Typeface(const std::shared_ptr<minikin::FontCollection> fc, minikin::FontStyle style, + Style apiStyle, int baseWeight, bool isVariationInstance) + : fFontCollection(fc) + , fStyle(style) + , fAPIStyle(apiStyle) + , fBaseWeight(baseWeight) + , fIsVariationInstance(isVariationInstance) {} + + const std::shared_ptr<minikin::FontCollection>& getFontCollection() const { + return fFontCollection; + } // resolved style actually used for rendering - minikin::FontStyle fStyle; + minikin::FontStyle getFontStyle() const { return fStyle; } // style used in the API - enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 }; - Style fAPIStyle; + Style getAPIStyle() const { return fAPIStyle; } // base weight in CSS-style units, 1..1000 - int fBaseWeight; + int getBaseWeight() const { return fBaseWeight; } // True if the Typeface is already created for variation settings. - bool fIsVariationInstance; + bool isVariationInstance() const { return fIsVariationInstance; } +private: + std::shared_ptr<minikin::FontCollection> fFontCollection; + minikin::FontStyle fStyle; + Style fAPIStyle; + int fBaseWeight; + bool fIsVariationInstance = false; + +public: static const Typeface* resolveDefault(const Typeface* src); // The following three functions create new Typeface from an existing Typeface with a different diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index 8d3a5eb2b4af..f6fdec1c82bc 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -609,7 +609,8 @@ namespace PaintGlue { SkFont* font = &paint->getSkFont(); const Typeface* typeface = paint->getAndroidTypeface(); typeface = Typeface::resolveDefault(typeface); - minikin::FakedFont baseFont = typeface->fFontCollection->baseFontFaked(typeface->fStyle); + minikin::FakedFont baseFont = + typeface->getFontCollection()->baseFontFaked(typeface->getFontStyle()); float saveSkewX = font->getSkewX(); bool savefakeBold = font->isEmbolden(); MinikinFontSkia::populateSkFont(font, baseFont.typeface().get(), baseFont.fakery); @@ -641,7 +642,7 @@ namespace PaintGlue { if (useLocale) { minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface); minikin::MinikinExtent extent = - typeface->fFontCollection->getReferenceExtentForLocale(minikinPaint); + typeface->getFontCollection()->getReferenceExtentForLocale(minikinPaint); metrics->fAscent = std::min(extent.ascent, metrics->fAscent); metrics->fDescent = std::max(extent.descent, metrics->fDescent); metrics->fTop = std::min(metrics->fAscent, metrics->fTop); diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index c5095c1a0704..63906de80745 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -99,17 +99,17 @@ static jlong Typeface_getReleaseFunc(CRITICAL_JNI_PARAMS) { // CriticalNative static jint Typeface_getStyle(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fAPIStyle; + return toTypeface(faceHandle)->getAPIStyle(); } // CriticalNative static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fStyle.weight(); + return toTypeface(faceHandle)->getFontStyle().weight(); } // Critical Native static jboolean Typeface_isVariationInstance(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fIsVariationInstance; + return toTypeface(faceHandle)->isVariationInstance(); } static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray, @@ -128,18 +128,18 @@ static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArr // CriticalNative static void Typeface_setDefault(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { Typeface::setDefault(toTypeface(faceHandle)); - minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->fFontCollection); + minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->getFontCollection()); } static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) { Typeface* face = toTypeface(faceHandle); - const size_t length = face->fFontCollection->getSupportedAxesCount(); + const size_t length = face->getFontCollection()->getSupportedAxesCount(); if (length == 0) { return nullptr; } std::vector<jint> tagVec(length); for (size_t i = 0; i < length; i++) { - tagVec[i] = face->fFontCollection->getSupportedAxisAt(i); + tagVec[i] = face->getFontCollection()->getSupportedAxisAt(i); } std::sort(tagVec.begin(), tagVec.end()); const jintArray result = env->NewIntArray(length); @@ -150,7 +150,7 @@ static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) static void Typeface_registerGenericFamily(JNIEnv *env, jobject, jstring familyName, jlong ptr) { ScopedUtfChars familyNameChars(env, familyName); minikin::SystemFonts::registerFallback(familyNameChars.c_str(), - toTypeface(ptr)->fFontCollection); + toTypeface(ptr)->getFontCollection()); } #ifdef __ANDROID__ @@ -315,18 +315,19 @@ static jint Typeface_writeTypefaces(JNIEnv* env, jobject, jobject buffer, jint p std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections; std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex; for (Typeface* typeface : typefaces) { - bool inserted = fcToIndex.emplace(typeface->fFontCollection, fontCollections.size()).second; + bool inserted = + fcToIndex.emplace(typeface->getFontCollection(), fontCollections.size()).second; if (inserted) { - fontCollections.push_back(typeface->fFontCollection); + fontCollections.push_back(typeface->getFontCollection()); } } minikin::FontCollection::writeVector(&writer, fontCollections); writer.write<uint32_t>(typefaces.size()); for (Typeface* typeface : typefaces) { - writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second); - typeface->fStyle.writeTo(&writer); - writer.write<Typeface::Style>(typeface->fAPIStyle); - writer.write<int>(typeface->fBaseWeight); + writer.write<uint32_t>(fcToIndex.find(typeface->getFontCollection())->second); + typeface->getFontStyle().writeTo(&writer); + writer.write<Typeface::Style>(typeface->getAPIStyle()); + writer.write<int>(typeface->getBaseWeight()); } return static_cast<jint>(writer.size()); } @@ -349,12 +350,10 @@ static jlongArray Typeface_readTypefaces(JNIEnv* env, jobject, jobject buffer, j std::vector<jlong> faceHandles; faceHandles.reserve(typefaceCount); for (uint32_t i = 0; i < typefaceCount; i++) { - Typeface* typeface = new Typeface; - typeface->fFontCollection = fontCollections[reader.read<uint32_t>()]; - typeface->fStyle = minikin::FontStyle(&reader); - typeface->fAPIStyle = reader.read<Typeface::Style>(); - typeface->fBaseWeight = reader.read<int>(); - typeface->fIsVariationInstance = false; + Typeface* typeface = + new Typeface(fontCollections[reader.read<uint32_t>()], minikin::FontStyle(&reader), + reader.read<Typeface::Style>(), reader.read<int>(), + false /* isVariationInstance */); faceHandles.push_back(toJLong(typeface)); } const jlongArray result = env->NewLongArray(typefaceCount); @@ -382,7 +381,8 @@ static void Typeface_warmUpCache(JNIEnv* env, jobject, jstring jFilePath) { // Critical Native static void Typeface_addFontCollection(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - std::shared_ptr<minikin::FontCollection> collection = toTypeface(faceHandle)->fFontCollection; + std::shared_ptr<minikin::FontCollection> collection = + toTypeface(faceHandle)->getFontCollection(); minikin::SystemFonts::addFontMap(std::move(collection)); } diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp index d1782b285b34..7a4ae8330de8 100644 --- a/libs/hwui/jni/text/TextShaper.cpp +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -104,7 +104,7 @@ static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int cou } else { fontId = fonts.size(); // This is new to us. Create new one. std::shared_ptr<minikin::Font> font; - if (resolvedFace->fIsVariationInstance) { + if (resolvedFace->isVariationInstance()) { // The optimization for target SDK 35 or before because the variation instance // is already created and no runtime variation resolution happens on such // environment. diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 6571d92aeafa..a67aea466c1c 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -729,7 +729,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { VkSemaphore semaphore; VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore); ALOGE_IF(VK_SUCCESS != err, - "VulkanManager::makeSwapSemaphore(): Failed to create semaphore"); + "VulkanManager::finishFrame(): Failed to create semaphore"); if (err == VK_SUCCESS) { sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore); @@ -777,7 +777,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { int fenceFd = -1; VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd); - ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd"); + ALOGE_IF(VK_SUCCESS != err, "VulkanManager::finishFrame(): Failed to get semaphore Fd"); drawResult.presentFence.reset(fenceFd); } else { ALOGE("VulkanManager::finishFrame(): Semaphore submission failed"); diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index 93118aeafaaf..b51414fd3c02 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -183,8 +183,11 @@ SkRect TestUtils::getLocalClipBounds(const SkCanvas* canvas) { } SkFont TestUtils::defaultFont() { - const std::shared_ptr<minikin::MinikinFont>& minikinFont = - Typeface::resolveDefault(nullptr)->fFontCollection->getFamilyAt(0)->getFont(0)->baseTypeface(); + const std::shared_ptr<minikin::MinikinFont>& minikinFont = Typeface::resolveDefault(nullptr) + ->getFontCollection() + ->getFamilyAt(0) + ->getFont(0) + ->baseTypeface(); SkTypeface* skTypeface = reinterpret_cast<const MinikinFontSkia*>(minikinFont.get())->GetSkTypeface(); LOG_ALWAYS_FATAL_IF(skTypeface == nullptr); return SkFont(sk_ref_sp(skTypeface)); diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp index c71c4d243a8b..7bcd937397b0 100644 --- a/libs/hwui/tests/unit/TypefaceTests.cpp +++ b/libs/hwui/tests/unit/TypefaceTests.cpp @@ -90,40 +90,40 @@ TEST(TypefaceTest, resolveDefault_and_setDefaultTest) { TEST(TypefaceTest, createWithDifferentBaseWeight) { std::unique_ptr<Typeface> bold(Typeface::createWithDifferentBaseWeight(nullptr, 700)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, bold->getAPIStyle()); std::unique_ptr<Typeface> light(Typeface::createWithDifferentBaseWeight(nullptr, 300)); - EXPECT_EQ(300, light->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, light->fAPIStyle); + EXPECT_EQ(300, light->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, light->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromRegular) { // In Java, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(nullptr, Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(nullptr, Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(nullptr, Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic(Typeface::createRelative(nullptr, Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_BoldBase) { @@ -132,31 +132,31 @@ TEST(TypefaceTest, createRelativeTest_BoldBase) { // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(700, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(700, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(1000, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(1000, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(700, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(1000, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(1000, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_LightBase) { @@ -165,31 +165,31 @@ TEST(TypefaceTest, createRelativeTest_LightBase) { // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(300, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(300, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(600, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(600, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.ITLIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(300, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(300, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(600, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(600, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromBoldStyled) { @@ -198,32 +198,32 @@ TEST(TypefaceTest, createRelativeTest_fromBoldStyled) { // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromItalicStyled) { @@ -233,33 +233,33 @@ TEST(TypefaceTest, createRelativeTest_fromItalicStyled) { // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, // Typeface.ITALIC), Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { @@ -270,27 +270,27 @@ TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") @@ -298,9 +298,9 @@ TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { // Typeface.create(typeface, Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createAbsolute) { @@ -309,45 +309,45 @@ TEST(TypefaceTest, createAbsolute) { // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(false) // .build(); std::unique_ptr<Typeface> regular(Typeface::createAbsolute(nullptr, 400, false)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(false) // .build(); std::unique_ptr<Typeface> bold(Typeface::createAbsolute(nullptr, 700, false)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(true) // .build(); std::unique_ptr<Typeface> italic(Typeface::createAbsolute(nullptr, 400, true)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(true) // .build(); std::unique_ptr<Typeface> boldItalic(Typeface::createAbsolute(nullptr, 700, true)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(1100).setItalic(true) // .build(); std::unique_ptr<Typeface> over1000(Typeface::createAbsolute(nullptr, 1100, false)); - EXPECT_EQ(1000, over1000->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle); + EXPECT_EQ(1000, over1000->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Single) { @@ -355,43 +355,43 @@ TEST(TypefaceTest, createFromFamilies_Single) { // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build(); std::unique_ptr<Typeface> regular(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build(); std::unique_ptr<Typeface> bold(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build(); std::unique_ptr<Typeface> italic(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build(); std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build(); std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */)); - EXPECT_EQ(1000, over1000->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle); + EXPECT_EQ(1000, over1000->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { @@ -399,33 +399,33 @@ TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { std::unique_ptr<Typeface> regular( Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, new Typeface.Builder("Family-Bold.ttf").build(); std::unique_ptr<Typeface> bold( Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, new Typeface.Builder("Family-Italic.ttf").build(); std::unique_ptr<Typeface> italic( Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build(); std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies( makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Family) { @@ -435,8 +435,8 @@ TEST(TypefaceTest, createFromFamilies_Family) { std::unique_ptr<Typeface> typeface( Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, typeface->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant()); + EXPECT_EQ(400, typeface->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant()); } TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) { @@ -445,8 +445,8 @@ TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) { std::unique_ptr<Typeface> typeface( Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, typeface->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant()); + EXPECT_EQ(700, typeface->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant()); } TEST(TypefaceTest, createFromFamilies_Family_withFallback) { @@ -458,8 +458,8 @@ TEST(TypefaceTest, createFromFamilies_Family_withFallback) { std::unique_ptr<Typeface> regular( Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, fallback.get())); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); } } // namespace diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING index e52e0b16eca3..6a21496f1165 100644 --- a/media/TEST_MAPPING +++ b/media/TEST_MAPPING @@ -1,7 +1,10 @@ { "presubmit": [ { - "name": "CtsMediaBetterTogetherTestCases" + "name": "CtsMediaRouterTestCases" + }, + { + "name": "CtsMediaSessionTestCases" }, { "name": "mediaroutertest" diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 54a87ad7fd39..2a740f85aa72 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -32,6 +32,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; +import android.media.IAudioManagerNative; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; @@ -83,6 +84,7 @@ interface IAudioService { // When a method's argument list is changed, BpAudioManager's corresponding serialization code // (if any) in frameworks/native/services/audiomanager/IAudioManager.cpp must be updated. + IAudioManagerNative getNativeInterface(); int trackPlayer(in PlayerBase.PlayerIdCard pic); oneway void playerAttributes(in int piid, in AudioAttributes attr); diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index bbb03e77c8c9..88981eac9bb5 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -961,8 +961,7 @@ public final class MediaRoute2Info implements Parcelable { * * @hide */ - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) - public boolean isVisibleTo(String packageName) { + public boolean isVisibleTo(@NonNull String packageName) { return !mIsVisibilityRestricted || TextUtils.equals(getProviderPackageName(), packageName) || mAllowedPackages.contains(packageName); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3738312b762f..e57148fe5a6a 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -19,7 +19,6 @@ package android.media; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES; -import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME; import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING; @@ -1406,7 +1405,6 @@ public final class MediaRouter2 { requestCreateController(controller, route, managerRequestId); } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getSortedRoutes( List<MediaRoute2Info> routes, List<String> packageOrder) { if (packageOrder.isEmpty()) { @@ -1427,7 +1425,6 @@ public final class MediaRouter2 { } @GuardedBy("mLock") - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked( List<MediaRoute2Info> routes) { @@ -3654,7 +3651,6 @@ public final class MediaRouter2 { } } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) @Override public List<MediaRoute2Info> filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) { diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 3854747f46e0..3f18eef2f9aa 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -20,11 +20,9 @@ import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING; import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; -import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME; import android.Manifest; import android.annotation.CallbackExecutor; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -287,7 +285,6 @@ public final class MediaRouter2Manager { (route) -> sessionInfo.isSystemSession() ^ route.isSystemRoute()); } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getSortedRoutes(RouteDiscoveryPreference preference) { if (!preference.shouldRemoveDuplicates()) { synchronized (mRoutesLock) { @@ -311,7 +308,6 @@ public final class MediaRouter2Manager { return routes; } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getFilteredRoutes( @NonNull RoutingSessionInfo sessionInfo, boolean includeSelectedRoutes, diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 4398b261377b..c48b5f4e4aea 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -11,6 +11,16 @@ flag { } flag { + name: "disable_set_bluetooth_ad2p_on_calls" + namespace: "media_better_together" + description: "Prevents calls to AudioService.setBluetoothA2dpOn(), known to cause incorrect audio routing to the built-in speakers." + bug: "294968421" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_audio_input_device_routing_and_volume_control" namespace: "media_better_together" description: "Allows audio input devices routing and volume control via system settings." diff --git a/mime/Android.bp b/mime/Android.bp index 20110f1dfb47..b609548fcbab 100644 --- a/mime/Android.bp +++ b/mime/Android.bp @@ -49,6 +49,17 @@ java_library { ], } +java_library { + name: "mimemap-testing-alt", + defaults: ["mimemap-defaults"], + static_libs: ["mimemap-testing-alt-res.jar"], + jarjar_rules: "jarjar-rules-alt.txt", + visibility: [ + "//cts/tests/tests/mimemap:__subpackages__", + "//frameworks/base:__subpackages__", + ], +} + // The mimemap-res.jar and mimemap-testing-res.jar genrules produce a .jar that // has the resource file in a subdirectory res/ and testres/, respectively. // They need to be in different paths because one of them ends up in a @@ -86,6 +97,19 @@ java_genrule { cmd: "mkdir $(genDir)/testres/ && cp $(in) $(genDir)/testres/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres/", } +// The same as mimemap-testing-res.jar except that the resources are placed in a different directory. +// They get bundled with CTS so that CTS can compare a device's MimeMap implementation vs. +// the stock Android one from when CTS was built. +java_genrule { + name: "mimemap-testing-alt-res.jar", + tools: [ + "soong_zip", + ], + srcs: [":mime.types.minimized-alt"], + out: ["mimemap-testing-alt-res.jar"], + cmd: "mkdir $(genDir)/testres-alt/ && cp $(in) $(genDir)/testres-alt/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres-alt/", +} + // Combination of all *mime.types.minimized resources. filegroup { name: "mime.types.minimized", @@ -99,6 +123,19 @@ filegroup { ], } +// Combination of all *mime.types.minimized resources. +filegroup { + name: "mime.types.minimized-alt", + visibility: [ + "//visibility:private", + ], + device_common_srcs: [ + ":debian.mime.types.minimized-alt", + ":android.mime.types.minimized", + ":vendor.mime.types.minimized", + ], +} + java_genrule { name: "android.mime.types.minimized", visibility: [ diff --git a/mime/jarjar-rules-alt.txt b/mime/jarjar-rules-alt.txt new file mode 100644 index 000000000000..9a7644325336 --- /dev/null +++ b/mime/jarjar-rules-alt.txt @@ -0,0 +1 @@ +rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidAltMimeMapFactory diff --git a/mime/jarjar-rules.txt b/mime/jarjar-rules.txt index 145d1dbf3d11..e1ea8e10314c 100644 --- a/mime/jarjar-rules.txt +++ b/mime/jarjar-rules.txt @@ -1 +1 @@ -rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory
\ No newline at end of file +rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory diff --git a/native/android/tests/system_health/OWNERS b/native/android/tests/system_health/OWNERS new file mode 100644 index 000000000000..e3bbee92057d --- /dev/null +++ b/native/android/tests/system_health/OWNERS @@ -0,0 +1 @@ +include /ADPF_OWNERS diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java index 068074ae1b89..8e52a00fe545 100644 --- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java +++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java @@ -38,6 +38,7 @@ import android.location.provider.LocationProviderBase; import android.location.provider.ProviderProperties; import android.location.provider.ProviderRequest; import android.os.Bundle; +import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -301,8 +302,13 @@ public class FusedLocationProvider extends LocationProviderBase { .setWorkSource(mRequest.getWorkSource()) .setHiddenFromAppOps(true) .build(); - mLocationManager.requestLocationUpdates(mProvider, request, - mContext.getMainExecutor(), this); + + try { + mLocationManager.requestLocationUpdates( + mProvider, request, mContext.getMainExecutor(), this); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to request location updates"); + } } } } @@ -311,7 +317,11 @@ public class FusedLocationProvider extends LocationProviderBase { synchronized (mLock) { int requestCode = mNextFlushCode++; mPendingFlushes.put(requestCode, callback); - mLocationManager.requestFlush(mProvider, this, requestCode); + try { + mLocationManager.requestFlush(mProvider, this, requestCode); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to request flush"); + } } } diff --git a/packages/PrintSpooler/Android.bp b/packages/PrintSpooler/Android.bp index 6af3c6624f62..000e20fb4280 100644 --- a/packages/PrintSpooler/Android.bp +++ b/packages/PrintSpooler/Android.bp @@ -59,6 +59,21 @@ android_library { "android-support-core-ui", "android-support-fragment", "android-support-annotations", + "printspooler_aconfig_flags_java_lib", ], manifest: "AndroidManifest.xml", } + +aconfig_declarations { + name: "printspooler_aconfig_declarations", + package: "com.android.printspooler.flags", + container: "system", + srcs: [ + "flags/flags.aconfig", + ], +} + +java_aconfig_library { + name: "printspooler_aconfig_flags_java_lib", + aconfig_declarations: "printspooler_aconfig_declarations", +} diff --git a/packages/PrintSpooler/flags/flags.aconfig b/packages/PrintSpooler/flags/flags.aconfig new file mode 100644 index 000000000000..4a76dff405d0 --- /dev/null +++ b/packages/PrintSpooler/flags/flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.printspooler.flags" +container: "system" + +flag { + name: "log_print_jobs" + namespace: "printing" + description: "Log print job creation and state transitions." + bug: "385340868" +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java index bba57d5fe0a2..1a9309c13bd7 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java @@ -68,6 +68,7 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.dump.DualDumpOutputStream; import com.android.internal.util.function.pooled.PooledLambda; import com.android.printspooler.R; +import com.android.printspooler.flags.Flags; import com.android.printspooler.util.ApprovedPrintServices; import libcore.io.IoUtils; @@ -493,7 +494,7 @@ public final class PrintSpoolerService extends Service { keepAwakeLocked(); } - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[ADD] " + printJob); } } @@ -506,7 +507,7 @@ public final class PrintSpoolerService extends Service { PrintJobInfo printJob = mPrintJobs.get(i); if (isObsoleteState(printJob.getState())) { mPrintJobs.remove(i); - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[REMOVE] " + printJob.getId().flattenToString()); } removePrintJobFileLocked(printJob.getId()); @@ -568,7 +569,7 @@ public final class PrintSpoolerService extends Service { checkIfStillKeepAwakeLocked(); } - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob); } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt index 2fac54557bef..6fc6b5405eb2 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt @@ -22,6 +22,7 @@ import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.ipc.ApiDescriptor import com.android.settingslib.ipc.ApiHandler import com.android.settingslib.ipc.ApiPermissionChecker +import com.android.settingslib.metadata.PreferenceCoordinate import com.android.settingslib.metadata.PreferenceHierarchyNode import com.android.settingslib.metadata.PreferenceScreenRegistry diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt index ff14eb5aae55..70ce62c8383c 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.os.Parcel import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.ipc.MessageCodec +import com.android.settingslib.metadata.PreferenceCoordinate import java.util.Arrays /** Message codec for [PreferenceGetterRequest]. */ diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt index 68aa2d258295..2dd736ae6083 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * Copyright (C) 2025 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settingslib.graph +package com.android.settingslib.metadata import android.os.Parcel import android.os.Parcelable diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index e1e1ee5a8feb..78d6c31ac783 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt @@ -88,6 +88,7 @@ class AppListRepositoryImpl( matchAnyUserForAdmin: Boolean, ): List<ApplicationInfo> = try { coroutineScope { + // TODO(b/382016780): to be removed after flag cleanup. val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() } val hideWhenDisabledPackagesDeferred = async { context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames) @@ -95,6 +96,7 @@ class AppListRepositoryImpl( val installedApplicationsAsUser = getInstalledApplications(userId, matchAnyUserForAdmin) + // TODO(b/382016780): to be removed after flag cleanup. val hiddenSystemModules = hiddenSystemModulesDeferred.await() val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await() installedApplicationsAsUser.filter { app -> @@ -206,6 +208,7 @@ class AppListRepositoryImpl( private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean = app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages + // TODO(b/382016780): to be removed after flag cleanup. private fun PackageManager.getHiddenSystemModules(): Set<String> { val moduleInfos = getInstalledModules(0).filter { it.isHidden } val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet() @@ -218,13 +221,14 @@ class AppListRepositoryImpl( companion object { private const val TAG = "AppListRepository" + // TODO(b/382016780): to be removed after flag cleanup. private fun ApplicationInfo.isInAppList( showInstantApps: Boolean, hiddenSystemModules: Set<String>, hideWhenDisabledPackages: Array<String>, ) = when { !showInstantApps && isInstantApp -> false - packageName in hiddenSystemModules -> false + !Flags.removeHiddenModuleUsage() && (packageName in hiddenSystemModules) -> false packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed enabled -> true else -> enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt index b1baa8601f28..fd4b189c51ff 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt @@ -281,6 +281,23 @@ class AppListRepositoryTest { ) } + @EnableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) + @Test + fun loadApps_shouldIncludeAllSystemModuleApps() = runTest { + packageManager.stub { + on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE) + } + mockInstalledApplications( + listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP), + ADMIN_USER_ID + ) + + val appList = repository.loadApps(userId = ADMIN_USER_ID) + + assertThat(appList).containsExactly(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP) + } + + @DisableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) @EnableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) @Test fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest { @@ -297,7 +314,7 @@ class AppListRepositoryTest { assertThat(appList).containsExactly(NORMAL_APP) } - @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) + @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX, Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) @Test fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest { packageManager.stub { @@ -456,6 +473,7 @@ class AppListRepositoryTest { isArchived = true } + // TODO(b/382016780): to be removed after flag cleanup. val HIDDEN_APEX_APP = ApplicationInfo().apply { packageName = "hidden.apex.package" } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java index c4829951d61a..3390296ef6fc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java @@ -137,6 +137,7 @@ public class AppUtils { /** * Returns a boolean indicating whether the given package is a hidden system module + * TODO(b/382016780): to be removed after flag cleanup. */ public static boolean isHiddenSystemModule(Context context, String packageName) { return ApplicationsState.getInstance((Application) context.getApplicationContext()) diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index fd9a008ee078..4110d536da61 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -157,6 +157,7 @@ public class ApplicationsState { int mCurComputingSizeUserId; boolean mSessionsChanged; // Maps all installed modules on the system to whether they're hidden or not. + // TODO(b/382016780): to be removed after flag cleanup. final HashMap<String, Boolean> mSystemModules = new HashMap<>(); // Temporary for dispatching session callbacks. Only touched by main thread. @@ -226,12 +227,14 @@ public class ApplicationsState { mRetrieveFlags = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; - final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */); - for (ModuleInfo info : moduleInfos) { - mSystemModules.put(info.getPackageName(), info.isHidden()); - if (Flags.provideInfoOfApkInApex()) { - for (String apkInApexPackageName : info.getApkInApexPackageNames()) { - mSystemModules.put(apkInApexPackageName, info.isHidden()); + if (!Flags.removeHiddenModuleUsage()) { + final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */); + for (ModuleInfo info : moduleInfos) { + mSystemModules.put(info.getPackageName(), info.isHidden()); + if (Flags.provideInfoOfApkInApex()) { + for (String apkInApexPackageName : info.getApkInApexPackageNames()) { + mSystemModules.put(apkInApexPackageName, info.isHidden()); + } } } } @@ -336,7 +339,7 @@ public class ApplicationsState { } mHaveDisabledApps = true; } - if (isHiddenModule(info.packageName)) { + if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) { mApplications.remove(i--); continue; } @@ -453,6 +456,7 @@ public class ApplicationsState { return mHaveInstantApps; } + // TODO(b/382016780): to be removed after flag cleanup. boolean isHiddenModule(String packageName) { Boolean isHidden = mSystemModules.get(packageName); if (isHidden == null) { @@ -462,6 +466,7 @@ public class ApplicationsState { return isHidden; } + // TODO(b/382016780): to be removed after flag cleanup. boolean isSystemModule(String packageName) { return mSystemModules.containsKey(packageName); } @@ -755,7 +760,7 @@ public class ApplicationsState { Log.i(TAG, "Looking up entry of pkg " + info.packageName + ": " + entry); } if (entry == null) { - if (isHiddenModule(info.packageName)) { + if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) { if (DEBUG) { Log.i(TAG, "No AppEntry for " + info.packageName + " (hidden module)"); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index b2c279466ee4..e05f0a1bcde0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -483,14 +483,18 @@ public class HearingAidDeviceManager { void onActiveDeviceChanged(CachedBluetoothDevice device) { if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) { - if (device.isConnectedHearingAidDevice()) { + if (device.isConnectedHearingAidDevice() + && (device.isActiveDevice(BluetoothProfile.HEARING_AID) + || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) { setAudioRoutingConfig(device); } else { clearAudioRoutingConfig(); } } if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { - if (device.isConnectedHearingAidDevice()) { + if (device.isConnectedHearingAidDevice() + && (device.isActiveDevice(BluetoothProfile.HEARING_AID) + || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) { setMicrophoneForCalls(device); } else { clearMicrophoneForCalls(); diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java index dc40304ba24a..51259e2f311d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java @@ -16,6 +16,8 @@ package com.android.settingslib.dream; +import static android.service.dreams.Flags.allowDreamWhenPostured; + import android.annotation.IntDef; import android.content.ComponentName; import android.content.Context; @@ -78,14 +80,21 @@ public class DreamBackend { } @Retention(RetentionPolicy.SOURCE) - @IntDef({WHILE_CHARGING, WHILE_DOCKED, EITHER, NEVER}) + @IntDef({ + WHILE_CHARGING, + WHILE_DOCKED, + WHILE_POSTURED, + WHILE_CHARGING_OR_DOCKED, + NEVER + }) public @interface WhenToDream { } public static final int WHILE_CHARGING = 0; public static final int WHILE_DOCKED = 1; - public static final int EITHER = 2; - public static final int NEVER = 3; + public static final int WHILE_POSTURED = 2; + public static final int WHILE_CHARGING_OR_DOCKED = 3; + public static final int NEVER = 4; /** * The type of dream complications which can be provided by a @@ -134,6 +143,8 @@ public class DreamBackend { .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_CHARGING_ONLY; private static final int WHEN_TO_DREAM_DOCKED = FrameworkStatsLog .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_DOCKED_ONLY; + private static final int WHEN_TO_DREAM_POSTURED = FrameworkStatsLog + .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_POSTURED_ONLY; private static final int WHEN_TO_DREAM_CHARGING_OR_DOCKED = FrameworkStatsLog .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_EITHER_CHARGING_OR_DOCKED; @@ -143,6 +154,7 @@ public class DreamBackend { private final boolean mDreamsEnabledByDefault; private final boolean mDreamsActivatedOnSleepByDefault; private final boolean mDreamsActivatedOnDockByDefault; + private final boolean mDreamsActivatedOnPosturedByDefault; private final Set<ComponentName> mDisabledDreams; private final List<String> mLoggableDreamPrefixes; private Set<Integer> mSupportedComplications; @@ -168,6 +180,8 @@ public class DreamBackend { com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault); mDreamsActivatedOnDockByDefault = resources.getBoolean( com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault); + mDreamsActivatedOnPosturedByDefault = resources.getBoolean( + com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault); mDisabledDreams = Arrays.stream(resources.getStringArray( com.android.internal.R.array.config_disabledDreamComponents)) .map(ComponentName::unflattenFromString) @@ -280,10 +294,11 @@ public class DreamBackend { @WhenToDream public int getWhenToDreamSetting() { - return isActivatedOnDock() && isActivatedOnSleep() ? EITHER + return isActivatedOnDock() && isActivatedOnSleep() ? WHILE_CHARGING_OR_DOCKED : isActivatedOnDock() ? WHILE_DOCKED - : isActivatedOnSleep() ? WHILE_CHARGING - : NEVER; + : isActivatedOnPostured() ? WHILE_POSTURED + : isActivatedOnSleep() ? WHILE_CHARGING + : NEVER; } public void setWhenToDream(@WhenToDream int whenToDream) { @@ -293,16 +308,25 @@ public class DreamBackend { case WHILE_CHARGING: setActivatedOnDock(false); setActivatedOnSleep(true); + setActivatedOnPostured(false); break; case WHILE_DOCKED: setActivatedOnDock(true); setActivatedOnSleep(false); + setActivatedOnPostured(false); break; - case EITHER: + case WHILE_CHARGING_OR_DOCKED: setActivatedOnDock(true); setActivatedOnSleep(true); + setActivatedOnPostured(false); + break; + + case WHILE_POSTURED: + setActivatedOnPostured(true); + setActivatedOnSleep(false); + setActivatedOnDock(false); break; case NEVER: @@ -407,6 +431,22 @@ public class DreamBackend { setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, value); } + public boolean isActivatedOnPostured() { + return allowDreamWhenPostured() + && getBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + mDreamsActivatedOnPosturedByDefault); + } + + /** + * Sets whether dreams should be activated when the device is postured (stationary and upright) + */ + public void setActivatedOnPostured(boolean value) { + if (allowDreamWhenPostured()) { + logd("setActivatedOnPostured(%s)", value); + setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, value); + } + } + private boolean getBoolean(String key, boolean def) { return Settings.Secure.getInt(mContext.getContentResolver(), key, def ? 1 : 0) == 1; } @@ -548,7 +588,9 @@ public class DreamBackend { return WHEN_TO_DREAM_CHARGING; case WHILE_DOCKED: return WHEN_TO_DREAM_DOCKED; - case EITHER: + case WHILE_POSTURED: + return WHEN_TO_DREAM_POSTURED; + case WHILE_CHARGING_OR_DOCKED: return WHEN_TO_DREAM_CHARGING_OR_DOCKED; case NEVER: default: diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java index 7516d2e6ab1b..e3d7902f34b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java @@ -22,6 +22,7 @@ import static com.android.settingslib.enterprise.ActionDisabledLearnMoreButtonLa import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER; import android.app.admin.DevicePolicyManager; +import android.app.supervision.SupervisionManager; import android.content.ComponentName; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; @@ -59,12 +60,18 @@ public final class ActionDisabledByAdminControllerFactory { } private static boolean isSupervisedDevice(Context context) { - DevicePolicyManager devicePolicyManager = - context.getSystemService(DevicePolicyManager.class); - ComponentName supervisionComponent = - devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( - new UserHandle(UserHandle.myUserId())); - return supervisionComponent != null; + if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) { + SupervisionManager supervisionManager = + context.getSystemService(SupervisionManager.class); + return supervisionManager.isSupervisionEnabledForUser(UserHandle.myUserId()); + } else { + DevicePolicyManager devicePolicyManager = + context.getSystemService(DevicePolicyManager.class); + ComponentName supervisionComponent = + devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( + new UserHandle(UserHandle.myUserId())); + return supervisionComponent != null; + } } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 496c3e6c74cc..9aaefe47fda2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -37,7 +37,8 @@ class FakeZenModeRepository : ZenModeRepository { override val globalZenMode: StateFlow<Int> get() = mutableZenMode.asStateFlow() - private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf()) + private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = + MutableStateFlow(listOf(TestModeBuilder.MANUAL_DND)) override val modes: Flow<List<ZenMode>> get() = mutableModesFlow.asStateFlow() @@ -65,8 +66,11 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value += mode } - fun addMode(id: String, @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, - active: Boolean = false) { + fun addMode( + id: String, + @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, + active: Boolean = false, + ) { mutableModesFlow.value += newMode(id, type, active) } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index abc163867248..64a2de5025de 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -31,7 +31,6 @@ import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Random; @@ -44,22 +43,7 @@ public class TestModeBuilder { private boolean mIsManualDnd; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); - - public static final ZenMode MANUAL_DND_ACTIVE = manualDnd( - INTERRUPTION_FILTER_PRIORITY, true); - - public static final ZenMode MANUAL_DND_INACTIVE = manualDnd( - INTERRUPTION_FILTER_PRIORITY, false); - - @NonNull - public static ZenMode manualDnd(@NotificationManager.InterruptionFilter int filter, - boolean isActive) { - return new TestModeBuilder() - .makeManualDnd() - .setInterruptionFilter(filter) - .setActive(isActive) - .build(); - } + public static final ZenMode MANUAL_DND = new TestModeBuilder().makeManualDnd().build(); public TestModeBuilder() { // Reasonable defaults diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java index 3b18aa310c91..4e821ca50dce 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java @@ -16,6 +16,7 @@ package com.android.settingslib.applications; +import static android.content.pm.Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE; import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX; import static android.os.UserHandle.MU_ENABLED; import static android.os.UserHandle.USER_SYSTEM; @@ -59,6 +60,8 @@ import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.text.TextUtils; import android.util.IconDrawableFactory; @@ -204,6 +207,7 @@ public class ApplicationsStateRoboTest { info.setPackageName(packageName); info.setApkInApexPackageNames(Collections.singletonList(apexPackageName)); // will treat any app with package name that contains "hidden" as hidden module + // TODO(b/382016780): to be removed after flag cleanup. info.setHidden(!TextUtils.isEmpty(packageName) && packageName.contains("hidden")); return info; } @@ -414,6 +418,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void onResume_shouldNotIncludeSystemHiddenModule() { mSession.onResume(); @@ -424,6 +429,18 @@ public class ApplicationsStateRoboTest { } @Test + @EnableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) + public void onResume_shouldIncludeSystemModule() { + mSession.onResume(); + + final List<ApplicationInfo> mApplications = mApplicationsState.mApplications; + assertThat(mApplications).hasSize(3); + assertThat(mApplications.get(0).packageName).isEqualTo("test.package.1"); + assertThat(mApplications.get(1).packageName).isEqualTo("test.hidden.module.2"); + assertThat(mApplications.get(2).packageName).isEqualTo("test.package.3"); + } + + @Test public void removeAndInstall_noWorkprofile_doResumeIfNeededLocked_shouldClearEntries() throws RemoteException { // scenario: only owner user @@ -832,6 +849,7 @@ public class ApplicationsStateRoboTest { mApplicationsState.mEntriesMap.clear(); ApplicationInfo appInfo = createApplicationInfo(PKG_1, /* uid= */ 0); mApplicationsState.mApplications.add(appInfo); + // TODO(b/382016780): to be removed after flag cleanup. mApplicationsState.mSystemModules.put(PKG_1, /* value= */ false); assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName) @@ -839,6 +857,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void isHiddenModule_hasApkInApexInfo_shouldSupportHiddenApexPackage() { mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX); ApplicationsState.sInstance = null; @@ -853,6 +872,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void isHiddenModule_noApkInApexInfo_onlySupportHiddenModule() { mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX); ApplicationsState.sInstance = null; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index 21dde1fd9411..a215464f66c2 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -50,6 +50,9 @@ import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.audiopolicy.AudioProductStrategy; import android.os.Parcel; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.FeatureFlagUtils; import androidx.test.core.app.ApplicationProvider; @@ -72,6 +75,8 @@ import java.util.List; public class HearingAidDeviceManagerTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final long HISYNCID1 = 10; private static final long HISYNCID2 = 11; @@ -736,6 +741,7 @@ public class HearingAidDeviceManagerTest { @Test public void onActiveDeviceChanged_connected_callSetStrategies() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); @@ -750,6 +756,7 @@ public class HearingAidDeviceManagerTest { @Test public void onActiveDeviceChanged_disconnected_callSetStrategiesWithAutoValue() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(false); when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false); @@ -952,6 +959,38 @@ public class HearingAidDeviceManagerTest { ConnectionStatus.CONNECTED); } + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void onActiveDeviceChanged_activeHearingAidProfile_callSetInputDeviceForCalls() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); + when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true); + doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(), + anyInt()); + + mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1); + + verify(mHelper).setPreferredInputDeviceForCalls( + eq(mCachedDevice1), eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO)); + + } + + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void onActiveDeviceChanged_notActiveHearingAidProfile_callClearInputDeviceForCalls() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false); + when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true); + doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(), + anyInt()); + + mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1); + + verify(mHelper).clearPreferredInputDeviceForCalls(); + } + private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) { return new HearingAidInfo.Builder() .setAshaDeviceSide(HearingAidInfo.DeviceSide.SIDE_LEFT) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index d08d91d18b27..6b30f159129e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -87,7 +87,7 @@ public class ZenModeTest { @Test public void testBasicMethods_manualDnd() { - ZenMode manualMode = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualMode = TestModeBuilder.MANUAL_DND; assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID); assertThat(manualMode.isManualDnd()).isTrue(); @@ -271,7 +271,7 @@ public class ZenModeTest { @Test public void setInterruptionFilter_manualDnd_throws() { - ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualDnd = TestModeBuilder.MANUAL_DND; assertThrows(IllegalStateException.class, () -> manualDnd.setInterruptionFilter(INTERRUPTION_FILTER_ALL)); @@ -280,24 +280,46 @@ public class ZenModeTest { @Test public void canEditPolicy_onlyFalseForSpecialDnd() { assertThat(TestModeBuilder.EXAMPLE.canEditPolicy()).isTrue(); - assertThat(TestModeBuilder.MANUAL_DND_ACTIVE.canEditPolicy()).isTrue(); - assertThat(TestModeBuilder.MANUAL_DND_INACTIVE.canEditPolicy()).isTrue(); - ZenMode dndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true); + ZenMode inactiveDnd = new TestModeBuilder().makeManualDnd().setActive(false).build(); + assertThat(inactiveDnd.canEditPolicy()).isTrue(); + + ZenMode activeDnd = new TestModeBuilder().makeManualDnd().setActive(true).build(); + assertThat(activeDnd.canEditPolicy()).isTrue(); + + ZenMode dndWithAlarms = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(true) + .build(); assertThat(dndWithAlarms.canEditPolicy()).isFalse(); - ZenMode dndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, true); + + ZenMode dndWithNone = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(true) + .build(); assertThat(dndWithNone.canEditPolicy()).isFalse(); // Note: Backend will never return an inactive manual mode with custom filter. - ZenMode badDndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, false); + ZenMode badDndWithAlarms = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(false) + .build(); assertThat(badDndWithAlarms.canEditPolicy()).isFalse(); - ZenMode badDndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, false); + + ZenMode badDndWithNone = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(false) + .build(); assertThat(badDndWithNone.canEditPolicy()).isFalse(); } @Test public void canEditPolicy_whenTrue_allowsSettingPolicyAndEffects() { - ZenMode normalDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_PRIORITY, true); + ZenMode normalDnd = new TestModeBuilder().makeManualDnd().setActive(true).build(); assertThat(normalDnd.canEditPolicy()).isTrue(); @@ -313,7 +335,11 @@ public class ZenModeTest { @Test public void canEditPolicy_whenFalse_preventsSettingFilterPolicyOrEffects() { - ZenMode specialDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true); + ZenMode specialDnd = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(true) + .build(); assertThat(specialDnd.canEditPolicy()).isFalse(); assertThrows(IllegalStateException.class, @@ -324,7 +350,7 @@ public class ZenModeTest { @Test public void comparator_prioritizes() { - ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualDnd = TestModeBuilder.MANUAL_DND; ZenMode driving1 = new TestModeBuilder().setName("b1").setType(TYPE_DRIVING).build(); ZenMode driving2 = new TestModeBuilder().setName("b2").setType(TYPE_DRIVING).build(); ZenMode bedtime1 = new TestModeBuilder().setName("c1").setType(TYPE_BEDTIME).build(); @@ -403,7 +429,7 @@ public class ZenModeTest { @Test public void getIconKey_manualDnd_isDndIcon() { - ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND_INACTIVE.getIconKey(); + ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND.getIconKey(); assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index dd28402d705f..7b4a2ca5de39 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -155,6 +155,7 @@ public class SecureSettings { Settings.Secure.SCREENSAVER_COMPONENTS, Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index b01f6229af16..b0309a8fa5a5 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -230,6 +230,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.SCREENSAVER_COMPONENTS, COMMA_SEPARATED_COMPONENT_LIST_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_DOCK, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index f79a60f5be96..c1c3e04d46fd 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -212,6 +212,8 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static final String ERROR_IO_EXCEPTION = "io_exception"; private static final String ERROR_FAILED_TO_RESTORE_SOFTAP_CONFIG = "failed_to_restore_softap_config"; + private static final String ERROR_FAILED_TO_RESTORE_WIFI_CONFIG = + "failed_to_restore_wifi_config"; // Name of the temporary file we use during full backup/restore. This is @@ -1455,8 +1457,14 @@ public class SettingsBackupAgent extends BackupAgentHelper { return baos.toByteArray(); } - private byte[] getNewWifiConfigData() { - return mWifiManager.retrieveBackupData(); + @VisibleForTesting + byte[] getNewWifiConfigData() { + byte[] data = mWifiManager.retrieveBackupData(); + if (areAgentMetricsEnabled) { + // We're unable to determine how many settings this includes, so we'll just log 1. + numberOfSettingsPerKey.put(KEY_WIFI_NEW_CONFIG, 1); + } + return data; } private byte[] getLocaleSettings() { @@ -1468,11 +1476,22 @@ public class SettingsBackupAgent extends BackupAgentHelper { return localeList.toLanguageTags().getBytes(); } - private void restoreNewWifiConfigData(byte[] bytes) { + @VisibleForTesting + void restoreNewWifiConfigData(byte[] bytes) { if (DEBUG_BACKUP) { Log.v(TAG, "Applying restored wifi data"); } - mWifiManager.restoreBackupData(bytes); + if (areAgentMetricsEnabled) { + try { + mWifiManager.restoreBackupData(bytes); + mBackupRestoreEventLogger.logItemsRestored(KEY_WIFI_NEW_CONFIG, /* count= */ 1); + } catch (Exception e) { + mBackupRestoreEventLogger.logItemsRestoreFailed( + KEY_WIFI_NEW_CONFIG, /* count= */ 1, ERROR_FAILED_TO_RESTORE_WIFI_CONFIG); + } + } else { + mWifiManager.restoreBackupData(bytes); + } } private void restoreNetworkPolicies(byte[] data) { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 5ad4b8a6dffe..1c6d6816e9b4 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2550,6 +2550,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT, SecureSettingsProto.Screensaver.DEFAULT_COMPONENT); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + SecureSettingsProto.Screensaver.ACTIVATE_ON_POSTURED); p.end(screensaverToken); final long searchToken = p.start(SecureSettingsProto.SEARCH); diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java index 95dd0db40c0e..6e5b602c02c5 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java @@ -16,6 +16,7 @@ package com.android.providers.settings; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_WIFI_NEW_CONFIG; import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SOFTAP_CONFIG; import static junit.framework.Assert.assertEquals; @@ -28,6 +29,8 @@ import static org.junit.Assert.assertArrayEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import android.annotation.Nullable; @@ -69,6 +72,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -834,6 +838,74 @@ public class SettingsBackupAgentTest extends BaseSettingsProviderTest { assertNull(getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest)); } + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getNewWifiConfigData_flagIsEnabled_numberOfSettingsInKeyAreRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveBackupData()).thenReturn(null); + + mAgentUnderTest.getNewWifiConfigData(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getNewWifiConfigData_flagIsNotEnabled_numberOfSettingsInKeyAreNotRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveBackupData()).thenReturn(null); + + mAgentUnderTest.getNewWifiConfigData(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 0); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreNewWifiConfigData_flagIsEnabled_restoreIsSuccessful_successMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doNothing().when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getSuccessCount(), 1); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreNewWifiConfigData_flagIsEnabled_restoreIsNotSuccessful_failureMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doThrow(new RuntimeException()).when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getFailCount(), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void restoreNewWifiConfigData_flagIsNotEnabled_metricsAreNotLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doNothing().when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + assertNull(getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest)); + } + private byte[] generateBackupData(Map<String, String> keyValueData) { int totalBytes = 0; for (String key : keyValueData.keySet()) { diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 9adc95a01216..6b2449fdaa49 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -92,7 +92,6 @@ filegroup { "tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt", "tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt", - "tests/src/**/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt", "tests/src/**/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt", "tests/src/**/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt", "tests/src/**/systemui/education/domain/ui/view/ContextualEduDialogTest.kt", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 7d5fd903c01b..70d4cc2e4e26 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -424,24 +424,6 @@ flag { } flag { - name: "status_bar_use_repos_for_call_chip" - namespace: "systemui" - description: "Use repositories as the source of truth for call notifications shown as a chip in" - "the status bar" - bug: "328584859" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - name: "status_bar_call_chip_notification_icon" - namespace: "systemui" - description: "Use the small icon set on the notification for the status bar call chip" - bug: "354930838" -} - -flag { name: "status_bar_signal_policy_refactor" namespace: "systemui" description: "Use a settings observer for airplane mode and make StatusBarSignalPolicy startable" @@ -1344,6 +1326,13 @@ flag { } flag { + name: "output_switcher_redesign" + namespace: "systemui" + description: "Enables visual update for Media Output Switcher" + bug: "388296370" +} + +flag { namespace: "systemui" name: "enable_view_capture_tracing" description: "Enables view capture tracing in System UI." diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index c7d6e8aed3b4..96401ce6e1c7 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -147,21 +147,66 @@ private data class NestedDraggableElement( private val orientation: Orientation, private val overscrollEffect: OverscrollEffect?, private val enabled: Boolean, -) : ModifierNodeElement<NestedDraggableNode>() { - override fun create(): NestedDraggableNode { - return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled) +) : ModifierNodeElement<NestedDraggableRootNode>() { + override fun create(): NestedDraggableRootNode { + return NestedDraggableRootNode(draggable, orientation, overscrollEffect, enabled) } - override fun update(node: NestedDraggableNode) { + override fun update(node: NestedDraggableRootNode) { node.update(draggable, orientation, overscrollEffect, enabled) } } +/** + * A root node on top of [NestedDraggableNode] so that no [PointerInputModifierNode] is installed + * when this draggable is disabled. + */ +private class NestedDraggableRootNode( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + enabled: Boolean, +) : DelegatingNode() { + private var delegateNode = + if (enabled) create(draggable, orientation, overscrollEffect) else null + + fun update( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + enabled: Boolean, + ) { + // Disabled. + if (!enabled) { + delegateNode?.let { undelegate(it) } + delegateNode = null + return + } + + // Disabled => Enabled. + val nullableDelegate = delegateNode + if (nullableDelegate == null) { + delegateNode = create(draggable, orientation, overscrollEffect) + return + } + + // Enabled => Enabled (update). + nullableDelegate.update(draggable, orientation, overscrollEffect) + } + + private fun create( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + ): NestedDraggableNode { + return delegate(NestedDraggableNode(draggable, orientation, overscrollEffect)) + } +} + private class NestedDraggableNode( private var draggable: NestedDraggable, override var orientation: Orientation, private var overscrollEffect: OverscrollEffect?, - private var enabled: Boolean, ) : DelegatingNode(), PointerInputModifierNode, @@ -169,23 +214,11 @@ private class NestedDraggableNode( CompositionLocalConsumerModifierNode, OrientationAware { private val nestedScrollDispatcher = NestedScrollDispatcher() - private var trackWheelScroll: SuspendingPointerInputModifierNode? = null - set(value) { - field?.let { undelegate(it) } - field = value?.also { delegate(it) } - } - - private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null - set(value) { - field?.let { undelegate(it) } - field = value?.also { delegate(it) } - } - - private var detectDragsDelegate: SuspendingPointerInputModifierNode? = null - set(value) { - field?.let { undelegate(it) } - field = value?.also { delegate(it) } - } + private val trackWheelScroll = + delegate(SuspendingPointerInputModifierNode { trackWheelScroll() }) + private val trackDownPositionDelegate = + delegate(SuspendingPointerInputModifierNode { trackDownPosition() }) + private val detectDragsDelegate = delegate(SuspendingPointerInputModifierNode { detectDrags() }) /** The controller created by the nested scroll logic (and *not* the drag logic). */ private var nestedScrollController: NestedScrollController? = null @@ -214,26 +247,25 @@ private class NestedDraggableNode( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, - enabled: Boolean, ) { + if ( + draggable == this.draggable && + orientation == this.orientation && + overscrollEffect == this.overscrollEffect + ) { + return + } + this.draggable = draggable this.orientation = orientation this.overscrollEffect = overscrollEffect - this.enabled = enabled - trackDownPositionDelegate?.resetPointerInputHandler() - detectDragsDelegate?.resetPointerInputHandler() + trackWheelScroll.resetPointerInputHandler() + trackDownPositionDelegate.resetPointerInputHandler() + detectDragsDelegate.resetPointerInputHandler() + nestedScrollController?.ensureOnDragStoppedIsCalled() nestedScrollController = null - - if (!enabled && trackWheelScroll != null) { - check(trackDownPositionDelegate != null) - check(detectDragsDelegate != null) - - trackWheelScroll = null - trackDownPositionDelegate = null - detectDragsDelegate = null - } } override fun onPointerEvent( @@ -241,26 +273,15 @@ private class NestedDraggableNode( pass: PointerEventPass, bounds: IntSize, ) { - if (!enabled) return - - if (trackWheelScroll == null) { - check(trackDownPositionDelegate == null) - check(detectDragsDelegate == null) - - trackWheelScroll = SuspendingPointerInputModifierNode { trackWheelScroll() } - trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() } - detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() } - } - - checkNotNull(trackWheelScroll).onPointerEvent(pointerEvent, pass, bounds) - checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds) - checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds) + trackWheelScroll.onPointerEvent(pointerEvent, pass, bounds) + trackDownPositionDelegate.onPointerEvent(pointerEvent, pass, bounds) + detectDragsDelegate.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { - trackWheelScroll?.onCancelPointerInput() - trackDownPositionDelegate?.onCancelPointerInput() - detectDragsDelegate?.onCancelPointerInput() + trackWheelScroll.onCancelPointerInput() + trackDownPositionDelegate.onCancelPointerInput() + detectDragsDelegate.onCancelPointerInput() } /* diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index f9cf495d9d9f..5de0f1221f0f 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt @@ -25,11 +25,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +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.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -37,10 +40,14 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ScrollWheel +import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown @@ -693,6 +700,7 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw } @Test + @Ignore("b/388507816: re-enable this when the crash in HitPath is fixed") fun pointersDown_clearedWhenDisabled() { val draggable = TestDraggable() var enabled by mutableStateOf(true) @@ -740,6 +748,31 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw assertThat(draggable.onDragStartedCalled).isFalse() } + @Test + fun doesNotConsumeGesturesWhenDisabled() { + val buttonTag = "button" + rule.setContent { + Box { + var count by remember { mutableStateOf(0) } + Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) { + Text("Count: $count") + } + + Box( + Modifier.fillMaxSize() + .nestedDraggable(remember { TestDraggable() }, orientation, enabled = false) + ) + } + } + + rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0") + + // Click on the root at its center, where the button is located. Clicks should go through + // the draggable and reach the button given that it is disabled. + repeat(3) { rule.onRoot().performClick() } + rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3") + } + private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 9c53afecad11..a2a91fcd5d52 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -78,6 +78,18 @@ object TransitionDuration { const val EDIT_MODE_TO_HUB_GRID_END_MS = EDIT_MODE_TO_HUB_GRID_DELAY_MS + EDIT_MODE_TO_HUB_CONTENT_MS const val HUB_TO_EDIT_MODE_CONTENT_MS = 250 + const val TO_GLANCEABLE_HUB_DURATION_MS = 1000 +} + +val sceneTransitionsV2 = transitions { + to(CommunalScenes.Communal) { + spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS) + fade(AllElements) + } + to(CommunalScenes.Blank) { + spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS)) + fade(AllElements) + } } val sceneTransitions = transitions { @@ -157,7 +169,7 @@ fun CommunalContainer( MutableSceneTransitionLayoutState( initialScene = currentSceneKey, canChangeScene = { _ -> viewModel.canChangeScene() }, - transitions = sceneTransitions, + transitions = if (viewModel.v2FlagEnabled()) sceneTransitionsV2 else sceneTransitions, ) } val isUiBlurred by viewModel.isUiBlurred.collectAsStateWithLifecycle() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt index 93c10b6224ab..6ca9c7934979 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt @@ -17,17 +17,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.communal.ui.compose.AllElements -import com.android.systemui.communal.ui.compose.Communal fun TransitionBuilder.dreamToCommunalTransition() { spec = tween(durationMillis = 1000) - // Translate communal hub grid from the end direction. - translate(Communal.Elements.Grid, Edge.End) - - // Fade all communal hub elements. - timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } + // Fade in all communal hub elements. + fade(AllElements) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt index 826a2550cca3..de9a78c497f8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt @@ -17,21 +17,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.communal.ui.compose.AllElements -import com.android.systemui.communal.ui.compose.Communal -import com.android.systemui.scene.shared.model.Scenes fun TransitionBuilder.lockscreenToCommunalTransition() { spec = tween(durationMillis = 1000) - // Translate lockscreen to the start direction. - translate(Scenes.Lockscreen.rootElementKey, Edge.Start) - - // Translate communal hub grid from the end direction. - translate(Communal.Elements.Grid, Edge.End) - - // Fade all communal hub elements. - timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } + // Fade all communal hub elements in. + fade(AllElements) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index b41c55858c75..2ca846424d93 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -429,6 +429,58 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol } /** + * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. + * Prioritizes actions with matching [Swipe.Resolved.fromSource]. + * + * @param swipe The swipe to match against. + * @return The best matching [UserActionResult], or `null` if no match is found. + */ + private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { + if (!areSwipesAllowed()) { + return null + } + + var bestPoints = Int.MIN_VALUE + var bestMatch: UserActionResult? = null + userActions.forEach { (actionSwipe, actionResult) -> + if ( + actionSwipe !is Swipe.Resolved || + // The direction must match. + actionSwipe.direction != swipe.direction || + // The number of pointers down must match. + actionSwipe.pointerCount != swipe.pointerCount || + // The action requires a specific fromSource. + (actionSwipe.fromSource != null && + actionSwipe.fromSource != swipe.fromSource) || + // The action requires a specific pointerType. + (actionSwipe.pointersType != null && + actionSwipe.pointersType != swipe.pointersType) + ) { + // This action is not eligible. + return@forEach + } + + val sameFromSource = actionSwipe.fromSource == swipe.fromSource + val samePointerType = actionSwipe.pointersType == swipe.pointersType + // Prioritize actions with a perfect match. + if (sameFromSource && samePointerType) { + return actionResult + } + + var points = 0 + if (sameFromSource) points++ + if (samePointerType) points++ + + // Otherwise, keep track of the best eligible action. + if (points > bestPoints) { + bestPoints = points + bestMatch = actionResult + } + } + return bestMatch + } + + /** * Update the swipes results. * * Usually we don't want to update them while doing a drag, because this could change the target diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index 3f6bce724b1b..e2212113404d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -37,11 +37,7 @@ internal fun Modifier.swipeToScene( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ): Modifier { - return if (draggableHandler.enabled()) { - this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) - } else { - this - } + return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled())) } private fun DraggableHandlerImpl.enabled(): Boolean { @@ -61,84 +57,62 @@ internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation } } -/** - * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. - * Prioritizes actions with matching [Swipe.Resolved.fromSource]. - * - * @param swipe The swipe to match against. - * @return The best matching [UserActionResult], or `null` if no match is found. - */ -internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { - if (!areSwipesAllowed()) { - return null - } - - var bestPoints = Int.MIN_VALUE - var bestMatch: UserActionResult? = null - userActions.forEach { (actionSwipe, actionResult) -> - if ( - actionSwipe !is Swipe.Resolved || - // The direction must match. - actionSwipe.direction != swipe.direction || - // The number of pointers down must match. - actionSwipe.pointerCount != swipe.pointerCount || - // The action requires a specific fromSource. - (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) || - // The action requires a specific pointerType. - (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType) - ) { - // This action is not eligible. - return@forEach - } - - val sameFromSource = actionSwipe.fromSource == swipe.fromSource - val samePointerType = actionSwipe.pointersType == swipe.pointersType - // Prioritize actions with a perfect match. - if (sameFromSource && samePointerType) { - return actionResult - } - - var points = 0 - if (sameFromSource) points++ - if (samePointerType) points++ - - // Otherwise, keep track of the best eligible action. - if (points > bestPoints) { - bestPoints = points - bestMatch = actionResult - } - } - return bestMatch -} - private data class SwipeToSceneElement( val draggableHandler: DraggableHandlerImpl, val swipeDetector: SwipeDetector, + val enabled: Boolean, ) : ModifierNodeElement<SwipeToSceneRootNode>() { override fun create(): SwipeToSceneRootNode = - SwipeToSceneRootNode(draggableHandler, swipeDetector) + SwipeToSceneRootNode(draggableHandler, swipeDetector, enabled) override fun update(node: SwipeToSceneRootNode) { - node.update(draggableHandler, swipeDetector) + node.update(draggableHandler, swipeDetector, enabled) } } private class SwipeToSceneRootNode( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, + enabled: Boolean, ) : DelegatingNode() { - private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) + private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null + + fun update( + draggableHandler: DraggableHandlerImpl, + swipeDetector: SwipeDetector, + enabled: Boolean, + ) { + // Disabled. + if (!enabled) { + delegateNode?.let { undelegate(it) } + delegateNode = null + return + } + + // Disabled => Enabled. + val nullableDelegate = delegateNode + if (nullableDelegate == null) { + delegateNode = create(draggableHandler, swipeDetector) + return + } - fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) { - if (draggableHandler == delegateNode.draggableHandler) { + // Enabled => Enabled (update). + if (draggableHandler == nullableDelegate.draggableHandler) { // Simple update, just update the swipe detector directly and keep the node. - delegateNode.swipeDetector = swipeDetector + nullableDelegate.swipeDetector = swipeDetector } else { // The draggableHandler changed, force recreate the underlying SwipeToSceneNode. - undelegate(delegateNode) - delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) + undelegate(nullableDelegate) + delegateNode = create(draggableHandler, swipeDetector) } } + + private fun create( + draggableHandler: DraggableHandlerImpl, + swipeDetector: SwipeDetector, + ): SwipeToSceneNode { + return delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) + } } private class SwipeToSceneNode( diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 9135fdd15b3a..e80805a4e374 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -936,4 +936,45 @@ class SwipeToSceneTest { assertThat(state.transitionState).isIdle() assertThat(state.transitionState).hasCurrentScene(SceneC) } + + @Test + fun swipeToSceneNodeIsKeptWhenDisabled() { + var hasHorizontalActions by mutableStateOf(false) + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene( + SceneA, + userActions = + buildList { + add(Swipe.Down to SceneB) + + if (hasHorizontalActions) { + add(Swipe.Left to SceneC) + } + } + .toMap(), + ) { + Box(Modifier.fillMaxSize()) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + // Swipe down to start a transition to B. + rule.onRoot().performTouchInput { + down(middle) + moveBy(Offset(0f, touchSlop)) + } + + assertThat(state.transitionState).isSceneTransition() + + // Add new horizontal user actions. This should not stop the current transition, even if a + // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was. + hasHorizontalActions = true + rule.waitForIdle() + assertThat(state.transitionState).isSceneTransition() + } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index aed3a2df0436..e69fa994931d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -50,7 +50,6 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController DEFAULT_CLOCK_ID, clockCtx.resources.getString(R.string.clock_default_name), clockCtx.resources.getString(R.string.clock_default_description), - isReactiveToTone = true, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt index a11dace0505c..4c329dcf2f2b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt @@ -18,12 +18,20 @@ package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata +import android.content.ContentResolver +import android.content.applicationContext import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.BluetoothEventManager import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.settingslib.bluetooth.VolumeControlProfile +import com.android.settingslib.volume.shared.AudioSharingLogger import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -38,10 +46,16 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -50,8 +64,11 @@ import org.mockito.kotlin.any class AudioSharingInteractorTest : SysuiTestCase() { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos() + @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast + @Mock private lateinit var bluetoothLeBroadcastMetadata: BluetoothLeBroadcastMetadata + @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback> private lateinit var underTest: AudioSharingInteractor @@ -157,13 +174,15 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + runCurrent() assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() job.cancel() @@ -174,15 +193,14 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - verify(localBluetoothLeBroadcast) - .registerServiceCallBack(any(), callbackCaptor.capture()) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) runCurrent() assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() @@ -194,13 +212,15 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + runCurrent() verify(localBluetoothLeBroadcast) .registerServiceCallBack(any(), callbackCaptor.capture()) runCurrent() @@ -211,4 +231,100 @@ class AudioSharingInteractorTest : SysuiTestCase() { job.cancel() } } + + @Test + fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() = + with(kosmos) { + testScope.runTest { + val (broadcast, repository) = setupRepositoryImpl() + val interactor = + object : + AudioSharingInteractorImpl( + applicationContext, + localBluetoothManager, + repository, + testDispatcher, + ) { + override suspend fun audioSharingAvailable() = true + } + val job = launch { interactor.handleAudioSourceWhenReady() } + runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped + verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) + runCurrent() + // Verify source is not added + verify(repository, never()).addSource() + job.cancel() + } + } + + @Test + fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() = + with(kosmos) { + testScope.runTest { + val (broadcast, repository) = setupRepositoryImpl() + val interactor = + object : + AudioSharingInteractorImpl( + applicationContext, + localBluetoothManager, + repository, + testDispatcher, + ) { + override suspend fun audioSharingAvailable() = true + } + val job = launch { interactor.handleAudioSourceWhenReady() } + runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped + verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) + // Audio sharing started, trigger onBroadcastStarted + whenever(broadcast.isEnabled(null)).thenReturn(true) + callbackCaptor.value.onBroadcastStarted(0, 0) + runCurrent() + // Verify callback registered for onBroadcastMetadataChanged + verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture()) + runCurrent() + // Trigger onBroadcastMetadataChanged (ready to add source) + callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata) + runCurrent() + // Verify source added + verify(repository).addSource() + job.cancel() + } + } + + private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> { + with(kosmos) { + val broadcast = + mock<LocalBluetoothLeBroadcast> { + on { isProfileReady } doReturn true + on { isEnabled(null) } doReturn false + } + val assistant = + mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true } + val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true } + val profileManager = + mock<LocalBluetoothProfileManager> { + on { leAudioBroadcastProfile } doReturn broadcast + on { leAudioBroadcastAssistantProfile } doReturn assistant + on { volumeControlProfile } doReturn volumeControl + } + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {}) + + val repository = + AudioSharingRepositoryImpl( + localBluetoothManager, + com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl( + mock<ContentResolver> {}, + localBluetoothManager, + testScope.backgroundScope, + testScope.testScheduler, + mock<AudioSharingLogger> {}, + ), + testDispatcher, + ) + return Pair(broadcast, spy(repository)) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt index acfe9dd45f75..f0746064f67f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt @@ -111,6 +111,28 @@ class AudioSharingRepositoryTest : SysuiTestCase() { } @Test + fun testStopAudioSharing() = + with(kosmos) { + testScope.runTest { + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile) + audioSharingRepository.setAudioSharingAvailable(true) + underTest.stopAudioSharing() + verify(leAudioBroadcastProfile).stopLatestBroadcast() + } + } + + @Test + fun testStopAudioSharing_flagOff_doNothing() = + with(kosmos) { + testScope.runTest { + audioSharingRepository.setAudioSharingAvailable(false) + underTest.stopAudioSharing() + verify(leAudioBroadcastProfile, never()).stopLatestBroadcast() + } + } + + @Test fun testAddSource_flagOff_doesNothing() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt index 44f9720cb9e4..ad0337e5ce86 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -15,14 +15,15 @@ */ package com.android.systemui.bluetooth.qsdialog -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -48,6 +49,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { private lateinit var notConnectedDeviceItem: DeviceItem private lateinit var connectedMediaDeviceItem: DeviceItem private lateinit var connectedOtherDeviceItem: DeviceItem + private lateinit var audioSharingDeviceItem: DeviceItem @Mock private lateinit var dialog: SystemUIDialog @Before @@ -59,7 +61,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) notConnectedDeviceItem = DeviceItem( @@ -68,7 +70,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) connectedMediaDeviceItem = DeviceItem( @@ -77,7 +79,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) connectedOtherDeviceItem = DeviceItem( @@ -86,7 +88,16 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, + ) + audioSharingDeviceItem = + DeviceItem( + type = DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null, ) actionInteractorImpl = kosmos.deviceItemActionInteractorImpl } @@ -135,6 +146,29 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { } } + @Test + fun onActionIconClick_onIntent() { + with(kosmos) { + testScope.runTest { + var onIntentCalledOnAddress = "" + whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) + actionInteractorImpl.onActionIconClick(connectedMediaDeviceItem) { + onIntentCalledOnAddress = connectedMediaDeviceItem.cachedBluetoothDevice.address + } + assertThat(onIntentCalledOnAddress).isEqualTo(DEVICE_ADDRESS) + } + } + } + + @Test(expected = IllegalArgumentException::class) + fun onActionIconClick_audioSharingDeviceType_throwException() { + with(kosmos) { + testScope.runTest { + actionInteractorImpl.onActionIconClick(audioSharingDeviceItem) {} + } + } + } + private companion object { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 20d66155e5ca..6c955bf1818d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -256,6 +256,22 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @EnableSceneContainer @Test + fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() = + testScope.runTest { + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + + enrollFace() + kosmos.configureKeyguardBypass(isBypassAvailable = false) + runCurrent() + configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false) + kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true + + assertThat(playSuccessHaptic).isNotNull() + } + + @EnableSceneContainer + @Test fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() = testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) @@ -299,6 +315,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { private fun configureDeviceEntryFromBiometricSource( isFpUnlock: Boolean = false, isFaceUnlock: Boolean = false, + bypassEnabled: Boolean = true, ) { // Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState if (isFpUnlock) { @@ -314,11 +331,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { ) // Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING - kosmos.sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(Scenes.Lockscreen) + // if the successful face authentication will bypass keyguard + if (bypassEnabled) { + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(Scenes.Lockscreen) + ) ) - ) + } } underTest = kosmos.deviceEntryHapticsInteractor } diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt index 74e8257f4f08..5e023a203267 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt @@ -292,8 +292,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -306,8 +305,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @@ -321,8 +319,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -335,8 +332,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @@ -347,8 +343,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) assertThat(model?.lastShortcutTriggeredTime).isNull() - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType) assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) } @@ -358,15 +353,14 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge testScope.runTest { setUpForDeviceConnection() tutorialSchedulerRepository.setScheduledTutorialLaunchTime( - DeviceType.TOUCHPAD, + getTargetDevice(gestureType), eduClock.instant(), ) val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount eduClock.offset(initialDelayElapsedDuration) - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -376,33 +370,92 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge testScope.runTest { setUpForDeviceConnection() tutorialSchedulerRepository.setScheduledTutorialLaunchTime( - DeviceType.TOUCHPAD, + getTargetDevice(gestureType), eduClock.instant(), ) val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount // No offset to the clock to simulate update before initial delay - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @Test - fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() = + fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchOrNotifyTime() = testScope.runTest { - // No update to OOBE launch time to simulate no OOBE is launched yet + // No update to OOBE launch/notify time to simulate no OOBE is launched yet setUpForDeviceConnection() val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } + @Test + fun dataUpdatedOnIncrementSignalCountAfterNotifyTimeDelayWithoutLaunchTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + eduClock.offset(initialDelayElapsedDuration) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountBeforeLaunchTimeDelayWithNotifyTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + eduClock.offset(initialDelayElapsedDuration) + + tutorialSchedulerRepository.setScheduledTutorialLaunchTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + // No offset to the clock to simulate update before initial delay of launch time + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountAfterLaunchTimeDelayWithNotifyTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + eduClock.offset(initialDelayElapsedDuration) + + tutorialSchedulerRepository.setScheduledTutorialLaunchTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + eduClock.offset(initialDelayElapsedDuration) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + private suspend fun setUpForInitialDelayElapse() { tutorialSchedulerRepository.setScheduledTutorialLaunchTime( DeviceType.TOUCHPAD, @@ -465,12 +518,18 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge keyboardRepository.setIsAnyKeyboardConnected(true) } - private fun getOverviewProxyListener(): OverviewProxyListener { + private fun updateContextualEduStats(isTrackpadGesture: Boolean, gestureType: GestureType) { val listenerCaptor = argumentCaptor<OverviewProxyListener>() verify(overviewProxyService).addCallback(listenerCaptor.capture()) - return listenerCaptor.firstValue + listenerCaptor.firstValue.updateContextualEduStats(isTrackpadGesture, gestureType) } + private fun getTargetDevice(gestureType: GestureType) = + when (gestureType) { + ALL_APPS -> DeviceType.KEYBOARD + else -> DeviceType.TOUCHPAD + } + companion object { private val USER_INFOS = listOf(UserInfo(101, "Second User", 0)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 692b9c67f322..692b9c67f322 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 7c88d76f28bd..183e4d6f624b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -24,6 +24,7 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME import android.os.SystemClock import android.view.KeyEvent import android.view.KeyEvent.ACTION_DOWN +import android.view.KeyEvent.ACTION_UP import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON @@ -540,11 +541,7 @@ object TestShortcuts { simpleShortcutCategory(System, "System apps", "Take a note"), simpleShortcutCategory(System, "System controls", "Take screenshot"), simpleShortcutCategory(System, "System controls", "Go back"), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Switch to full screen", - ), + simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"), simpleShortcutCategory( MultiTasking, "Split screen", @@ -704,7 +701,7 @@ object TestShortcuts { android.view.KeyEvent( /* downTime = */ SystemClock.uptimeMillis(), /* eventTime = */ SystemClock.uptimeMillis(), - /* action = */ ACTION_DOWN, + /* action = */ ACTION_UP, /* code = */ KEYCODE_A, /* repeat = */ 0, /* metaState = */ 0, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt index 755c218f6789..d9d34f5ace7b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt @@ -92,13 +92,14 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - assertThat(uiState).isEqualTo( - AddShortcutDialog( - shortcutLabel = "Standard shortcut", - defaultCustomShortcutModifierKey = - ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + assertThat(uiState) + .isEqualTo( + AddShortcutDialog( + shortcutLabel = "Standard shortcut", + defaultCustomShortcutModifierKey = + ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + ) ) - ) } } @@ -137,8 +138,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - assertThat((uiState as AddShortcutDialog).pressedKeys) - .isEmpty() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } @@ -161,8 +161,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) - assertThat((uiState as AddShortcutDialog).errorMessage) - .isEmpty() + assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty() } } @@ -244,32 +243,34 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() { + fun onShortcutKeyCombinationSelected_handlesKeyEvents_whereActionKeyIsAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - val isHandled = viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) + val isHandled = + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) assertThat(isHandled).isTrue() } } @Test - fun onKeyPressed_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() { + fun onShortcutKeyCombinationSelected_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - val isHandled = viewModel.onKeyPressed(keyDownEventWithoutActionKeyPressed) + val isHandled = + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithoutActionKeyPressed) assertThat(isHandled).isFalse() } } @Test - fun onKeyPressed_convertsKeyEventsAndUpdatesUiStatesPressedKey() { + fun onShortcutKeyCombinationSelected_convertsKeyEventsAndUpdatesUiStatesPressedKey() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) // Note that Action Key is excluded as it's already displayed on the UI assertThat((uiState as AddShortcutDialog).pressedKeys) @@ -282,8 +283,8 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) // Note that Action Key is excluded as it's already displayed on the UI assertThat((uiState as AddShortcutDialog).pressedKeys) @@ -292,16 +293,15 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { // Close the dialog and show it again viewModel.onDialogDismissed() viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - assertThat((uiState as AddShortcutDialog).pressedKeys) - .isEmpty() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } private suspend fun openAddShortcutDialogAndSetShortcut() { viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) viewModel.onSetShortcut() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index fde9b8ce6a50..bf49186a7f01 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -28,7 +28,7 @@ import android.provider.Settings.Secure.ZEN_DURATION_PROMPT import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.EnableZenModeDialog -import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -187,7 +187,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + zenModeRepository.activateMode(MANUAL_DND) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2) collectLastValue(underTest.lockScreenState) runCurrent() @@ -233,7 +233,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER) collectLastValue(underTest.lockScreenState) runCurrent() @@ -278,7 +277,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() = testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900) runCurrent() @@ -323,7 +321,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() = testScope.runTest { val expandable: Expandable = mock() - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT) whenever(enableZenModeDialog.createDialog()).thenReturn(mock()) collectLastValue(underTest.lockScreenState) @@ -405,10 +402,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val lockScreenState by collectLastValue(underTest.lockScreenState) - val manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE - zenModeRepository.addMode(manualDnd) - runCurrent() - assertThat(lockScreenState) .isEqualTo( KeyguardQuickAffordanceConfig.LockScreenState.Visible( @@ -420,7 +413,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) ) - zenModeRepository.activateMode(manualDnd) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() assertThat(lockScreenState) 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 b3417b9de36d..c44f27ef348b 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 @@ -46,8 +46,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.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -76,7 +74,6 @@ class KeyguardInteractorTest : SysuiTestCase() { 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 } @@ -444,7 +441,6 @@ class KeyguardInteractorTest : SysuiTestCase() { repository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - powerInteractor.setAwakeForTest() advanceTimeBy(1000L) assertThat(isAbleToDream).isEqualTo(false) @@ -460,9 +456,6 @@ class KeyguardInteractorTest : SysuiTestCase() { repository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - powerInteractor.setAwakeForTest() - runCurrent() - // After some delay, still false advanceTimeBy(300L) assertThat(isAbleToDream).isEqualTo(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt index feaf06aca29a..ade7614ae853 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt @@ -16,10 +16,13 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.BrokenWithSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -72,6 +75,28 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notifications_areFullyVisible_whenShadeIsOpen() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.1f), + step(0.2f), + step(0.3f), + step(1f), + ), + testScope, + ) + + values.forEach { assertThat(it).isEqualTo(1f) } + } + + @Test fun blurRadiusGoesToMaximumWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -88,6 +113,25 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notificationBlur_isNonZero_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + @Test fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt index d909c5ab5f1b..914094fa39df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt @@ -16,9 +16,11 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -153,7 +155,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test - @BrokenWithSceneContainer(330311871) + @BrokenWithSceneContainer(388068805) fun blurRadiusIsMaxWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -170,7 +172,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test - @BrokenWithSceneContainer(330311871) + @BrokenWithSceneContainer(388068805) fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -185,6 +187,44 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza ) } + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notificationBlur_isNonZero_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + runCurrent() + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notifications_areFullyVisible_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + runCurrent() + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = 1.0f, + endValue = 1.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + private fun step( value: Float, state: TransitionState = TransitionState.RUNNING, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt index eeccbdf20540..79556baed067 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.qs.tiles import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf import android.service.quicksettings.Tile @@ -24,18 +26,26 @@ import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger +import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake +import com.android.systemui.flags.setFlagValue +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.flags.QSComposeFragment +import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tiles.dialog.InternetDialogManager import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.connectivity.AccessPointController +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor @@ -256,6 +266,41 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true) } + @Test + @DisableFlags(QsDetailedView.FLAG_NAME) + fun click_withQsDetailedViewDisabled() { + underTest.click(null) + looper.processAllMessages() + + verify(dialogManager, times(1)).create( + aboveStatusBar = true, + accessPointController.canConfigMobileData(), + accessPointController.canConfigWifi(), + null, + ) + } + + @Test + @EnableFlags( + value = [ + QsDetailedView.FLAG_NAME, + FLAG_SCENE_CONTAINER, + KeyguardWmStateRefactor.FLAG_NAME, + NotificationThrottleHun.FLAG_NAME, + DualShade.FLAG_NAME] + ) + fun click_withQsDetailedViewEnabled() { + underTest.click(null) + looper.processAllMessages() + + verify(dialogManager, times(0)).create( + aboveStatusBar = true, + accessPointController.canConfigMobileData(), + accessPointController.canConfigWifi(), + null, + ) + } + companion object { const val WIFI_SSID = "test ssid" val ACTIVE_WIFI = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index fc1d73b62abd..3a3f5371d195 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import android.app.Dialog; import android.media.projection.StopReason; import android.os.Handler; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.service.quicksettings.Tile; import android.testing.TestableLooper; @@ -52,6 +53,7 @@ import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.flags.QsDetailedView; import com.android.systemui.qs.flags.QsInCompose; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; @@ -63,6 +65,7 @@ import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,11 +73,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.List; - import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; +import java.util.List; + @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest @@ -82,7 +85,8 @@ public class ScreenRecordTileTest extends SysuiTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX); + return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + QsDetailedView.FLAG_NAME); } @Mock @@ -336,6 +340,30 @@ public class ScreenRecordTileTest extends SysuiTestCase { .notifyPermissionRequestDisplayed(mContext.getUserId()); } + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testNotStartingAndRecording_returnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(false); + mTile.getDetailsViewModel(Assert::assertNotNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testStarting_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(true); + when(mController.isRecording()).thenReturn(false); + mTile.getDetailsViewModel(Assert::assertNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testRecording_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(true); + mTile.getDetailsViewModel(Assert::assertNull); + } + private QSTile.Icon createExpectedIcon(int resId) { if (QsInCompose.isEnabled()) { return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 04e094f25f5d..c8b3aba9b846 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -24,6 +24,7 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable @@ -87,9 +88,9 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val activeModes by collectLastValue(zenModeInteractor.activeModes) + zenModeRepository.activateMode(MANUAL_DND) zenModeRepository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder().setName("Mode 1").setActive(true).build(), TestModeBuilder().setName("Mode 2").setActive(true).build(), ) @@ -111,7 +112,7 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(zenModeInteractor.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + zenModeRepository.activateMode(MANUAL_DND) assertThat(dndMode?.isActive).isTrue() underTest.handleInput( @@ -127,7 +128,6 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(zenModeInteractor.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) assertThat(dndMode?.isActive).isFalse() underTest.handleInput( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 48edded5df18..de54e75281aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -575,4 +575,50 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(currentScene).isNotEqualTo(disabledScene) } + + @Test + fun transitionAnimations() = + kosmos.runTest { + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationCancelled() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.setVisible(true, "test") + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationStart() + underTest.onTransitionAnimationStart() + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // One animation is still active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 06dd046564df..51f056aa18da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -36,6 +36,8 @@ import com.android.keyguard.AuthInteractionProperties import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.authenticationInteractor @@ -141,6 +143,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever @SmallTest @@ -169,6 +172,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private val uiEventLoggerFake = kosmos.uiEventLoggerFake private val msdlPlayer = kosmos.fakeMSDLPlayer private val authInteractionProperties = AuthInteractionProperties() + private val mockActivityTransitionAnimator = mock<ActivityTransitionAnimator>() private lateinit var underTest: SceneContainerStartable @@ -177,6 +181,8 @@ class SceneContainerStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) .thenReturn(true) + kosmos.activityTransitionAnimator = mockActivityTransitionAnimator + underTest = kosmos.sceneContainerStartable } @@ -2716,6 +2722,27 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentOverlays).isEmpty() } + @Test + fun hydrateActivityTransitionAnimationState() = + kosmos.runTest { + underTest.start() + + val isVisible by collectLastValue(sceneInteractor.isVisible) + assertThat(isVisible).isTrue() + + sceneInteractor.setVisible(false, "reason") + assertThat(isVisible).isFalse() + + val argumentCaptor = argumentCaptor<ActivityTransitionAnimator.Listener>() + verify(mockActivityTransitionAnimator).addListener(argumentCaptor.capture()) + + val listeners = argumentCaptor.allValues + listeners.forEach { it.onTransitionAnimationStart() } + assertThat(isVisible).isTrue() + listeners.forEach { it.onTransitionAnimationEnd() } + assertThat(isVisible).isFalse() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 789ca5158dbf..62c360400582 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -85,7 +85,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; @@ -335,16 +334,14 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false); mMainDispatcher = getMainDispatcher(); - KeyguardInteractorFactory.WithDependencies keyguardInteractorDeps = - KeyguardInteractorFactory.create(); - mFakeKeyguardRepository = keyguardInteractorDeps.getRepository(); + mFakeKeyguardRepository = mKosmos.getKeyguardRepository(); mFakeKeyguardClockRepository = new FakeKeyguardClockRepository(); mKeyguardClockInteractor = mKosmos.getKeyguardClockInteractor(); - mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor(); + mKeyguardInteractor = mKosmos.getKeyguardInteractor(); mShadeRepository = new FakeShadeRepository(); mShadeAnimationInteractor = new ShadeAnimationInteractorLegacyImpl( new ShadeAnimationRepository(), mShadeRepository); - mPowerInteractor = keyguardInteractorDeps.getPowerInteractor(); + mPowerInteractor = mKosmos.getPowerInteractor(); when(mKeyguardTransitionInteractor.isInTransitionWhere(any(), any())).thenReturn( MutableStateFlow(false)); when(mKeyguardTransitionInteractor.isInTransition(any(), any())) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 20474c842b51..deaf57999b21 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -526,7 +526,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1)); // Device is not currently locked - when(mKeyguardManager.isDeviceLocked()).thenReturn(false); + mLockscreenUserManager.mLocked.set(false); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, @@ -540,7 +540,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, mCurrentUser.id); changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Device was locked after this notification arrived mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1)); @@ -560,7 +560,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { // Device has been locked for 1 second before the notification came in, which is too short mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1)); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, @@ -577,7 +577,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { // Claim the device was last locked 1 day ago mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1)); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 2d7dc2e63650..0a0564994e69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.WallpaperController import com.android.systemui.util.mockito.eq import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor +import com.android.wm.shell.appzoomout.AppZoomOut import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import org.junit.Before @@ -65,6 +66,7 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit +import java.util.Optional @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -82,6 +84,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Mock private lateinit var wallpaperController: WallpaperController @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var appZoomOutOptional: Optional<AppZoomOut> @Mock private lateinit var root: View @Mock private lateinit var viewRootImpl: ViewRootImpl @Mock private lateinit var windowToken: IBinder @@ -128,6 +131,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { ResourcesSplitShadeStateController(), windowRootViewBlurInteractor, applicationScope, + appZoomOutOptional, dumpManager, configurationController, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 0061c4142e8b..75d000b63d62 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -22,7 +22,6 @@ import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue @@ -126,65 +125,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_positiveStartTime_notifIconFlagOff_iconIsPhone() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, notificationIcon = createStatusBarIconViewOrNull()) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) - val icon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.SingleColorIcon) - .impl as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) - assertThat(icon.contentDescription).isNotNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - val notifIcon = createStatusBarIconViewOrNull() - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon)) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java) - val actualIcon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.StatusBarView) - .impl - assertThat(actualIcon).isEqualTo(notifIcon) - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel( - startTimeMs = 1000, - notificationIcon = createStatusBarIconViewOrNull(), - notificationKey = "notifKey", - ) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_positiveStartTime_connectedDisplaysFlagOn_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -205,29 +147,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_zeroStartTime_notifIconFlagOff_iconIsPhone() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 0, notificationIcon = createStatusBarIconViewOrNull()) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) - val icon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.SingleColorIcon) - .impl as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) - assertThat(icon.contentDescription).isNotNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_zeroStartTime_notifIconFlagOn_cdFlagOff_iconIsNotifIcon() = + fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -244,8 +165,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_zeroStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -262,7 +183,6 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() = testScope.runTest { @@ -281,7 +201,7 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() = testScope.runTest { val latest by collectLastValue(underTest.chip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index e561e3ea27d7..902db5e10589 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -501,7 +501,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_hasHeadsUpByUser_onlyShowsIcon() = + fun chips_hasHeadsUpBySystem_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -523,7 +523,99 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) ) - // WHEN there's a HUN pinned by a user + // WHEN there's a HUN pinned by the system + kosmos.headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "notif", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + // THEN the chip keeps showing time + // (In real life the chip won't show at all, but that's handled in a different part of + // the system. What we know here is that the chip shouldn't shrink to icon only.) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + val otherPromotedContentBuilder = + PromotedNotificationContentModel.Builder("other notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 654321L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + val icon = createStatusBarIconViewOrNull() + val otherIcon = createStatusBarIconViewOrNull() + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = icon, + promotedContent = promotedContentBuilder.build(), + ), + activeNotificationModel( + key = "other notif", + statusBarChipIcon = otherIcon, + promotedContent = otherPromotedContentBuilder.build(), + ), + ) + ) + + // WHEN there's a HUN pinned for the "other notif" chip + kosmos.headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "other notif", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + // THEN the "notif" chip keeps showing time + val chip = latest!![0] + assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) + assertIsNotifChip(chip, icon, "notif") + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock<StatusBarIconView>(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + // WHEN this notification is pinned by the user kosmos.headsUpNotificationRepository.setNotifications( UnconfinedFakeHeadsUpRowRepository( key = "notif", @@ -531,6 +623,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) ) + // THEN the chip shrinks to icon only assertThat(latest!![0]) .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt index c9c961791e89..49b95d92129c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt @@ -45,7 +45,7 @@ import com.google.common.truth.Truth.assertWithMessage import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt new file mode 100644 index 000000000000..72001758d01f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import android.content.res.Configuration +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.kosmos.Kosmos +import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.fake +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) +class StatusBarContentInsetsViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val configuration = Configuration() + + private val Kosmos.underTest by Kosmos.Fixture { statusBarContentInsetsViewModel } + + @Test + fun contentArea_onMaxBoundsChanged_emitsNewValue() = + kosmos.runTest { + statusBarContentInsetsProvider.start() + + val values by collectValues(underTest.contentArea) + + // WHEN the content area changes + configurationController.fake.notifyLayoutDirectionChanged(isRtl = true) + configurationController.fake.notifyDensityOrFontScaleChanged() + + // THEN the flow emits the new bounds + assertThat(values[0]).isNotEqualTo(values[1]) + } + + @Test + fun contentArea_onDensityOrFontScaleChanged_emitsLastBounds() = + kosmos.runTest { + configuration.densityDpi = 12 + statusBarContentInsetsProvider.start() + + val values by collectValues(underTest.contentArea) + + // WHEN a change happens but it doesn't affect content area + configuration.densityDpi = 20 + configurationController.onConfigurationChanged(configuration) + configurationController.fake.notifyDensityOrFontScaleChanged() + + // THEN it still has the last bounds + assertThat(values).hasSize(1) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index a3ffd91b1728..609885d0214b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -458,7 +458,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_hasNotifEntry_shownAsHUN() = + fun onPromotedNotificationChipTapped_hasNotifEntry_shownAsHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -473,7 +473,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_noNotifEntry_noHUN() = + fun onPromotedNotificationChipTapped_noNotifEntry_noHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(null) @@ -488,7 +488,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() = + fun onPromotedNotificationChipTapped_shownAsHUNEvenIfEntryShouldNot() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -511,7 +511,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() = + fun onPromotedNotificationChipTapped_atSameTimeAsOnAdded_promotedShownAsHUN() = testScope.runTest { // First, the promoted notification appears as not heads up val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build() @@ -548,6 +548,33 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTap() = + testScope.runTest { + whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) + + // WHEN chip tapped first + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN HUN is shown + finishBind(entry) + verify(headsUpManager).showNotification(entry, isPinnedByUser = true) + addHUN(entry) + + // WHEN chip is tapped again + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN HUN is hidden + verify(headsUpManager).removeNotification(eq(entry.key), eq(false), any()) + } + + @Test fun testTransferIsolatedChildAlert_withGroupAlertSummary() { setShouldHeadsUp(groupSummary) whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, groupSibling1)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index 22ef408e266c..fae7d515d305 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -412,46 +413,53 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { @Test fun statusBarHeadsUpState_pinnedBySystem() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedBySystem) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedBySystem) + assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedBySystem)) + assertThat(status).isEqualTo(PinnedStatus.PinnedBySystem) } @Test fun statusBarHeadsUpState_pinnedByUser() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedByUser) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedByUser) + assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedByUser)) + assertThat(status).isEqualTo(PinnedStatus.PinnedByUser) } @Test fun statusBarHeadsUpState_withoutPinnedNotifications_notPinned() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", PinnedStatus.NotPinned) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.NotPinned) + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status).isEqualTo(PinnedStatus.NotPinned) } @Test fun statusBarHeadsUpState_whenShadeExpanded_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -463,13 +471,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // should emit `false`. kosmos.fakeShadeRepository.setLegacyShadeExpansion(1.0f) - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_notificationsAreHidden_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -477,13 +487,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND the notifications are hidden keyguardViewStateRepository.areNotificationsFullyHidden.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_onLockScreen_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -494,13 +506,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { testSetup = true, ) - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_onByPassLockScreen_true() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -513,13 +527,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isTrue() + assertThat(state).isInstanceOf(TopPinnedState.Pinned::class.java) + assertThat(status!!.isPinned).isTrue() } @Test fun statusBarHeadsUpState_onByPassLockScreen_withoutNotifications_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN no pinned rows // AND the lock screen is shown @@ -530,7 +546,8 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 72a91bc12f8d..14bbd38ece2c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -279,7 +279,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { notification); RemoteViews headerRemoteViews; if (lowPriority) { - headerRemoteViews = builder.makeLowPriorityContentView(true, false); + headerRemoteViews = builder.makeLowPriorityContentView(true); } else { headerRemoteViews = builder.makeNotificationGroupHeader(); } 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 459778868ccd..a045b37a8119 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 @@ -70,6 +70,7 @@ import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition import com.android.systemui.testKosmos +import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlin.test.assertIs @@ -1395,6 +1396,19 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S assertThat(stackAbsoluteBottom).isEqualTo(100F) } + @Test + fun blurRadius_emitsValues_fromPrimaryBouncerTransitions() = + testScope.runTest { + val blurRadius by collectLastValue(underTest.blurRadius) + assertThat(blurRadius).isEqualTo(0.0f) + + kosmos.fakeBouncerTransitions.first().notificationBlurRadius.value = 30.0f + assertThat(blurRadius).isEqualTo(30.0f) + + kosmos.fakeBouncerTransitions.last().notificationBlurRadius.value = 40.0f + assertThat(blurRadius).isEqualTo(40.0f) + } + private suspend fun TestScope.showLockscreen() { shadeTestUtil.setQsExpansion(0f) shadeTestUtil.setLockscreenShadeExpansion(0f) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 9eafcdbadfa5..e2330f448a1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.hardware.biometrics.BiometricSourceType; +import android.hardware.fingerprint.FingerprintManager; import android.os.Handler; import android.os.PowerManager; import android.os.UserHandle; @@ -431,9 +432,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test - public void onUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() { - // GIVEN UDFPS is supported - when(mUpdateMonitor.isUdfpsSupported()).thenReturn(true); + public void onOpticalUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() { + // GIVEN optical UDFPS is supported + when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(true); // WHEN udfps fails once - then don't show the bouncer yet mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); @@ -451,6 +452,25 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void onUltrasonicUdfpsLockout_showPrimaryBouncer() { + // GIVEN ultrasonic UDFPS is supported + when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(false); + + // WHEN udfps fails three times, don't show bouncer + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + + // WHEN lockout is received + mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "Lockout", BiometricSourceType.FINGERPRINT); + + // THEN show bouncer + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true); + } + + @Test public void onFinishedGoingToSleep_authenticatesWhenPending() { when(mUpdateMonitor.isGoingToSleep()).thenReturn(true); mBiometricUnlockController.mWakefulnessObserver.onFinishedGoingToSleep(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index ffd349d744a8..43ad042ecf78 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt @@ -53,7 +53,7 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler -import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider +import com.android.systemui.statusbar.layout.mockStatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.policy.BatteryController @@ -153,7 +153,8 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { shadeViewStateProvider = TestShadeViewStateProvider() Mockito.`when`( - kosmos.statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation() + kosmos.mockStatusBarContentInsetsProvider + .getStatusBarContentInsetsForCurrentRotation() ) .thenReturn(Insets.of(0, 0, 0, 0)) @@ -162,7 +163,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { Mockito.`when`(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(iconManager) Mockito.`when`(statusBarContentInsetsProviderStore.defaultDisplay) - .thenReturn(kosmos.statusBarContentInsetsProvider) + .thenReturn(kosmos.mockStatusBarContentInsetsProvider) allowTestableLooperAsMainThread() looper.runWithLooper { keyguardStatusBarView = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index cf512cdee800..b98409906f8d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -28,9 +28,7 @@ import android.view.View import android.widget.LinearLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP import com.android.systemui.SysuiTestCase import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.dump.DumpManager @@ -42,7 +40,6 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -76,9 +73,8 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @OptIn(ExperimentalCoroutinesApi::class) -@EnableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP) @DisableFlags(StatusBarChipsModernization.FLAG_NAME) -class OngoingCallControllerViaRepoTest : SysuiTestCase() { +class OngoingCallControllerTest : SysuiTestCase() { private val kosmos = Kosmos() private val clock = kosmos.fakeSystemClock @@ -114,7 +110,6 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { testScope.backgroundScope, context, ongoingCallRepository, - mock<CommonNotifCollection>(), kosmos.activeNotificationsInteractor, clock, mockActivityStarter, @@ -162,28 +157,7 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun interactorHasOngoingCallNotif_notifIconFlagOff_repoHasNoNotifIcon() = - testScope.runTest { - val icon = mock<StatusBarIconView>() - setNotifOnRepo( - activeNotificationModel( - key = "ongoingNotif", - callType = CallType.Ongoing, - uid = CALL_UID, - statusBarChipIcon = icon, - whenTime = 567, - ) - ) - - val repoState = ongoingCallRepository.ongoingCallState.value - assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) - assertThat((repoState as OngoingCallModel.InCall).notificationIconView).isNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun interactorHasOngoingCallNotif_notifIconFlagOn_repoHasNotifIcon() = + fun interactorHasOngoingCallNotif_repoHasNotifIcon() = testScope.runTest { val icon = mock<StatusBarIconView>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt deleted file mode 100644 index cd3539d6b9a5..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt +++ /dev/null @@ -1,694 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.ongoingcall - -import android.app.ActivityManager -import android.app.IActivityManager -import android.app.IUidObserver -import android.app.Notification -import android.app.PendingIntent -import android.app.Person -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags -import android.service.notification.NotificationListenerService.REASON_USER_STOPPED -import android.testing.TestableLooper -import android.view.LayoutInflater -import android.view.View -import android.widget.LinearLayout -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP -import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.log.logcatLogBuffer -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.res.R -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository -import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor -import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.window.StatusBarWindowController -import com.android.systemui.statusbar.window.StatusBarWindowControllerStore -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.nullable -import org.mockito.Mock -import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.reset -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.whenever - -private const val CALL_UID = 900 - -// A process state that represents the process being visible to the user. -private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP - -// A process state that represents the process being invisible to the user. -private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE - -@SmallTest -@RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper -@OptIn(ExperimentalCoroutinesApi::class) -@DisableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, StatusBarChipsModernization.FLAG_NAME) -class OngoingCallControllerViaListenerTest : SysuiTestCase() { - private val kosmos = Kosmos() - - private val clock = FakeSystemClock() - private val mainExecutor = FakeExecutor(clock) - private val testScope = TestScope() - private val statusBarModeRepository = FakeStatusBarModeRepository() - private val ongoingCallRepository = kosmos.ongoingCallRepository - - private lateinit var controller: OngoingCallController - private lateinit var notifCollectionListener: NotifCollectionListener - - @Mock - private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler - @Mock private lateinit var mockOngoingCallListener: OngoingCallListener - @Mock private lateinit var mockActivityStarter: ActivityStarter - @Mock private lateinit var mockIActivityManager: IActivityManager - @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController - @Mock private lateinit var mockStatusBarWindowControllerStore: StatusBarWindowControllerStore - - private lateinit var chipView: View - - @Before - fun setUp() { - allowTestableLooperAsMainThread() - TestableLooper.get(this).runWithLooper { - chipView = - LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null) - } - - MockitoAnnotations.initMocks(this) - val notificationCollection = mock(CommonNotifCollection::class.java) - whenever(mockStatusBarWindowControllerStore.defaultDisplay) - .thenReturn(mockStatusBarWindowController) - - controller = - OngoingCallController( - testScope.backgroundScope, - context, - ongoingCallRepository, - notificationCollection, - kosmos.activeNotificationsInteractor, - clock, - mockActivityStarter, - mainExecutor, - mockIActivityManager, - DumpManager(), - mockStatusBarWindowControllerStore, - mockSwipeStatusBarAwayGestureHandler, - statusBarModeRepository, - logcatLogBuffer("OngoingCallControllerViaListenerTest"), - ) - controller.start() - controller.addCallback(mockOngoingCallListener) - controller.setChipView(chipView) - onTeardown { controller.tearDownChipView() } - - val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) - verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) - notifCollectionListener = collectionListenerCaptor.value!! - - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - } - - @Test - fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(567) - notifCollectionListener.onEntryUpdated(notification.build()) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - val repoState = ongoingCallRepository.ongoingCallState.value - assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) - assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567) - } - - @Test - fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true) - } - - @Test - fun onEntryUpdated_notOngoingCallNotif_listenerAndRepoNotNotified() { - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_repoUpdated() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - /** Regression test for b/191472854. */ - @Test - fun onEntryUpdated_notifHasNullContentIntent_noCrash() { - notifCollectionListener.onEntryUpdated( - createCallNotifEntry(ongoingCallStyle, nullContentIntent = true) - ) - } - - /** Regression test for b/192379214. */ - @Test - @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsZero_timeHidden() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(0) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isEqualTo(0) - } - - @Test - @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsZero_timeShown() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(0) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsValid_timeShown() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(clock.currentTimeMillis()) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - - /** Regression test for b/194731244. */ - @Test - fun onEntryUpdated_calledManyTimes_uidObserverOnlyRegisteredOnce() { - for (i in 0 until 4) { - // Re-create the notification each time so that it's considered a different object and - // will re-trigger the whole flow. - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_getUidProcessStateThrowsException_noCrash() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenThrow(SecurityException()) - - // No assert required, just check no crash - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_registerUidObserverThrowsException_noCrash() { - `when`( - mockIActivityManager.registerUidObserver( - any(), - any(), - any(), - nullable(String::class.java), - ) - ) - .thenThrow(SecurityException()) - - // No assert required, just check no crash - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_packageNameProvidedToActivityManager() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - val packageNameCaptor = ArgumentCaptor.forClass(String::class.java) - verify(mockIActivityManager) - .registerUidObserver(any(), any(), any(), packageNameCaptor.capture()) - assertThat(packageNameCaptor.value).isNotNull() - } - - /** - * If a call notification is never added before #onEntryRemoved is called, then the listener - * should never be notified. - */ - @Test - fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() { - notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockOngoingCallListener) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_callNotifAddedThenRemoved_repoUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false) - } - - /** Regression test for b/188491504. */ - @Test - fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockOngoingCallListener) - - // Create another notification based on the ongoing call one, but remove the features that - // made it a call notification. - val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) - removedEntryBuilder.modifyNotification(context).style = null - - notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - /** Regression test for b/188491504. */ - @Test - fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_repoUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - - // Create another notification based on the ongoing call one, but remove the features that - // made it a call notification. - val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) - removedEntryBuilder.modifyNotification(context).style = null - - notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() { - notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_repoNotUpdated() { - notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) - - notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - } - - @Test - fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() { - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_unrelatedNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_screeningCallNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isTrue() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { - val invalidChipView = LinearLayout(context) - controller.setChipView(invalidChipView) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - - notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry) - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isTrue() - } - - /** - * This test fakes a theme change during an ongoing call. - * - * When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so - * [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing - * call when the theme changes, the new view needs to be updated with the call information. - */ - @Test - fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() { - // Start an ongoing call. - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - lateinit var newChipView: View - TestableLooper.get(this).runWithLooper { - newChipView = - LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null) - } - - // Change the chip view associated with the controller. - controller.setChipView(newChipView) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun callProcessChangesToVisible_listenerNotified() { - // Start the call while the process is invisible. - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager) - .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) - val uidObserver = captor.value - - // Update the process to visible. - uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0) - mainExecutor.advanceClockToLast() - mainExecutor.runAllReady() - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun callProcessChangesToInvisible_listenerNotified() { - // Start the call while the process is visible. - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager) - .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) - val uidObserver = captor.value - - // Update the process to invisible. - uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0) - mainExecutor.advanceClockToLast() - mainExecutor.runAllReady() - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - /** Regression test for b/212467440. */ - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() { - val notifEntry = createOngoingCallNotifEntry() - val pendingIntent = notifEntry.sbn.notification.contentIntent - notifCollectionListener.onEntryUpdated(notifEntry) - - chipView.performClick() - - // Ensure that the sysui didn't modify the notification's intent -- see b/212467440. - verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any()) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_chipIsClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_newChipsEnabled_chipNotClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(chipView.hasOnClickListeners()).isFalse() - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun fullscreenIsTrue_chipStillClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - // Swipe gesture tests - - @Test - fun callStartedInImmersiveMode_swipeGestureCallbackAdded() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockSwipeStatusBarAwayGestureHandler) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - testScope.runCurrent() - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockSwipeStatusBarAwayGestureHandler, never()) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun transitionToImmersiveMode_swipeGestureCallbackAdded() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - verify(mockSwipeStatusBarAwayGestureHandler) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockSwipeStatusBarAwayGestureHandler) - - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - testScope.runCurrent() - - verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) - } - - @Test - fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockSwipeStatusBarAwayGestureHandler) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) - } - - // TODO(b/195839150): Add test - // swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's - // difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s - // callbacks in test. - - // END swipe gesture tests - - private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle) - - private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle) - - private fun createCallNotifEntry( - callStyle: Notification.CallStyle, - nullContentIntent: Boolean = false, - ): NotificationEntry { - val notificationEntryBuilder = NotificationEntryBuilder() - notificationEntryBuilder.modifyNotification(context).style = callStyle - notificationEntryBuilder.setUid(CALL_UID) - - if (nullContentIntent) { - notificationEntryBuilder.modifyNotification(context).setContentIntent(null) - } else { - val contentIntent = mock(PendingIntent::class.java) - notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) - } - - return notificationEntryBuilder.build() - } - - private fun createNotCallNotifEntry() = NotificationEntryBuilder().build() -} - -private val person = Person.Builder().setName("name").build() -private val hangUpIntent = mock(PendingIntent::class.java) - -private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent) -private val screeningCallStyle = - Notification.CallStyle.forScreeningCall( - person, - hangUpIntent, - /* answerIntent= */ mock(PendingIntent::class.java), - ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index a9db0b70dd4d..6feada1c9769 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeHomeStatusBarViewModel( override val operatorNameViewModel: StatusBarOperatorNameViewModel ) : HomeStatusBarViewModel { - private val areNotificationLightsOut = MutableStateFlow(false) + override val areNotificationsLightsOut = MutableStateFlow(false) override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false) @@ -77,14 +77,14 @@ class FakeHomeStatusBarViewModel( override val iconBlockList: MutableStateFlow<List<String>> = MutableStateFlow(listOf()) - override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut + override val contentArea = MutableStateFlow(Rect(0, 0, 1, 1)) val darkRegions = mutableListOf<Rect>() var darkIconTint = Color.BLACK var lightIconTint = Color.WHITE - override fun areaTint(displayId: Int): Flow<StatusBarTintColor> = + override val areaTint: Flow<StatusBarTintColor> = MutableStateFlow( StatusBarTintColor { viewBounds -> if (DarkIconDispatcher.isInAreas(darkRegions, viewBounds)) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt index e91875cd0885..e95bc3378423 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt @@ -22,13 +22,17 @@ import android.app.StatusBarManager.DISABLE_CLOCK import android.app.StatusBarManager.DISABLE_NONE import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS import android.app.StatusBarManager.DISABLE_SYSTEM_INFO +import android.content.testableContext import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.view.Display.DEFAULT_DISPLAY import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.display.data.repository.fake import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -59,7 +63,6 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip import com.android.systemui.statusbar.data.model.StatusBarMode -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel @@ -85,6 +88,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.Before import org.junit.Test @@ -104,6 +108,9 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { setUpPackageManagerForMediaProjection(kosmos) } + @Before + fun addDisplays() = runBlocking { kosmos.displayRepository.fake.addDisplay(DEFAULT_DISPLAY) } + @Test fun isTransitioningFromLockscreenToOccluded_started_isTrue() = kosmos.runTest { @@ -363,7 +370,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(testNotifications) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isTrue() } @@ -377,7 +384,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(emptyList()) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -391,7 +398,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(emptyList()) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -405,7 +412,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(testNotifications) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -415,7 +422,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { fun areNotificationsLightsOut_requiresFlagEnabled() = kosmos.runTest { assertLogsWtf { - val flow = underTest.areNotificationsLightsOut(DISPLAY_ID) + val flow = underTest.areNotificationsLightsOut assertThat(flow).isEqualTo(emptyFlow<Boolean>()) } } @@ -1005,11 +1012,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsInDarkBounds_getsDarkTint() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) val tint = areaTint?.tint(Rect(1, 1, 3, 3)) @@ -1019,11 +1026,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsNotInDarkBounds_getsDefaultTint() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) val tint = areaTint?.tint(Rect(6, 6, 7, 7)) @@ -1033,11 +1040,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsInDarkBounds_darkBoundsChange_viewUpdates() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) var tint = areaTint?.tint(Rect(1, 1, 3, 3)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index c6bae197ad76..7802b921eae0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest import com.android.internal.R import com.android.settingslib.notification.data.repository.updateNotificationPolicy import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -204,7 +205,7 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun shouldAskForZenDuration_changesWithSetting() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE + val manualDnd = TestModeBuilder().makeManualDnd().setActive(true).build() settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() @@ -233,29 +234,27 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun activateMode_usesCorrectDuration() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE - zenModeRepository.addModes(listOf(manualDnd)) settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() - underTest.activateMode(manualDnd) - assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)).isNull() + underTest.activateMode(MANUAL_DND) + assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)).isNull() - zenModeRepository.deactivateMode(manualDnd.id) + zenModeRepository.deactivateMode(MANUAL_DND) settingsRepository.setInt(ZEN_DURATION, 60) runCurrent() - underTest.activateMode(manualDnd) - assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)) + underTest.activateMode(MANUAL_DND) + assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)) .isEqualTo(Duration.ofMinutes(60)) } @Test fun deactivateAllModes_updatesCorrectModes() = testScope.runTest { + zenModeRepository.activateMode(MANUAL_DND) zenModeRepository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder().setName("Inactive").setActive(false).build(), TestModeBuilder().setName("Active").setActive(true).build(), ) @@ -389,12 +388,9 @@ class ZenModeInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(underTest.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) - runCurrent() - assertThat(dndMode!!.isActive).isFalse() - zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND_INACTIVE.id) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() assertThat(dndMode!!.isActive).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index 07d088bbb3d6..856de8ee1c80 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -28,6 +28,7 @@ import android.service.notification.ZenModeConfig.ScheduleInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -111,7 +112,6 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setName("Disabled by other") .setEnabled(false, /* byUser= */ false) .build(), - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setName("Enabled") .setEnabled(true) @@ -128,14 +128,14 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles).hasSize(3) with(tiles?.elementAt(0)!!) { - assertThat(this.text).isEqualTo("Disabled by other") - assertThat(this.subtext).isEqualTo("Not set") + assertThat(this.text).isEqualTo("Do Not Disturb") + assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(1)!!) { - assertThat(this.text).isEqualTo("Do Not Disturb") - assertThat(this.subtext).isEqualTo("On") - assertThat(this.enabled).isEqualTo(true) + assertThat(this.text).isEqualTo("Disabled by other") + assertThat(this.subtext).isEqualTo("Not set") + assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Enabled") @@ -176,18 +176,24 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Do Not Disturb") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Active with manual") assertThat(this.subtext).isEqualTo("On • trigger description") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(2)!!) { + with(tiles?.elementAt(3)!!) { assertThat(this.text).isEqualTo("Inactive with manual") assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) @@ -226,10 +232,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) // Check that tile is initially present - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) @@ -239,8 +246,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() } // Check that tile is still present at the same location, but turned off - assertThat(tiles).hasSize(3) - with(tiles?.elementAt(0)!!) { + assertThat(tiles).hasSize(4) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -252,9 +259,9 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() // Check that tile is now gone - assertThat(tiles2).hasSize(2) - assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual") - assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual") + assertThat(tiles2).hasSize(3) + assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Active with manual") + assertThat(tiles2?.elementAt(2)!!.text).isEqualTo("Inactive with manual") } @Test @@ -287,22 +294,23 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) repository.removeMode("A") runCurrent() - assertThat(tiles).hasSize(2) + assertThat(tiles).hasSize(3) repository.removeMode("B") runCurrent() - assertThat(tiles).hasSize(1) + assertThat(tiles).hasSize(2) repository.removeMode("C") runCurrent() - assertThat(tiles).hasSize(0) + assertThat(tiles).hasSize(1) } @Test @@ -353,14 +361,15 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles!!).hasSize(7) - assertThat(tiles!![0].subtext).isEqualTo("When the going gets tough") - assertThat(tiles!![1].subtext).isEqualTo("On • When in Rome") - assertThat(tiles!![2].subtext).isEqualTo("Not set") - assertThat(tiles!![3].subtext).isEqualTo("Off") - assertThat(tiles!![4].subtext).isEqualTo("On") - assertThat(tiles!![5].subtext).isEqualTo("Not set") - assertThat(tiles!![6].subtext).isEqualTo(timeScheduleMode.triggerDescription) + // Manual DND is included by default + assertThat(tiles!!).hasSize(8) + assertThat(tiles!![1].subtext).isEqualTo("When the going gets tough") + assertThat(tiles!![2].subtext).isEqualTo("On • When in Rome") + assertThat(tiles!![3].subtext).isEqualTo("Not set") + assertThat(tiles!![4].subtext).isEqualTo("Off") + assertThat(tiles!![5].subtext).isEqualTo("On") + assertThat(tiles!![6].subtext).isEqualTo("Not set") + assertThat(tiles!![7].subtext).isEqualTo(timeScheduleMode.triggerDescription) } @Test @@ -411,32 +420,33 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles!!).hasSize(7) - with(tiles?.elementAt(0)!!) { + // Manual DND is included by default + assertThat(tiles!!).hasSize(8) + with(tiles?.elementAt(1)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("When the going gets tough") } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.stateDescription).isEqualTo("On") assertThat(this.subtextDescription).isEqualTo("When in Rome") } - with(tiles?.elementAt(2)!!) { + with(tiles?.elementAt(3)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("Not set") } - with(tiles?.elementAt(3)!!) { + with(tiles?.elementAt(4)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEmpty() } - with(tiles?.elementAt(4)!!) { + with(tiles?.elementAt(5)!!) { assertThat(this.stateDescription).isEqualTo("On") assertThat(this.subtextDescription).isEmpty() } - with(tiles?.elementAt(5)!!) { + with(tiles?.elementAt(6)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("Not set") } - with(tiles?.elementAt(6)!!) { + with(tiles?.elementAt(7)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription) .isEqualTo( @@ -456,31 +466,30 @@ class ModesDialogViewModelTest : SysuiTestCase() { val tiles by collectLastValue(underTest.tiles) val modeId = "id" - repository.addModes( - listOf( - TestModeBuilder() - .setId(modeId) - .setName("Test") - .setManualInvocationAllowed(true) - .build() - ) + repository.addMode( + TestModeBuilder() + .setId(modeId) + .setName("Test") + .setManualInvocationAllowed(true) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) - assertThat(tiles?.elementAt(0)?.enabled).isFalse() + // Manual DND is included by default + assertThat(tiles).hasSize(2) + assertThat(tiles?.elementAt(1)?.enabled).isFalse() // Trigger onClick - tiles?.first()?.onClick?.let { it() } + tiles?.elementAt(1)?.onClick?.let { it() } runCurrent() - assertThat(tiles?.first()?.enabled).isTrue() + assertThat(tiles?.elementAt(1)?.enabled).isTrue() // Trigger onClick - tiles?.first()?.onClick?.let { it() } + tiles?.elementAt(1)?.onClick?.let { it() } runCurrent() - assertThat(tiles?.first()?.enabled).isFalse() + assertThat(tiles?.elementAt(1)?.enabled).isFalse() } @Test @@ -489,25 +498,24 @@ class ModesDialogViewModelTest : SysuiTestCase() { val job = Job() val tiles by collectLastValue(underTest.tiles, context = job) - repository.addModes( - listOf( - TestModeBuilder() - .setName("Active without manual") - .setActive(true) - .setManualInvocationAllowed(false) - .build() - ) + repository.addMode( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) + // Manual DND is included by default + assertThat(tiles).hasSize(2) // Click tile to toggle it off - tiles?.elementAt(0)!!.onClick() + tiles?.elementAt(1)!!.onClick() runCurrent() - assertThat(tiles).hasSize(1) - with(tiles?.elementAt(0)!!) { + assertThat(tiles).hasSize(2) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -518,7 +526,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { } // Check that nothing happened - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -530,19 +538,18 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) - repository.addModes( - listOf( - TestModeBuilder() - .setId("ID") - .setName("Disabled by other") - .setEnabled(false, /* byUser= */ false) - .build() - ) + repository.addMode( + TestModeBuilder() + .setId("ID") + .setName("Disabled by other") + .setEnabled(false, /* byUser= */ false) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) - with(tiles?.elementAt(0)!!) { + // Manual DND is included by default + assertThat(tiles).hasSize(2) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") assertThat(this.enabled).isEqualTo(false) @@ -561,7 +568,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { .isEqualTo("ID") // Check that nothing happened to the tile - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") assertThat(this.enabled).isEqualTo(false) @@ -593,10 +600,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(2) + // Manual DND is included by default + assertThat(tiles).hasSize(3) // Trigger onLongClick for A - tiles?.first()?.onLongClick?.let { it() } + tiles?.elementAt(1)?.onLongClick?.let { it() } runCurrent() // Check that it launched the correct intent @@ -625,9 +633,9 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) + repository.activateMode(MANUAL_DND) repository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setId("id1") .setName("Inactive Mode One") @@ -644,6 +652,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() + // Manual DND is included by default assertThat(tiles).hasSize(3) // Trigger onClick for each tile in sequence @@ -672,19 +681,17 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) - repository.addModes( - listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, - TestModeBuilder() - .setId("id1") - .setName("Inactive Mode One") - .setActive(false) - .setManualInvocationAllowed(true) - .build(), - ) + repository.addMode( + TestModeBuilder() + .setId("id1") + .setName("Inactive Mode One") + .setActive(false) + .setManualInvocationAllowed(true) + .build() ) runCurrent() + // Manual DND is included by default assertThat(tiles).hasSize(2) val modeCaptor = argumentCaptor<ZenMode>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt index 61c719319de8..824955de83e2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.policy.statusBarConfigurationController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -41,13 +42,18 @@ class StatusBarWindowControllerImplTest : SysuiTestCase() { private val kosmos = testKosmos().also { it.statusBarWindowViewInflater = it.fakeStatusBarWindowViewInflater } - private val underTest = kosmos.statusBarWindowControllerImpl + private lateinit var underTest: StatusBarWindowControllerImpl private val fakeExecutor = kosmos.fakeExecutor private val fakeWindowManager = kosmos.fakeWindowManager private val mockFragmentService = kosmos.fragmentService private val fakeStatusBarWindowViewInflater = kosmos.fakeStatusBarWindowViewInflater private val statusBarConfigurationController = kosmos.statusBarConfigurationController + @Before + fun setUp() { + underTest = kosmos.statusBarWindowControllerImpl + } + @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun attach_connectedDisplaysFlagEnabled_setsConfigControllerOnWindowView() { diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt index d84d89087349..812a964bf8bc 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt @@ -30,10 +30,6 @@ data class ClockConfig( /** Transition to AOD should move smartspace like large clock instead of small clock */ val useAlternateSmartspaceAODTransition: Boolean = false, - /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */ - @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone") - val isReactiveToTone: Boolean = true, - /** True if the clock is large frame clock, which will use weather in compose. */ val useCustomClockScene: Boolean = false, ) diff --git a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt index d93f7d3093b8..81156d9698d8 100644 --- a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt +++ b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt @@ -24,6 +24,7 @@ import javax.annotation.processing.RoundEnvironment import javax.lang.model.element.Element import javax.lang.model.element.ElementKind import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier import javax.lang.model.element.PackageElement import javax.lang.model.element.TypeElement import javax.lang.model.type.TypeKind @@ -183,11 +184,17 @@ class ProtectedPluginProcessor : AbstractProcessor() { // Method implementations for (method in methods) { val methodName = method.simpleName + if (methods.any { methodName.startsWith("${it.simpleName}\$") }) { + continue + } val returnTypeName = method.returnType.toString() val callArgs = StringBuilder() var isFirst = true + val isStatic = method.modifiers.contains(Modifier.STATIC) - line("@Override") + if (!isStatic) { + line("@Override") + } parenBlock("public $returnTypeName $methodName") { // While copying the method signature for the proxy type, we also // accumulate arguments for the nested callsite. @@ -202,7 +209,8 @@ class ProtectedPluginProcessor : AbstractProcessor() { } val isVoid = method.returnType.kind == TypeKind.VOID - val nestedCall = "mInstance.$methodName($callArgs)" + val methodContainer = if (isStatic) sourceName else "mInstance" + val nestedCall = "$methodContainer.$methodName($callArgs)" val callStatement = when { isVoid -> "$nestedCall;" diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml new file mode 100644 index 000000000000..f8c0fa04cd39 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml @@ -0,0 +1,199 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="scaleX" + android:startOffset="1000" + android:valueFrom="0.45561" + android:valueTo="0.69699" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="scaleY" + android:startOffset="1000" + android:valueFrom="0.6288400000000001" + android:valueTo="0.81618" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleX" + android:startOffset="1083" + android:valueFrom="0.69699" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleY" + android:startOffset="1083" + android:valueFrom="0.81618" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="500" + android:propertyName="rotation" + android:startOffset="0" + android:valueFrom="90" + android:valueTo="135" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="500" + android:propertyName="rotation" + android:startOffset="500" + android:valueFrom="135" + android:valueTo="180" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="scaleX" + android:startOffset="1000" + android:valueFrom="0.0434" + android:valueTo="0.05063" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="scaleY" + android:startOffset="1000" + android:valueFrom="0.0434" + android:valueTo="0.042350000000000006" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleX" + android:startOffset="1083" + android:valueFrom="0.05063" + android:valueTo="0.06146" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleY" + android:startOffset="1083" + android:valueFrom="0.042350000000000006" + android:valueTo="0.040780000000000004" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="1017" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="88dp" + android:height="56dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972" + android:translateX="43.528999999999996" + android:translateY="27.898"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " /> + </group> + <group + android:name="_R_G_L_0_G" + android:rotation="0" + android:scaleX="0.06146" + android:scaleY="0.040780000000000004" + android:translateX="44" + android:translateY="28"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M-0.65 -437.37 C-0.65,-437.37 8.33,-437.66 8.33,-437.66 C8.33,-437.66 17.31,-437.95 17.31,-437.95 C17.31,-437.95 26.25,-438.78 26.25,-438.78 C26.25,-438.78 35.16,-439.95 35.16,-439.95 C35.16,-439.95 44.07,-441.11 44.07,-441.11 C44.07,-441.11 52.85,-443 52.85,-443 C52.85,-443 61.6,-445.03 61.6,-445.03 C61.6,-445.03 70.35,-447.09 70.35,-447.09 C70.35,-447.09 78.91,-449.83 78.91,-449.83 C78.91,-449.83 87.43,-452.67 87.43,-452.67 C87.43,-452.67 95.79,-455.97 95.79,-455.97 C95.79,-455.97 104.11,-459.35 104.11,-459.35 C104.11,-459.35 112.36,-462.93 112.36,-462.93 C112.36,-462.93 120.6,-466.51 120.6,-466.51 C120.6,-466.51 128.84,-470.09 128.84,-470.09 C128.84,-470.09 137.09,-473.67 137.09,-473.67 C137.09,-473.67 145.49,-476.84 145.49,-476.84 C145.49,-476.84 153.9,-480.01 153.9,-480.01 C153.9,-480.01 162.31,-483.18 162.31,-483.18 C162.31,-483.18 170.98,-485.54 170.98,-485.54 C170.98,-485.54 179.66,-487.85 179.66,-487.85 C179.66,-487.85 188.35,-490.15 188.35,-490.15 C188.35,-490.15 197.22,-491.58 197.22,-491.58 C197.22,-491.58 206.09,-493.01 206.09,-493.01 C206.09,-493.01 214.98,-494.28 214.98,-494.28 C214.98,-494.28 223.95,-494.81 223.95,-494.81 C223.95,-494.81 232.93,-495.33 232.93,-495.33 C232.93,-495.33 241.9,-495.5 241.9,-495.5 C241.9,-495.5 250.88,-495.13 250.88,-495.13 C250.88,-495.13 259.86,-494.75 259.86,-494.75 C259.86,-494.75 268.78,-493.78 268.78,-493.78 C268.78,-493.78 277.68,-492.52 277.68,-492.52 C277.68,-492.52 286.57,-491.26 286.57,-491.26 C286.57,-491.26 295.31,-489.16 295.31,-489.16 C295.31,-489.16 304.04,-487.04 304.04,-487.04 C304.04,-487.04 312.7,-484.65 312.7,-484.65 C312.7,-484.65 321.19,-481.72 321.19,-481.72 C321.19,-481.72 329.68,-478.78 329.68,-478.78 C329.68,-478.78 337.96,-475.31 337.96,-475.31 C337.96,-475.31 346.14,-471.59 346.14,-471.59 C346.14,-471.59 354.3,-467.82 354.3,-467.82 C354.3,-467.82 362.11,-463.38 362.11,-463.38 C362.11,-463.38 369.92,-458.93 369.92,-458.93 C369.92,-458.93 377.53,-454.17 377.53,-454.17 C377.53,-454.17 384.91,-449.04 384.91,-449.04 C384.91,-449.04 392.29,-443.91 392.29,-443.91 C392.29,-443.91 399.26,-438.24 399.26,-438.24 C399.26,-438.24 406.15,-432.48 406.15,-432.48 C406.15,-432.48 412.92,-426.57 412.92,-426.57 C412.92,-426.57 419.27,-420.22 419.27,-420.22 C419.27,-420.22 425.62,-413.87 425.62,-413.87 C425.62,-413.87 431.61,-407.18 431.61,-407.18 C431.61,-407.18 437.38,-400.29 437.38,-400.29 C437.38,-400.29 443.14,-393.39 443.14,-393.39 C443.14,-393.39 448.27,-386.01 448.27,-386.01 C448.27,-386.01 453.4,-378.64 453.4,-378.64 C453.4,-378.64 458.26,-371.09 458.26,-371.09 C458.26,-371.09 462.71,-363.28 462.71,-363.28 C462.71,-363.28 467.16,-355.47 467.16,-355.47 C467.16,-355.47 471.03,-347.37 471.03,-347.37 C471.03,-347.37 474.75,-339.19 474.75,-339.19 C474.75,-339.19 478.34,-330.95 478.34,-330.95 C478.34,-330.95 481.28,-322.46 481.28,-322.46 C481.28,-322.46 484.21,-313.97 484.21,-313.97 C484.21,-313.97 486.72,-305.35 486.72,-305.35 C486.72,-305.35 488.84,-296.62 488.84,-296.62 C488.84,-296.62 490.96,-287.88 490.96,-287.88 C490.96,-287.88 492.33,-279.01 492.33,-279.01 C492.33,-279.01 493.59,-270.11 493.59,-270.11 C493.59,-270.11 494.69,-261.2 494.69,-261.2 C494.69,-261.2 495.07,-252.22 495.07,-252.22 C495.07,-252.22 495.44,-243.24 495.44,-243.24 C495.44,-243.24 495.41,-234.27 495.41,-234.27 C495.41,-234.27 494.88,-225.29 494.88,-225.29 C494.88,-225.29 494.35,-216.32 494.35,-216.32 C494.35,-216.32 493.22,-207.42 493.22,-207.42 C493.22,-207.42 491.79,-198.55 491.79,-198.55 C491.79,-198.55 490.36,-189.68 490.36,-189.68 C490.36,-189.68 488.19,-180.96 488.19,-180.96 C488.19,-180.96 485.88,-172.28 485.88,-172.28 C485.88,-172.28 483.56,-163.6 483.56,-163.6 C483.56,-163.6 480.48,-155.16 480.48,-155.16 C480.48,-155.16 477.31,-146.75 477.31,-146.75 C477.31,-146.75 474.14,-138.34 474.14,-138.34 C474.14,-138.34 470.62,-130.07 470.62,-130.07 C470.62,-130.07 467.04,-121.83 467.04,-121.83 C467.04,-121.83 463.46,-113.59 463.46,-113.59 C463.46,-113.59 459.88,-105.35 459.88,-105.35 C459.88,-105.35 456.54,-97.01 456.54,-97.01 C456.54,-97.01 453.37,-88.6 453.37,-88.6 C453.37,-88.6 450.21,-80.19 450.21,-80.19 C450.21,-80.19 447.68,-71.57 447.68,-71.57 C447.68,-71.57 445.36,-62.89 445.36,-62.89 C445.36,-62.89 443.04,-54.21 443.04,-54.21 C443.04,-54.21 441.54,-45.35 441.54,-45.35 C441.54,-45.35 440.09,-36.48 440.09,-36.48 C440.09,-36.48 438.78,-27.6 438.78,-27.6 C438.78,-27.6 438.19,-18.63 438.19,-18.63 C438.19,-18.63 437.61,-9.66 437.61,-9.66 C437.61,-9.66 437.36,-0.69 437.36,-0.69 C437.36,-0.69 437.65,8.29 437.65,8.29 C437.65,8.29 437.95,17.27 437.95,17.27 C437.95,17.27 438.77,26.21 438.77,26.21 C438.77,26.21 439.94,35.12 439.94,35.12 C439.94,35.12 441.11,44.03 441.11,44.03 C441.11,44.03 442.99,52.81 442.99,52.81 C442.99,52.81 445.02,61.57 445.02,61.57 C445.02,61.57 447.07,70.31 447.07,70.31 C447.07,70.31 449.82,78.87 449.82,78.87 C449.82,78.87 452.65,87.4 452.65,87.4 C452.65,87.4 455.96,95.75 455.96,95.75 C455.96,95.75 459.33,104.08 459.33,104.08 C459.33,104.08 462.91,112.32 462.91,112.32 C462.91,112.32 466.49,120.57 466.49,120.57 C466.49,120.57 470.07,128.81 470.07,128.81 C470.07,128.81 473.65,137.05 473.65,137.05 C473.65,137.05 476.82,145.46 476.82,145.46 C476.82,145.46 479.99,153.87 479.99,153.87 C479.99,153.87 483.17,162.28 483.17,162.28 C483.17,162.28 485.52,170.94 485.52,170.94 C485.52,170.94 487.84,179.63 487.84,179.63 C487.84,179.63 490.14,188.31 490.14,188.31 C490.14,188.31 491.57,197.18 491.57,197.18 C491.57,197.18 493,206.06 493,206.06 C493,206.06 494.27,214.95 494.27,214.95 C494.27,214.95 494.8,223.92 494.8,223.92 C494.8,223.92 495.33,232.89 495.33,232.89 C495.33,232.89 495.5,241.86 495.5,241.86 C495.5,241.86 495.12,250.84 495.12,250.84 C495.12,250.84 494.75,259.82 494.75,259.82 C494.75,259.82 493.78,268.74 493.78,268.74 C493.78,268.74 492.52,277.64 492.52,277.64 C492.52,277.64 491.27,286.54 491.27,286.54 C491.27,286.54 489.16,295.27 489.16,295.27 C489.16,295.27 487.05,304.01 487.05,304.01 C487.05,304.01 484.66,312.66 484.66,312.66 C484.66,312.66 481.73,321.16 481.73,321.16 C481.73,321.16 478.79,329.65 478.79,329.65 C478.79,329.65 475.32,337.93 475.32,337.93 C475.32,337.93 471.6,346.11 471.6,346.11 C471.6,346.11 467.84,354.27 467.84,354.27 C467.84,354.27 463.39,362.08 463.39,362.08 C463.39,362.08 458.94,369.89 458.94,369.89 C458.94,369.89 454.19,377.5 454.19,377.5 C454.19,377.5 449.06,384.88 449.06,384.88 C449.06,384.88 443.93,392.26 443.93,392.26 C443.93,392.26 438.26,399.23 438.26,399.23 C438.26,399.23 432.5,406.12 432.5,406.12 C432.5,406.12 426.6,412.89 426.6,412.89 C426.6,412.89 420.24,419.24 420.24,419.24 C420.24,419.24 413.89,425.6 413.89,425.6 C413.89,425.6 407.2,431.59 407.2,431.59 C407.2,431.59 400.31,437.36 400.31,437.36 C400.31,437.36 393.42,443.12 393.42,443.12 C393.42,443.12 386.04,448.25 386.04,448.25 C386.04,448.25 378.66,453.38 378.66,453.38 C378.66,453.38 371.11,458.24 371.11,458.24 C371.11,458.24 363.31,462.69 363.31,462.69 C363.31,462.69 355.5,467.14 355.5,467.14 C355.5,467.14 347.4,471.02 347.4,471.02 C347.4,471.02 339.22,474.73 339.22,474.73 C339.22,474.73 330.99,478.33 330.99,478.33 C330.99,478.33 322.49,481.27 322.49,481.27 C322.49,481.27 314,484.2 314,484.2 C314,484.2 305.38,486.71 305.38,486.71 C305.38,486.71 296.65,488.83 296.65,488.83 C296.65,488.83 287.91,490.95 287.91,490.95 C287.91,490.95 279.04,492.33 279.04,492.33 C279.04,492.33 270.14,493.59 270.14,493.59 C270.14,493.59 261.23,494.69 261.23,494.69 C261.23,494.69 252.25,495.07 252.25,495.07 C252.25,495.07 243.28,495.44 243.28,495.44 C243.28,495.44 234.3,495.41 234.3,495.41 C234.3,495.41 225.33,494.88 225.33,494.88 C225.33,494.88 216.36,494.35 216.36,494.35 C216.36,494.35 207.45,493.23 207.45,493.23 C207.45,493.23 198.58,491.8 198.58,491.8 C198.58,491.8 189.71,490.37 189.71,490.37 C189.71,490.37 180.99,488.21 180.99,488.21 C180.99,488.21 172.31,485.89 172.31,485.89 C172.31,485.89 163.63,483.57 163.63,483.57 C163.63,483.57 155.19,480.5 155.19,480.5 C155.19,480.5 146.78,477.32 146.78,477.32 C146.78,477.32 138.37,474.15 138.37,474.15 C138.37,474.15 130.11,470.63 130.11,470.63 C130.11,470.63 121.86,467.06 121.86,467.06 C121.86,467.06 113.62,463.48 113.62,463.48 C113.62,463.48 105.38,459.9 105.38,459.9 C105.38,459.9 97.04,456.56 97.04,456.56 C97.04,456.56 88.63,453.39 88.63,453.39 C88.63,453.39 80.22,450.22 80.22,450.22 C80.22,450.22 71.6,447.7 71.6,447.7 C71.6,447.7 62.92,445.37 62.92,445.37 C62.92,445.37 54.24,443.05 54.24,443.05 C54.24,443.05 45.38,441.55 45.38,441.55 C45.38,441.55 36.52,440.1 36.52,440.1 C36.52,440.1 27.63,438.78 27.63,438.78 C27.63,438.78 18.66,438.2 18.66,438.2 C18.66,438.2 9.7,437.61 9.7,437.61 C9.7,437.61 0.72,437.36 0.72,437.36 C0.72,437.36 -8.26,437.65 -8.26,437.65 C-8.26,437.65 -17.24,437.95 -17.24,437.95 C-17.24,437.95 -26.18,438.77 -26.18,438.77 C-26.18,438.77 -35.09,439.94 -35.09,439.94 C-35.09,439.94 -44,441.1 -44,441.1 C-44,441.1 -52.78,442.98 -52.78,442.98 C-52.78,442.98 -61.53,445.02 -61.53,445.02 C-61.53,445.02 -70.28,447.07 -70.28,447.07 C-70.28,447.07 -78.84,449.81 -78.84,449.81 C-78.84,449.81 -87.37,452.64 -87.37,452.64 C-87.37,452.64 -95.72,455.95 -95.72,455.95 C-95.72,455.95 -104.05,459.32 -104.05,459.32 C-104.05,459.32 -112.29,462.9 -112.29,462.9 C-112.29,462.9 -120.53,466.48 -120.53,466.48 C-120.53,466.48 -128.78,470.06 -128.78,470.06 C-128.78,470.06 -137.02,473.63 -137.02,473.63 C-137.02,473.63 -145.43,476.81 -145.43,476.81 C-145.43,476.81 -153.84,479.98 -153.84,479.98 C-153.84,479.98 -162.24,483.15 -162.24,483.15 C-162.24,483.15 -170.91,485.52 -170.91,485.52 C-170.91,485.52 -179.59,487.83 -179.59,487.83 C-179.59,487.83 -188.28,490.13 -188.28,490.13 C-188.28,490.13 -197.15,491.56 -197.15,491.56 C-197.15,491.56 -206.02,492.99 -206.02,492.99 C-206.02,492.99 -214.91,494.27 -214.91,494.27 C-214.91,494.27 -223.88,494.8 -223.88,494.8 C-223.88,494.8 -232.85,495.33 -232.85,495.33 C-232.85,495.33 -241.83,495.5 -241.83,495.5 C-241.83,495.5 -250.81,495.13 -250.81,495.13 C-250.81,495.13 -259.79,494.75 -259.79,494.75 C-259.79,494.75 -268.71,493.79 -268.71,493.79 C-268.71,493.79 -277.61,492.53 -277.61,492.53 C-277.61,492.53 -286.51,491.27 -286.51,491.27 C-286.51,491.27 -295.24,489.17 -295.24,489.17 C-295.24,489.17 -303.98,487.06 -303.98,487.06 C-303.98,487.06 -312.63,484.67 -312.63,484.67 C-312.63,484.67 -321.12,481.74 -321.12,481.74 C-321.12,481.74 -329.62,478.8 -329.62,478.8 C-329.62,478.8 -337.9,475.33 -337.9,475.33 C-337.9,475.33 -346.08,471.62 -346.08,471.62 C-346.08,471.62 -354.24,467.85 -354.24,467.85 C-354.24,467.85 -362.05,463.41 -362.05,463.41 C-362.05,463.41 -369.86,458.96 -369.86,458.96 C-369.86,458.96 -377.47,454.21 -377.47,454.21 C-377.47,454.21 -384.85,449.08 -384.85,449.08 C-384.85,449.08 -392.23,443.95 -392.23,443.95 C-392.23,443.95 -399.2,438.29 -399.2,438.29 C-399.2,438.29 -406.09,432.52 -406.09,432.52 C-406.09,432.52 -412.86,426.62 -412.86,426.62 C-412.86,426.62 -419.22,420.27 -419.22,420.27 C-419.22,420.27 -425.57,413.91 -425.57,413.91 C-425.57,413.91 -431.57,407.23 -431.57,407.23 C-431.57,407.23 -437.33,400.34 -437.33,400.34 C-437.33,400.34 -443.1,393.44 -443.1,393.44 C-443.1,393.44 -448.23,386.07 -448.23,386.07 C-448.23,386.07 -453.36,378.69 -453.36,378.69 C-453.36,378.69 -458.23,371.15 -458.23,371.15 C-458.23,371.15 -462.67,363.33 -462.67,363.33 C-462.67,363.33 -467.12,355.53 -467.12,355.53 C-467.12,355.53 -471,347.43 -471,347.43 C-471,347.43 -474.72,339.25 -474.72,339.25 C-474.72,339.25 -478.32,331.02 -478.32,331.02 C-478.32,331.02 -481.25,322.52 -481.25,322.52 C-481.25,322.52 -484.19,314.03 -484.19,314.03 C-484.19,314.03 -486.71,305.42 -486.71,305.42 C-486.71,305.42 -488.82,296.68 -488.82,296.68 C-488.82,296.68 -490.94,287.95 -490.94,287.95 C-490.94,287.95 -492.32,279.07 -492.32,279.07 C-492.32,279.07 -493.58,270.18 -493.58,270.18 C-493.58,270.18 -494.69,261.27 -494.69,261.27 C-494.69,261.27 -495.07,252.29 -495.07,252.29 C-495.07,252.29 -495.44,243.31 -495.44,243.31 C-495.44,243.31 -495.42,234.33 -495.42,234.33 C-495.42,234.33 -494.89,225.36 -494.89,225.36 C-494.89,225.36 -494.36,216.39 -494.36,216.39 C-494.36,216.39 -493.23,207.49 -493.23,207.49 C-493.23,207.49 -491.8,198.61 -491.8,198.61 C-491.8,198.61 -490.37,189.74 -490.37,189.74 C-490.37,189.74 -488.22,181.02 -488.22,181.02 C-488.22,181.02 -485.9,172.34 -485.9,172.34 C-485.9,172.34 -483.58,163.66 -483.58,163.66 C-483.58,163.66 -480.51,155.22 -480.51,155.22 C-480.51,155.22 -477.34,146.81 -477.34,146.81 C-477.34,146.81 -474.17,138.41 -474.17,138.41 C-474.17,138.41 -470.65,130.14 -470.65,130.14 C-470.65,130.14 -467.07,121.9 -467.07,121.9 C-467.07,121.9 -463.49,113.65 -463.49,113.65 C-463.49,113.65 -459.91,105.41 -459.91,105.41 C-459.91,105.41 -456.57,97.07 -456.57,97.07 C-456.57,97.07 -453.4,88.66 -453.4,88.66 C-453.4,88.66 -450.23,80.25 -450.23,80.25 C-450.23,80.25 -447.7,71.64 -447.7,71.64 C-447.7,71.64 -445.38,62.96 -445.38,62.96 C-445.38,62.96 -443.06,54.28 -443.06,54.28 C-443.06,54.28 -441.56,45.42 -441.56,45.42 C-441.56,45.42 -440.1,36.55 -440.1,36.55 C-440.1,36.55 -438.78,27.67 -438.78,27.67 C-438.78,27.67 -438.2,18.7 -438.2,18.7 C-438.2,18.7 -437.62,9.73 -437.62,9.73 C-437.62,9.73 -437.36,0.76 -437.36,0.76 C-437.36,0.76 -437.66,-8.22 -437.66,-8.22 C-437.66,-8.22 -437.95,-17.2 -437.95,-17.2 C-437.95,-17.2 -438.77,-26.14 -438.77,-26.14 C-438.77,-26.14 -439.93,-35.05 -439.93,-35.05 C-439.93,-35.05 -441.1,-43.96 -441.1,-43.96 C-441.1,-43.96 -442.98,-52.75 -442.98,-52.75 C-442.98,-52.75 -445.01,-61.5 -445.01,-61.5 C-445.01,-61.5 -447.06,-70.25 -447.06,-70.25 C-447.06,-70.25 -449.8,-78.81 -449.8,-78.81 C-449.8,-78.81 -452.63,-87.33 -452.63,-87.33 C-452.63,-87.33 -455.94,-95.69 -455.94,-95.69 C-455.94,-95.69 -459.31,-104.02 -459.31,-104.02 C-459.31,-104.02 -462.89,-112.26 -462.89,-112.26 C-462.89,-112.26 -466.47,-120.5 -466.47,-120.5 C-466.47,-120.5 -470.05,-128.74 -470.05,-128.74 C-470.05,-128.74 -473.68,-137.12 -473.68,-137.12 C-473.68,-137.12 -476.85,-145.53 -476.85,-145.53 C-476.85,-145.53 -480.03,-153.94 -480.03,-153.94 C-480.03,-153.94 -483.2,-162.34 -483.2,-162.34 C-483.2,-162.34 -485.55,-171.02 -485.55,-171.02 C-485.55,-171.02 -487.86,-179.7 -487.86,-179.7 C-487.86,-179.7 -490.15,-188.39 -490.15,-188.39 C-490.15,-188.39 -491.58,-197.26 -491.58,-197.26 C-491.58,-197.26 -493.01,-206.13 -493.01,-206.13 C-493.01,-206.13 -494.28,-215.02 -494.28,-215.02 C-494.28,-215.02 -494.81,-223.99 -494.81,-223.99 C-494.81,-223.99 -495.33,-232.96 -495.33,-232.96 C-495.33,-232.96 -495.5,-241.94 -495.5,-241.94 C-495.5,-241.94 -495.12,-250.92 -495.12,-250.92 C-495.12,-250.92 -494.75,-259.9 -494.75,-259.9 C-494.75,-259.9 -493.78,-268.82 -493.78,-268.82 C-493.78,-268.82 -492.52,-277.72 -492.52,-277.72 C-492.52,-277.72 -491.26,-286.61 -491.26,-286.61 C-491.26,-286.61 -489.15,-295.35 -489.15,-295.35 C-489.15,-295.35 -487.03,-304.08 -487.03,-304.08 C-487.03,-304.08 -484.64,-312.73 -484.64,-312.73 C-484.64,-312.73 -481.7,-321.23 -481.7,-321.23 C-481.7,-321.23 -478.77,-329.72 -478.77,-329.72 C-478.77,-329.72 -475.29,-338 -475.29,-338 C-475.29,-338 -471.57,-346.18 -471.57,-346.18 C-471.57,-346.18 -467.8,-354.33 -467.8,-354.33 C-467.8,-354.33 -463.36,-362.14 -463.36,-362.14 C-463.36,-362.14 -458.91,-369.95 -458.91,-369.95 C-458.91,-369.95 -454.15,-377.56 -454.15,-377.56 C-454.15,-377.56 -449.02,-384.94 -449.02,-384.94 C-449.02,-384.94 -443.88,-392.32 -443.88,-392.32 C-443.88,-392.32 -438.22,-399.28 -438.22,-399.28 C-438.22,-399.28 -432.45,-406.18 -432.45,-406.18 C-432.45,-406.18 -426.55,-412.94 -426.55,-412.94 C-426.55,-412.94 -420.19,-419.3 -420.19,-419.3 C-420.19,-419.3 -413.84,-425.65 -413.84,-425.65 C-413.84,-425.65 -407.15,-431.64 -407.15,-431.64 C-407.15,-431.64 -400.26,-437.41 -400.26,-437.41 C-400.26,-437.41 -393.36,-443.16 -393.36,-443.16 C-393.36,-443.16 -385.98,-448.29 -385.98,-448.29 C-385.98,-448.29 -378.6,-453.43 -378.6,-453.43 C-378.6,-453.43 -371.05,-458.28 -371.05,-458.28 C-371.05,-458.28 -363.24,-462.73 -363.24,-462.73 C-363.24,-462.73 -355.43,-467.18 -355.43,-467.18 C-355.43,-467.18 -347.33,-471.05 -347.33,-471.05 C-347.33,-471.05 -339.15,-474.76 -339.15,-474.76 C-339.15,-474.76 -330.92,-478.35 -330.92,-478.35 C-330.92,-478.35 -322.42,-481.29 -322.42,-481.29 C-322.42,-481.29 -313.93,-484.23 -313.93,-484.23 C-313.93,-484.23 -305.31,-486.73 -305.31,-486.73 C-305.31,-486.73 -296.58,-488.85 -296.58,-488.85 C-296.58,-488.85 -287.85,-490.97 -287.85,-490.97 C-287.85,-490.97 -278.97,-492.34 -278.97,-492.34 C-278.97,-492.34 -270.07,-493.6 -270.07,-493.6 C-270.07,-493.6 -261.16,-494.7 -261.16,-494.7 C-261.16,-494.7 -252.18,-495.07 -252.18,-495.07 C-252.18,-495.07 -243.2,-495.44 -243.2,-495.44 C-243.2,-495.44 -234.23,-495.41 -234.23,-495.41 C-234.23,-495.41 -225.26,-494.88 -225.26,-494.88 C-225.26,-494.88 -216.29,-494.35 -216.29,-494.35 C-216.29,-494.35 -207.38,-493.22 -207.38,-493.22 C-207.38,-493.22 -198.51,-491.79 -198.51,-491.79 C-198.51,-491.79 -189.64,-490.36 -189.64,-490.36 C-189.64,-490.36 -180.92,-488.19 -180.92,-488.19 C-180.92,-488.19 -172.24,-485.87 -172.24,-485.87 C-172.24,-485.87 -163.56,-483.56 -163.56,-483.56 C-163.56,-483.56 -155.12,-480.47 -155.12,-480.47 C-155.12,-480.47 -146.72,-477.3 -146.72,-477.3 C-146.72,-477.3 -138.31,-474.13 -138.31,-474.13 C-138.31,-474.13 -130.04,-470.61 -130.04,-470.61 C-130.04,-470.61 -121.8,-467.03 -121.8,-467.03 C-121.8,-467.03 -113.55,-463.45 -113.55,-463.45 C-113.55,-463.45 -105.31,-459.87 -105.31,-459.87 C-105.31,-459.87 -96.97,-456.53 -96.97,-456.53 C-96.97,-456.53 -88.56,-453.37 -88.56,-453.37 C-88.56,-453.37 -80.15,-450.2 -80.15,-450.2 C-80.15,-450.2 -71.53,-447.68 -71.53,-447.68 C-71.53,-447.68 -62.85,-445.36 -62.85,-445.36 C-62.85,-445.36 -54.17,-443.04 -54.17,-443.04 C-54.17,-443.04 -45.31,-441.54 -45.31,-441.54 C-45.31,-441.54 -36.44,-440.09 -36.44,-440.09 C-36.44,-440.09 -27.56,-438.78 -27.56,-438.78 C-27.56,-438.78 -18.59,-438.19 -18.59,-438.19 C-18.59,-438.19 -9.62,-437.61 -9.62,-437.61 C-9.62,-437.61 -0.65,-437.37 -0.65,-437.37c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button.xml b/packages/SystemUI/res/drawable/ic_media_pause_button.xml new file mode 100644 index 000000000000..6ae89f91c5ee --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause_button.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="56" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="15.485" + android:valueTo="12.321" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="278" + android:propertyName="translateX" + android:startOffset="56" + android:valueFrom="12.321" + android:valueTo="7.576" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="-12.031" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="19.524" + android:translateY="12.084"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="15.485" + android:translateY="12.084"> + <group + android:name="_R_G_L_0_G" + android:translateX="12.031"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml new file mode 100644 index 000000000000..571f69d51ac4 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="88dp" + android:height="56dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972" + android:translateX="43.528999999999996" + android:translateY="27.898"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="133" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="367" + android:propertyName="pathData" + android:startOffset="133" + android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="167" + android:propertyName="scaleX" + android:startOffset="0" + android:valueFrom="1.05905" + android:valueTo="1.17758" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="167" + android:propertyName="scaleY" + android:startOffset="0" + android:valueFrom="1.0972" + android:valueTo="1.22" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="333" + android:propertyName="scaleX" + android:startOffset="167" + android:valueFrom="1.17758" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="333" + android:propertyName="scaleY" + android:startOffset="167" + android:valueFrom="1.22" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_play_button.xml b/packages/SystemUI/res/drawable/ic_media_play_button.xml new file mode 100644 index 000000000000..f64690268cfe --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play_button.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="7.576" + android:valueTo="15.485" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.583,0 0.089,0.874 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="-12.031" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="19.524" + android:translateY="12.084"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="7.576" + android:translateY="12.084"> + <group + android:name="_R_G_L_0_G" + android:translateX="12.031"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_play_button_container.xml b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml new file mode 100644 index 000000000000..aa4e09fa4033 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:height="56dp" + android:width="88dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:translateX="43.528999999999996" + android:translateY="27.898" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillColor="#3d90ff" + android:fillAlpha="1" + android:fillType="nonZero" + android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "/> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="pathData" + android:duration="167" + android:startOffset="0" + android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="pathData" + android:duration="333" + android:startOffset="167" + android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="scaleX" + android:duration="167" + android:startOffset="0" + android:valueFrom="1.05905" + android:valueTo="1.17758" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleY" + android:duration="167" + android:startOffset="0" + android:valueFrom="1.0972" + android:valueTo="1.22" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleX" + android:duration="333" + android:startOffset="167" + android:valueFrom="1.17758" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleY" + android:duration="333" + android:startOffset="167" + android:valueFrom="1.22" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="translateX" + android:duration="517" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml index e65d0b938b65..6748cfa05c35 100644 --- a/packages/SystemUI/res/layout/volume_ringer_button.xml +++ b/packages/SystemUI/res/layout/volume_ringer_button.xml @@ -20,10 +20,9 @@ <ImageButton android:id="@+id/volume_drawer_button" - android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size" - android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size" + android:layout_width="match_parent" + android:layout_height="match_parent" android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" - android:layout_marginBottom="@dimen/volume_dialog_components_spacing" android:contentDescription="@string/volume_ringer_mode" android:gravity="center" android:tint="@androidprv:color/materialColorOnSurface" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 1f7889214bd5..2ffa3d19e161 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1278,6 +1278,7 @@ <dimen name="qs_center_guideline_padding">10dp</dimen> <dimen name="qs_media_action_spacing">4dp</dimen> <dimen name="qs_media_action_margin">12dp</dimen> + <dimen name="qs_media_action_play_pause_width">72dp</dimen> <dimen name="qs_seamless_height">24dp</dimen> <dimen name="qs_seamless_icon_size">12dp</dimen> <dimen name="qs_media_disabled_seekbar_height">1dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d445ed927a25..80fb8b9fcebc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2309,6 +2309,9 @@ <string name="group_system_lock_screen">Lock screen</string> <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] --> <string name="group_system_quick_memo">Take a note</string> + <!-- TODO(b/383734125): make it translatable once string is finalized by UXW.--> + <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] --> + <string name="group_system_toggle_voice_access" translatable="false">Toggle Voice Access</string> <!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] --> <string name="keyboard_shortcut_group_system_multitasking">Multitasking</string> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java index 51892aac606a..ff6bcdb150f8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java @@ -19,6 +19,7 @@ package com.android.systemui.shared.system; import android.graphics.Rect; import android.os.Bundle; import android.view.RemoteAnimationTarget; +import android.window.TransitionInfo; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -30,7 +31,7 @@ public interface RecentsAnimationListener { */ void onAnimationStart(RecentsAnimationControllerCompat controller, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, - Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras); + Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras, TransitionInfo info); /** * Called when the animation into Recents was canceled. This call is made on the binder thread. diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index acfa08643b63..c7ae02b61bff 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -142,33 +142,28 @@ public class KeyguardDisplayManager { private boolean isKeyguardShowable(Display display) { if (display == null) { - if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display"); + Log.i(TAG, "Cannot show Keyguard on null display"); return false; } if (ShadeWindowGoesAround.isEnabled()) { int shadeDisplayId = mShadePositionRepositoryProvider.get().getDisplayId().getValue(); if (display.getDisplayId() == shadeDisplayId) { - if (DEBUG) { - Log.i(TAG, - "Do not show KeyguardPresentation on the shade window display"); - } + Log.i(TAG, "Do not show KeyguardPresentation on the shade window display"); return false; } } else { if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) { - if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display"); + Log.i(TAG, "Do not show KeyguardPresentation on the default display"); return false; } } display.getDisplayInfo(mTmpDisplayInfo); if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) { - if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on a private display"); + Log.i(TAG, "Do not show KeyguardPresentation on a private display"); return false; } if ((mTmpDisplayInfo.flags & Display.FLAG_ALWAYS_UNLOCKED) != 0) { - if (DEBUG) { - Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display"); - } + Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display"); return false; } @@ -176,14 +171,11 @@ public class KeyguardDisplayManager { mDeviceStateHelper.isConcurrentDisplayActive(display) || mDeviceStateHelper.isRearDisplayOuterDefaultActive(display); if (mKeyguardStateController.isOccluded() && deviceStateOccludesKeyguard) { - if (DEBUG) { - // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the - // Keyguard state becomes "occluded". In this case, we should not show the - // KeyguardPresentation, since the activity is presenting content onto the - // non-default display. - Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear" - + " display is active"); - } + // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the Keyguard + // state becomes "occluded". In this case, we should not show the KeyguardPresentation, + // since the activity is presenting content onto the non-default display. + Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear" + + " display is active"); return false; } @@ -197,7 +189,7 @@ public class KeyguardDisplayManager { */ private boolean showPresentation(Display display) { if (!isKeyguardShowable(display)) return false; - if (DEBUG) Log.i(TAG, "Keyguard enabled on display: " + display); + Log.i(TAG, "Keyguard enabled on display: " + display); final int displayId = display.getDisplayId(); Presentation presentation = mPresentations.get(displayId); if (presentation == null) { @@ -239,7 +231,7 @@ public class KeyguardDisplayManager { public void show() { if (!mShowing) { - if (DEBUG) Log.v(TAG, "show"); + Log.v(TAG, "show"); if (mMediaRouter != null) { mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); @@ -253,7 +245,7 @@ public class KeyguardDisplayManager { public void hide() { if (mShowing) { - if (DEBUG) Log.v(TAG, "hide"); + Log.v(TAG, "hide"); if (mMediaRouter != null) { mMediaRouter.removeCallback(mMediaRouterCallback); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index a703b027b691..7d291c311ca3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2591,6 +2591,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * @return true if optical udfps HW is supported on this device. Can return true even if the + * user has not enrolled udfps. This may be false if called before + * onAllAuthenticatorsRegistered. + */ + public boolean isOpticalUdfpsSupported() { + return mAuthController.isOpticalUdfpsSupported(); + } + + /** * @return true if there's at least one sfps enrollment for the current user. */ public boolean isSfpsEnrolled() { diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index f530522fb707..5f79c8cada45 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -100,7 +100,8 @@ public abstract class SystemUIInitializer { .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper()) .setRecentTasks(mWMComponent.getRecentTasks()) .setBackAnimation(mWMComponent.getBackAnimation()) - .setDesktopMode(mWMComponent.getDesktopMode()); + .setDesktopMode(mWMComponent.getDesktopMode()) + .setAppZoomOut(mWMComponent.getAppZoomOut()); // Only initialize when not starting from tests since this currently initializes some // components that shouldn't be run in the test environment @@ -121,7 +122,8 @@ public abstract class SystemUIInitializer { .setStartingSurface(Optional.ofNullable(null)) .setRecentTasks(Optional.ofNullable(null)) .setBackAnimation(Optional.ofNullable(null)) - .setDesktopMode(Optional.ofNullable(null)); + .setDesktopMode(Optional.ofNullable(null)) + .setAppZoomOut(Optional.ofNullable(null)); } mSysUIComponent = builder.build(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index d0cb507789a1..eee5f9e34317 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -1011,6 +1011,16 @@ public class AuthController implements } /** + * @return true if optical udfps HW is supported on this device. Can return true even if + * the user has not enrolled udfps. This may be false if called before + * onAllAuthenticatorsRegistered. + */ + public boolean isOpticalUdfpsSupported() { + return getUdfpsProps() != null && !getUdfpsProps().isEmpty() && getUdfpsProps() + .get(0).isOpticalUdfps(); + } + + /** * @return true if ultrasonic udfps HW is supported on this device. Can return true even if * the user has not enrolled udfps. This may be false if called before * onAllAuthenticatorsRegistered. diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt index 4dc2a13480f5..0303048436c9 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt @@ -104,6 +104,31 @@ constructor( } } + override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) { + withContext(backgroundDispatcher) { + if (!audioSharingInteractor.audioSharingAvailable()) { + return@withContext deviceItemActionInteractorImpl.onActionIconClick( + deviceItem, + onIntent, + ) + } + + when (deviceItem.type) { + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.CHECK_MARK_ACTION_BUTTON_CLICKED) + audioSharingInteractor.stopAudioSharing() + } + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.PLUS_ACTION_BUTTON_CLICKED) + audioSharingInteractor.startAudioSharing() + } + else -> { + deviceItemActionInteractorImpl.onActionIconClick(deviceItem, onIntent) + } + } + } + } + private fun inSharingAndDeviceNoSource( inAudioSharing: Boolean, deviceItem: DeviceItem, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt index c4f26cd46bf8..116e76c82008 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -29,6 +29,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest @@ -54,6 +55,8 @@ interface AudioSharingInteractor { suspend fun startAudioSharing() + suspend fun stopAudioSharing() + suspend fun audioSharingAvailable(): Boolean suspend fun qsDialogImprovementAvailable(): Boolean @@ -61,7 +64,7 @@ interface AudioSharingInteractor { @SysUISingleton @OptIn(ExperimentalCoroutinesApi::class) -class AudioSharingInteractorImpl +open class AudioSharingInteractorImpl @Inject constructor( private val context: Context, @@ -99,6 +102,9 @@ constructor( if (audioSharingAvailable()) { audioSharingRepository.leAudioBroadcastProfile?.let { profile -> isAudioSharingOn + // Skip the default value, we only care about adding source for newly + // started audio sharing session + .drop(1) .mapNotNull { audioSharingOn -> if (audioSharingOn) { // onBroadcastMetadataChanged could emit multiple times during one @@ -145,6 +151,13 @@ constructor( audioSharingRepository.startAudioSharing() } + override suspend fun stopAudioSharing() { + if (!audioSharingAvailable()) { + return + } + audioSharingRepository.stopAudioSharing() + } + // TODO(b/367965193): Move this after flags rollout override suspend fun audioSharingAvailable(): Boolean { return audioSharingRepository.audioSharingAvailable() @@ -181,6 +194,8 @@ class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingIntera override suspend fun startAudioSharing() {} + override suspend fun stopAudioSharing() {} + override suspend fun audioSharingAvailable(): Boolean = false override suspend fun qsDialogImprovementAvailable(): Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt index b9b8d36d41e6..44f9769f5930 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt @@ -45,6 +45,8 @@ interface AudioSharingRepository { suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) suspend fun startAudioSharing() + + suspend fun stopAudioSharing() } @SysUISingleton @@ -100,6 +102,15 @@ class AudioSharingRepositoryImpl( leAudioBroadcastProfile?.startPrivateBroadcast() } } + + override suspend fun stopAudioSharing() { + withContext(backgroundDispatcher) { + if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { + return@withContext + } + leAudioBroadcastProfile?.stopLatestBroadcast() + } + } } @SysUISingleton @@ -117,4 +128,6 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} override suspend fun startAudioSharing() {} + + override suspend fun stopAudioSharing() {} } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index b294dd1b0b71..56caddfbd637 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -56,6 +56,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) { + enum class Target { + ENTIRE_ROW, + ACTION_ICON, + } +} + /** Dialog for showing active, connected and saved bluetooth devices. */ class BluetoothTileDialogDelegate @AssistedInject @@ -80,7 +87,7 @@ internal constructor( internal val bluetoothAutoOnToggle get() = mutableBluetoothAutoOnToggle.asStateFlow() - private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> = + private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> = MutableSharedFlow(extraBufferCapacity = 1) internal val deviceItemClick get() = mutableDeviceItemClick.asSharedFlow() @@ -90,7 +97,7 @@ internal constructor( internal val contentHeight get() = mutableContentHeight.asSharedFlow() - private val deviceItemAdapter: Adapter = Adapter(bluetoothTileDialogCallback) + private val deviceItemAdapter: Adapter = Adapter() private var lastUiUpdateMs: Long = -1 @@ -334,8 +341,7 @@ internal constructor( } } - internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) : - RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { + internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { private val diffUtilCallback = object : DiffUtil.ItemCallback<DeviceItem>() { @@ -376,7 +382,7 @@ internal constructor( override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) { val item = getItem(position) - holder.bind(item, onClickCallback) + holder.bind(item) } internal fun getItem(position: Int) = asyncListDiffer.currentList[position] @@ -390,19 +396,18 @@ internal constructor( private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name) private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary) private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon) - private val iconGear = view.requireViewById<ImageView>(R.id.gear_icon_image) - private val gearView = view.requireViewById<View>(R.id.gear_icon) + private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image) + private val actionIconView = view.requireViewById<View>(R.id.gear_icon) private val divider = view.requireViewById<View>(R.id.divider) - internal fun bind( - item: DeviceItem, - deviceItemOnClickCallback: BluetoothTileDialogCallback, - ) { + internal fun bind(item: DeviceItem) { container.apply { isEnabled = item.isEnabled background = item.background?.let { context.getDrawable(it) } setOnClickListener { - mutableDeviceItemClick.tryEmit(item) + mutableDeviceItemClick.tryEmit( + DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW) + ) uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED) } @@ -421,7 +426,8 @@ internal constructor( } } - iconGear.apply { drawable?.let { it.mutate()?.setTint(tintColor) } } + actionIcon.setImageResource(item.actionIconRes) + actionIcon.drawable?.setTint(tintColor) divider.setBackgroundColor(tintColor) @@ -454,8 +460,10 @@ internal constructor( nameView.text = item.deviceName summaryView.text = item.connectionSummary - gearView.setOnClickListener { - deviceItemOnClickCallback.onDeviceItemGearClicked(item, it) + actionIconView.setOnClickListener { + mutableDeviceItemClick.tryEmit( + DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON) + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt index aad233fe40ca..7c66ec059e64 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -49,7 +49,7 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED(1719), @Deprecated( "Use case no longer needed", - ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED") + ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED"), ) @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked") LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720), @@ -59,7 +59,11 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent @UiEvent(doc = "Clicked on switch active button on audio sharing dialog") AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890), @UiEvent(doc = "Clicked on share audio button on audio sharing dialog") - AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891); + AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891), + @UiEvent(doc = "Clicked on plus action button") + PLUS_ACTION_BUTTON_CLICKED(2061), + @UiEvent(doc = "Clicked on checkmark action button") + CHECK_MARK_ACTION_BUTTON_CLICKED(2062); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index 497d8cf2e159..9460e7c2c8d5 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -35,7 +35,6 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE import com.android.systemui.dagger.SysUISingleton @@ -227,8 +226,22 @@ constructor( // deviceItemClick is emitted when user clicked on a device item. dialogDelegate.deviceItemClick .onEach { - deviceItemActionInteractor.onClick(it, dialog) - logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type) + when (it.target) { + DeviceItemClick.Target.ENTIRE_ROW -> { + deviceItemActionInteractor.onClick(it.deviceItem, dialog) + logger.logDeviceClick( + it.deviceItem.cachedBluetoothDevice.address, + it.deviceItem.type, + ) + } + + DeviceItemClick.Target.ACTION_ICON -> { + deviceItemActionInteractor.onActionIconClick(it.deviceItem) { intent + -> + startSettingsActivity(intent, it.clickedView) + } + } + } } .launchIn(this) @@ -287,20 +300,6 @@ constructor( ) } - override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) { - uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED) - val intent = - Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply { - putExtra( - EXTRA_SHOW_FRAGMENT_ARGUMENTS, - Bundle().apply { - putString("device_address", deviceItem.cachedBluetoothDevice.address) - }, - ) - } - startSettingsActivity(intent, view) - } - override fun onSeeAllClicked(view: View) { uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED) startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view) @@ -382,8 +381,6 @@ constructor( } interface BluetoothTileDialogCallback { - fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) - fun onSeeAllClicked(view: View) fun onPairNewDeviceClicked(view: View) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt index 2ba4c73a0293..f7af16d99fbf 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt @@ -53,5 +53,6 @@ data class DeviceItem( val background: Int? = null, var isEnabled: Boolean = true, var actionAccessibilityLabel: String = "", - var isActive: Boolean = false + var isActive: Boolean = false, + val actionIconRes: Int = -1, ) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt index 2b55e1c51f5f..cb4ec37a1a66 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -16,6 +16,8 @@ package com.android.systemui.bluetooth.qsdialog +import android.content.Intent +import android.os.Bundle import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -25,7 +27,9 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext interface DeviceItemActionInteractor { - suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {} + suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) + + suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) } @SysUISingleton @@ -67,4 +71,44 @@ constructor( } } } + + override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) { + withContext(backgroundDispatcher) { + deviceItem.cachedBluetoothDevice.apply { + when (deviceItem.type) { + DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, + DeviceItemType.SAVED_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED) + val intent = + Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply { + putExtra( + EXTRA_SHOW_FRAGMENT_ARGUMENTS, + Bundle().apply { + putString( + "device_address", + deviceItem.cachedBluetoothDevice.address, + ) + }, + ) + } + onIntent(intent) + } + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + throw IllegalArgumentException("Invalid device type: ${deviceItem.type}") + // Throw exception. Should already be handled in + // AudioSharingDeviceItemActionInteractor. + } + } + } + } + } + + private companion object { + const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" + const val ACTION_BLUETOOTH_DEVICE_DETAILS = + "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS" + } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index 92f05803f7cf..095e6e741584 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -30,6 +30,8 @@ private val backgroundOff = R.drawable.bluetooth_tile_dialog_bg_off private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy private val connected = R.string.quick_settings_bluetooth_device_connected private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing +private val audioSharingAddIcon = R.drawable.ic_add +private val audioSharingOnGoingIcon = R.drawable.ic_check private val saved = R.string.quick_settings_bluetooth_device_saved private val actionAccessibilityLabelActivate = R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate @@ -63,6 +65,7 @@ abstract class DeviceItemFactory { background: Int, actionAccessibilityLabel: String, isActive: Boolean, + actionIconRes: Int = R.drawable.ic_settings_24dp, ): DeviceItem { return DeviceItem( type = type, @@ -75,6 +78,7 @@ abstract class DeviceItemFactory { isEnabled = !cachedDevice.isBusy, actionAccessibilityLabel = actionAccessibilityLabel, isActive = isActive, + actionIconRes = actionIconRes, ) } } @@ -125,6 +129,7 @@ internal class AudioSharingMediaDeviceItemFactory( if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn, "", isActive = !cachedDevice.isBusy, + actionIconRes = audioSharingOnGoingIcon, ) } } @@ -156,6 +161,7 @@ internal class AvailableAudioSharingMediaDeviceItemFactory( if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, "", isActive = false, + actionIconRes = audioSharingAddIcon, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt index 78156dbc8964..ca49de3b1510 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt @@ -25,6 +25,7 @@ import com.android.compose.animation.scene.TransitionKey */ object CommunalTransitionKeys { /** Fades the glanceable hub without any translation */ + @Deprecated("No longer supported as all hub transitions will be fades.") val SimpleFade = TransitionKey("SimpleFade") /** Transition from the glanceable hub before entering edit mode */ val ToEditMode = TransitionKey("ToEditMode") diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 00eead6eb7fc..555fe6ef157d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.phone.ConfigurationForwarder; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.wm.shell.appzoomout.AppZoomOut; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -115,6 +116,9 @@ public interface SysUIComponent { @BindsInstance Builder setDesktopMode(Optional<DesktopMode> d); + @BindsInstance + Builder setAppZoomOut(Optional<AppZoomOut> a); + SysUIComponent build(); } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 41a59a959771..ae6238724042 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.util.kotlin.FlowDumperImpl @@ -50,6 +51,8 @@ class DeviceEntryHapticsInteractor constructor( biometricSettingsRepository: BiometricSettingsRepository, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, + deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, + keyguardBypassInteractor: KeyguardBypassInteractor, deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor: DeviceEntrySourceInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, @@ -82,7 +85,7 @@ constructor( emit(recentPowerButtonPressThresholdMs * -1L - 1L) } - val playSuccessHaptic: Flow<Unit> = + private val playHapticsOnDeviceEntry: Flow<Boolean> = deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( @@ -92,17 +95,29 @@ constructor( ::Triple, ) ) - .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> + .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> val sideFpsAllowsHaptic = !powerButtonDown && systemClock.uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic if (!allowHaptic) { - logger.d("Skip success haptic. Recent power button press or button is down.") + logger.d( + "Skip success entry haptic from power button. Recent power button press or button is down." + ) } allowHaptic } + + private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> = + deviceEntryFaceAuthInteractor.isAuthenticated + .filter { it } + .sample(keyguardBypassInteractor.isBypassAvailable) + .map { !it } + + val playSuccessHaptic: Flow<Unit> = + merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled) + .filter { it } // map to Unit .map {} .dumpWhileCollecting("playSuccessHaptic") diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index 21002c676ec6..d7a4dba3188a 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -278,11 +278,11 @@ constructor( } private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean { - val oobeLaunchTime = - tutorialRepository.getScheduledTutorialLaunchTime(deviceType) ?: return false - return clock - .instant() - .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds)) + val oobeTime = + tutorialRepository.getScheduledTutorialLaunchTime(deviceType) + ?: tutorialRepository.getNotifiedTime(deviceType) + ?: return false + return clock.instant().isAfter(oobeTime.plusSeconds(initialDelayDuration.inWholeSeconds)) } private data class StatsUpdateRequest( diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 129a6bb72996..2ed0671a570b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -23,13 +23,7 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP import com.android.systemui.Flags.communalHub -import com.android.systemui.Flags.statusBarCallChipNotificationIcon -import com.android.systemui.Flags.statusBarScreenSharingChips -import com.android.systemui.Flags.statusBarUseReposForCallChip import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade @@ -63,10 +57,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() - - // Status bar chip dependencies - statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken - statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken } private inline val politeNotifications @@ -86,17 +76,4 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) - - private inline val statusBarCallChipNotificationIconToken - get() = - FlagToken( - FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, - statusBarCallChipNotificationIcon(), - ) - - private inline val statusBarScreenSharingChipsToken - get() = FlagToken(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, statusBarScreenSharingChips()) - - private inline val statusBarUseReposForCallChipToken - get() = FlagToken(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, statusBarUseReposForCallChip()) } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index e1ebf7cdf472..cf5c3402792e 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -463,6 +463,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mTelephonyListenerManager.removeServiceStateListener(mPhoneStateListener); mGlobalSettings.unregisterContentObserverSync(mAirplaneModeObserver); mConfigurationController.removeCallback(this); + if (mShowSilentToggle) { + mRingerModeTracker.getRingerMode().removeObservers(this); + } } protected Context getContext() { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index e255bdea6100..6a42cdc876ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt @@ -41,6 +41,8 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVI import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System @@ -63,6 +65,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System, KEY_GESTURE_TYPE_ALL_APPS to System, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to System, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking, @@ -100,6 +103,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.shortcut_helper_category_system_apps, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcut_helper_category_system_apps, // Multitasking Category KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to @@ -148,6 +152,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.group_system_access_google_assistant, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.group_system_toggle_voice_access, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt index 5060abdda247..c3c9df97a682 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt @@ -32,12 +32,14 @@ import android.view.KeyEvent.KEYCODE_RECENT_APPS import android.view.KeyEvent.KEYCODE_S import android.view.KeyEvent.KEYCODE_SLASH import android.view.KeyEvent.KEYCODE_TAB +import android.view.KeyEvent.KEYCODE_V import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_META_ON import android.view.KeyEvent.META_SHIFT_ON import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.Flags.shortcutHelperKeyGlyph import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo @@ -118,8 +120,8 @@ constructor(@Main private val resources: Resources, private val inputManager: In return shortcuts } - private fun systemControlsShortcuts() = - listOf( + private fun systemControlsShortcuts(): List<KeyboardShortcutInfo> { + val shortcuts = mutableListOf( // Access list of all apps and search (i.e. Search/Launcher): // - Meta shortcutInfo(resources.getString(R.string.group_system_access_all_apps_search)) { @@ -176,6 +178,19 @@ constructor(@Main private val resources: Resources, private val inputManager: In }, ) + if (enableVoiceAccessKeyGestures()) { + shortcuts.add( + // Toggle voice access: + // - Meta + Alt + V + shortcutInfo(resources.getString(R.string.group_system_toggle_voice_access)) { + command(META_META_ON or META_ALT_ON, KEYCODE_V) + } + ) + } + + return shortcuts + } + private fun systemAppsShortcuts() = listOf( // Pull up Notes app for quick memo: diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index 274fa59045d7..a16b4a6892b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt @@ -53,11 +53,11 @@ constructor( override suspend fun onActivated(): Nothing { viewModel.shortcutCustomizationUiState.collect { uiState -> - when(uiState){ + when (uiState) { is AddShortcutDialog, is DeleteShortcutDialog, is ResetShortcutDialog -> { - if (dialog == null){ + if (dialog == null) { dialog = createDialog().also { it.show() } } } @@ -85,7 +85,9 @@ constructor( ShortcutCustomizationDialog( uiState = uiState, modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp), - onKeyPress = { viewModel.onKeyPressed(it) }, + onShortcutKeyCombinationSelected = { + viewModel.onShortcutKeyCombinationSelected(it) + }, onCancel = { dialog.dismiss() }, onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } }, onConfirmDeleteShortcut = { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt index 3819f6d41856..d9e55f89cda5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt @@ -49,8 +49,12 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -65,7 +69,7 @@ import com.android.systemui.res.R fun ShortcutCustomizationDialog( uiState: ShortcutCustomizationUiState, modifier: Modifier = Modifier, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, onConfirmSetShortcut: () -> Unit, onConfirmDeleteShortcut: () -> Unit, @@ -73,7 +77,13 @@ fun ShortcutCustomizationDialog( ) { when (uiState) { is ShortcutCustomizationUiState.AddShortcutDialog -> { - AddShortcutDialog(modifier, uiState, onKeyPress, onCancel, onConfirmSetShortcut) + AddShortcutDialog( + modifier, + uiState, + onShortcutKeyCombinationSelected, + onCancel, + onConfirmSetShortcut, + ) } is ShortcutCustomizationUiState.DeleteShortcutDialog -> { DeleteShortcutDialog(modifier, onCancel, onConfirmDeleteShortcut) @@ -91,29 +101,27 @@ fun ShortcutCustomizationDialog( private fun AddShortcutDialog( modifier: Modifier, uiState: ShortcutCustomizationUiState.AddShortcutDialog, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, - onConfirmSetShortcut: () -> Unit -){ + onConfirmSetShortcut: () -> Unit, +) { Column(modifier = modifier) { Title(uiState.shortcutLabel) Description( - text = - stringResource( - id = R.string.shortcut_customize_mode_add_shortcut_description - ) + text = stringResource(id = R.string.shortcut_customize_mode_add_shortcut_description) ) PromptShortcutModifier( modifier = - Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp) - .width(131.dp) - .height(48.dp), + Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp) + .width(131.dp) + .height(48.dp), defaultModifierKey = uiState.defaultCustomShortcutModifierKey, ) SelectedKeyCombinationContainer( shouldShowError = uiState.errorMessage.isNotEmpty(), - onKeyPress = onKeyPress, + onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected, pressedKeys = uiState.pressedKeys, + onConfirmSetShortcut = onConfirmSetShortcut, ) ErrorMessageContainer(uiState.errorMessage) DialogButtons( @@ -121,9 +129,7 @@ private fun AddShortcutDialog( isConfirmButtonEnabled = uiState.pressedKeys.isNotEmpty(), onConfirm = onConfirmSetShortcut, confirmButtonText = - stringResource( - R.string.shortcut_helper_customize_dialog_set_shortcut_button_label - ), + stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label), ) } } @@ -132,20 +138,15 @@ private fun AddShortcutDialog( private fun DeleteShortcutDialog( modifier: Modifier, onCancel: () -> Unit, - onConfirmDeleteShortcut: () -> Unit -){ + onConfirmDeleteShortcut: () -> Unit, +) { ConfirmationDialog( modifier = modifier, - title = - stringResource( - id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title - ), + title = stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title), description = - stringResource( - id = R.string.shortcut_customize_mode_remove_shortcut_description - ), + stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_description), confirmButtonText = - stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label), + stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label), onCancel = onCancel, onConfirm = onConfirmDeleteShortcut, ) @@ -155,20 +156,15 @@ private fun DeleteShortcutDialog( private fun ResetShortcutDialog( modifier: Modifier, onCancel: () -> Unit, - onConfirmResetShortcut: () -> Unit -){ + onConfirmResetShortcut: () -> Unit, +) { ConfirmationDialog( modifier = modifier, - title = - stringResource( - id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title - ), + title = stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title), description = - stringResource( - id = R.string.shortcut_customize_mode_reset_shortcut_description - ), + stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_description), confirmButtonText = - stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label), + stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label), onCancel = onCancel, onConfirm = onConfirmResetShortcut, ) @@ -201,6 +197,9 @@ private fun DialogButtons( onConfirm: () -> Unit, confirmButtonText: String, ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.requestFocus() } + Row( modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp) @@ -218,6 +217,10 @@ private fun DialogButtons( ) Spacer(modifier = Modifier.width(8.dp)) ShortcutHelperButton( + modifier = + Modifier.focusRequester(focusRequester).focusProperties { + canFocus = true + }, // enable focus on touch/click mode onClick = onConfirm, color = MaterialTheme.colorScheme.primary, width = 116.dp, @@ -248,8 +251,9 @@ private fun ErrorMessageContainer(errorMessage: String) { @Composable private fun SelectedKeyCombinationContainer( shouldShowError: Boolean, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, pressedKeys: List<ShortcutKey>, + onConfirmSetShortcut: () -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() @@ -269,7 +273,17 @@ private fun SelectedKeyCombinationContainer( Modifier.padding(all = 16.dp) .sizeIn(minWidth = 332.dp, minHeight = 56.dp) .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp)) - .onKeyEvent { onKeyPress(it) } + .onPreviewKeyEvent { keyEvent -> + val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent) + if ( + !keyEventProcessed && + keyEvent.key == Key.Enter && + keyEvent.type == KeyEventType.KeyUp + ) { + onConfirmSetShortcut() + true + } else keyEventProcessed + } .focusProperties { canFocus = true } // enables keyboard focus when in touch mode .focusRequester(focusRequester), interactionSource = interactionSource, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt index 373eb250d61d..915a66c43a12 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt @@ -46,6 +46,7 @@ constructor( private val context: Context, private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor, ) { + private var keyDownEventCache: KeyEvent? = null private val _shortcutCustomizationUiState = MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive) @@ -94,9 +95,16 @@ constructor( shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) } - fun onKeyPressed(keyEvent: KeyEvent): Boolean { - if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) { - updatePressedKeys(keyEvent) + fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean { + if (isModifier(keyEvent)) { + return false + } + if (keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown) { + keyDownEventCache = keyEvent + return true + } else if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == keyDownEventCache?.key) { + updatePressedKeys(keyDownEventCache!!) + clearKeyDownEventCache() return true } return false @@ -157,16 +165,21 @@ constructor( return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState } + private fun isModifier(keyEvent: KeyEvent) = SUPPORTED_MODIFIERS.contains(keyEvent.key) + private fun updatePressedKeys(keyEvent: KeyEvent) { - val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key) val keyCombination = KeyCombination( modifiers = keyEvent.nativeKeyEvent.modifiers, - keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null, + keyCode = if (!isModifier(keyEvent)) keyEvent.key.nativeKeyCode else null, ) shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination) } + private fun clearKeyDownEventCache() { + keyDownEventCache = null + } + @AssistedFactory interface Factory { fun create(): ShortcutCustomizationViewModel 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 fbe31bbf36e6..8f7f2a0a8cbb 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 @@ -44,7 +44,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -84,7 +83,6 @@ class KeyguardInteractor @Inject constructor( private val repository: KeyguardRepository, - powerInteractor: PowerInteractor, bouncerRepository: KeyguardBouncerRepository, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, shadeRepository: ShadeRepository, @@ -216,11 +214,7 @@ constructor( // should actually be quite strange to leave AOD and then go straight to // DREAMING so this should be fine. delay(IS_ABLE_TO_DREAM_DELAY_MS) - isDreaming - .sample(powerInteractor.isAwake) { isDreaming, isAwake -> - isDreaming && isAwake - } - .debounce(50L) + isDreaming.debounce(50L) } else { flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt index 542fb9b46bef..3eb8522e0338 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt @@ -23,4 +23,10 @@ data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) { // No-op config that will be used by dagger of other SysUI variants which don't blur the // background surface. @Inject constructor() : this(0.0f, 0.0f) + + companion object { + // Blur the shade much lesser than the background surface so that the surface is + // distinguishable from the background. + @JvmStatic fun Float.maxBlurRadiusToNotificationPanelBlurRadius(): Float = this / 3.0f + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt index e77e9dd9e9ed..eb1afb406d2b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt @@ -30,6 +30,9 @@ interface PrimaryBouncerTransition { /** Radius of blur applied to the window's root view. */ val windowBlurRadius: Flow<Float> + /** Radius of blur applied to the notifications on expanded shade */ + val notificationBlurRadius: Flow<Float> + fun transitionProgressToBlurRadius( starBlurRadius: Float, endBlurRadius: Float, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index f17455788d6e..92bb5e6029cb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCE import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -73,7 +75,28 @@ constructor( val lockscreenAlpha: Flow<Float> = if (WindowBlurFlag.isEnabled) alphaFlow else emptyFlow() - val notificationAlpha: Flow<Float> = alphaFlow + val notificationAlpha: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = lockscreenAlpha, + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f), + ) + } else { + alphaFlow + } + + override val notificationBlurRadius: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = emptyFlow(), + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo( + blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() + ), + ) + } else { + emptyFlow<Float>() + } override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt index dbb6a49e7844..e3b55874de6f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt @@ -53,4 +53,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt index d8b617a60129..c937d5c6453d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt @@ -64,4 +64,6 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio }, onFinish = { blurConfig.maxBlurRadiusPx }, ) + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt index 597df15a2b55..5ab458334a25 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt index c373fd01ba20..44c4c8723dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -32,6 +34,7 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow /** * Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to @@ -70,6 +73,29 @@ constructor( val lockscreenAlpha: Flow<Float> = shortcutsAlpha + val notificationAlpha: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = lockscreenAlpha, + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f), + ) + } else { + lockscreenAlpha + } + + override val notificationBlurRadius: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = emptyFlow(), + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo( + blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() + ), + ) + } else { + emptyFlow() + } + override val deviceEntryParentViewAlpha: Flow<Float> = shadeDependentFlows.transitionFlow( flowWhenShadeIsNotExpanded = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt index 44598107fa4b..4d3e27265cea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt index fab8008cbfa7..224191b64f5f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt @@ -91,4 +91,7 @@ constructor( }, onFinish = { blurConfig.minBlurRadiusPx }, ) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt index eebdf2ef418e..0f8495f34d22 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt @@ -80,4 +80,6 @@ constructor( }, onFinish = { blurConfig.minBlurRadiusPx }, ) + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt index 3636b747d5c9..a13eef2388f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt @@ -43,4 +43,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 4ed3e6cde230..d1233f220f47 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -166,6 +166,9 @@ constructor( createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard) } + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) + val scrimAlpha: Flow<ScrimAlpha> = bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, PRIMARY_BOUNCER) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt index 2edc93cb5617..c53a408a88e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt @@ -91,4 +91,7 @@ constructor( }, ), ) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt index 3a54a26858d4..fe1708efea2f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt index 09544827a51a..a6b9442b1270 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt @@ -31,6 +31,7 @@ import androidx.media3.session.CommandButton import androidx.media3.session.MediaController as Media3Controller import androidx.media3.session.SessionCommand import androidx.media3.session.SessionToken +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -128,7 +129,11 @@ constructor( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_connecting_status_container) + } else { + context.getDrawable(R.drawable.ic_media_connecting_container) + }, // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material, ) @@ -230,17 +235,33 @@ constructor( Player.COMMAND_PLAY_PAUSE -> { if (!controller.isPlaying) { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button) + } else { + context.getDrawable(R.drawable.ic_media_play) + }, { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } else { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button) + } else { + context.getDrawable(R.drawable.ic_media_pause) + }, { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button_container) + } else { + context.getDrawable(R.drawable.ic_media_pause_container) + }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt index 4f9791353b8a..9bf556cf07c2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -29,6 +29,7 @@ import android.media.session.PlaybackState import android.service.notification.StatusBarNotification import android.util.Log import androidx.media.utils.MediaConstants +import com.android.systemui.Flags import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS import com.android.systemui.media.controls.shared.MediaControlDrawables @@ -69,7 +70,11 @@ fun createActionsFromState( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_connecting_status_container) + } else { + context.getDrawable(R.drawable.ic_media_connecting_container) + }, // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material, ) @@ -157,18 +162,34 @@ private fun getStandardAction( return when (action) { PlaybackState.ACTION_PLAY -> { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button) + } else { + context.getDrawable(R.drawable.ic_media_play) + }, { controller.transportControls.play() }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } PlaybackState.ACTION_PAUSE -> { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button) + } else { + context.getDrawable(R.drawable.ic_media_pause) + }, { controller.transportControls.pause() }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button_container) + } else { + context.getDrawable(R.drawable.ic_media_pause_container) + }, ) } PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 3928a711f840..a2ddc20844e7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -1016,9 +1016,24 @@ constructor( expandedLayout.load(context, R.xml.media_recommendations_expanded) } } + readjustPlayPauseWidth() refreshState() } + private fun readjustPlayPauseWidth() { + // TODO: move to xml file when flag is removed. + if (Flags.mediaControlsUiUpdate()) { + collapsedLayout.constrainWidth( + R.id.actionPlayPause, + context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), + ) + expandedLayout.constrainWidth( + R.id.actionPlayPause, + context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), + ) + } + } + /** Get a view state based on the width and height set by the scene */ private fun obtainSceneContainerViewState(state: MediaHostState?): TransitionViewState? { logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index ec8d30b01eab..e93cec875429 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -41,12 +41,14 @@ import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.plugins.qs.TileDetailsViewModel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel; import com.android.systemui.res.R; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.screenrecord.data.model.ScreenRecordModel; @@ -54,6 +56,8 @@ import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.function.Consumer; + import javax.inject.Inject; /** @@ -122,17 +126,78 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> @Override protected void handleClick(@Nullable Expandable expandable) { + handleClick(() -> showDialog(expandable)); + } + + private void showDialog(@Nullable Expandable expandable) { + final Dialog dialog = mController.createScreenRecordDialog( + this::onStartRecordingClicked); + + executeWhenUnlockedKeyguard(() -> { + // We animate from the touched view only if we are not on the keyguard, given that if we + // are we will dismiss it which will also collapse the shade. + boolean shouldAnimateFromExpandable = + expandable != null && !mKeyguardStateController.isShowing(); + + if (shouldAnimateFromExpandable) { + DialogTransitionAnimator.Controller controller = + expandable.dialogTransitionController(new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)); + if (controller != null) { + mDialogTransitionAnimator.show(dialog, + controller, /* animateBackgroundBoundsChange= */ true); + } else { + dialog.show(); + } + } else { + dialog.show(); + } + }); + } + + private void onStartRecordingClicked() { + // We dismiss the shade. Since starting the recording will also dismiss the dialog (if + // there is one showing), we disable the exit animation which looks weird when it happens + // at the same time as the shade collapsing. + mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); + mPanelInteractor.collapsePanels(); + } + + private void executeWhenUnlockedKeyguard(Runnable dismissActionCallback) { + ActivityStarter.OnDismissAction dismissAction = () -> { + dismissActionCallback.run(); + + int uid = mUserContextProvider.getUserContext().getUserId(); + mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid); + + return false; + }; + + mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */, + true /* afterKeyguardDone */); + } + + private void handleClick(Runnable showPromptCallback) { if (mController.isStarting()) { cancelCountdown(); } else if (mController.isRecording()) { stopRecording(); } else { - mUiHandler.post(() -> showPrompt(expandable)); + mUiHandler.post(showPromptCallback); } refreshState(); } @Override + public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) { + handleClick(() -> + callback.accept(new ScreenRecordDetailsViewModel()) + ); + return true; + } + + @Override protected void handleUpdateState(BooleanState state, Object arg) { boolean isStarting = mController.isStarting(); boolean isRecording = mController.isRecording(); @@ -178,49 +243,6 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> return mContext.getString(R.string.quick_settings_screen_record_label); } - private void showPrompt(@Nullable Expandable expandable) { - // We animate from the touched view only if we are not on the keyguard, given that if we - // are we will dismiss it which will also collapse the shade. - boolean shouldAnimateFromExpandable = - expandable != null && !mKeyguardStateController.isShowing(); - - // Create the recording dialog that will collapse the shade only if we start the recording. - Runnable onStartRecordingClicked = () -> { - // We dismiss the shade. Since starting the recording will also dismiss the dialog, we - // disable the exit animation which looks weird when it happens at the same time as the - // shade collapsing. - mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); - mPanelInteractor.collapsePanels(); - }; - - final Dialog dialog = mController.createScreenRecordDialog(onStartRecordingClicked); - - ActivityStarter.OnDismissAction dismissAction = () -> { - if (shouldAnimateFromExpandable) { - DialogTransitionAnimator.Controller controller = - expandable.dialogTransitionController(new DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG)); - if (controller != null) { - mDialogTransitionAnimator.show(dialog, - controller, /* animateBackgroundBoundsChange= */ true); - } else { - dialog.show(); - } - } else { - dialog.show(); - } - - int uid = mUserContextProvider.getUserContext().getUserId(); - mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid); - - return false; - }; - - mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */, - true /* afterKeyguardDone */); - } - private void cancelCountdown() { Log.d(TAG, "Cancelling countdown"); mController.cancelCountdown(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt new file mode 100644 index 000000000000..42cb1248ccff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 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.qs.tiles.dialog + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.plugins.qs.TileDetailsViewModel +import com.android.systemui.res.R + +/** The view model used for the screen record details view in the Quick Settings */ +class ScreenRecordDetailsViewModel() : TileDetailsViewModel() { + @Composable + override fun GetContentView() { + // TODO(b/378514312): Finish implementing this function. + AndroidView( + modifier = Modifier.fillMaxWidth().heightIn(max = VIEW_MAX_HEIGHT), + factory = { context -> + // Inflate with the existing dialog xml layout + LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null) + }, + ) + } + + override fun clickOnSettingsButton() { + // No settings button in this tile. + } + + override fun getTitle(): String { + // TODO(b/388321032): Replace this string with a string in a translatable xml file, + return "Screen recording" + } + + override fun getSubTitle(): String { + // No sub-title in this tile. + return "" + } + + companion object { + private val VIEW_MAX_HEIGHT: Dp = 320.dp + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index c34edc81bfe7..30d1f05771d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -118,7 +118,8 @@ constructor( override fun addCallback(callback: QSTile.Callback?) { callback ?: return callbacks.add(callback) - state?.let(callback::onStateChanged) + state.copyTo(cachedState) + state.let(callback::onStateChanged) } override fun removeCallback(callback: QSTile.Callback?) { @@ -212,9 +213,9 @@ constructor( qsTileViewModel.destroy() } - override fun getState(): QSTile.State = + override fun getState(): QSTile.AdapterState = qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) } - ?: QSTile.State() + ?: QSTile.AdapterState() override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId @@ -241,7 +242,7 @@ constructor( context: Context, viewModelState: QSTileState, config: QSTileConfig, - ): QSTile.State = + ): QSTile.AdapterState = // we have to use QSTile.BooleanState to support different side icons // which are bound to instanceof QSTile.BooleanState in QSTileView. QSTile.AdapterState().apply { diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index d60f05e685bb..0488962fd076 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -90,22 +90,15 @@ constructor( initialValue = defaultTransitionState, ) - fun changeScene( - toScene: SceneKey, - transitionKey: TransitionKey? = null, - ) { - dataSource.changeScene( - toScene = toScene, - transitionKey = transitionKey, - ) + /** Number of currently active transition animations. */ + val activeTransitionAnimationCount = MutableStateFlow(0) + + fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) { + dataSource.changeScene(toScene = toScene, transitionKey = transitionKey) } - fun snapToScene( - toScene: SceneKey, - ) { - dataSource.snapToScene( - toScene = toScene, - ) + fun snapToScene(toScene: SceneKey) { + dataSource.snapToScene(toScene = toScene) } /** @@ -116,10 +109,7 @@ constructor( * [overlay] is already shown. */ fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.showOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.showOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -130,10 +120,7 @@ constructor( * if [overlay] is already hidden. */ fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.hideOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.hideOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -143,11 +130,7 @@ constructor( * This throws if [from] is not currently shown or if [to] is already shown. */ fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.replaceOverlay( - from = from, - to = to, - transitionKey = transitionKey, - ) + dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey) } /** Sets whether the container is visible. */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 0e6fc36fb96a..ba9dc7625769 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update /** * Generic business logic and app state accessors for the scene framework. @@ -165,12 +166,15 @@ constructor( /** Whether the scene container is visible. */ val isVisible: StateFlow<Boolean> = - combine(repository.isVisible, repository.isRemoteUserInputOngoing) { - isVisible, - isRemoteUserInteractionOngoing -> + combine( + repository.isVisible, + repository.isRemoteUserInputOngoing, + repository.activeTransitionAnimationCount, + ) { isVisible, isRemoteUserInteractionOngoing, activeTransitionAnimationCount -> isVisibleInternal( raw = isVisible, isRemoteUserInputOngoing = isRemoteUserInteractionOngoing, + activeTransitionAnimationCount = activeTransitionAnimationCount, ) } .stateIn( @@ -436,8 +440,9 @@ constructor( private fun isVisibleInternal( raw: Boolean = repository.isVisible.value, isRemoteUserInputOngoing: Boolean = repository.isRemoteUserInputOngoing.value, + activeTransitionAnimationCount: Int = repository.activeTransitionAnimationCount.value, ): Boolean { - return raw || isRemoteUserInputOngoing + return raw || isRemoteUserInputOngoing || activeTransitionAnimationCount > 0 } /** @@ -525,4 +530,50 @@ constructor( ): Flow<Map<UserAction, UserActionResult>> { return disabledContentInteractor.filteredUserActions(unfiltered) } + + /** + * Notifies that a transition animation has started. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationStart() { + repository.activeTransitionAnimationCount.update { current -> + (current + 1).also { + check(it < 10) { + "Number of active transition animations is too high. Something must be" + + " calling onTransitionAnimationStart too many times!" + } + } + } + } + + /** + * Notifies that a transition animation has ended. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationEnd() { + decrementActiveTransitionAnimationCount() + } + + /** + * Notifies that a transition animation has been canceled. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationCancelled() { + decrementActiveTransitionAnimationCount() + } + + private fun decrementActiveTransitionAnimationCount() { + repository.activeTransitionAnimationCount.update { current -> + (current - 1).also { + check(it >= 0) { + "Number of active transition animations is negative. Something must be" + + " calling onTransitionAnimationEnd or onTransitionAnimationCancelled too" + + " many times!" + } + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 8d8c24eae9e2..3a23a71cf7bf 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -25,6 +25,7 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.AuthInteractionProperties import com.android.systemui.CoreStartable import com.android.systemui.Flags +import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor @@ -146,6 +147,7 @@ constructor( private val vibratorHelper: VibratorHelper, private val msdlPlayer: MSDLPlayer, private val disabledContentInteractor: DisabledContentInteractor, + private val activityTransitionAnimator: ActivityTransitionAnimator, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -169,6 +171,7 @@ constructor( handleKeyguardEnabledness() notifyKeyguardDismissCancelledCallbacks() refreshLockscreenEnabled() + hydrateActivityTransitionAnimationState() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -929,6 +932,35 @@ constructor( } } + /** + * Wires the scene framework to activity transition animations that originate from anywhere. A + * subset of these may actually originate from UI inside one of the scenes in the framework. + * + * Telling the scene framework about ongoing activity transition animations is critical so the + * scene framework doesn't make its scene container invisible during a transition. + * + * As it turns out, making the scene container view invisible during a transition animation + * disrupts the animation and causes interaction jank CUJ tracking to ignore reports of the CUJ + * ending or being canceled. + */ + private fun hydrateActivityTransitionAnimationState() { + activityTransitionAnimator.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + sceneInteractor.onTransitionAnimationStart() + } + + override fun onTransitionAnimationEnd() { + sceneInteractor.onTransitionAnimationEnd() + } + + override fun onTransitionAnimationCancelled() { + sceneInteractor.onTransitionAnimationCancelled() + } + } + ) + } + companion object { private const val TAG = "SceneContainerStartable" } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 19152170757c..c4306d3f7530 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -109,6 +109,7 @@ import com.android.systemui.keyguard.shared.model.Edge; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; @@ -914,13 +915,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (!com.android.systemui.Flags.bouncerUiRevamp()) return; if (isBouncerShowing && isExpanded()) { - // Blur the shade much lesser than the background surface so that the surface is - // distinguishable from the background. - float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx() / 3; + float shadeBlurEffect = BlurConfig.maxBlurRadiusToNotificationPanelBlurRadius( + mDepthController.getMaxBlurRadiusPx()); mView.setRenderEffect(RenderEffect.createBlurEffect( shadeBlurEffect, shadeBlurEffect, - Shader.TileMode.MIRROR)); + Shader.TileMode.CLAMP)); } else { mView.setRenderEffect(null); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt index 4d35d0eba178..e358dcec8b10 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt @@ -24,7 +24,6 @@ import com.android.systemui.common.ui.view.ChoreographerUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.ui.view.WindowRootView -import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker.Companion.TIMEOUT import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.util.kotlin.getOrNull import java.util.Optional @@ -33,7 +32,6 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter @@ -135,7 +133,7 @@ constructor( private companion object { const val TAG = "ShadeDisplayLatency" - val t = TrackTracer(trackName = TAG) + val t = TrackTracer(trackName = TAG, trackGroup = "shade") val TIMEOUT = 3.seconds const val SHADE_MOVE_ACTION = LatencyTracker.ACTION_SHADE_WINDOW_DISPLAY_CHANGE } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index 359ddd86f115..5fab889735a6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -18,13 +18,16 @@ package com.android.systemui.shade import android.annotation.IntDef import android.os.Trace +import android.os.Trace.TRACE_TAG_APP as TRACE_TAG import android.util.Log import androidx.annotation.FloatRange +import com.android.app.tracing.TraceStateLogger +import com.android.app.tracing.TrackGroupUtils.trackGroup +import com.android.app.tracing.coroutines.TrackTracer import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.Compile import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject -import android.os.Trace.TRACE_TAG_APP as TRACE_TAG /** * A class responsible for managing the notification panel's current state. @@ -38,6 +41,8 @@ class ShadeExpansionStateManager @Inject constructor() { private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>() private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>() + private val stateLogger = TraceStateLogger(trackGroup("shade", TRACK_NAME)) + @PanelState private var state: Int = STATE_CLOSED @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f private var expanded: Boolean = false @@ -75,7 +80,7 @@ class ShadeExpansionStateManager @Inject constructor() { fun onPanelExpansionChanged( @FloatRange(from = 0.0, to = 1.0) fraction: Float, expanded: Boolean, - tracking: Boolean + tracking: Boolean, ) { require(!fraction.isNaN()) { "fraction cannot be NaN" } val oldState = state @@ -113,11 +118,8 @@ class ShadeExpansionStateManager @Inject constructor() { ) if (Trace.isTagEnabled(TRACE_TAG)) { - Trace.traceCounter(TRACE_TAG, "panel_expansion", (fraction * 100).toInt()) - if (state != oldState) { - Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK_NAME, 0) - Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK_NAME, state.panelStateToString(), 0) - } + TrackTracer.instantForGroup("shade", "panel_expansion", fraction) + stateLogger.log(state.panelStateToString()) } val expansionChangeEvent = ShadeExpansionChangeEvent(fraction, expanded, tracking) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt new file mode 100644 index 000000000000..2705cdafb4de --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 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.shade + +import com.android.app.tracing.TraceStateLogger +import com.android.app.tracing.TrackGroupUtils.trackGroup +import com.android.app.tracing.coroutines.TrackTracer.Companion.instantForGroup +import com.android.app.tracing.coroutines.launchTraced +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@SysUISingleton +class ShadeStateTraceLogger +@Inject +constructor( + private val shadeInteractor: ShadeInteractor, + private val shadeDisplaysRepository: ShadeDisplaysRepository, + @Application private val scope: CoroutineScope, +) : CoreStartable { + override fun start() { + scope.launchTraced("ShadeStateTraceLogger") { + launch { + val stateLogger = createTraceStateLogger("isShadeLayoutWide") + shadeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) } + } + launch { + val stateLogger = createTraceStateLogger("shadeMode") + shadeInteractor.shadeMode.collect { stateLogger.log(it.toString()) } + } + launch { + shadeInteractor.shadeExpansion.collect { + instantForGroup(TRACK_GROUP_NAME, "shadeExpansion", it) + } + } + launch { + shadeDisplaysRepository.displayId.collect { + instantForGroup(TRACK_GROUP_NAME, "displayId", it) + } + } + } + } + + private fun createTraceStateLogger(trackName: String): TraceStateLogger { + return TraceStateLogger(trackGroup(TRACK_GROUP_NAME, trackName)) + } + + private companion object { + const val TRACK_GROUP_NAME = "shade" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt index a36c56eafbfc..11805992fd6a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt @@ -27,7 +27,7 @@ import com.android.app.tracing.coroutines.TrackTracer * them across various threads' logs. */ object ShadeTraceLogger { - private val t = TrackTracer(trackName = "ShadeTraceLogger") + private val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade") @JvmStatic fun logOnMovedToDisplay(displayId: Int, config: Configuration) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt index c4de78b8a28e..570a7853c394 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt @@ -40,4 +40,9 @@ internal abstract class StartShadeModule { @IntoMap @ClassKey(ShadeStartable::class) abstract fun provideShadeStartable(startable: ShadeStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(ShadeStateTraceLogger::class) + abstract fun provideShadeStateTraceLogger(startable: ShadeStateTraceLogger): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt index 37989f56d559..2885ce80bda9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt @@ -11,13 +11,13 @@ import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffXfermode import android.graphics.RadialGradient import android.graphics.Shader -import android.os.Trace import android.util.AttributeSet import android.util.MathUtils.lerp import android.view.MotionEvent import android.view.View import android.view.animation.PathInterpolator import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.TrackTracer import com.android.keyguard.logging.ScrimLogger import com.android.systemui.shade.TouchLogger import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold @@ -321,9 +321,8 @@ constructor( } revealEffect.setRevealAmountOnScrim(value, this) updateScrimOpaque() - Trace.traceCounter( - Trace.TRACE_TAG_APP, - "light_reveal_amount $logString", + TrackTracer.instantForGroup( + "scrim", { "light_reveal_amount $logString" }, (field * 100).toInt() ) invalidate() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 2bcd3fcfed17..10b726b90894 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -92,6 +92,7 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import javax.inject.Inject; @@ -297,6 +298,9 @@ public class NotificationLockscreenUserManagerImpl implements // The last lock time. Uses currentTimeMillis @VisibleForTesting protected final AtomicLong mLastLockTime = new AtomicLong(-1); + // Whether or not the device is locked + @VisibleForTesting + protected final AtomicBoolean mLocked = new AtomicBoolean(true); protected int mCurrentUserId = 0; @@ -369,6 +373,7 @@ public class NotificationLockscreenUserManagerImpl implements if (!unlocked) { mLastLockTime.set(System.currentTimeMillis()); } + mLocked.set(!unlocked); })); } } @@ -737,7 +742,7 @@ public class NotificationLockscreenUserManagerImpl implements return false; } - if (!mKeyguardManager.isDeviceLocked()) { + if (!mLocked.get()) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index e83cded4e2ce..38f7c39203f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -22,7 +22,6 @@ import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration import android.os.SystemClock -import android.os.Trace import android.util.IndentingPrintWriter import android.util.Log import android.util.MathUtils @@ -33,7 +32,9 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.TrackTracer import com.android.systemui.Dumpable +import com.android.systemui.Flags.spatialModelAppPushback import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -52,7 +53,9 @@ import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.WallpaperController import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.systemui.window.flag.WindowBlurFlag +import com.android.wm.shell.appzoomout.AppZoomOut import java.io.PrintWriter +import java.util.Optional import javax.inject.Inject import kotlin.math.max import kotlin.math.sign @@ -79,6 +82,7 @@ constructor( private val splitShadeStateController: SplitShadeStateController, private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor, @Application private val applicationScope: CoroutineScope, + private val appZoomOutOptional: Optional<AppZoomOut>, dumpManager: DumpManager, configurationController: ConfigurationController, ) : ShadeExpansionListener, Dumpable { @@ -263,7 +267,7 @@ constructor( updateScheduled = false val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut() val opaque = shouldBlurBeOpaque - Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur) + TrackTracer.instantForGroup("shade", "shade_blur_radius", blur) blurUtils.applyBlur(root.viewRootImpl, blur, opaque) onBlurApplied(blur, zoomOutFromShadeRadius) } @@ -271,6 +275,13 @@ constructor( private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) { lastAppliedBlur = appliedBlurRadius wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius) + if (spatialModelAppPushback()) { + appZoomOutOptional.ifPresent { appZoomOut -> + appZoomOut.setProgress( + zoomOutFromShadeRadius + ) + } + } listeners.forEach { it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius) it.onBlurRadiusChanged(appliedBlurRadius) @@ -384,7 +395,7 @@ constructor( windowRootViewBlurInteractor.onBlurAppliedEvent.collect { appliedBlurRadius -> if (updateScheduled) { // Process the blur applied event only if we scheduled the update - Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", appliedBlurRadius) + TrackTracer.instantForGroup("shade", "shade_blur_radius", appliedBlurRadius) updateScheduled = false onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius) } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 48cf7a83c324..155049f512d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.IndentingPrintWriter; import android.util.MathUtils; @@ -30,6 +31,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; @@ -1014,12 +1016,24 @@ public class NotificationShelf extends ActivatableNotificationView { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); if (mInteractive) { + // Add two accessibility actions that both performs expanding the notification shade info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); - AccessibilityNodeInfo.AccessibilityAction unlock - = new AccessibilityNodeInfo.AccessibilityAction( + + AccessibilityAction seeAll = new AccessibilityAction( AccessibilityNodeInfo.ACTION_CLICK, - getContext().getString(R.string.accessibility_overflow_action)); - info.addAction(unlock); + getContext().getString(R.string.accessibility_overflow_action) + ); + info.addAction(seeAll); + } + } + + @Override + public boolean performAccessibilityAction(int action, Bundle args) { + // override ACTION_EXPAND with ACTION_CLICK + if (action == AccessibilityNodeInfo.ACTION_EXPAND) { + return super.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, args); + } else { + return super.performAccessibilityAction(action, args); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index a7ad46296e08..ead8f6a1123e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -37,6 +37,7 @@ import android.view.animation.Interpolator; import androidx.annotation.NonNull; import com.android.app.animation.Interpolators; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.compose.animation.scene.OverlayKey; import com.android.compose.animation.scene.SceneKey; import com.android.internal.annotations.GuardedBy; @@ -671,7 +672,7 @@ public class StatusBarStateControllerImpl implements } private void recordHistoricalState(int newState, int lastState, boolean upcoming) { - Trace.traceCounter(Trace.TRACE_TAG_APP, "statusBarState", newState); + TrackTracer.instantForGroup("statusBar", "state", newState); mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE; HistoricalState state = mHistoricalRecords[mHistoryIndex]; state.mNewState = newState; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index de08e3891902..86954d569199 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel import android.view.View import com.android.internal.jank.InteractionJankMonitor -import com.android.systemui.Flags import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon @@ -64,18 +63,12 @@ constructor( is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Hidden() is OngoingCallModel.InCall -> { val icon = - if ( - Flags.statusBarCallChipNotificationIcon() && - state.notificationIconView != null - ) { + if (state.notificationIconView != null) { StatusBarConnectedDisplays.assertInLegacyMode() OngoingActivityChipModel.ChipIcon.StatusBarView( state.notificationIconView ) - } else if ( - StatusBarConnectedDisplays.isEnabled && - Flags.statusBarCallChipNotificationIcon() - ) { + } else if (StatusBarConnectedDisplays.isEnabled) { OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( state.notificationKey ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 2f6431b05c8b..ec3a5b271e35 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import javax.inject.Inject @@ -60,7 +61,7 @@ constructor( /** Converts the notification to the [OngoingActivityChipModel] object. */ private fun NotificationChipModel.toActivityChipModel( - headsUpState: PinnedStatus + headsUpState: TopPinnedState ): OngoingActivityChipModel.Shown { StatusBarNotifChips.assertInNewMode() val icon = @@ -87,8 +88,12 @@ constructor( } } - if (headsUpState == PinnedStatus.PinnedByUser) { - // If the user tapped the chip to show the HUN, we want to just show the icon because + val isShowingHeadsUpFromChipTap = + headsUpState is TopPinnedState.Pinned && + headsUpState.status == PinnedStatus.PinnedByUser && + headsUpState.key == this.key + if (isShowingHeadsUpFromChipTap) { + // If the user tapped this chip to show the HUN, we want to just show the icon because // the HUN will show the rest of the information. return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 69ef09d8bf5e..b0fa9d842480 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -25,6 +25,7 @@ import android.widget.DateTimeView import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.annotation.UiThread import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView @@ -38,24 +39,24 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.Notificati /** Binder for ongoing activity chip views. */ object OngoingActivityChipBinder { /** Binds the given [chipModel] data to the given [chipView]. */ - fun bind(chipModel: OngoingActivityChipModel, chipView: View, iconViewStore: IconViewStore?) { - val chipContext = chipView.context - val chipDefaultIconView: ImageView = - chipView.requireViewById(R.id.ongoing_activity_chip_icon) - val chipTimeView: ChipChronometer = - chipView.requireViewById(R.id.ongoing_activity_chip_time) - val chipTextView: TextView = chipView.requireViewById(R.id.ongoing_activity_chip_text) - val chipShortTimeDeltaView: DateTimeView = - chipView.requireViewById(R.id.ongoing_activity_chip_short_time_delta) - val chipBackgroundView: ChipBackgroundContainer = - chipView.requireViewById(R.id.ongoing_activity_chip_background) + fun bind( + chipModel: OngoingActivityChipModel, + viewBinding: OngoingActivityChipViewBinding, + iconViewStore: IconViewStore?, + ) { + val chipContext = viewBinding.rootView.context + val chipDefaultIconView = viewBinding.defaultIconView + val chipTimeView = viewBinding.timeView + val chipTextView = viewBinding.textView + val chipShortTimeDeltaView = viewBinding.shortTimeDeltaView + val chipBackgroundView = viewBinding.backgroundView when (chipModel) { is OngoingActivityChipModel.Shown -> { // Data setChipIcon(chipModel, chipBackgroundView, chipDefaultIconView, iconViewStore) setChipMainContent(chipModel, chipTextView, chipTimeView, chipShortTimeDeltaView) - chipView.setOnClickListener(chipModel.onClickListener) + viewBinding.rootView.setOnClickListener(chipModel.onClickListener) updateChipPadding( chipModel, chipBackgroundView, @@ -65,7 +66,7 @@ object OngoingActivityChipBinder { ) // Accessibility - setChipAccessibility(chipModel, chipView, chipBackgroundView) + setChipAccessibility(chipModel, viewBinding.rootView, chipBackgroundView) // Colors val textColor = chipModel.colors.text(chipContext) @@ -83,6 +84,85 @@ object OngoingActivityChipBinder { } } + /** Stores [rootView] and relevant child views in an object for easy reference. */ + fun createBinding(rootView: View): OngoingActivityChipViewBinding { + return OngoingActivityChipViewBinding( + rootView = rootView, + timeView = rootView.requireViewById(R.id.ongoing_activity_chip_time), + textView = rootView.requireViewById(R.id.ongoing_activity_chip_text), + shortTimeDeltaView = + rootView.requireViewById(R.id.ongoing_activity_chip_short_time_delta), + defaultIconView = rootView.requireViewById(R.id.ongoing_activity_chip_icon), + backgroundView = rootView.requireViewById(R.id.ongoing_activity_chip_background), + ) + } + + /** + * Resets any width restrictions that were placed on the primary chip's contents. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + fun resetPrimaryChipWidthRestrictions( + primaryChipViewBinding: OngoingActivityChipViewBinding, + currentPrimaryChipViewModel: OngoingActivityChipModel, + ) { + if (currentPrimaryChipViewModel is OngoingActivityChipModel.Hidden) { + return + } + resetChipMainContentWidthRestrictions( + primaryChipViewBinding, + currentPrimaryChipViewModel as OngoingActivityChipModel.Shown, + ) + } + + /** + * Resets any width restrictions that were placed on the secondary chip and its contents. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + fun resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding: OngoingActivityChipViewBinding, + currentSecondaryChipModel: OngoingActivityChipModel, + ) { + if (currentSecondaryChipModel is OngoingActivityChipModel.Hidden) { + return + } + secondaryChipViewBinding.rootView.resetWidthRestriction() + resetChipMainContentWidthRestrictions( + secondaryChipViewBinding, + currentSecondaryChipModel as OngoingActivityChipModel.Shown, + ) + } + + private fun resetChipMainContentWidthRestrictions( + viewBinding: OngoingActivityChipViewBinding, + model: OngoingActivityChipModel.Shown, + ) { + when (model) { + is OngoingActivityChipModel.Shown.Text -> viewBinding.textView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.Timer -> viewBinding.timeView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.ShortTimeDelta -> + viewBinding.shortTimeDeltaView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.IconOnly, + is OngoingActivityChipModel.Shown.Countdown -> {} + } + } + + /** + * Resets any width restrictions that were placed on the given view. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + @UiThread + fun View.resetWidthRestriction() { + // View needs to be visible in order to be re-measured + visibility = View.VISIBLE + forceLayout() + } + private fun setChipIcon( chipModel: OngoingActivityChipModel.Shown, backgroundView: ChipBackgroundContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt new file mode 100644 index 000000000000..1814b7430330 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 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.chips.ui.binder + +import android.view.View +import android.widget.ImageView +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.view.ChipChronometer +import com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView +import com.android.systemui.statusbar.chips.ui.view.ChipTextView + +/** Stores bound views for a given chip. */ +data class OngoingActivityChipViewBinding( + val rootView: View, + val timeView: ChipChronometer, + val textView: ChipTextView, + val shortTimeDeltaView: ChipDateTimeView, + val defaultIconView: ImageView, + val backgroundView: ChipBackgroundContainer, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt new file mode 100644 index 000000000000..1be5842bceeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -0,0 +1,208 @@ +/* + * 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.chips.ui.compose + +import android.content.res.ColorStateList +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +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.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.compose.modifiers.neverDecreaseWidth +import com.android.systemui.statusbar.chips.ui.model.ColorsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel + +@Composable +fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { + val context = LocalContext.current + val isClickable = model.onClickListener != null + val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView + + // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible + // height of the chip is determined by the height of the background of the Row below. + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .fillMaxHeight() + .clickable( + enabled = isClickable, + onClick = { + // TODO(b/372657935): Implement click actions. + }, + ), + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.clip( + RoundedCornerShape( + dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius) + ) + ) + .height(dimensionResource(R.dimen.ongoing_appops_chip_height)) + .widthIn( + min = + if (isClickable) { + dimensionResource(id = R.dimen.min_clickable_item_size) + } else { + 0.dp + } + ) + .background(Color(model.colors.background(context).defaultColor)) + .padding( + horizontal = + if (hasEmbeddedIcon) { + 0.dp + } else { + dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) + } + ), + ) { + model.icon?.let { ChipIcon(viewModel = it, colors = model.colors) } + + val isIconOnly = model is OngoingActivityChipModel.Shown.IconOnly + val isTextOnly = model.icon == null + if (!isIconOnly) { + ChipContent( + viewModel = model, + modifier = + Modifier.padding( + start = + if (isTextOnly || hasEmbeddedIcon) { + 0.dp + } else { + dimensionResource( + id = R.dimen.ongoing_activity_chip_icon_text_padding + ) + }, + end = + if (hasEmbeddedIcon) { + dimensionResource( + id = + R.dimen + .ongoing_activity_chip_text_end_padding_for_embedded_padding_icon + ) + } else { + 0.dp + }, + ), + ) + } + } + } +} + +@Composable +private fun ChipIcon( + viewModel: OngoingActivityChipModel.ChipIcon, + colors: ColorsModel, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + when (viewModel) { + is OngoingActivityChipModel.ChipIcon.StatusBarView -> { + val originalIcon = viewModel.impl + val iconSizePx = + context.resources.getDimensionPixelSize( + R.dimen.ongoing_activity_chip_embedded_padding_icon_size + ) + AndroidView( + modifier = modifier, + factory = { _ -> + originalIcon.apply { + layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx) + imageTintList = ColorStateList.valueOf(colors.text(context)) + } + }, + ) + } + + is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> { + Icon( + icon = viewModel.impl, + tint = Color(colors.text(context)), + modifier = + modifier.size(dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size)), + ) + } + + // TODO(b/372657935): Add recommended architecture implementation for + // StatusBarNotificationIcons + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> {} + } +} + +@Composable +private fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { + val context = LocalContext.current + when (viewModel) { + is OngoingActivityChipModel.Shown.Timer -> { + ChronometerText( + startTimeMillis = viewModel.startTimeMs, + style = MaterialTheme.typography.labelLarge, + color = Color(viewModel.colors.text(context)), + modifier = modifier, + ) + } + + is OngoingActivityChipModel.Shown.Countdown -> { + ChipText( + text = viewModel.secondsUntilStarted.toString(), + color = Color(viewModel.colors.text(context)), + style = MaterialTheme.typography.labelLarge, + modifier = modifier.neverDecreaseWidth(), + backgroundColor = Color(viewModel.colors.background(context).defaultColor), + ) + } + + is OngoingActivityChipModel.Shown.Text -> { + ChipText( + text = viewModel.text, + color = Color(viewModel.colors.text(context)), + style = MaterialTheme.typography.labelLarge, + modifier = modifier, + backgroundColor = Color(viewModel.colors.background(context).defaultColor), + ) + } + + is OngoingActivityChipModel.Shown.ShortTimeDelta -> { + // TODO(b/372657935): Implement ShortTimeDelta content in compose. + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt new file mode 100644 index 000000000000..85ea087f531b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt @@ -0,0 +1,47 @@ +/* + * 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.chips.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel + +@Composable +fun OngoingActivityChips(chips: MultipleOngoingActivityChipsModel, modifier: Modifier = Modifier) { + Row( + // TODO(b/372657935): Remove magic numbers for padding and spacing. + modifier = modifier.fillMaxHeight().padding(horizontal = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // TODO(b/372657935): Make sure chips are only shown when there is enough horizontal + // space. + if (chips.primary is OngoingActivityChipModel.Shown) { + OngoingActivityChip(model = chips.primary) + } + if (chips.secondary is OngoingActivityChipModel.Shown) { + OngoingActivityChip(model = chips.secondary) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index c81e8e211507..956d99e46766 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.ui.model import android.view.View -import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -130,10 +129,6 @@ sealed class OngoingActivityChipModel { */ data class StatusBarView(val impl: StatusBarIconView) : ChipIcon { init { - check(Flags.statusBarCallChipNotificationIcon()) { - "OngoingActivityChipModel.ChipIcon.StatusBarView created even though " + - "Flags.statusBarCallChipNotificationIcon is not enabled" - } StatusBarConnectedDisplays.assertInLegacyMode() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt index ff3061e850d9..7b4b79d7c852 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt @@ -33,10 +33,8 @@ import androidx.annotation.UiThread * that wide. This means the chip may get larger over time (e.g. in the transition from 59:59 to * 1:00:00), but never smaller. * 2) Hiding the text if the time gets too long for the space available. Once the text has been - * hidden, it remains hidden for the duration of the activity. - * - * Note that if the text was too big in portrait mode, resulting in the text being hidden, then the - * text will also be hidden in landscape (even if there is enough space for it in landscape). + * hidden, it remains hidden for the duration of the activity (or until [resetWidthRestriction] + * is called). */ class ChipChronometer @JvmOverloads @@ -51,12 +49,23 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : private var shouldHideText: Boolean = false override fun setBase(base: Long) { - // These variables may have changed during the previous activity, so re-set them before the - // new activity starts. + resetWidthRestriction() + super.setBase(base) + } + + /** + * Resets any width restrictions that were placed on the chronometer. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + @UiThread + fun resetWidthRestriction() { minimumTextWidth = 0 shouldHideText = false + // View needs to be visible in order to be re-measured visibility = VISIBLE - super.setBase(base) + forceLayout() } /** Sets whether this view should hide its text or not. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt index eff959d0f83b..351cdc8e7f36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.data.StatusBarDataLayerModule import com.android.systemui.statusbar.data.repository.LightBarControllerStore import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModel import com.android.systemui.statusbar.phone.AutoHideController import com.android.systemui.statusbar.phone.AutoHideControllerImpl import com.android.systemui.statusbar.phone.LightBarController @@ -39,6 +40,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.ui.StatusBarUiLayerModule import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore @@ -60,7 +62,14 @@ import dagger.multibindings.IntoMap * ([com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule], * [com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule], etc.). */ -@Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class]) +@Module( + includes = + [ + StatusBarDataLayerModule::class, + StatusBarUiLayerModule::class, + SystemBarUtilsProxyImpl.Module::class, + ] +) interface StatusBarModule { @Binds @@ -169,5 +178,13 @@ interface StatusBarModule { ): StatusBarContentInsetsProvider { return factory.create(context, configurationController, sysUICutoutProvider) } + + @Provides + @SysUISingleton + fun contentInsetsViewModel( + insetsProvider: StatusBarContentInsetsProvider + ): StatusBarContentInsetsViewModel { + return StatusBarContentInsetsViewModel(insetsProvider) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt new file mode 100644 index 000000000000..03c07480ecea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import android.graphics.Rect +import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener +import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onStart + +/** A recommended architecture version of [StatusBarContentInsetsProvider]. */ +class StatusBarContentInsetsViewModel( + private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider +) { + /** Emits the status bar content area for the given rotation in absolute bounds. */ + val contentArea: Flow<Rect> = + conflatedCallbackFlow { + val listener = + object : StatusBarContentInsetsChangedListener { + override fun onStatusBarContentInsetsChanged() { + trySend( + statusBarContentInsetsProvider + .getStatusBarContentAreaForCurrentRotation() + ) + } + } + statusBarContentInsetsProvider.addCallback(listener) + awaitClose { statusBarContentInsetsProvider.removeCallback(listener) } + } + .onStart { + emit(statusBarContentInsetsProvider.getStatusBarContentAreaForCurrentRotation()) + } + .distinctUntilChanged() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt new file mode 100644 index 000000000000..d2dccc49ffd7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.data.repository.PerDisplayStore +import com.android.systemui.display.data.repository.PerDisplayStoreImpl +import com.android.systemui.display.data.repository.SingleDisplayStore +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore +import dagger.Lazy +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** Provides per-display instances of [StatusBarContentInsetsViewModel]. */ +interface StatusBarContentInsetsViewModelStore : PerDisplayStore<StatusBarContentInsetsViewModel> + +@SysUISingleton +class MultiDisplayStatusBarContentInsetsViewModelStore +@Inject +constructor( + @Background backgroundApplicationScope: CoroutineScope, + displayRepository: DisplayRepository, + private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, +) : + StatusBarContentInsetsViewModelStore, + PerDisplayStoreImpl<StatusBarContentInsetsViewModel>( + backgroundApplicationScope, + displayRepository, + ) { + + override fun createInstanceForDisplay(displayId: Int): StatusBarContentInsetsViewModel? { + val insetsProvider = + statusBarContentInsetsProviderStore.forDisplay(displayId) ?: return null + return StatusBarContentInsetsViewModel(insetsProvider) + } + + override val instanceClass = StatusBarContentInsetsViewModel::class.java +} + +@SysUISingleton +class SingleDisplayStatusBarContentInsetsViewModelStore +@Inject +constructor(statusBarContentInsetsViewModel: StatusBarContentInsetsViewModel) : + StatusBarContentInsetsViewModelStore, + PerDisplayStore<StatusBarContentInsetsViewModel> by SingleDisplayStore( + defaultInstance = statusBarContentInsetsViewModel + ) + +@Module +object StatusBarContentInsetsViewModelStoreModule { + @Provides + @SysUISingleton + @IntoMap + @ClassKey(StatusBarContentInsetsViewModelStore::class) + fun storeAsCoreStartable( + multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore> + ): CoreStartable { + return if (StatusBarConnectedDisplays.isEnabled) { + return multiDisplayLazy.get() + } else { + CoreStartable.NOP + } + } + + @Provides + @SysUISingleton + fun store( + singleDisplayLazy: Lazy<SingleDisplayStatusBarContentInsetsViewModelStore>, + multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore>, + ): StatusBarContentInsetsViewModelStore { + return if (StatusBarConnectedDisplays.isEnabled) { + multiDisplayLazy.get() + } else { + singleDisplayLazy.get() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index c7535ec14d5d..eb5a3703bcfb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -112,20 +112,20 @@ constructor( if (StatusBarNotifChips.isEnabled) { applicationScope.launch { statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent.collect { - showPromotedNotificationHeadsUp(it) + onPromotedNotificationChipTapEvent(it) } } } } /** - * Shows the promoted notification with the given [key] as heads-up. + * Updates the heads-up state based on which promoted notification with the given [key] was + * tapped. * * Must be run on the main thread. */ - private fun showPromotedNotificationHeadsUp(key: String) { + private fun onPromotedNotificationChipTapEvent(key: String) { StatusBarNotifChips.assertInNewMode() - mLogger.logShowPromotedNotificationHeadsUp(key) val entry = notifCollection.getEntry(key) if (entry == null) { @@ -135,22 +135,29 @@ constructor( // TODO(b/364653005): Validate that the given key indeed matches a promoted notification, // not just any notification. + val isCurrentlyHeadsUp = mHeadsUpManager.isHeadsUpEntry(entry.key) val posted = PostedEntry( entry, wasAdded = false, wasUpdated = false, - // Force-set this notification to show heads-up. - shouldHeadsUpEver = true, - shouldHeadsUpAgain = true, + // We want the chip to act as a toggle, so if the chip's notification is currently + // showing as heads up, then we should stop showing it. + shouldHeadsUpEver = !isCurrentlyHeadsUp, + shouldHeadsUpAgain = !isCurrentlyHeadsUp, isPinnedByUser = true, - isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key), + isHeadsUpEntry = isCurrentlyHeadsUp, isBinding = isEntryBinding(entry), ) + if (isCurrentlyHeadsUp) { + mLogger.logHidePromotedNotificationHeadsUp(key) + } else { + mLogger.logShowPromotedNotificationHeadsUp(key) + } mExecutor.execute { mPostedEntries[entry.key] = posted - mNotifPromoter.invalidateList("showPromotedNotificationHeadsUp: ${entry.logKey}") + mNotifPromoter.invalidateList("onPromotedNotificationChipTapEvent: ${entry.logKey}") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index e443a0418ffd..5141aa35b041 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -148,6 +148,15 @@ class HeadsUpCoordinatorLogger(private val buffer: LogBuffer, private val verbos ) } + fun logHidePromotedNotificationHeadsUp(key: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "requesting promoted entry to hide heads up: $str1" }, + ) + } + fun logPromotedNotificationForHeadsUpNotFound(key: String) { buffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index 75c7d2d5be98..6140c92369b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import javax.inject.Inject @@ -98,21 +99,39 @@ constructor( } } - /** What [PinnedStatus] does the top row have? */ - private val topPinnedStatus: Flow<PinnedStatus> = + /** What [PinnedStatus] and key does the top row have? */ + private val topPinnedState: Flow<TopPinnedState> = headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> if (rows.isNotEmpty()) { - combine(rows.map { it.pinnedStatus }) { pinnedStatus -> - pinnedStatus.firstOrNull { it.isPinned } ?: PinnedStatus.NotPinned + // For each row, emits a (key, pinnedStatus) pair each time any row's + // `pinnedStatus` changes + val toCombine: List<Flow<Pair<String, PinnedStatus>>> = + rows.map { row -> row.pinnedStatus.map { status -> row.key to status } } + combine(toCombine) { pairs -> + val topPinnedRow: Pair<String, PinnedStatus>? = + pairs.firstOrNull { it.second.isPinned } + if (topPinnedRow != null) { + TopPinnedState.Pinned( + key = topPinnedRow.first, + status = topPinnedRow.second, + ) + } else { + TopPinnedState.NothingPinned + } } } else { - // if the set is empty, there are no flows to combine - flowOf(PinnedStatus.NotPinned) + flowOf(TopPinnedState.NothingPinned) } } /** Are there any pinned heads up rows to display? */ - val hasPinnedRows: Flow<Boolean> = topPinnedStatus.map { it.isPinned } + val hasPinnedRows: Flow<Boolean> = + topPinnedState.map { + when (it) { + is TopPinnedState.Pinned -> it.status.isPinned + is TopPinnedState.NothingPinned -> false + } + } val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { @@ -142,13 +161,25 @@ constructor( } } - /** Emits the pinned notification state as it relates to the status bar. */ - val statusBarHeadsUpState: Flow<PinnedStatus> = - combine(topPinnedStatus, canShowHeadsUp) { topPinnedStatus, canShowHeadsUp -> + /** + * Emits the pinned notification state as it relates to the status bar. Includes both the pinned + * status and key of the notification that's pinned (if there is a pinned notification). + */ + val statusBarHeadsUpState: Flow<TopPinnedState> = + combine(topPinnedState, canShowHeadsUp) { topPinnedState, canShowHeadsUp -> if (canShowHeadsUp) { - topPinnedStatus + topPinnedState } else { - PinnedStatus.NotPinned + TopPinnedState.NothingPinned + } + } + + /** Emits the pinned notification status as it relates to the status bar. */ + val statusBarHeadsUpStatus: Flow<PinnedStatus> = + statusBarHeadsUpState.map { + when (it) { + is TopPinnedState.Pinned -> it.status + is TopPinnedState.NothingPinned -> PinnedStatus.NotPinned } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index 042389f7fde7..fd5973e0ab3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -25,7 +25,6 @@ import android.graphics.drawable.Icon import android.service.notification.StatusBarNotification import android.util.ArrayMap import com.android.app.tracing.traceSection -import com.android.systemui.Flags import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry @@ -132,12 +131,6 @@ private class ActiveNotificationsStoreBuilder( } private fun NotificationEntry.toModel(): ActiveNotificationModel { - val statusBarChipIcon = - if (Flags.statusBarCallChipNotificationIcon()) { - icons.statusBarChipIcon - } else { - null - } val promotedContent = if (PromotedNotificationContentModel.featureFlagEnabled()) { promotedNotificationContentModel @@ -158,7 +151,7 @@ private class ActiveNotificationsStoreBuilder( aodIcon = icons.aodIcon?.sourceIcon, shelfIcon = icons.shelfIcon?.sourceIcon, statusBarIcon = icons.statusBarIcon?.sourceIcon, - statusBarChipIconView = statusBarChipIcon, + statusBarChipIconView = icons.statusBarChipIcon, uid = sbn.uid, packageName = sbn.packageName, contentIntent = sbn.notification.contentIntent, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt new file mode 100644 index 000000000000..51c448adf998 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 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.notification.domain.model + +import com.android.systemui.statusbar.notification.headsup.PinnedStatus + +/** A class representing the state of the top pinned row. */ +sealed interface TopPinnedState { + /** Nothing is pinned. */ + data object NothingPinned : TopPinnedState + + /** + * The top pinned row is a notification with the given key and status. + * + * @property status must have [PinnedStatus.isPinned] as true. + */ + data class Pinned(val key: String, val status: PinnedStatus) : TopPinnedState { + init { + check(status.isPinned) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index b56a838a80a5..31375cc4a03a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -162,9 +162,7 @@ constructor( val sbIcon = iconBuilder.createIconView(entry) sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE val sbChipIcon: StatusBarIconView? - if ( - Flags.statusBarCallChipNotificationIcon() && !StatusBarConnectedDisplays.isEnabled - ) { + if (!StatusBarConnectedDisplays.isEnabled) { sbChipIcon = iconBuilder.createIconView(entry) sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE } else { @@ -186,7 +184,7 @@ constructor( try { setIcon(entry, normalIconDescriptor, sbIcon) - if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) { + if (sbChipIcon != null) { setIcon(entry, normalIconDescriptor, sbChipIcon) } setIcon(entry, sensitiveIconDescriptor, shelfIcon) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java index c6832bc20e6d..cc4be57168cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; -import android.os.Trace; import android.service.notification.NotificationListenerService; import android.util.ArrayMap; import android.util.ArraySet; @@ -29,6 +28,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; @@ -152,8 +152,8 @@ public class NotificationLogger implements StateListener, CoreStartable, mExpansionStateLogger.onVisibilityChanged( mTmpCurrentlyVisibleNotifications, mTmpCurrentlyVisibleNotifications); - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", N); - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", + TrackTracer.instantForGroup("Notifications", "Active", N); + TrackTracer.instantForGroup("Notifications", "Visible", mCurrentlyVisibleNotifications.size()); recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt index 10e67a40ebc9..640d364895ae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt @@ -25,7 +25,7 @@ import android.app.NotificationManager.IMPORTANCE_NONE import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED import android.content.Context import android.graphics.drawable.Drawable -import android.text.TextUtils +import android.text.TextUtils.isEmpty import android.transition.AutoTransition import android.transition.Transition import android.transition.TransitionManager @@ -37,13 +37,10 @@ import android.widget.LinearLayout import android.widget.Switch import android.widget.TextView import com.android.settingslib.Utils - import com.android.systemui.res.R import com.android.systemui.util.Assert -/** - * Half-shelf for notification channel controls - */ +/** Half-shelf for notification channel controls */ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { lateinit var controller: ChannelEditorDialogController var appIcon: Drawable? = null @@ -84,23 +81,21 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a val transition = AutoTransition() transition.duration = 200 - transition.addListener(object : Transition.TransitionListener { - override fun onTransitionEnd(p0: Transition?) { - notifySubtreeAccessibilityStateChangedIfNeeded() - } + transition.addListener( + object : Transition.TransitionListener { + override fun onTransitionEnd(p0: Transition?) { + notifySubtreeAccessibilityStateChangedIfNeeded() + } - override fun onTransitionResume(p0: Transition?) { - } + override fun onTransitionResume(p0: Transition?) {} - override fun onTransitionPause(p0: Transition?) { - } + override fun onTransitionPause(p0: Transition?) {} - override fun onTransitionCancel(p0: Transition?) { - } + override fun onTransitionCancel(p0: Transition?) {} - override fun onTransitionStart(p0: Transition?) { + override fun onTransitionStart(p0: Transition?) {} } - }) + ) TransitionManager.beginDelayedTransition(this, transition) // Remove any rows @@ -130,8 +125,9 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a private fun updateAppControlRow(enabled: Boolean) { appControlRow.iconView.setImageDrawable(appIcon) - appControlRow.channelName.text = context.resources - .getString(R.string.notification_channel_dialog_title, appName) + val title = context.resources.getString(R.string.notification_channel_dialog_title, appName) + appControlRow.channelName.text = title + appControlRow.switch.contentDescription = title appControlRow.switch.isChecked = enabled appControlRow.switch.setOnCheckedChangeListener { _, b -> controller.proposeSetAppNotificationsEnabled(b) @@ -164,8 +160,8 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { var gentle = false init { - highlightColor = Utils.getColorAttrDefaultColor( - context, android.R.attr.colorControlHighlight) + highlightColor = + Utils.getColorAttrDefaultColor(context, android.R.attr.colorControlHighlight) } var channel: NotificationChannel? = null @@ -182,17 +178,16 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { switch = requireViewById(R.id.toggle) switch.setOnCheckedChangeListener { _, b -> channel?.let { - controller.proposeEditForChannel(it, - if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) - else IMPORTANCE_NONE) + controller.proposeEditForChannel( + it, + if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) else IMPORTANCE_NONE, + ) } } setOnClickListener { switch.toggle() } } - /** - * Play an animation that highlights this row - */ + /** Play an animation that highlights this row */ fun playHighlight() { // Use 0 for the start value because our background is given to us by our parent val fadeInLoop = ValueAnimator.ofObject(ArgbEvaluator(), 0, highlightColor) @@ -211,17 +206,21 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { channelName.text = nc.name ?: "" - nc.group?.let { groupId -> - channelDescription.text = controller.groupNameForId(groupId) - } + nc.group?.let { groupId -> channelDescription.text = controller.groupNameForId(groupId) } - if (nc.group == null || TextUtils.isEmpty(channelDescription.text)) { + if (nc.group == null || isEmpty(channelDescription.text)) { channelDescription.visibility = View.GONE } else { channelDescription.visibility = View.VISIBLE } switch.isChecked = nc.importance != IMPORTANCE_NONE + switch.contentDescription = + if (isEmpty(channelDescription.text)) { + channelName.text + } else { + "${channelName.text} ${channelDescription.text}" + } } private fun updateImportance() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 3866f79ed701..95604c113a15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -1267,6 +1267,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } if (mExpandedWhenPinned) { return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); + } else if (android.app.Flags.compactHeadsUpNotification() + && getShowingLayout().isHUNCompact()) { + return getHeadsUpHeight(); } else if (atLeastMinHeight) { return Math.max(getCollapsedHeight(), getHeadsUpHeight()); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt new file mode 100644 index 000000000000..e27ff7d6746b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 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.notification.row + +import android.content.Context +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.drawable.Drawable + +/** + * A background style for smarter-smart-actions. + * + * TODO(b/383567383) implement final UX + */ +class MagicActionBackgroundDrawable(context: Context) : Drawable() { + + private var _alpha: Int = 255 + private var _colorFilter: ColorFilter? = null + private val paint = + Paint().apply { + color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) + } + + override fun draw(canvas: Canvas) { + canvas.drawRect(bounds, paint) + } + + override fun setAlpha(alpha: Int) { + _alpha = alpha + invalidateSelf() + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + _colorFilter = colorFilter + invalidateSelf() + } + + override fun getOpacity(): Int = PixelFormat.TRANSLUCENT +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 7c44eae6c0b8..70e27a981b49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.notification.row; -import static android.app.Flags.notificationsRedesignTemplates; - import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; @@ -481,16 +479,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder logger.logAsyncTaskProgress(entryForLogging, "creating low-priority group summary remote view"); result.mNewMinimizedGroupHeaderView = - builder.makeLowPriorityContentView(/* useRegularSubtext = */ true, - /* highlightExpander = */ notificationsRedesignTemplates()); + builder.makeLowPriorityContentView(true /* useRegularSubtext */); } } setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider); result.packageContext = packageContext; result.headsUpStatusBarText = builder.getHeadsUpStatusBarText( - /* showingPublic = */ false); + false /* showingPublic */); result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( - /* showingPublic = */ true); + true /* showingPublic */); return result; }); @@ -1139,8 +1136,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private static RemoteViews createContentView(Notification.Builder builder, boolean isMinimized, boolean useLarge) { if (isMinimized) { - return builder.makeLowPriorityContentView(/* useRegularSubtext = */ false, - /* highlightExpander = */ false); + return builder.makeLowPriorityContentView(false /* useRegularSubtext */); } return builder.createContentView(useLarge); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 786d7d9ea0f3..0d2998174121 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -207,6 +207,8 @@ public class NotificationContentView extends FrameLayout implements Notification private boolean mContentAnimating; private UiEventLogger mUiEventLogger; + private boolean mIsHUNCompact; + public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mHybridGroupManager = new HybridGroupManager(getContext()); @@ -543,6 +545,7 @@ public class NotificationContentView extends FrameLayout implements Notification if (child == null) { mHeadsUpChild = null; mHeadsUpWrapper = null; + mIsHUNCompact = false; if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) { mTransformationStartVisibleType = VISIBLE_TYPE_NONE; } @@ -556,8 +559,9 @@ public class NotificationContentView extends FrameLayout implements Notification mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); - if (Flags.compactHeadsUpNotification() - && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper) { + mIsHUNCompact = Flags.compactHeadsUpNotification() + && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper; + if (mIsHUNCompact) { logCompactHUNShownEvent(); } @@ -902,6 +906,10 @@ public class NotificationContentView extends FrameLayout implements Notification } } + public boolean isHUNCompact() { + return mIsHUNCompact; + } + private boolean isGroupExpanded() { return mContainingNotification.isGroupExpanded(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index ae9b69c8f6bf..c619b17f1ad8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.row import android.annotation.SuppressLint -import android.app.Flags.notificationsRedesignTemplates import android.app.Notification import android.app.Notification.MessagingStyle import android.content.Context @@ -888,10 +887,7 @@ constructor( entryForLogging, "creating low-priority group summary remote view", ) - builder.makeLowPriorityContentView( - /* useRegularSubtext = */ true, - /* highlightExpander = */ notificationsRedesignTemplates(), - ) + builder.makeLowPriorityContentView(true /* useRegularSubtext */) } else null NewRemoteViews( contracted = contracted, @@ -1661,10 +1657,7 @@ constructor( useLarge: Boolean, ): RemoteViews { return if (isMinimized) { - builder.makeLowPriorityContentView( - /* useRegularSubtext = */ false, - /* highlightExpander = */ false, - ) + builder.makeLowPriorityContentView(false /* useRegularSubtext */) } else builder.createContentView(useLarge) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index e477c7430262..8e48065d9d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -568,8 +568,7 @@ public class NotificationChildrenContainer extends ViewGroup builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); } - header = builder.makeLowPriorityContentView(true /* useRegularSubtext */, - notificationsRedesignTemplates() /* highlightExpander */); + header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); if (mMinimizedGroupHeader == null) { mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(), this); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index e752e6581421..c717e3b229be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -35,6 +35,8 @@ import static com.android.systemui.statusbar.notification.stack.StackStateAnimat import android.animation.ObjectAnimator; import android.content.res.Configuration; import android.graphics.Point; +import android.graphics.RenderEffect; +import android.graphics.Shader; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; @@ -1237,6 +1239,22 @@ public class NotificationStackScrollLayoutController implements Dumpable { updateAlpha(); } + /** + * Applies a blur effect to the view. + * + * @param blurRadius Radius of blur + */ + public void setBlurRadius(float blurRadius) { + if (blurRadius > 0.0f) { + mView.setRenderEffect(RenderEffect.createBlurEffect( + blurRadius, + blurRadius, + Shader.TileMode.CLAMP)); + } else { + mView.setRenderEffect(null); + } + } + private void updateAlpha() { if (mView != null) { mView.setAlpha(Math.min(Math.min(mMaxAlphaFromView, mMaxAlphaForKeyguard), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt index 53749ff24394..c8c798d00a06 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.stack.ui.view -import android.os.Trace import android.service.notification.NotificationListenerService import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.TrackTracer import com.android.internal.statusbar.IStatusBarService import com.android.internal.statusbar.NotificationVisibility import com.android.systemui.dagger.SysUISingleton @@ -183,8 +183,8 @@ constructor( maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount) updateExpansionStates(newlyVisible, noLongerVisible) - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", activeNotifCount) - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", newVisibilities.size) + TrackTracer.instantForGroup("Notifications", "Active", activeNotifCount) + TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size) lastLoggedVisibilities.clear() lastLoggedVisibilities.putAll(newVisibilities) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 0b2b84e60f4b..3ea4d488357d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -179,6 +179,10 @@ constructor( } } + if (Flags.bouncerUiRevamp()) { + launch { viewModel.blurRadius.collect { controller.setBlurRadius(it) } } + } + if (communalSettingsInteractor.isCommunalFlagEnabled()) { launch { viewModel.glanceableHubAlpha.collect { 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 fc8c70fb8e9a..f0455fc3a22b 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 @@ -42,6 +42,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel @@ -154,6 +155,7 @@ constructor( private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, private val primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, + private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>, aodBurnInViewModel: AodBurnInViewModel, private val communalSceneInteractor: CommunalSceneInteractor, // Lazy because it's only used in the SceneContainer + Dual Shade configuration. @@ -562,7 +564,7 @@ constructor( lockscreenToDreamingTransitionViewModel.lockscreenAlpha, lockscreenToGoneTransitionViewModel.notificationAlpha(viewState), lockscreenToOccludedTransitionViewModel.lockscreenAlpha, - lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha, + lockscreenToPrimaryBouncerTransitionViewModel.notificationAlpha, alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha, occludedToAodTransitionViewModel.lockscreenAlpha, occludedToGoneTransitionViewModel.notificationAlpha(viewState), @@ -626,6 +628,12 @@ constructor( .dumpWhileCollecting("keyguardAlpha") } + val blurRadius = + primaryBouncerTransitions + .map { transition -> transition.notificationBlurRadius } + .merge() + .dumpWhileCollecting("blurRadius") + /** * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or * DREAMING<->GLANCEABLE_HUB transition or idle on the hub. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 6f29f618ee0d..afc5bc67c0a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -750,7 +750,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType) ); } else if (biometricSourceType == BiometricSourceType.FINGERPRINT - && mUpdateMonitor.isUdfpsSupported()) { + && mUpdateMonitor.isOpticalUdfpsSupported()) { long currUptimeMillis = mSystemClock.uptimeMillis(); if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) { mNumConsecutiveFpFailures += 1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 324db79a4078..d43fed0cbf59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -43,6 +43,7 @@ import android.view.animation.Interpolator; import androidx.annotation.FloatRange; import androidx.annotation.Nullable; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor.GradientColors; import com.android.internal.graphics.ColorUtils; @@ -554,7 +555,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump final ScrimState oldState = mState; mState = state; - Trace.traceCounter(Trace.TRACE_TAG_APP, "scrim_state", mState.ordinal()); + TrackTracer.instantForGroup("scrim", "state", mState.ordinal()); if (mCallback != null) { mCallback.onCancelled(); @@ -1279,10 +1280,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump tint = getDebugScrimTint(scrimView); } - Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_alpha", + TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_alpha", (int) (alpha * 255)); - - Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_tint", + TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_tint", Color.alpha(tint)); scrimView.setTint(tint); if (!mIsBouncerToGoneTransitionRunning) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 198859a9013d..8dcb66312558 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.phone; import android.graphics.Color; -import android.os.Trace; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.systemui.dock.DockManager; import com.android.systemui.res.R; import com.android.systemui.scrim.ScrimView; @@ -425,11 +425,11 @@ public enum ScrimState { tint = scrim == mScrimInFront ? ScrimController.DEBUG_FRONT_TINT : ScrimController.DEBUG_BEHIND_TINT; } - Trace.traceCounter(Trace.TRACE_TAG_APP, + TrackTracer.instantForGroup("scrim", scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha", (int) (alpha * 255)); - Trace.traceCounter(Trace.TRACE_TAG_APP, + TrackTracer.instantForGroup("scrim", scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint", Color.alpha(tint)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index c31e34c50b06..e622d8f52894 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -81,6 +81,7 @@ import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; @@ -142,6 +143,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private StatusBarVisibilityModel mLastModifiedVisibility = StatusBarVisibilityModel.createDefaultModel(); private DarkIconManager mDarkIconManager; + private HomeStatusBarViewModel mHomeStatusBarViewModel; + private final HomeStatusBarComponent.Factory mHomeStatusBarComponentFactory; private final CommandQueue mCommandQueue; private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger; @@ -151,8 +154,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final ShadeExpansionStateManager mShadeExpansionStateManager; private final StatusBarIconController mStatusBarIconController; private final CarrierConfigTracker mCarrierConfigTracker; - private final HomeStatusBarViewModel mHomeStatusBarViewModel; private final HomeStatusBarViewBinder mHomeStatusBarViewBinder; + private final HomeStatusBarViewModelFactory mHomeStatusBarViewModelFactory; private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; private final DarkIconManager.Factory mDarkIconManagerFactory; private final SecureSettings mSecureSettings; @@ -256,7 +259,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue ShadeExpansionStateManager shadeExpansionStateManager, StatusBarIconController statusBarIconController, DarkIconManager.Factory darkIconManagerFactory, - HomeStatusBarViewModel homeStatusBarViewModel, + HomeStatusBarViewModelFactory homeStatusBarViewModelFactory, HomeStatusBarViewBinder homeStatusBarViewBinder, StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager, KeyguardStateController keyguardStateController, @@ -281,7 +284,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mAnimationScheduler = animationScheduler; mShadeExpansionStateManager = shadeExpansionStateManager; mStatusBarIconController = statusBarIconController; - mHomeStatusBarViewModel = homeStatusBarViewModel; + mHomeStatusBarViewModelFactory = homeStatusBarViewModelFactory; mHomeStatusBarViewBinder = homeStatusBarViewBinder; mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager; mDarkIconManagerFactory = darkIconManagerFactory; @@ -410,6 +413,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mCarrierConfigTracker.addCallback(mCarrierConfigCallback); mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener); + mHomeStatusBarViewModel = mHomeStatusBarViewModelFactory.create(displayId); mHomeStatusBarViewBinder.bind( view.getContext().getDisplayId(), mStatusBar, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index c57cede754d3..f56c2d5dc5e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.phone.ongoingcall import android.app.ActivityManager import android.app.IActivityManager -import android.app.Notification -import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.app.PendingIntent import android.app.UidObserver import android.content.Context @@ -44,9 +42,6 @@ import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.view.ChipChronometer import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType @@ -60,7 +55,9 @@ import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -/** A controller to handle the ongoing call chip in the collapsed status bar. +/** + * A controller to handle the ongoing call chip in the collapsed status bar. + * * @deprecated Use [OngoingCallInteractor] instead, which follows recommended architecture patterns */ @Deprecated("Use OngoingCallInteractor instead") @@ -71,7 +68,6 @@ constructor( @Application private val scope: CoroutineScope, private val context: Context, private val ongoingCallRepository: OngoingCallRepository, - private val notifCollection: CommonNotifCollection, private val activeNotificationsInteractor: ActiveNotificationsInteractor, private val systemClock: SystemClock, private val activityStarter: ActivityStarter, @@ -90,105 +86,24 @@ constructor( private val mListeners: MutableList<OngoingCallListener> = mutableListOf() private val uidObserver = CallAppUidObserver() - private val notifListener = - object : NotifCollectionListener { - // Temporary workaround for b/178406514 for testing purposes. - // - // b/178406514 means that posting an incoming call notif then updating it to an ongoing - // call notif does not work (SysUI never receives the update). This workaround allows us - // to trigger the ongoing call chip when an ongoing call notif is *added* rather than - // *updated*, allowing us to test the chip. - // - // TODO(b/183229367): Remove this function override when b/178406514 is fixed. - override fun onEntryAdded(entry: NotificationEntry) { - onEntryUpdated(entry, true) - } - - override fun onEntryUpdated(entry: NotificationEntry) { - StatusBarUseReposForCallChip.assertInLegacyMode() - // We have a new call notification or our existing call notification has been - // updated. - // TODO(b/183229367): This likely won't work if you take a call from one app then - // switch to a call from another app. - if ( - callNotificationInfo == null && isCallNotification(entry) || - (entry.sbn.key == callNotificationInfo?.key) - ) { - val newOngoingCallInfo = - CallNotificationInfo( - entry.sbn.key, - entry.sbn.notification.getWhen(), - // In this old listener pattern, we don't have access to the - // notification icon. - notificationIconView = null, - entry.sbn.notification.contentIntent, - entry.sbn.uid, - entry.sbn.notification.extras.getInt( - Notification.EXTRA_CALL_TYPE, - -1, - ) == CALL_TYPE_ONGOING, - statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false, - ) - if (newOngoingCallInfo == callNotificationInfo) { - return - } - - callNotificationInfo = newOngoingCallInfo - if (newOngoingCallInfo.isOngoing) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = newOngoingCallInfo.key }, - { "Call notif *is* ongoing -> showing chip. key=$str1" }, - ) - updateChip() - } else { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = newOngoingCallInfo.key }, - { "Call notif not ongoing -> hiding chip. key=$str1" }, - ) - removeChip() - } - } - } - - override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (entry.sbn.key == callNotificationInfo?.key) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = entry.sbn.key }, - { "Call notif removed -> hiding chip. key=$str1" }, - ) - removeChip() - } - } - } override fun start() { - if (StatusBarChipsModernization.isEnabled) - return + if (StatusBarChipsModernization.isEnabled) return dumpManager.registerDumpable(this) - if (Flags.statusBarUseReposForCallChip()) { - scope.launch { - // Listening to [ActiveNotificationsInteractor] instead of using - // [NotifCollectionListener#onEntryUpdated] is better for two reasons: - // 1. ActiveNotificationsInteractor automatically filters the notification list to - // just notifications for the current user, which ensures we don't show a call chip - // for User 1's call while User 2 is active (see b/328584859). - // 2. ActiveNotificationsInteractor only emits notifications that are currently - // present in the shade, which means we know we've already inflated the icon that we - // might use for the call chip (see b/354930838). - activeNotificationsInteractor.ongoingCallNotification.collect { - updateInfoFromNotifModel(it) - } + scope.launch { + // Listening to [ActiveNotificationsInteractor] instead of using + // [NotifCollectionListener#onEntryUpdated] is better for two reasons: + // 1. ActiveNotificationsInteractor automatically filters the notification list to + // just notifications for the current user, which ensures we don't show a call chip + // for User 1's call while User 2 is active (see b/328584859). + // 2. ActiveNotificationsInteractor only emits notifications that are currently + // present in the shade, which means we know we've already inflated the icon that we + // might use for the call chip (see b/354930838). + activeNotificationsInteractor.ongoingCallNotification.collect { + updateInfoFromNotifModel(it) } - } else { - notifCollection.addCollectionListener(notifListener) } scope.launch { @@ -244,21 +159,12 @@ constructor( logger.log( TAG, LogLevel.DEBUG, - { - bool1 = Flags.statusBarCallChipNotificationIcon() - bool2 = currentInfo.notificationIconView != null - }, - { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" }, + { bool1 = currentInfo.notificationIconView != null }, + { "Creating OngoingCallModel.InCall. hasIcon=$bool1" }, ) - val icon = - if (Flags.statusBarCallChipNotificationIcon()) { - currentInfo.notificationIconView - } else { - null - } return OngoingCallModel.InCall( startTimeMs = currentInfo.callStartTime, - notificationIconView = icon, + notificationIconView = currentInfo.notificationIconView, intent = currentInfo.intent, notificationKey = currentInfo.key, ) @@ -597,8 +503,4 @@ constructor( } } -private fun isCallNotification(entry: NotificationEntry): Boolean { - return entry.sbn.notification.isStyle(Notification.CallStyle::class.java) -} - private const val TAG = OngoingCallRepository.TAG diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt deleted file mode 100644 index 4bdd90ebff3e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.ongoingcall - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the status bar use repos for call chip flag state. */ -@Suppress("NOTHING_TO_INLINE") -object StatusBarUseReposForCallChip { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.statusBarUseReposForCallChip() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 96666d83b39b..c71162a22d2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -56,8 +56,8 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinderImpl -import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel -import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl.HomeStatusBarViewModelFactoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher @@ -148,7 +148,9 @@ abstract class StatusBarPipelineModule { abstract fun bindCarrierConfigStartable(impl: CarrierConfigCoreStartable): CoreStartable @Binds - abstract fun homeStatusBarViewModel(impl: HomeStatusBarViewModelImpl): HomeStatusBarViewModel + abstract fun homeStatusBarViewModelFactory( + impl: HomeStatusBarViewModelFactoryImpl + ): HomeStatusBarViewModelFactory @Binds abstract fun homeStatusBarViewBinder(impl: HomeStatusBarViewBinderImpl): HomeStatusBarViewBinder diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 0dd7c8499861..31d6d86d1b37 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernizat import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch /** @@ -107,10 +108,9 @@ constructor( } if (NotificationsLiveDataStoreRefactor.isEnabled) { - val displayId = view.display.displayId val lightsOutView: View = view.requireViewById(R.id.notification_lights_out) launch { - viewModel.areNotificationsLightsOut(displayId).collect { show -> + viewModel.areNotificationsLightsOut.collect { show -> animateLightsOutView(lightsOutView, show) } } @@ -121,22 +121,26 @@ constructor( !StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled ) { - val primaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_primary) + val primaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_primary) + ) launch { viewModel.primaryOngoingActivityChip.collect { primaryChipModel -> OngoingActivityChipBinder.bind( primaryChipModel, - primaryChipView, + primaryChipViewBinding, iconViewStore, ) if (StatusBarRootModernization.isEnabled) { when (primaryChipModel) { is OngoingActivityChipModel.Shown -> - primaryChipView.show(shouldAnimateChange = true) + primaryChipViewBinding.rootView.show( + shouldAnimateChange = true + ) is OngoingActivityChipModel.Hidden -> - primaryChipView.hide( + primaryChipViewBinding.rootView.hide( state = View.GONE, shouldAnimateChange = primaryChipModel.shouldAnimate, ) @@ -167,28 +171,34 @@ constructor( StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled ) { - val primaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_primary) - val secondaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_secondary) + // Create view bindings here so we don't keep re-fetching child views each time + // the chip model changes. + val primaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_primary) + ) + val secondaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_secondary) + ) launch { - viewModel.ongoingActivityChips.collect { chips -> + viewModel.ongoingActivityChips.collectLatest { chips -> OngoingActivityChipBinder.bind( chips.primary, - primaryChipView, + primaryChipViewBinding, iconViewStore, ) - // TODO(b/364653005): Don't show the secondary chip if there isn't - // enough space for it. OngoingActivityChipBinder.bind( chips.secondary, - secondaryChipView, + secondaryChipViewBinding, iconViewStore, ) if (StatusBarRootModernization.isEnabled) { - primaryChipView.adjustVisibility(chips.primary.toVisibilityModel()) - secondaryChipView.adjustVisibility( + primaryChipViewBinding.rootView.adjustVisibility( + chips.primary.toVisibilityModel() + ) + secondaryChipViewBinding.rootView.adjustVisibility( chips.secondary.toVisibilityModel() ) } else { @@ -201,6 +211,18 @@ constructor( shouldAnimate = true, ) } + + viewModel.contentArea.collect { _ -> + OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions( + primaryChipViewBinding, + viewModel.ongoingActivityChips.value.primary, + ) + OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding, + viewModel.ongoingActivityChips.value.secondary, + ) + view.requestLayout() + } } } } @@ -218,7 +240,7 @@ constructor( StatusBarOperatorNameViewBinder.bind( operatorNameView, viewModel.operatorNameViewModel, - viewModel::areaTint, + viewModel.areaTint, ) launch { viewModel.shouldShowOperatorNameView.collect { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt index b7744d34560d..5dd76f4434f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt @@ -32,19 +32,16 @@ object StatusBarOperatorNameViewBinder { fun bind( operatorFrameView: View, viewModel: StatusBarOperatorNameViewModel, - areaTint: (Int) -> Flow<StatusBarTintColor>, + areaTint: Flow<StatusBarTintColor>, ) { operatorFrameView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - val displayId = operatorFrameView.display.displayId - val operatorNameText = operatorFrameView.requireViewById<TextView>(R.id.operator_name) launch { viewModel.operatorName.collect { operatorNameText.text = it } } launch { - val tint = areaTint(displayId) - tint.collect { statusBarTintColors -> + areaTint.collect { statusBarTintColors -> operatorNameText.setTextColor( statusBarTintColors.tint(operatorNameText.viewBoundsOnScreen()) ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index f286a1a148fa..71e19188f309 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,9 +34,11 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.theme.PlatformTheme import com.android.keyguard.AlphaOptimizedLinearLayout import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips @@ -53,13 +56,14 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarIco import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory import javax.inject.Inject /** Factory to simplify the dependency management for [StatusBarRoot] */ class StatusBarRootFactory @Inject constructor( - private val homeStatusBarViewModel: HomeStatusBarViewModel, + private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory, private val homeStatusBarViewBinder: HomeStatusBarViewBinder, private val notificationIconsBinder: NotificationIconContainerStatusBarViewBinder, private val darkIconManagerFactory: DarkIconManager.Factory, @@ -70,13 +74,14 @@ constructor( ) { fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView { val composeView = ComposeView(root.context) + val displayId = root.context.displayId val darkIconDispatcher = darkIconDispatcherStore.forDisplay(root.context.displayId) ?: return composeView composeView.apply { setContent { StatusBarRoot( parent = root, - statusBarViewModel = homeStatusBarViewModel, + statusBarViewModel = homeStatusBarViewModelFactory.create(displayId), statusBarViewBinder = homeStatusBarViewBinder, notificationIconsBinder = notificationIconsBinder, darkIconManagerFactory = darkIconManagerFactory, @@ -158,6 +163,45 @@ fun StatusBarRoot( darkIconDispatcher, ) iconController.addIconGroup(darkIconManager) + + if (StatusBarChipsModernization.isEnabled) { + val startSideExceptHeadsUp = + phoneStatusBarView.requireViewById<LinearLayout>( + R.id.status_bar_start_side_except_heads_up + ) + + val composeView = + ComposeView(context).apply { + layoutParams = + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + ) + + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + + setContent { + PlatformTheme { + val chips by + statusBarViewModel.ongoingActivityChips + .collectAsStateWithLifecycle() + OngoingActivityChips(chips = chips) + } + } + } + + // Add the composable container for ongoingActivityChips before the + // notification_icon_area to maintain the same ordering for ongoing activity + // chips in the status bar layout. + val notificationIconAreaIndex = + startSideExceptHeadsUp.indexOfChild( + startSideExceptHeadsUp.findViewById(R.id.notification_icon_area) + ) + startSideExceptHeadsUp.addView(composeView, notificationIconAreaIndex) + } + HomeStatusBarIconBlockListBinder.bind( statusIconContainer, darkIconManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index c9cc17389c17..d9d9a29ee2b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.annotation.ColorInt import android.graphics.Rect import android.view.View -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.headsup.PinnedStatus @@ -53,7 +53,9 @@ import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteracto import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel -import javax.inject.Inject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -118,6 +120,7 @@ interface HomeStatusBarViewModel { val shouldShowOperatorNameView: Flow<Boolean> val isClockVisible: Flow<VisibilityModel> val isNotificationIconContainerVisible: Flow<VisibilityModel> + /** * Pair of (system info visibility, event animation state). The animation state can be used to * respond to the system event chip animations. In all cases, system info visibility correctly @@ -128,6 +131,9 @@ interface HomeStatusBarViewModel { /** Which icons to block from the home status bar */ val iconBlockList: Flow<List<String>> + /** This status bar's current content area for the given rotation in absolute bounds. */ + val contentArea: Flow<Rect> + /** * 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 @@ -137,13 +143,13 @@ interface HomeStatusBarViewModel { * whether there are notifications when the device is in * [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE]. */ - fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> + val areNotificationsLightsOut: Flow<Boolean> /** - * Given a displayId, returns a flow of [StatusBarTintColor], a functional interface that will - * allow a view to calculate its correct tint depending on location + * A flow of [StatusBarTintColor], a functional interface that will allow a view to calculate + * its correct tint depending on location */ - fun areaTint(displayId: Int): Flow<StatusBarTintColor> + val areaTint: Flow<StatusBarTintColor> /** Models the current visibility for a specific child view of status bar. */ data class VisibilityModel( @@ -157,17 +163,22 @@ interface HomeStatusBarViewModel { val baseVisibility: VisibilityModel, val animationState: SystemEventAnimationState, ) + + /** Interface for the assisted factory, to allow for providing a fake in tests */ + interface HomeStatusBarViewModelFactory { + fun create(displayId: Int): HomeStatusBarViewModel + } } -@SysUISingleton class HomeStatusBarViewModelImpl -@Inject +@AssistedInject constructor( + @Assisted thisDisplayId: Int, homeStatusBarInteractor: HomeStatusBarInteractor, homeStatusBarIconBlockListInteractor: HomeStatusBarIconBlockListInteractor, - private val lightsOutInteractor: LightsOutInteractor, - private val notificationsInteractor: ActiveNotificationsInteractor, - private val darkIconInteractor: DarkIconInteractor, + lightsOutInteractor: LightsOutInteractor, + notificationsInteractor: ActiveNotificationsInteractor, + darkIconInteractor: DarkIconInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, keyguardInteractor: KeyguardInteractor, @@ -178,6 +189,7 @@ constructor( ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel, animations: SystemStatusEventAnimationInteractor, + statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore, @Application coroutineScope: CoroutineScope, ) : HomeStatusBarViewModel { override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> = @@ -211,22 +223,22 @@ constructor( } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = false) - override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = + override val areNotificationsLightsOut: Flow<Boolean> = if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) { emptyFlow() } else { combine( notificationsInteractor.areAnyNotificationsPresent, - lightsOutInteractor.isLowProfile(displayId) ?: flowOf(false), + lightsOutInteractor.isLowProfile(thisDisplayId) ?: flowOf(false), ) { hasNotifications, isLowProfile -> hasNotifications && isLowProfile } .distinctUntilChanged() } - override fun areaTint(displayId: Int): Flow<StatusBarTintColor> = + override val areaTint: Flow<StatusBarTintColor> = darkIconInteractor - .darkState(displayId) + .darkState(thisDisplayId) .map { (areas: Collection<Rect>, tint: Int) -> StatusBarTintColor { viewBounds: Rect -> if (DarkIconDispatcher.isInAreas(areas, viewBounds)) { @@ -283,11 +295,12 @@ constructor( override val shouldShowOperatorNameView: Flow<Boolean> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpState, + headsUpNotificationInteractor.statusBarHeadsUpStatus, homeStatusBarInteractor.visibilityViaDisableFlags, homeStatusBarInteractor.shouldShowOperatorName, - ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags, shouldShowOperator -> - val hideForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags, shouldShowOperator + -> + val hideForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem shouldStatusBarBeVisible && !hideForHeadsUp && visibilityViaDisableFlags.isSystemInfoAllowed && @@ -297,10 +310,10 @@ constructor( override val isClockVisible: Flow<VisibilityModel> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpState, + headsUpNotificationInteractor.statusBarHeadsUpStatus, homeStatusBarInteractor.visibilityViaDisableFlags, - ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags -> - val hideClockForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags -> + val hideClockForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem val showClock = shouldStatusBarBeVisible && visibilityViaDisableFlags.isClockAllowed && @@ -356,6 +369,10 @@ constructor( override val iconBlockList: Flow<List<String>> = homeStatusBarIconBlockListInteractor.iconBlockList + override val contentArea: Flow<Rect> = + statusBarContentInsetsViewModelStore.forDisplay(thisDisplayId)?.contentArea + ?: flowOf(Rect(0, 0, 0, 0)) + @View.Visibility private fun Boolean.toVisibleOrGone(): Int { return if (this) View.VISIBLE else View.GONE @@ -364,6 +381,13 @@ constructor( // Similar to the above, but uses INVISIBLE in place of GONE @View.Visibility private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE + + /** Inject this to create the display-dependent view model */ + @AssistedFactory + interface HomeStatusBarViewModelFactoryImpl : + HomeStatusBarViewModel.HomeStatusBarViewModelFactory { + override fun create(displayId: Int): HomeStatusBarViewModelImpl + } } /** Lookup the color for a given view in the status bar */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index 56c9e9abbc36..cb26679434ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -41,6 +41,7 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.Button +import com.android.systemui.Flags import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.shared.system.ActivityManagerWrapper @@ -52,6 +53,7 @@ import com.android.systemui.statusbar.SmartReplyController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions @@ -400,6 +402,15 @@ constructor( .apply { text = action.title + if (Flags.notificationMagicActionsTreatment()) { + if ( + smartActions.fromAssistant && + action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false) + ) { + background = MagicActionBackgroundDrawable(parent.context) + } + } + // We received the Icon from the application - so use the Context of the application // to // reference icon resources. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt new file mode 100644 index 000000000000..8e81d78d60f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 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.ui + +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStoreModule +import dagger.Module + +@Module(includes = [StatusBarContentInsetsViewModelStoreModule::class]) +object StatusBarUiLayerModule diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt index 6175ea190697..a98a9e0c16d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt @@ -60,7 +60,7 @@ constructor( private val showingHeadsUpStatusBar: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { - headsUpNotificationInteractor.statusBarHeadsUpState.map { it.isPinned } + headsUpNotificationInteractor.statusBarHeadsUpStatus.map { it.isPinned } } else { flowOf(false) } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 4abbbacd800b..bac2c47f51c7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -24,13 +24,15 @@ import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INACTIVE +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING @@ -97,8 +99,9 @@ import org.mockito.kotlin.clearInvocations class ClockEventControllerTest : SysuiTestCase() { private val kosmos = testKosmos() - private val zenModeRepository = kosmos.fakeZenModeRepository private val testScope = kosmos.testScope + private val zenModeRepository by lazy { kosmos.fakeZenModeRepository } + private val zenModeInteractor by lazy { kosmos.zenModeInteractor } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -106,7 +109,6 @@ class ClockEventControllerTest : SysuiTestCase() { private lateinit var repository: FakeKeyguardRepository private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG)) private lateinit var underTest: ClockEventController - private lateinit var dndModeId: String @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @@ -156,17 +158,12 @@ class ClockEventControllerTest : SysuiTestCase() { whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null)) whenever(userTracker.userId).thenReturn(1) - dndModeId = MANUAL_DND_INACTIVE.id - zenModeRepository.addMode(MANUAL_DND_INACTIVE) + repository = kosmos.fakeKeyguardRepository - repository = FakeKeyguardRepository() - - val withDeps = KeyguardInteractorFactory.create(repository = repository) - - withDeps.featureFlags.apply { set(Flags.REGION_SAMPLING, false) } + kosmos.fakeFeatureFlagsClassic.set(Flags.REGION_SAMPLING, false) underTest = ClockEventController( - withDeps.keyguardInteractor, + kosmos.keyguardInteractor, keyguardTransitionInteractor, broadcastDispatcher, batteryController, @@ -177,9 +174,9 @@ class ClockEventControllerTest : SysuiTestCase() { mainExecutor, bgExecutor, clockBuffers, - withDeps.featureFlags, + kosmos.fakeFeatureFlagsClassic, zenModeController, - kosmos.zenModeInteractor, + zenModeInteractor, userTracker, ) underTest.clock = clock @@ -504,7 +501,7 @@ class ClockEventControllerTest : SysuiTestCase() { runCurrent() clearInvocations(events) - zenModeRepository.activateMode(dndModeId) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() verify(events) @@ -512,7 +509,7 @@ class ClockEventControllerTest : SysuiTestCase() { eq(ZenData(ZenMode.IMPORTANT_INTERRUPTIONS, R.string::dnd_is_on.name)) ) - zenModeRepository.deactivateMode(dndModeId) + zenModeRepository.deactivateMode(MANUAL_DND) runCurrent() verify(events).onZenDataChanged(eq(ZenData(ZenMode.OFF, R.string::dnd_is_off.name))) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt index 5d622eaeb1aa..e61acc4e1d0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -224,6 +225,30 @@ class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() { } } + @Test + fun testOnActionIconClick_audioSharingMediaDevice_stopBroadcast() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + actionInteractorImpl.onActionIconClick(inAudioSharingMediaDeviceItem) {} + assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted) + .isEqualTo(false) + } + } + } + + @Test + fun testOnActionIconClick_availableAudioSharingMediaDevice_startBroadcast() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + actionInteractorImpl.onActionIconClick(connectedAudioSharingMediaDeviceItem) {} + assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted) + .isEqualTo(true) + } + } + } + private companion object { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt index 6bfd08025833..4396b0a42ae6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt @@ -32,6 +32,9 @@ import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.model.SysUiState import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeDialogContextInteractor @@ -43,9 +46,8 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -93,7 +95,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { private val fakeSystemClock = FakeSystemClock() - private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher private lateinit var testScope: TestScope private lateinit var icon: Pair<Drawable, String> @@ -104,9 +105,8 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { @Before fun setUp() { - scheduler = TestCoroutineScheduler() - dispatcher = UnconfinedTestDispatcher(scheduler) - testScope = TestScope(dispatcher) + dispatcher = kosmos.testDispatcher + testScope = kosmos.testScope whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState) @@ -124,23 +124,19 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { kosmos.shadeDialogContextInteractor, ) - whenever( - sysuiDialogFactory.create( - any(SystemUIDialog.Delegate::class.java), - any() - ) - ).thenAnswer { - SystemUIDialog( - mContext, - 0, - SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, - dialogManager, - sysuiState, - fakeBroadcastDispatcher, - dialogTransitionAnimator, - it.getArgument(0), - ) - } + whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any())) + .thenAnswer { + SystemUIDialog( + mContext, + 0, + SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, + dialogManager, + sysuiState, + fakeBroadcastDispatcher, + dialogTransitionAnimator, + it.getArgument(0), + ) + } icon = Pair(drawable, DEVICE_NAME) deviceItem = @@ -194,20 +190,29 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { @Test fun testDeviceItemViewHolder_cachedDeviceNotBusy() { - deviceItem.isEnabled = true - - val view = - LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) - val viewHolder = - mBluetoothTileDialogDelegate - .Adapter(bluetoothTileDialogCallback) - .DeviceItemViewHolder(view) - viewHolder.bind(deviceItem, bluetoothTileDialogCallback) - val container = view.requireViewById<View>(R.id.bluetooth_device_row) - - assertThat(container).isNotNull() - assertThat(container.isEnabled).isTrue() - assertThat(container.hasOnClickListeners()).isTrue() + testScope.runTest { + deviceItem.isEnabled = true + + val view = + LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val container = view.requireViewById<View>(R.id.bluetooth_device_row) + + assertThat(container).isNotNull() + assertThat(container.isEnabled).isTrue() + assertThat(container.hasOnClickListeners()).isTrue() + val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick) + runCurrent() + container.performClick() + runCurrent() + assertThat(value).isNotNull() + value?.let { + assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW) + assertThat(it.clickedView).isEqualTo(container) + assertThat(it.deviceItem).isEqualTo(deviceItem) + } + } } @Test @@ -229,9 +234,9 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { sysuiDialogFactory, kosmos.shadeDialogContextInteractor, ) - .Adapter(bluetoothTileDialogCallback) + .Adapter() .DeviceItemViewHolder(view) - viewHolder.bind(deviceItem, bluetoothTileDialogCallback) + viewHolder.bind(deviceItem) val container = view.requireViewById<View>(R.id.bluetooth_device_row) assertThat(container).isNotNull() @@ -240,6 +245,32 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { } @Test + fun testDeviceItemViewHolder_clickActionIcon() { + testScope.runTest { + deviceItem.isEnabled = true + + val view = + LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val actionIconView = view.requireViewById<View>(R.id.gear_icon) + + assertThat(actionIconView).isNotNull() + assertThat(actionIconView.hasOnClickListeners()).isTrue() + val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick) + runCurrent() + actionIconView.performClick() + runCurrent() + assertThat(value).isNotNull() + value?.let { + assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON) + assertThat(it.clickedView).isEqualTo(actionIconView) + assertThat(it.deviceItem).isEqualTo(deviceItem) + } + } + } + + @Test fun testOnDeviceUpdated_hideSeeAll_showPairNew() { testScope.runTest { val dialog = mBluetoothTileDialogDelegate.createDialog() diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt index 5bf15137b834..0aa5199cb20e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt @@ -118,6 +118,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { .isEqualTo(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE) assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice) assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME) + assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_add) assertThat(deviceItem.isActive).isFalse() assertThat(deviceItem.connectionSummary) .isEqualTo( @@ -292,6 +293,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice) assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME) assertThat(deviceItem.connectionSummary).isEqualTo(CONNECTION_SUMMARY) + assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_settings_24dp) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt index 57a12df0cfee..c4ef4f978ff8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt @@ -35,7 +35,6 @@ import android.platform.test.annotations.EnableFlags import androidx.test.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any import com.android.systemui.statusbar.core.StatusBarConnectedDisplays @@ -112,20 +111,8 @@ class IconManagerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testCreateIcons_chipNotifIconFlagDisabled_statusBarChipIconIsNull() { - val entry = - notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) - entry?.let { iconManager.createIcons(it) } - testScope.runCurrent() - - assertThat(entry?.icons?.statusBarChipIcon).isNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagDisabled_statusBarChipIconIsNotNull() { + fun testCreateIcons_cdFlagDisabled_statusBarChipIconIsNotNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } @@ -135,8 +122,8 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagEnabled_statusBarChipIconIsNull() { + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_cdFlagEnabled_statusBarChipIconIsNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } @@ -217,7 +204,6 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testCreateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = @@ -233,7 +219,7 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testCreateIcons_cdFlagEnabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) @@ -248,7 +234,6 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = @@ -266,7 +251,7 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testUpdateIcons_cdFlagEnabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 3a99328fa8ed..30ab416b1cbd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -42,6 +42,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.view.View; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; @@ -77,6 +78,8 @@ import com.android.systemui.statusbar.phone.ui.DarkIconManager; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.StatusBarOperatorNameViewModel; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; @@ -1268,6 +1271,15 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mock(StatusBarOperatorNameViewModel.class)); mCollapsedStatusBarViewBinder = new FakeHomeStatusBarViewBinder(); + HomeStatusBarViewModelFactory homeStatusBarViewModelFactory = + new HomeStatusBarViewModelFactory() { + @NonNull + @Override + public HomeStatusBarViewModel create(int displayId) { + return mCollapsedStatusBarViewModel; + } + }; + return new CollapsedStatusBarFragment( mStatusBarFragmentComponentFactory, mOngoingCallController, @@ -1275,7 +1287,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mShadeExpansionStateManager, mStatusBarIconController, mIconManagerFactory, - mCollapsedStatusBarViewModel, + homeStatusBarViewModelFactory, mCollapsedStatusBarViewBinder, mStatusBarHideIconsForBouncerManager, mKeyguardStateController, diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt index 25d1c377ecbd..7ed736158a53 100644 --- a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt +++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt @@ -435,6 +435,8 @@ class FakeStatusBarService : IStatusBarService.Stub() { override fun unbundleNotification(key: String) {} + override fun rebundleNotification(key: String) {} + companion object { const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY const val SECONDARY_DISPLAY_ID = 2 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt index 5ac41ec6741c..f380789968f5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt @@ -23,11 +23,11 @@ import com.android.systemui.util.mockito.mock val Kosmos.mockActivityTransitionAnimatorController by Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() } -val Kosmos.activityTransitionAnimator by +var Kosmos.activityTransitionAnimator by Kosmos.Fixture { ActivityTransitionAnimator( // The main thread is checked in a bunch of places inside the different transitions // animators, so we have to pass the real main executor here. - mainExecutor = testCase.context.mainExecutor, + mainExecutor = testCase.context.mainExecutor ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt index a839f17aad82..c744eacfa3f4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt @@ -33,6 +33,9 @@ class FakeAudioSharingRepository : AudioSharingRepository { var sourceAdded: Boolean = false private set + var audioSharingStarted: Boolean = false + private set + private var profile: LocalBluetoothLeBroadcast? = null override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? @@ -50,7 +53,13 @@ class FakeAudioSharingRepository : AudioSharingRepository { override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} - override suspend fun startAudioSharing() {} + override suspend fun startAudioSharing() { + audioSharingStarted = true + } + + override suspend fun stopAudioSharing() { + audioSharingStarted = false + } fun setAudioSharingAvailable(available: Boolean) { mutableAvailable = available diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index 2a7e3e903737..490b89bf6b13 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi import com.android.systemui.dump.dumpManager import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.time.systemClock @@ -34,6 +35,8 @@ val Kosmos.deviceEntryHapticsInteractor by DeviceEntryHapticsInteractor( biometricSettingsRepository = biometricSettingsRepository, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, + keyguardBypassInteractor = keyguardBypassInteractor, deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor = deviceEntrySourceInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index 3fc60e339543..a64fc2413246 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -115,3 +115,6 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { interface FakeDisplayRepositoryModule { @Binds fun bindFake(fake: FakeDisplayRepository): DisplayRepository } + +val DisplayRepository.fake + get() = this as FakeDisplayRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt index 3de809308702..ee21bdc0b4c2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt @@ -24,8 +24,6 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.util.mockito.mock @@ -55,7 +53,6 @@ object KeyguardInteractorFactory { fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(), fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(), fromAlternateBouncerTransitionInteractor: FromAlternateBouncerTransitionInteractor = mock(), - powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor, testScope: CoroutineScope = TestScope(), ): WithDependencies { // Mock these until they are replaced by kosmos @@ -73,10 +70,8 @@ object KeyguardInteractorFactory { bouncerRepository = bouncerRepository, configurationRepository = configurationRepository, shadeRepository = shadeRepository, - powerInteractor = powerInteractor, KeyguardInteractor( repository = repository, - powerInteractor = powerInteractor, bouncerRepository = bouncerRepository, configurationInteractor = ConfigurationInteractorImpl(configurationRepository), shadeRepository = shadeRepository, @@ -99,7 +94,6 @@ object KeyguardInteractorFactory { val bouncerRepository: FakeKeyguardBouncerRepository, val configurationRepository: FakeConfigurationRepository, val shadeRepository: FakeShadeRepository, - val powerInteractor: PowerInteractor, val keyguardInteractor: KeyguardInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt index f5f8ef75065f..869bae236d5c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt @@ -21,7 +21,6 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.data.repository.shadeRepository @@ -29,7 +28,6 @@ val Kosmos.keyguardInteractor: KeyguardInteractor by Kosmos.Fixture { KeyguardInteractor( repository = keyguardRepository, - powerInteractor = powerInteractor, bouncerRepository = keyguardBouncerRepository, configurationInteractor = configurationInteractor, shadeRepository = shadeRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt index 15d00d9f6994..edc1cce326c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt @@ -20,4 +20,5 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeBouncerTransition : PrimaryBouncerTransition { override val windowBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) + override val notificationBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index afe48214832f..439df543b9fb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -54,8 +54,8 @@ var Kosmos.brightnessWarningToast: BrightnessWarningToast by * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in * that Kosmos instance */ -fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) { - testScope.runTestWithSnapshots testBody@{ this@runTest.testBody() } +fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = let { kosmos -> + testScope.runTestWithSnapshots { kosmos.testBody() } } fun Kosmos.runCurrent() = testScope.runCurrent() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 82b5f6332b23..72c75000ebf4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.scene.domain.startable import com.android.internal.logging.uiEventLogger +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor @@ -85,5 +86,6 @@ val Kosmos.sceneContainerStartable by Fixture { vibratorHelper = vibratorHelper, msdlPlayer = msdlPlayer, disabledContentInteractor = disabledContentInteractor, + activityTransitionAnimator = activityTransitionAnimator, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt index 69e215dcba6a..90897faaa6f8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt @@ -16,13 +16,28 @@ package com.android.systemui.statusbar.layout +import android.content.applicationContext +import com.android.systemui.SysUICutoutProvider +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.commandline.commandRegistry +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.fake import org.mockito.kotlin.mock val Kosmos.mockStatusBarContentInsetsProvider by Kosmos.Fixture { mock<StatusBarContentInsetsProvider>() } -var Kosmos.statusBarContentInsetsProvider by Kosmos.Fixture { mockStatusBarContentInsetsProvider } +val Kosmos.statusBarContentInsetsProvider by + Kosmos.Fixture { + StatusBarContentInsetsProviderImpl( + applicationContext, + configurationController.fake, + dumpManager, + commandRegistry, + mock<SysUICutoutProvider>(), + ) + } val Kosmos.fakeStatusBarContentInsetsProviderFactory by Kosmos.Fixture { FakeStatusBarContentInsetsProviderFactory() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt new file mode 100644 index 000000000000..889d469489f2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.data.repository.multiDisplayStatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider + +val Kosmos.statusBarContentInsetsViewModel by + Kosmos.Fixture { StatusBarContentInsetsViewModel(statusBarContentInsetsProvider) } + +val Kosmos.multiDisplayStatusBarContentInsetsViewModelStore by + Kosmos.Fixture { + MultiDisplayStatusBarContentInsetsViewModelStore( + applicationCoroutineScope, + displayRepository, + multiDisplayStatusBarContentInsetsProviderStore, + ) + } 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 d1619b7959f2..60e092c9709b 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 @@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.heads import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor +import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) @@ -99,6 +100,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel = primaryBouncerToLockscreenTransitionViewModel, + primaryBouncerTransitions = fakeBouncerTransitions, aodBurnInViewModel = aodBurnInViewModel, communalSceneInteractor = communalSceneInteractor, headsUpNotificationInteractor = { headsUpNotificationInteractor }, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index b38a723f1fa7..db7e31bb2cb6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel +import android.content.testableContext import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos @@ -26,6 +27,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel +import com.android.systemui.statusbar.layout.ui.viewmodel.multiDisplayStatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor @@ -36,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStat var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by Kosmos.Fixture { HomeStatusBarViewModelImpl( + testableContext.displayId, homeStatusBarInteractor, homeStatusBarIconBlockListInteractor, lightsOutInteractor, @@ -51,6 +54,7 @@ var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by ongoingActivityChipsViewModel, statusBarPopupChipsViewModel, systemStatusEventAnimationInteractor, + multiDisplayStatusBarContentInsetsViewModelStore, applicationCoroutineScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt index 282f5947636c..1e4701333857 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt @@ -25,3 +25,6 @@ val Kosmos.fakeConfigurationController: FakeConfigurationController by Kosmos.Fixture { FakeConfigurationController() } val Kosmos.statusBarConfigurationController: StatusBarConfigurationController by Kosmos.Fixture { fakeConfigurationController } + +val ConfigurationController.fake + get() = this as FakeConfigurationController diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt index 1ba5ddbf0337..fc0c92e974f7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt @@ -20,5 +20,5 @@ import com.android.settingslib.notification.data.repository.FakeZenModeRepositor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.zenModeRepository by Fixture { fakeZenModeRepository } +var Kosmos.zenModeRepository by Fixture { fakeZenModeRepository } val Kosmos.fakeZenModeRepository by Fixture { FakeZenModeRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt index ed5322ed098e..db19d6ee9077 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt @@ -39,7 +39,7 @@ val Kosmos.localMediaRepositoryFactory by val Kosmos.mediaOutputActionsInteractor by Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogManager) } -val Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() } +var Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() } val Kosmos.mediaOutputInteractor by Kosmos.Fixture { MediaOutputInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt index 712ec41bbf2d..3f2b47948c1c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt @@ -19,4 +19,4 @@ package com.android.systemui.volume.data.repository import com.android.systemui.kosmos.Kosmos val Kosmos.fakeAudioRepository by Kosmos.Fixture { FakeAudioRepository() } -val Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository } +var Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt new file mode 100644 index 000000000000..e2431934bc40 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.volume.dialog + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.dagger.volumeDialogComponentFactory +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor + +val Kosmos.volumeDialog by + Kosmos.Fixture { + VolumeDialog( + context = applicationContext, + visibilityInteractor = volumeDialogVisibilityInteractor, + componentFactory = volumeDialogComponentFactory, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt new file mode 100644 index 000000000000..73e5d8d40985 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt @@ -0,0 +1,41 @@ +/* + * 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.volume.dialog.dagger + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent +import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory +import com.android.systemui.volume.dialog.ui.binder.VolumeDialogViewBinder +import com.android.systemui.volume.dialog.ui.binder.volumeDialogViewBinder +import kotlinx.coroutines.CoroutineScope + +val Kosmos.volumeDialogComponentFactory by + Kosmos.Fixture { + object : VolumeDialogComponent.Factory { + override fun create(scope: CoroutineScope): VolumeDialogComponent = + volumeDialogComponent + } + } +val Kosmos.volumeDialogComponent by + Kosmos.Fixture { + object : VolumeDialogComponent { + override fun volumeDialogViewBinder(): VolumeDialogViewBinder = volumeDialogViewBinder + + override fun sliderComponentFactory(): VolumeDialogSliderComponent.Factory = + volumeDialogSliderComponentFactory + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt index 291dfc0430e2..3d5698b193e1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt @@ -19,4 +19,4 @@ package com.android.systemui.volume.dialog.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository -val Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() } +var Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt index db9c48d9be6f..8f122b57e9d4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt @@ -16,8 +16,6 @@ package com.android.systemui.volume.dialog.domain.interactor -import android.os.Handler -import android.os.looper import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController @@ -27,6 +25,6 @@ val Kosmos.volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor by VolumeDialogCallbacksInteractor( volumeDialogController = volumeDialogController, coroutineScope = applicationCoroutineScope, - bgHandler = Handler(looper), + bgHandler = null, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt new file mode 100644 index 000000000000..7cbdc3d9f6ee --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.volume.dialog.ringer + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder +import com.android.systemui.volume.dialog.ringer.ui.viewmodel.volumeDialogRingerDrawerViewModel + +val Kosmos.volumeDialogRingerViewBinder by + Kosmos.Fixture { VolumeDialogRingerViewBinder(volumeDialogRingerDrawerViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt index 44371b4615df..cf357b498621 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt @@ -20,5 +20,5 @@ import com.android.systemui.kosmos.Kosmos val Kosmos.fakeVolumeDialogRingerFeedbackRepository by Kosmos.Fixture { FakeVolumeDialogRingerFeedbackRepository() } -val Kosmos.volumeDialogRingerFeedbackRepository by +var Kosmos.volumeDialogRingerFeedbackRepository: VolumeDialogRingerFeedbackRepository by Kosmos.Fixture { fakeVolumeDialogRingerFeedbackRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt index a494d04ec741..4bebf8911613 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt @@ -21,7 +21,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController import com.android.systemui.volume.data.repository.audioSystemRepository import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor -import com.android.systemui.volume.dialog.ringer.data.repository.fakeVolumeDialogRingerFeedbackRepository +import com.android.systemui.volume.dialog.ringer.data.repository.volumeDialogRingerFeedbackRepository val Kosmos.volumeDialogRingerInteractor by Kosmos.Fixture { @@ -30,6 +30,6 @@ val Kosmos.volumeDialogRingerInteractor by volumeDialogStateInteractor = volumeDialogStateInteractor, controller = volumeDialogController, audioSystemRepository = audioSystemRepository, - ringerFeedbackRepository = fakeVolumeDialogRingerFeedbackRepository, + ringerFeedbackRepository = volumeDialogRingerFeedbackRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt new file mode 100644 index 000000000000..26b8bca6344b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.settings.domain + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.policy.deviceProvisionedController +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor + +val Kosmos.volumeDialogSettingsButtonInteractor by + Kosmos.Fixture { + VolumeDialogSettingsButtonInteractor( + applicationCoroutineScope, + deviceProvisionedController, + volumePanelGlobalStateInteractor, + volumeDialogVisibilityInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt new file mode 100644 index 000000000000..f9e128ddd810 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.settings.ui.binder + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.settings.ui.viewmodel.volumeDialogSettingsButtonViewModel + +val Kosmos.volumeDialogSettingsButtonViewBinder by + Kosmos.Fixture { VolumeDialogSettingsButtonViewBinder(volumeDialogSettingsButtonViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt new file mode 100644 index 000000000000..0ae3b037b50a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt @@ -0,0 +1,37 @@ +/* + * 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.volume.dialog.settings.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.volume.dialog.settings.domain.volumeDialogSettingsButtonInteractor +import com.android.systemui.volume.mediaDeviceSessionInteractor +import com.android.systemui.volume.mediaOutputInteractor + +val Kosmos.volumeDialogSettingsButtonViewModel by + Kosmos.Fixture { + VolumeDialogSettingsButtonViewModel( + applicationContext, + testScope.testScheduler, + applicationCoroutineScope, + mediaOutputInteractor, + mediaDeviceSessionInteractor, + volumeDialogSettingsButtonInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt new file mode 100644 index 000000000000..4f79f7b4b41a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt @@ -0,0 +1,84 @@ +/* + * 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.volume.dialog.sliders.dagger + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.statusbar.policy.data.repository.zenModeRepository +import com.android.systemui.volume.data.repository.audioRepository +import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository +import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor + +private val Kosmos.mutableSliderComponentKosmoses: MutableMap<VolumeDialogSliderType, Kosmos> by + Kosmos.Fixture { mutableMapOf() } + +val Kosmos.volumeDialogSliderComponentFactory by + Kosmos.Fixture { + object : VolumeDialogSliderComponent.Factory { + override fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderComponent = + volumeDialogSliderComponent(sliderType) + } + } + +fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDialogSliderComponent { + return object : VolumeDialogSliderComponent { + + private val localKosmos + get() = + mutableSliderComponentKosmoses.getOrPut(type) { + Kosmos().also { + it.setupVolumeDialogSliderComponent(this@volumeDialogSliderComponent, type) + } + } + + override fun sliderViewBinder(): VolumeDialogSliderViewBinder = + localKosmos.volumeDialogSliderViewBinder + + override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder = + localKosmos.volumeDialogSliderHapticsViewBinder + + override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder = + localKosmos.volumeDialogOverscrollViewBinder + } +} + +private fun Kosmos.setupVolumeDialogSliderComponent( + parentKosmos: Kosmos, + type: VolumeDialogSliderType, +) { + volumeDialogSliderType = type + applicationContext = parentKosmos.applicationContext + testScope = parentKosmos.testScope + + volumeDialogController = parentKosmos.volumeDialogController + mediaControllerInteractor = parentKosmos.mediaControllerInteractor + mediaControllerRepository = parentKosmos.mediaControllerRepository + zenModeRepository = parentKosmos.zenModeRepository + volumeDialogVisibilityRepository = parentKosmos.volumeDialogVisibilityRepository + audioRepository = parentKosmos.audioRepository +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt new file mode 100644 index 000000000000..13d6ca9732d1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogOverscrollViewModel + +val Kosmos.volumeDialogOverscrollViewBinder by + Kosmos.Fixture { VolumeDialogOverscrollViewBinder(volumeDialogOverscrollViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt new file mode 100644 index 000000000000..d6845b1ff7e3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui + +import com.android.systemui.haptics.msdl.msdlPlayer +import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.time.systemClock +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel + +val Kosmos.volumeDialogSliderHapticsViewBinder by + Kosmos.Fixture { + VolumeDialogSliderHapticsViewBinder( + volumeDialogSliderInputEventsViewModel, + vibratorHelper, + msdlPlayer, + systemClock, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt new file mode 100644 index 000000000000..c6db717e004f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel + +val Kosmos.volumeDialogSliderViewBinder by + Kosmos.Fixture { + VolumeDialogSliderViewBinder( + volumeDialogSliderViewModel, + volumeDialogSliderInputEventsViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt new file mode 100644 index 000000000000..83527d994e70 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSlidersViewModel + +val Kosmos.volumeDialogSlidersViewBinder by + Kosmos.Fixture { VolumeDialogSlidersViewBinder(volumeDialogSlidersViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt new file mode 100644 index 000000000000..fe2f3d806b6a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt @@ -0,0 +1,26 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor + +val Kosmos.volumeDialogOverscrollViewModel by + Kosmos.Fixture { + VolumeDialogOverscrollViewModel(applicationContext, volumeDialogSliderInputEventsInteractor) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt new file mode 100644 index 000000000000..09f9f1c6362e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.volume.domain.interactor.audioVolumeInteractor + +val Kosmos.volumeDialogSliderIconProvider by + Kosmos.Fixture { + VolumeDialogSliderIconProvider( + context = applicationContext, + audioVolumeInteractor = audioVolumeInteractor, + zenModeInteractor = zenModeInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt new file mode 100644 index 000000000000..2de0e8f76a4b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor + +val Kosmos.volumeDialogSliderInputEventsViewModel by + Kosmos.Fixture { + VolumeDialogSliderInputEventsViewModel( + applicationCoroutineScope, + volumeDialogSliderInputEventsInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt new file mode 100644 index 000000000000..63cd440a8633 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt @@ -0,0 +1,34 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.util.time.systemClock +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInteractor + +val Kosmos.volumeDialogSliderViewModel by + Kosmos.Fixture { + VolumeDialogSliderViewModel( + interactor = volumeDialogSliderInteractor, + visibilityInteractor = volumeDialogVisibilityInteractor, + coroutineScope = applicationCoroutineScope, + volumeDialogSliderIconProvider = volumeDialogSliderIconProvider, + systemClock = systemClock, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt new file mode 100644 index 000000000000..5531f7608b69 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor + +val Kosmos.volumeDialogSlidersViewModel by + Kosmos.Fixture { + VolumeDialogSlidersViewModel( + applicationCoroutineScope, + volumeDialogSlidersInteractor, + volumeDialogSliderComponentFactory, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt new file mode 100644 index 000000000000..dc09e3233b1e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt @@ -0,0 +1,39 @@ +/* + * 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.volume.dialog.ui.binder + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder +import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSlidersViewBinder +import com.android.systemui.volume.dialog.ui.utils.jankListenerFactory +import com.android.systemui.volume.dialog.ui.viewmodel.volumeDialogViewModel +import com.android.systemui.volume.dialog.utils.volumeTracer + +val Kosmos.volumeDialogViewBinder by + Kosmos.Fixture { + VolumeDialogViewBinder( + applicationContext.resources, + volumeDialogViewModel, + jankListenerFactory, + volumeTracer, + volumeDialogRingerViewBinder, + volumeDialogSlidersViewBinder, + volumeDialogSettingsButtonViewBinder, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt new file mode 100644 index 000000000000..35ec5d3cc9af --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.volume.dialog.ui.utils + +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.kosmos.Kosmos + +val Kosmos.jankListenerFactory by Kosmos.Fixture { JankListenerFactory(interactionJankMonitor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt new file mode 100644 index 000000000000..05ef462d4998 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt @@ -0,0 +1,37 @@ +/* + * 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.volume.dialog.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.devicePostureController +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor + +val Kosmos.volumeDialogViewModel by + Kosmos.Fixture { + VolumeDialogViewModel( + applicationContext, + volumeDialogVisibilityInteractor, + volumeDialogSlidersInteractor, + volumeDialogStateInteractor, + devicePostureController, + configurationController, + ) + } diff --git a/packages/Vcn/service-b/Android.bp b/packages/Vcn/service-b/Android.bp index 1370b0678cc5..97574e6e35e3 100644 --- a/packages/Vcn/service-b/Android.bp +++ b/packages/Vcn/service-b/Android.bp @@ -39,9 +39,7 @@ java_library { name: "connectivity-utils-service-vcn-internal", sdk_version: "module_current", min_sdk_version: "30", - srcs: [ - ":framework-connectivity-shared-srcs", - ], + srcs: ["service-utils/**/*.java"], libs: [ "framework-annotations-lib", "unsupportedappusage", diff --git a/packages/Vcn/service-b/service-utils/android/util/LocalLog.java b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java new file mode 100644 index 000000000000..5955d930aab1 --- /dev/null +++ b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.SystemClock; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +/** + * @hide + */ +// Exported to Mainline modules; cannot use annotations +// @android.ravenwood.annotation.RavenwoodKeepWholeClass +// TODO: b/374174952 This is an exact copy of frameworks/base/core/java/android/util/LocalLog.java. +// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag +// is fully ramped. When the flag is fully ramped and the development is finalized, this file can +// be removed. +public final class LocalLog { + + private final Deque<String> mLog; + private final int mMaxLines; + + /** + * {@code true} to use log timestamps expressed in local date/time, {@code false} to use log + * timestamped expressed with the elapsed realtime clock and UTC system clock. {@code false} is + * useful when logging behavior that modifies device time zone or system clock. + */ + private final boolean mUseLocalTimestamps; + + @UnsupportedAppUsage + public LocalLog(int maxLines) { + this(maxLines, true /* useLocalTimestamps */); + } + + public LocalLog(int maxLines, boolean useLocalTimestamps) { + mMaxLines = Math.max(0, maxLines); + mLog = new ArrayDeque<>(mMaxLines); + mUseLocalTimestamps = useLocalTimestamps; + } + + @UnsupportedAppUsage + public void log(String msg) { + if (mMaxLines <= 0) { + return; + } + final String logLine; + if (mUseLocalTimestamps) { + logLine = LocalDateTime.now() + " - " + msg; + } else { + logLine = Duration.ofMillis(SystemClock.elapsedRealtime()) + + " / " + Instant.now() + " - " + msg; + } + append(logLine); + } + + private synchronized void append(String logLine) { + while (mLog.size() >= mMaxLines) { + mLog.remove(); + } + mLog.add(logLine); + } + + @UnsupportedAppUsage + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + dump(pw); + } + + public synchronized void dump(PrintWriter pw) { + dump("", pw); + } + + /** + * Dumps the content of local log to print writer with each log entry predeced with indent + * + * @param indent indent that precedes each log entry + * @param pw printer writer to write into + */ + public synchronized void dump(String indent, PrintWriter pw) { + Iterator<String> itr = mLog.iterator(); + while (itr.hasNext()) { + pw.printf("%s%s\n", indent, itr.next()); + } + } + + public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + reverseDump(pw); + } + + public synchronized void reverseDump(PrintWriter pw) { + Iterator<String> itr = mLog.descendingIterator(); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } + + // @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public synchronized void clear() { + mLog.clear(); + } + + public static class ReadOnlyLocalLog { + private final LocalLog mLog; + ReadOnlyLocalLog(LocalLog log) { + mLog = log; + } + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.dump(pw); + } + public void dump(PrintWriter pw) { + mLog.dump(pw); + } + public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.reverseDump(pw); + } + public void reverseDump(PrintWriter pw) { + mLog.reverseDump(pw); + } + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public ReadOnlyLocalLog readOnlyLocalLog() { + return new ReadOnlyLocalLog(this); + } +}
\ No newline at end of file diff --git a/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java new file mode 100644 index 000000000000..7db62f8e9ffc --- /dev/null +++ b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +import android.app.AlarmManager; +import android.content.Context; +import android.os.Handler; +import android.os.Message; + +import com.android.internal.annotations.VisibleForTesting; + + /** + * An AlarmListener that sends the specified message to a Handler and keeps the system awake until + * the message is processed. + * + * This is useful when using the AlarmManager direct callback interface to wake up the system and + * request that an object whose API consists of messages (such as a StateMachine) perform some + * action. + * + * In this situation, using AlarmManager.onAlarmListener by itself will wake up the system to send + * the message, but does not guarantee that the system will be awake until the target object has + * processed it. This is because as soon as the onAlarmListener sends the message and returns, the + * AlarmManager releases its wakelock and the system is free to go to sleep again. + */ +// TODO: b/374174952 This is an exact copy of +// frameworks/base/core/java/com/android/internal/util/WakeupMessage.java. +// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag +// is fully ramped. When the flag is fully ramped and the development is finalized, this file can +// be removed. +public class WakeupMessage implements AlarmManager.OnAlarmListener { + private final AlarmManager mAlarmManager; + + @VisibleForTesting + protected final Handler mHandler; + @VisibleForTesting + protected final String mCmdName; + @VisibleForTesting + protected final int mCmd, mArg1, mArg2; + @VisibleForTesting + protected final Object mObj; + private final Runnable mRunnable; + private boolean mScheduled; + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2, Object obj) { + mAlarmManager = getAlarmManager(context); + mHandler = handler; + mCmdName = cmdName; + mCmd = cmd; + mArg1 = arg1; + mArg2 = arg2; + mObj = obj; + mRunnable = null; + } + + public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) { + this(context, handler, cmdName, cmd, arg1, 0, null); + } + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2) { + this(context, handler, cmdName, cmd, arg1, arg2, null); + } + + public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) { + this(context, handler, cmdName, cmd, 0, 0, null); + } + + public WakeupMessage(Context context, Handler handler, String cmdName, Runnable runnable) { + mAlarmManager = getAlarmManager(context); + mHandler = handler; + mCmdName = cmdName; + mCmd = 0; + mArg1 = 0; + mArg2 = 0; + mObj = null; + mRunnable = runnable; + } + + private static AlarmManager getAlarmManager(Context context) { + return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + + /** + * Schedule the message to be delivered at the time in milliseconds of the + * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup + * the device when it goes off. If schedule is called multiple times without the message being + * dispatched then the alarm is rescheduled to the new time. + */ + public synchronized void schedule(long when) { + mAlarmManager.setExact( + AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler); + mScheduled = true; + } + + /** + * Cancel all pending messages. This includes alarms that may have been fired, but have not been + * run on the handler yet. + */ + public synchronized void cancel() { + if (mScheduled) { + mAlarmManager.cancel(this); + mScheduled = false; + } + } + + @Override + public void onAlarm() { + // Once this method is called the alarm has already been fired and removed from + // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now + // be marked as unscheduled so that it can be rescheduled in the message handler. + final boolean stillScheduled; + synchronized (this) { + stillScheduled = mScheduled; + mScheduled = false; + } + if (stillScheduled) { + Message msg; + if (mRunnable == null) { + msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj); + } else { + msg = Message.obtain(mHandler, mRunnable); + } + mHandler.dispatchMessage(msg); + msg.recycle(); + } + } +}
\ No newline at end of file diff --git a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt index 36307277b4b9..6ec39d953266 100644 --- a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt +++ b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt @@ -1,5 +1,2 @@ -rule android.util.IndentingPrintWriter android.net.vcn.module.repackaged.android.util.IndentingPrintWriter rule android.util.LocalLog android.net.vcn.module.repackaged.android.util.LocalLog -rule com.android.internal.util.IndentingPrintWriter android.net.vcn.module.repackaged.com.android.internal.util.IndentingPrintWriter -rule com.android.internal.util.MessageUtils android.net.vcn.module.repackaged.com.android.internal.util.MessageUtils rule com.android.internal.util.WakeupMessage android.net.vcn.module.repackaged.com.android.internal.util.WakeupMessage
\ No newline at end of file diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 59043a8356ae..8e998426685b 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -345,14 +345,41 @@ java_library { ], } +// We define our own version of platform_compat_config's here, because: +// - The original version (e.g. "framework-platform-compat-config) is built from +// the output file of the device side jar, rather than the host jar, meaning +// they're slow to build because they depend on D8/R8 output. +// - The original services one ("services-platform-compat-config") is built from services.jar, +// which includes service.permission, which is very slow to rebuild because of kotlin. +// +// Because we're re-defining the same compat-IDs that are defined elsewhere, +// they should all have `include_in_merged_xml: false`. Otherwise, generating +// merged_compat_config.xml would fail due to duplicate IDs. +// +// These module names must end with "compat-config" because these will be used as the filename, +// and at runtime, we only loads files that match `*compat-config.xml`. +platform_compat_config { + name: "ravenwood-framework-platform-compat-config", + src: ":framework-minus-apex-for-host", + include_in_merged_xml: false, + visibility: ["//visibility:private"], +} + +platform_compat_config { + name: "ravenwood-services.core-platform-compat-config", + src: ":services.core-for-host", + include_in_merged_xml: false, + visibility: ["//visibility:private"], +} + filegroup { name: "ravenwood-data", device_common_srcs: [ ":system-build.prop", ":framework-res", ":ravenwood-empty-res", - ":framework-platform-compat-config", - ":services-platform-compat-config", + ":ravenwood-framework-platform-compat-config", + ":ravenwood-services.core-platform-compat-config", "texts/ravenwood-build.prop", ], device_first_srcs: [ @@ -616,6 +643,10 @@ android_ravenwood_libgroup { "android.test.mock.ravenwood", "ravenwood-helper-runtime", "hoststubgen-helper-runtime.ravenwood", + + // Note, when we include other services.* jars, we'll need to add + // platform_compat_config for that module too. + // See ravenwood-services.core-platform-compat-config above. "services.core.ravenwood-jarjar", "services.fakes.ravenwood-jarjar", diff --git a/ravenwood/CleanSpec.mk b/ravenwood/CleanSpec.mk new file mode 100644 index 000000000000..50d2fab40326 --- /dev/null +++ b/ravenwood/CleanSpec.mk @@ -0,0 +1,45 @@ +# 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. +# + +# If you don't need to do a full clean build but would like to touch +# a file or delete some intermediate files, add a clean step to the end +# of the list. These steps will only be run once, if they haven't been +# run before. +# +# E.g.: +# $(call add-clean-step, touch -c external/sqlite/sqlite3.h) +# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates) +# +# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with +# files that are missing or have been moved. +# +# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory. +# Use $(OUT_DIR) to refer to the "out" directory. +# +# If you need to re-do something that's already mentioned, just copy +# the command and add it to the bottom of the list. E.g., if a change +# that you made last week required touching a file and a change you +# made today requires touching the same file, just copy the old +# touch step and add it to the end of the list. +# +# ***************************************************************** +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THE BANNER +# ***************************************************************** + +$(call add-clean-step, rm -rf $(OUT_DIR)/host/linux-x86/testcases/ravenwood-runtime) + +# ****************************************************************** +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THIS BANNER +# ****************************************************************** diff --git a/ravenwood/tools/hoststubgen/scripts/dump-jar b/ravenwood/tools/hoststubgen/scripts/dump-jar index 87652451359d..998357b70dff 100755 --- a/ravenwood/tools/hoststubgen/scripts/dump-jar +++ b/ravenwood/tools/hoststubgen/scripts/dump-jar @@ -89,7 +89,7 @@ filter_output() { # - Some other transient lines # - Sometimes the javap shows mysterious warnings, so remove them too. # - # `/PATTERN-1/,/PATTERN-1/{//!d}` is a trick to delete lines between two patterns, without + # `/PATTERN-1/,/PATTERN-2/{//!d}` is a trick to delete lines between two patterns, without # the start and the end lines. sed -e 's/#[0-9][0-9]*/#x/g' \ -e 's/^\( *\)[0-9][0-9]*:/\1x:/' \ diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 6d8d7b768b91..cc704b2b32ed 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -27,7 +27,7 @@ import com.android.hoststubgen.filters.ImplicitOutputFilter import com.android.hoststubgen.filters.KeepNativeFilter import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.filters.SanitizationFilter -import com.android.hoststubgen.filters.TextFileFilterPolicyParser +import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder import com.android.hoststubgen.filters.printAsTextPolicy import com.android.hoststubgen.utils.ClassFilter import com.android.hoststubgen.visitors.BaseAdapter @@ -179,9 +179,9 @@ class HostStubGen(val options: HostStubGenOptions) { // Next, "text based" filter, which allows to override polices without touching // the target code. if (options.policyOverrideFiles.isNotEmpty()) { - val parser = TextFileFilterPolicyParser(allClasses, filter) - options.policyOverrideFiles.forEach(parser::parse) - filter = parser.createOutputFilter() + val builder = TextFileFilterPolicyBuilder(allClasses, filter) + options.policyOverrideFiles.forEach(builder::parse) + filter = builder.createOutputFilter() } // Apply the implicit filter. diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index 7462a8ce12c5..be1b6ca93869 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -23,10 +23,12 @@ import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine import com.android.hoststubgen.whitespaceRegex -import java.io.File +import org.objectweb.asm.tree.ClassNode +import java.io.BufferedReader +import java.io.FileReader import java.io.PrintWriter +import java.io.Reader import java.util.regex.Pattern -import org.objectweb.asm.tree.ClassNode /** * Print a class node as a "keep" policy. @@ -48,7 +50,7 @@ fun printAsTextPolicy(pw: PrintWriter, cn: ClassNode) { private const val FILTER_REASON = "file-override" -private enum class SpecialClass { +enum class SpecialClass { NotSpecial, Aidl, FeatureFlags, @@ -56,10 +58,58 @@ private enum class SpecialClass { RFile, } -class TextFileFilterPolicyParser( +/** + * This receives [TextFileFilterPolicyBuilder] parsing result. + */ +interface PolicyFileProcessor { + /** "package" directive. */ + fun onPackage(name: String, policy: FilterPolicyWithReason) + + /** "rename" directive. */ + fun onRename(pattern: Pattern, prefix: String) + + /** "class" directive. */ + fun onSimpleClassStart(className: String) + fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) + fun onSimpleClassEnd(className: String) + + fun onSubClassPolicy(superClassName: String, policy: FilterPolicyWithReason) + fun onRedirectionClass(fromClassName: String, toClassName: String) + fun onClassLoadHook(className: String, callback: String) + fun onSpecialClassPolicy(type: SpecialClass, policy: FilterPolicyWithReason) + + /** "field" directive. */ + fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) + + /** "method" directive. */ + fun onSimpleMethodPolicy( + className: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) + fun onMethodInClassReplace( + className: String, + methodName: String, + methodDesc: String, + targetName: String, + policy: FilterPolicyWithReason, + ) + fun onMethodOutClassReplace( + className: String, + methodName: String, + methodDesc: String, + replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + policy: FilterPolicyWithReason, + ) +} + +class TextFileFilterPolicyBuilder( private val classes: ClassNodes, fallback: OutputFilter ) { + private val parser = TextFileFilterPolicyParser() + private val subclassFilter = SubclassFilter(classes, fallback) private val packageFilter = PackageFilter(subclassFilter) private val imf = InMemoryOutputFilter(classes, packageFilter) @@ -71,30 +121,19 @@ class TextFileFilterPolicyParser( private val methodReplaceSpec = mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>() - private lateinit var currentClassName: String - /** - * Read a given "policy" file and return as an [OutputFilter] + * Parse a given policy file. This method can be called multiple times to read from + * multiple files. To get the resulting filter, use [createOutputFilter] */ fun parse(file: String) { - log.i("Loading offloaded annotations from $file ...") - log.withIndent { - var lineNo = 0 - try { - File(file).forEachLine { - lineNo++ - val line = normalizeTextLine(it) - if (line.isEmpty()) { - return@forEachLine // skip empty lines. - } - parseLine(line) - } - } catch (e: ParseException) { - throw e.withSourceInfo(file, lineNo) - } - } + // We may parse multiple files, but we reuse the same parser, because the parser + // will make sure there'll be no dupplicating "special class" policies. + parser.parse(FileReader(file), file, Processor()) } + /** + * Generate the resulting [OutputFilter]. + */ fun createOutputFilter(): OutputFilter { var ret: OutputFilter = imf if (typeRenameSpec.isNotEmpty()) { @@ -112,14 +151,200 @@ class TextFileFilterPolicyParser( return ret } + private inner class Processor : PolicyFileProcessor { + override fun onPackage(name: String, policy: FilterPolicyWithReason) { + packageFilter.addPolicy(name, policy) + } + + override fun onRename(pattern: Pattern, prefix: String) { + typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( + pattern, prefix + ) + } + + override fun onSimpleClassStart(className: String) { + } + + override fun onSimpleClassEnd(className: String) { + } + + override fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) { + imf.setPolicyForClass(className, policy) + } + + override fun onSubClassPolicy( + superClassName: String, + policy: FilterPolicyWithReason, + ) { + log.i("class extends $superClassName") + subclassFilter.addPolicy( superClassName, policy) + } + + override fun onRedirectionClass(fromClassName: String, toClassName: String) { + imf.setRedirectionClass(fromClassName, toClassName) + } + + override fun onClassLoadHook(className: String, callback: String) { + imf.setClassLoadHook(className, callback) + } + + override fun onSpecialClassPolicy( + type: SpecialClass, + policy: FilterPolicyWithReason, + ) { + log.i("class special $type $policy") + when (type) { + SpecialClass.NotSpecial -> {} // Shouldn't happen + + SpecialClass.Aidl -> { + aidlPolicy = policy + } + + SpecialClass.FeatureFlags -> { + featureFlagsPolicy = policy + } + + SpecialClass.Sysprops -> { + syspropsPolicy = policy + } + + SpecialClass.RFile -> { + rFilePolicy = policy + } + } + } + + override fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) { + imf.setPolicyForField(className, fieldName, policy) + } + + override fun onSimpleMethodPolicy( + className: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + } + + override fun onMethodInClassReplace( + className: String, + methodName: String, + methodDesc: String, + targetName: String, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + + // Make sure to keep the target method. + imf.setPolicyForMethod( + className, + targetName, + methodDesc, + FilterPolicy.Keep.withReason(FILTER_REASON) + ) + // Set up the rename. + imf.setRenameTo(className, targetName, methodDesc, methodName) + } + + override fun onMethodOutClassReplace( + className: String, + methodName: String, + methodDesc: String, + replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + methodReplaceSpec.add(replaceSpec) + } + } +} + +/** + * Parses a filer policy text file. + */ +class TextFileFilterPolicyParser { + private lateinit var processor: PolicyFileProcessor + private var currentClassName: String? = null + + private var aidlPolicy: FilterPolicyWithReason? = null + private var featureFlagsPolicy: FilterPolicyWithReason? = null + private var syspropsPolicy: FilterPolicyWithReason? = null + private var rFilePolicy: FilterPolicyWithReason? = null + + /** Name of the file that's currently being processed. */ + var filename: String? = null + private set + + /** 1-based line number in the current file */ + var lineNumber = -1 + private set + + /** + * Parse a given "policy" file. + */ + fun parse(reader: Reader, inputName: String, processor: PolicyFileProcessor) { + filename = inputName + + log.i("Parsing text policy file $inputName ...") + this.processor = processor + BufferedReader(reader).use { rd -> + lineNumber = 0 + try { + while (true) { + var line = rd.readLine() + if (line == null) { + break + } + lineNumber++ + line = normalizeTextLine(line) // Remove comment and trim. + if (line.isEmpty()) { + continue + } + parseLine(line) + } + finishLastClass() + } catch (e: ParseException) { + throw e.withSourceInfo(inputName, lineNumber) + } + } + } + + private fun finishLastClass() { + currentClassName?.let { className -> + processor.onSimpleClassEnd(className) + currentClassName = null + } + } + + private fun ensureInClass(directive: String): String { + return currentClassName ?: + throw ParseException("Directive '$directive' must follow a 'class' directive") + } + private fun parseLine(line: String) { val fields = line.split(whitespaceRegex).toTypedArray() when (fields[0].lowercase()) { - "p", "package" -> parsePackage(fields) - "c", "class" -> parseClass(fields) - "f", "field" -> parseField(fields) - "m", "method" -> parseMethod(fields) - "r", "rename" -> parseRename(fields) + "p", "package" -> { + finishLastClass() + parsePackage(fields) + } + "c", "class" -> { + finishLastClass() + parseClass(fields) + } + "f", "field" -> { + ensureInClass("field") + parseField(fields) + } + "m", "method" -> { + ensureInClass("method") + parseMethod(fields) + } + "r", "rename" -> { + finishLastClass() + parseRename(fields) + } else -> throw ParseException("Unknown directive \"${fields[0]}\"") } } @@ -184,20 +409,20 @@ class TextFileFilterPolicyParser( if (!policy.isUsableWithClasses) { throw ParseException("Package can't have policy '$policy'") } - packageFilter.addPolicy(name, policy.withReason(FILTER_REASON)) + processor.onPackage(name, policy.withReason(FILTER_REASON)) } private fun parseClass(fields: Array<String>) { if (fields.size < 3) { throw ParseException("Class ('c') expects 2 fields.") } - currentClassName = fields[1] + val className = fields[1] // superClass is set when the class name starts with a "*". - val superClass = resolveExtendingClass(currentClassName) + val superClass = resolveExtendingClass(className) // :aidl, etc? - val classType = resolveSpecialClass(currentClassName) + val classType = resolveSpecialClass(className) if (fields[2].startsWith("!")) { if (classType != SpecialClass.NotSpecial) { @@ -208,7 +433,8 @@ class TextFileFilterPolicyParser( } // It's a redirection class. val toClass = fields[2].substring(1) - imf.setRedirectionClass(currentClassName, toClass) + + processor.onRedirectionClass(className, toClass) } else if (fields[2].startsWith("~")) { if (classType != SpecialClass.NotSpecial) { // We could support it, but not needed at least for now. @@ -218,7 +444,8 @@ class TextFileFilterPolicyParser( } // It's a class-load hook val callback = fields[2].substring(1) - imf.setClassLoadHook(currentClassName, callback) + + processor.onClassLoadHook(className, callback) } else { val policy = parsePolicy(fields[2]) if (!policy.isUsableWithClasses) { @@ -229,26 +456,27 @@ class TextFileFilterPolicyParser( SpecialClass.NotSpecial -> { // TODO: Duplicate check, etc if (superClass == null) { - imf.setPolicyForClass( - currentClassName, policy.withReason(FILTER_REASON) - ) + currentClassName = className + processor.onSimpleClassStart(className) + processor.onSimpleClassPolicy(className, policy.withReason(FILTER_REASON)) } else { - subclassFilter.addPolicy( + processor.onSubClassPolicy( superClass, - policy.withReason("extends $superClass") + policy.withReason("extends $superClass"), ) } } - SpecialClass.Aidl -> { if (aidlPolicy != null) { throw ParseException( "Policy for AIDL classes already defined" ) } - aidlPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class AIDL)" ) + processor.onSpecialClassPolicy(classType, p) + aidlPolicy = p } SpecialClass.FeatureFlags -> { @@ -257,9 +485,11 @@ class TextFileFilterPolicyParser( "Policy for feature flags already defined" ) } - featureFlagsPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class feature flags)" ) + processor.onSpecialClassPolicy(classType, p) + featureFlagsPolicy = p } SpecialClass.Sysprops -> { @@ -268,9 +498,11 @@ class TextFileFilterPolicyParser( "Policy for sysprops already defined" ) } - syspropsPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class sysprops)" ) + processor.onSpecialClassPolicy(classType, p) + syspropsPolicy = p } SpecialClass.RFile -> { @@ -279,9 +511,11 @@ class TextFileFilterPolicyParser( "Policy for R file already defined" ) } - rFilePolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class R file)" ) + processor.onSpecialClassPolicy(classType, p) + rFilePolicy = p } } } @@ -296,17 +530,16 @@ class TextFileFilterPolicyParser( if (!policy.isUsableWithFields) { throw ParseException("Field can't have policy '$policy'") } - require(this::currentClassName.isInitialized) // TODO: Duplicate check, etc - imf.setPolicyForField(currentClassName, name, policy.withReason(FILTER_REASON)) + processor.onField(currentClassName!!, name, policy.withReason(FILTER_REASON)) } private fun parseMethod(fields: Array<String>) { if (fields.size < 3 || fields.size > 4) { throw ParseException("Method ('m') expects 3 or 4 fields.") } - val name = fields[1] + val methodName = fields[1] val signature: String val policyStr: String if (fields.size <= 3) { @@ -323,44 +556,48 @@ class TextFileFilterPolicyParser( throw ParseException("Method can't have policy '$policy'") } - require(this::currentClassName.isInitialized) + val className = currentClassName!! - imf.setPolicyForMethod( - currentClassName, name, signature, - policy.withReason(FILTER_REASON) - ) - if (policy == FilterPolicy.Substitute) { - val fromName = policyStr.substring(1) + val policyWithReason = policy.withReason(FILTER_REASON) + if (policy != FilterPolicy.Substitute) { + processor.onSimpleMethodPolicy(className, methodName, signature, policyWithReason) + } else { + val targetName = policyStr.substring(1) - if (fromName == name) { + if (targetName == methodName) { throw ParseException( "Substitution must have a different name" ) } - // Set the policy for the "from" method. - imf.setPolicyForMethod( - currentClassName, fromName, signature, - FilterPolicy.Keep.withReason(FILTER_REASON) - ) - - val classAndMethod = splitWithLastPeriod(fromName) + val classAndMethod = splitWithLastPeriod(targetName) if (classAndMethod != null) { // If the substitution target contains a ".", then // it's a method call redirect. - methodReplaceSpec.add( - TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( - currentClassName.toJvmClassName(), - name, + val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( + currentClassName!!.toJvmClassName(), + methodName, signature, classAndMethod.first.toJvmClassName(), classAndMethod.second, ) + processor.onMethodOutClassReplace( + className, + methodName, + signature, + spec, + policyWithReason, ) } else { // It's an in-class replace. // ("@RavenwoodReplace" equivalent) - imf.setRenameTo(currentClassName, fromName, signature, name) + processor.onMethodInClassReplace( + className, + methodName, + signature, + targetName, + policyWithReason, + ) } } } @@ -378,7 +615,7 @@ class TextFileFilterPolicyParser( // applied. (Which is needed for services.jar) val prefix = fields[2].trimStart('/') - typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( + processor.onRename( pattern, prefix ) } diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh index b389a67a8e4c..8408a18142eb 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh @@ -34,10 +34,11 @@ source "${0%/*}"/../common.sh SCRIPT_NAME="${0##*/}" -GOLDEN_DIR=golden-output +GOLDEN_DIR=${GOLDEN_DIR:-golden-output} mkdir -p $GOLDEN_DIR -DIFF_CMD=${DIFF:-diff -u --ignore-blank-lines --ignore-space-change} +# TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines. +DIFF_CMD=${DIFF_CMD:-diff -u --ignore-blank-lines --ignore-space-change --ignore-matching-lines='^\(Constant.pool:\|{\)$'} update=0 three_way=0 @@ -62,7 +63,7 @@ done shift $(($OPTIND - 1)) # Build the dump files, which are the input of this test. -run m dump-jar tiny-framework-dump-test +run ${BUILD_CMD:=m} dump-jar tiny-framework-dump-test # Get the path to the generate text files. (not the golden files.) diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py index 88fa492addb8..c35d6d106c8d 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py @@ -28,8 +28,11 @@ GOLDEN_DIRS = [ # Run diff. def run_diff(file1, file2): + # TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines. command = ['diff', '-u', '--ignore-blank-lines', - '--ignore-space-change', file1, file2] + '--ignore-space-change', + '--ignore-matching-lines=^\(Constant.pool:\|{\)$', + file1, file2] print(' '.join(command)) result = subprocess.run(command, stderr=sys.stdout) diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 6cd1f721d215..8e037c3ba90c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -542,7 +542,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub MagnificationController magnificationController, @Nullable AccessibilityInputFilter inputFilter, ProxyManager proxyManager, - PermissionEnforcer permissionEnforcer) { + PermissionEnforcer permissionEnforcer, + HearingDevicePhoneCallNotificationController hearingDeviceNotificationController) { super(permissionEnforcer); mContext = context; mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); @@ -571,8 +572,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mVisibleBgUserIds = null; mInputManager = context.getSystemService(InputManager.class); if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { - mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController( - context); + mHearingDeviceNotificationController = hearingDeviceNotificationController; } else { mHearingDeviceNotificationController = null; } diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java index 3f6ede95eaf9..8804faf2d312 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerService.java +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -22,9 +22,9 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.app.backup.BackupManager; +import android.app.backup.BackupManagerInternal; import android.app.backup.BackupRestoreEventLogger.DataTypeResult; import android.app.backup.IBackupManager; import android.app.backup.IBackupManagerMonitor; @@ -60,6 +60,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; import com.android.server.SystemConfig; import com.android.server.SystemService; import com.android.server.backup.utils.RandomAccessFileUtils; @@ -91,7 +92,7 @@ import java.util.Set; * privileged callers (currently {@link DevicePolicyManager}). If called on {@link * UserHandle#USER_SYSTEM}, backup is disabled for all users. */ -public class BackupManagerService extends IBackupManager.Stub { +public class BackupManagerService extends IBackupManager.Stub implements BackupManagerInternal { public static final String TAG = "BackupManagerService"; public static final boolean DEBUG = true; public static final boolean MORE_DEBUG = false; @@ -191,7 +192,6 @@ public class BackupManagerService extends IBackupManager.Stub { } } - // TODO: Remove this when we implement DI by injecting in the construtor. @VisibleForTesting Handler getBackupHandler() { return mHandler; @@ -637,51 +637,28 @@ public class BackupManagerService extends IBackupManager.Stub { } @Override - public void agentConnectedForUser(int userId, String packageName, IBinder agent) - throws RemoteException { - if (isUserReadyForBackup(userId)) { - agentConnected(userId, packageName, agent); + public void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent) { + if (!isUserReadyForBackup(userId)) { + return; } - } - @Override - public void agentConnected(String packageName, IBinder agent) throws RemoteException { - agentConnectedForUser(binderGetCallingUserId(), packageName, agent); - } - - /** - * Callback: a requested backup agent has been instantiated. This should only be called from the - * {@link ActivityManager}. - */ - public void agentConnected(@UserIdInt int userId, String packageName, IBinder agentBinder) { - UserBackupManagerService userBackupManagerService = - getServiceForUserIfCallerHasPermission(userId, "agentConnected()"); + UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission( + userId, "agentConnected()"); if (userBackupManagerService != null) { userBackupManagerService.getBackupAgentConnectionManager().agentConnected(packageName, - agentBinder); + agent); } } @Override - public void agentDisconnectedForUser(int userId, String packageName) throws RemoteException { - if (isUserReadyForBackup(userId)) { - agentDisconnected(userId, packageName); + public void agentDisconnectedForUser(String packageName, @UserIdInt int userId) { + if (!isUserReadyForBackup(userId)) { + return; } - } - @Override - public void agentDisconnected(String packageName) throws RemoteException { - agentDisconnectedForUser(binderGetCallingUserId(), packageName); - } - - /** - * Callback: a backup agent has failed to come up, or has unexpectedly quit. This should only be - * called from the {@link ActivityManager}. - */ - public void agentDisconnected(@UserIdInt int userId, String packageName) { - UserBackupManagerService userBackupManagerService = - getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()"); + UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission( + userId, "agentDisconnected()"); if (userBackupManagerService != null) { userBackupManagerService.getBackupAgentConnectionManager().agentDisconnected( @@ -1688,7 +1665,7 @@ public class BackupManagerService extends IBackupManager.Stub { * @param userId User id on which the backup operation is being requested. * @param message A message to include in the exception if it is thrown. */ - void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) { + private void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) { if (binderGetCallingUserId() != userId) { mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, message); @@ -1697,6 +1674,8 @@ public class BackupManagerService extends IBackupManager.Stub { /** Implementation to receive lifecycle event callbacks for system services. */ public static class Lifecycle extends SystemService { + private final BackupManagerService mBackupManagerService; + public Lifecycle(Context context) { this(context, new BackupManagerService(context)); } @@ -1704,12 +1683,14 @@ public class BackupManagerService extends IBackupManager.Stub { @VisibleForTesting Lifecycle(Context context, BackupManagerService backupManagerService) { super(context); + mBackupManagerService = backupManagerService; sInstance = backupManagerService; + LocalServices.addService(BackupManagerInternal.class, mBackupManagerService); } @Override public void onStart() { - publishService(Context.BACKUP_SERVICE, BackupManagerService.sInstance); + publishService(Context.BACKUP_SERVICE, mBackupManagerService); } @Override @@ -1717,17 +1698,17 @@ public class BackupManagerService extends IBackupManager.Stub { // Starts the backup service for this user if backup is active for this user. Offloads // work onto the handler thread {@link #mHandlerThread} to keep unlock time low since // backup is not essential for device functioning. - sInstance.postToHandler( + mBackupManagerService.postToHandler( () -> { - sInstance.updateDefaultBackupUserIdIfNeeded(); - sInstance.startServiceForUser(user.getUserIdentifier()); - sInstance.mHasFirstUserUnlockedSinceBoot = true; + mBackupManagerService.updateDefaultBackupUserIdIfNeeded(); + mBackupManagerService.startServiceForUser(user.getUserIdentifier()); + mBackupManagerService.mHasFirstUserUnlockedSinceBoot = true; }); } @Override public void onUserStopping(@NonNull TargetUser user) { - sInstance.onStopUser(user.getUserIdentifier()); + mBackupManagerService.onStopUser(user.getUserIdentifier()); } @VisibleForTesting diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index aeb2f5e9be84..40726b4331e2 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -767,7 +767,7 @@ public class VirtualDeviceManagerService extends SystemService { params, /* activityListener= */ null, /* soundEffectListener= */ null); - return new VirtualDeviceManager.VirtualDevice(mImpl, getContext(), virtualDevice); + return new VirtualDeviceManager.VirtualDevice(getContext(), virtualDevice); } @Override diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 31f6ef9fc062..1d914c89c570 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -729,8 +729,10 @@ public class BinaryTransparencyService extends SystemService { private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) { pw.println("--- Module Details ---"); pw.println("Module name: " + moduleInfo.getName()); - pw.println("Module visibility: " - + (moduleInfo.isHidden() ? "hidden" : "visible")); + if (!android.content.pm.Flags.removeHiddenModuleUsage()) { + pw.println("Module visibility: " + + (moduleInfo.isHidden() ? "hidden" : "visible")); + } } private void printAppDetails(PackageInfo packageInfo, diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b536dc524a80..d9c105c512fa 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -256,7 +256,7 @@ import android.app.ServiceStartNotAllowedException; import android.app.WaitResult; import android.app.assist.ActivityId; import android.app.backup.BackupAnnotations.BackupDestination; -import android.app.backup.IBackupManager; +import android.app.backup.BackupManagerInternal; import android.app.compat.CompatChanges; import android.app.job.JobParameters; import android.app.usage.UsageEvents; @@ -4490,11 +4490,8 @@ public class ActivityManagerService extends IActivityManager.Stub final int userId = app.userId; final String packageName = app.info.packageName; mHandler.post(() -> { - try { - getBackupManager().agentDisconnectedForUser(userId, packageName); - } catch (RemoteException e) { - // Can't happen; the backup manager is local - } + LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser( + packageName, userId); }); } } else { @@ -12864,6 +12861,28 @@ public class ActivityManagerService extends IActivityManager.Stub } } + final long kernelCmaUsage = Debug.getKernelCmaUsageKb(); + if (kernelCmaUsage >= 0) { + pw.print(" Kernel CMA: "); + pw.println(stringifyKBSize(kernelCmaUsage)); + // CMA memory can be in one of the following four states: + // + // 1. Free, in which case it is accounted for as part of MemFree, which + // is already considered in the lostRAM calculation below. + // + // 2. Allocated as part of a userspace allocated, in which case it is + // already accounted for in the total PSS value that was computed. + // + // 3. Allocated for storing compressed memory (ZRAM) on Android kernels. + // This is accounted for by calculating the amount of memory ZRAM + // consumes and including it in the lostRAM calculuation. + // + // 4. Allocated by a kernel driver, in which case, it is currently not + // attributed to any term that has been derived thus far. Since the + // allocations come from a kernel driver, add it to kernelUsed. + kernelUsed += kernelCmaUsage; + } + // Note: ION/DMA-BUF heap pools are reclaimable and hence, they are included as part of // memInfo.getCachedSizeKb(). final long lostRAM = memInfo.getTotalSizeKb() @@ -13381,12 +13400,32 @@ public class ActivityManagerService extends IActivityManager.Stub proto.write(MemInfoDumpProto.CACHED_KERNEL_KB, memInfo.getCachedSizeKb()); proto.write(MemInfoDumpProto.FREE_KB, memInfo.getFreeSizeKb()); } + // CMA memory can be in one of the following four states: + // + // 1. Free, in which case it is accounted for as part of MemFree, which + // is already considered in the lostRAM calculation below. + // + // 2. Allocated as part of a userspace allocated, in which case it is + // already accounted for in the total PSS value that was computed. + // + // 3. Allocated for storing compressed memory (ZRAM) on Android Kernels. + // This is accounted for by calculating hte amount of memory ZRAM + // consumes and including it in the lostRAM calculation. + // + // 4. Allocated by a kernel driver, in which case, it is currently not + // attributed to any term that has been derived thus far, so subtract + // it from lostRAM. + long kernelCmaUsage = Debug.getKernelCmaUsageKb(); + if (kernelCmaUsage < 0) { + kernelCmaUsage = 0; + } long lostRAM = memInfo.getTotalSizeKb() - (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS]) - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb() // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb()) + memInfo.getShmemSizeKb() - - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb(); + - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb() + - kernelCmaUsage; proto.write(MemInfoDumpProto.USED_PSS_KB, ss[INDEX_TOTAL_PSS] - cachedPss); proto.write(MemInfoDumpProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb()); proto.write(MemInfoDumpProto.LOST_RAM_KB, lostRAM); @@ -13505,11 +13544,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_BACKUP || DEBUG_CLEANUP) Slog.d(TAG_CLEANUP, "App " + backupTarget.appInfo + " died during backup"); mHandler.post(() -> { - try { - getBackupManager().agentDisconnectedForUser(app.userId, app.info.packageName); - } catch (RemoteException e) { - // can't happen; backup manager is local - } + LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser( + app.info.packageName, app.userId); }); } @@ -14223,9 +14259,8 @@ public class ActivityManagerService extends IActivityManager.Stub final long oldIdent = Binder.clearCallingIdentity(); try { - getBackupManager().agentConnectedForUser(userId, agentPackageName, agent); - } catch (RemoteException e) { - // can't happen; the backup manager service is local + LocalServices.getService(BackupManagerInternal.class).agentConnectedForUser( + agentPackageName, userId, agent); } catch (Exception e) { Slog.w(TAG, "Exception trying to deliver BackupAgent binding: "); e.printStackTrace(); @@ -16617,7 +16652,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void onUserRemoved(@UserIdInt int userId) { + public void onUserRemoving(@UserIdInt int userId) { // Clean up any ActivityTaskManager state (by telling it the user is stopped) mAtmInternal.onUserStopped(userId); // Clean up various services by removing the user @@ -16631,6 +16666,12 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public void onUserRemoved(int userId) { + // Clean up UserController state + mUserController.onUserRemoved(userId); + } + + @Override public boolean startUserInBackground(final int userId) { return ActivityManagerService.this.startUserInBackground(userId); } @@ -19374,7 +19415,7 @@ public class ActivityManagerService extends IActivityManager.Stub } if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) { // this flag will be ramped to public. - intent.collectExtraIntentKeys(); + intent.collectExtraIntentKeys(true); } } @@ -19440,8 +19481,4 @@ public class ActivityManagerService extends IActivityManager.Stub } return token; } - - private IBackupManager getBackupManager() { - return IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE)); - } } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index ec74f60539a2..d76c04ac7f31 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -125,7 +125,6 @@ import android.view.Display; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.ObjectUtils; @@ -161,7 +160,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; -import java.util.function.Consumer; /** * Helper class for {@link ActivityManagerService} responsible for multi-user functionality. @@ -227,14 +225,6 @@ class UserController implements Handler.Callback { private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000; /** - * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be - * called after dismissing the keyguard. - * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()} - * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}. - */ - private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000; - - /** * Time after last scheduleOnUserCompletedEvent() call at which USER_COMPLETED_EVENT_MSG will be * scheduled (although it may fire sooner instead). * When it fires, {@link #reportOnUserCompletedEvent} will be processed. @@ -455,11 +445,6 @@ class UserController implements Handler.Callback { public void onUserCreated(UserInfo user, Object token) { onUserAdded(user); } - - @Override - public void onUserRemoved(UserInfo user) { - UserController.this.onUserRemoved(user.id); - } }; UserController(ActivityManagerService service) { @@ -2010,7 +1995,7 @@ class UserController implements Handler.Callback { mInjector.getWindowManager().setSwitchingUser(true); // Only lock if the user has a secure keyguard PIN/Pattern/Pwd if (mInjector.getKeyguardManager().isDeviceSecure(userId)) { - // Make sure the device is locked before moving on with the user switch + Slogf.d(TAG, "Locking the device before moving on with the user switch"); mInjector.lockDeviceNowAndWaitForKeyguardShown(); } } @@ -2640,7 +2625,7 @@ class UserController implements Handler.Callback { EventLog.writeEvent(EventLogTags.UC_CONTINUE_USER_SWITCH, oldUserId, newUserId); - // Do the keyguard dismiss and dismiss the user switching dialog later + // Dismiss the user switching dialog and complete the user switch mHandler.removeMessages(COMPLETE_USER_SWITCH_MSG); mHandler.sendMessage(mHandler.obtainMessage( COMPLETE_USER_SWITCH_MSG, oldUserId, newUserId)); @@ -2655,31 +2640,17 @@ class UserController implements Handler.Callback { @VisibleForTesting void completeUserSwitch(int oldUserId, int newUserId) { - final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled(); - // serialize each conditional step - await( - // STEP 1 - If there is no challenge set, dismiss the keyguard right away - isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId), - mInjector::dismissKeyguard, - () -> await( - // STEP 2 - If user switch ui was enabled, dismiss user switch dialog - isUserSwitchUiEnabled, - this::dismissUserSwitchDialog, - () -> { - // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast - // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete - mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); - mHandler.sendMessage(mHandler.obtainMessage( - REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId)); - } - )); - } - - private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) { - if (condition) { - conditionalStep.accept(nextStep); + final Runnable runnable = () -> { + // Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast ACTION_USER_SWITCHED and call + // onUserSwitchComplete on UserSwitchObservers. + mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); + mHandler.sendMessage(mHandler.obtainMessage( + REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId)); + }; + if (isUserSwitchUiEnabled()) { + dismissUserSwitchDialog(runnable); } else { - nextStep.run(); + runnable.run(); } } @@ -3381,10 +3352,12 @@ class UserController implements Handler.Callback { if (mUserProfileGroupIds.keyAt(i) == userId || mUserProfileGroupIds.valueAt(i) == userId) { mUserProfileGroupIds.removeAt(i); - } } mCurrentProfileIds = ArrayUtils.removeInt(mCurrentProfileIds, userId); + mUserLru.remove((Integer) userId); + mStartedUsers.remove(userId); + updateStartedUserArrayLU(); } } @@ -4127,33 +4100,6 @@ class UserController implements Handler.Callback { return IStorageManager.Stub.asInterface(ServiceManager.getService("mount")); } - protected void dismissKeyguard(Runnable runnable) { - final AtomicBoolean isFirst = new AtomicBoolean(true); - final Runnable runOnce = () -> { - if (isFirst.getAndSet(false)) { - runnable.run(); - } - }; - - mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS); - getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() { - @Override - public void onDismissError() throws RemoteException { - mHandler.post(runOnce); - } - - @Override - public void onDismissSucceeded() throws RemoteException { - mHandler.post(runOnce); - } - - @Override - public void onDismissCancelled() throws RemoteException { - mHandler.post(runOnce); - } - }, /* message= */ null); - } - boolean isHeadlessSystemUserMode() { return UserManager.isHeadlessSystemUserMode(); } @@ -4178,6 +4124,7 @@ class UserController implements Handler.Callback { void lockDeviceNowAndWaitForKeyguardShown() { if (getWindowManager().isKeyguardLocked()) { + Slogf.w(TAG, "Not locking the device since the keyguard is already locked"); return; } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 3f6484f0f58e..b9b06701a11b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -47,6 +47,7 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_SYSTEM; +import static android.media.IAudioManagerNative.HardeningType; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; import static android.media.audio.Flags.concurrentAudioRecordBypassPermission; @@ -151,6 +152,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; +import android.media.IAudioManagerNative; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; @@ -835,6 +837,18 @@ public class AudioService extends IAudioService.Stub private final UserRestrictionsListener mUserRestrictionsListener = new AudioServiceUserRestrictionsListener(); + private final IAudioManagerNative mNativeShim = new IAudioManagerNative.Stub() { + // oneway + @Override + public void playbackHardeningEvent(int uid, byte type, boolean bypassed) { + } + + @Override + public void permissionUpdateBarrier() { + AudioService.this.permissionUpdateBarrier(); + } + }; + // List of binder death handlers for setMode() client processes. // The last process to have called setMode() is at the top of the list. // package-private so it can be accessed in AudioDeviceBroker.getSetModeDeathHandlers @@ -2811,6 +2825,11 @@ public class AudioService extends IAudioService.Stub args, callback, resultReceiver); } + @Override + public IAudioManagerNative getNativeInterface() { + return mNativeShim; + } + /** @see AudioManager#getSurroundFormats() */ @Override public Map<Integer, Boolean> getSurroundFormats() { @@ -7214,7 +7233,7 @@ public class AudioService extends IAudioService.Stub final int pid = Binder.getCallingPid(); final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on) .append(") from u/pid:").append(uid).append("/") - .append(pid).toString(); + .append(pid).append(" src:AudioService.setBtA2dpOn").toString(); new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR + "setBluetoothA2dpOn") diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 24296406da00..93fdbc787ed0 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -316,6 +316,31 @@ final class InputGestureManager { } } + @Nullable + public InputGestureData getInputGesture(int userId, InputGestureData.Trigger trigger) { + synchronized (mGestureLock) { + if (mBlockListedTriggers.contains(trigger)) { + return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build(); + } + if (trigger instanceof InputGestureData.KeyTrigger keyTrigger) { + if (KeyEvent.isModifierKey(keyTrigger.getKeycode()) || + KeyEvent.isSystemKey(keyTrigger.getKeycode())) { + return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build(); + } + } + InputGestureData gestureData = mSystemShortcuts.get(trigger); + if (gestureData != null) { + return gestureData; + } + if (!mCustomInputGestures.contains(userId)) { + return null; + } + return mCustomInputGestures.get(userId).get(trigger); + } + } + @InputManager.CustomInputGestureResult public int addCustomInputGesture(int userId, InputGestureData newGesture) { synchronized (mGestureLock) { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index b2c35e1f362e..2ba35d6a70d2 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -3061,6 +3061,16 @@ public class InputManagerService extends IInputManager.Stub @Override @PermissionManuallyEnforced + public AidlInputGestureData getInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData.Trigger trigger) { + enforceManageKeyGesturePermission(); + + Objects.requireNonNull(trigger); + return mKeyGestureController.getInputGesture(userId, trigger); + } + + @Override + @PermissionManuallyEnforced public int addCustomInputGesture(@UserIdInt int userId, @NonNull AidlInputGestureData inputGestureData) { enforceManageKeyGesturePermission(); diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 5f7ad2797368..fb5ce5b4e5fa 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -18,6 +18,8 @@ package com.android.server.input; import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_WATCH; +import static android.os.UserManager.isVisibleBackgroundUsersEnabled; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; import static com.android.hardware.input.Flags.enableNew25q2Keycodes; @@ -55,7 +57,6 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArray; -import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -64,6 +65,8 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IShortcutService; +import com.android.server.LocalServices; +import com.android.server.pm.UserManagerInternal; import com.android.server.policy.KeyCombinationManager; import java.util.ArrayDeque; @@ -159,6 +162,10 @@ final class KeyGestureController { /** Currently fully consumed key codes per device */ private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>(); + private final UserManagerInternal mUserManagerInternal; + + private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); + KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) { mContext = context; mHandler = new Handler(looper, this::handleMessage); @@ -180,6 +187,7 @@ final class KeyGestureController { mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext); mInputGestureManager = new InputGestureManager(mContext); mInputDataStore = inputDataStore; + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); initBehaviors(); initKeyCombinationRules(); } @@ -449,6 +457,9 @@ final class KeyGestureController { } public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { + if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) { + return false; + } final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0; if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures() && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { @@ -457,6 +468,24 @@ final class KeyGestureController { return false; } + private boolean shouldIgnoreKeyEventForVisibleBackgroundUser(KeyEvent event) { + final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay( + event.getDisplayId()); + final int currentUserId; + synchronized (mUserLock) { + currentUserId = mCurrentUserId; + } + if (currentUserId != displayAssignedUserId + && !KeyEvent.isVisibleBackgroundUserAllowedKey(event.getKeyCode())) { + if (DEBUG) { + Slog.w(TAG, "Ignored key event [" + event + "] for visible background user [" + + displayAssignedUserId + "]"); + } + return true; + } + return false; + } + public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event, int policyFlags) { // TODO(b/358569822): Handle shortcuts trigger logic here and pass it to appropriate @@ -895,7 +924,7 @@ final class KeyGestureController { private void handleMultiKeyGesture(int[] keycodes, @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) { handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0, - gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags, + gestureType, action, DEFAULT_DISPLAY, /* focusedToken = */null, flags, /* appLaunchData = */null); } @@ -903,7 +932,7 @@ final class KeyGestureController { @Nullable AppLaunchData appLaunchData) { handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, new int[0], /* modifierState= */0, keyGestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, - Display.DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData); + DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData); } @VisibleForTesting @@ -915,6 +944,11 @@ final class KeyGestureController { } private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) { + if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY + && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType, + event.displayId)) { + return false; + } synchronized (mKeyGestureHandlerRecords) { for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { if (handler.handleKeyGesture(event, focusedToken)) { @@ -927,6 +961,24 @@ final class KeyGestureController { return false; } + private boolean shouldIgnoreGestureEventForVisibleBackgroundUser( + @KeyGestureEvent.KeyGestureType int gestureType, int displayId) { + final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(displayId); + final int currentUserId; + synchronized (mUserLock) { + currentUserId = mCurrentUserId; + } + if (currentUserId != displayAssignedUserId + && !KeyGestureEvent.isVisibleBackgrounduserAllowedGesture(gestureType)) { + if (DEBUG) { + Slog.w(TAG, "Ignored gesture event [" + gestureType + + "] for visible background user [" + displayAssignedUserId + "]"); + } + return true; + } + return false; + } + private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { synchronized (mKeyGestureHandlerRecords) { for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { @@ -943,7 +995,7 @@ final class KeyGestureController { // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally // should not rely on PWM to tell us about the gesture start and end. AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY, /* flags = */0, /* appLaunchData = */null); mHandler.obtainMessage(MSG_NOTIFY_KEY_GESTURE_EVENT, event).sendToTarget(); } @@ -951,7 +1003,7 @@ final class KeyGestureController { public void handleKeyGesture(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType) { AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY, /* flags = */0, /* appLaunchData = */null); handleKeyGesture(event, null /*focusedToken*/); } @@ -1069,6 +1121,18 @@ final class KeyGestureController { } @BinderThread + @Nullable + public AidlInputGestureData getInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData.Trigger trigger) { + InputGestureData gestureData = mInputGestureManager.getInputGesture(userId, + InputGestureData.createTriggerFromAidlTrigger(trigger)); + if (gestureData == null) { + return null; + } + return gestureData.getAidlData(); + } + + @BinderThread @InputManager.CustomInputGestureResult public int addCustomInputGesture(@UserIdInt int userId, @NonNull AidlInputGestureData inputGestureData) { diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index b0dff22c6f03..281db0ae9518 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -387,8 +387,9 @@ public final class ImeVisibilityStateComputer { @GuardedBy("ImfLock.class") void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) { final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); - if (state != null && newState.hasEditorFocused() - && newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS) { + if (state != null && newState.hasEditorFocused() && ( + newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS + || Flags.refactorInsetsController())) { // Inherit the last requested IME visible state when the target window is still // focused with an editor. newState.setRequestedImeVisible(state.mRequestedImeVisible); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index 87d809b5e850..1e54beeb2d64 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -32,7 +32,10 @@ import android.hardware.location.ContextHubTransaction; import android.hardware.location.IContextHubTransactionCallback; import android.os.Binder; import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.os.RemoteException; +import android.os.WorkSource; import android.util.Log; import android.util.SparseArray; @@ -54,6 +57,16 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** Message used by noteOp when this client receives a message from an endpoint. */ private static final String RECEIVE_MSG_NOTE = "ContextHubEndpointMessageDelivery"; + /** The duration of wakelocks acquired during HAL callbacks */ + private static final long WAKELOCK_TIMEOUT_MILLIS = 5 * 1000; + + /* + * Internal interface used to invoke client callbacks. + */ + interface CallbackConsumer { + void accept(IContextHubEndpointCallback callback) throws RemoteException; + } + /** The context of the service. */ private final Context mContext; @@ -134,6 +147,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private final int mUid; + /** Wakelock held while nanoapp message are in flight to the client */ + private final WakeLock mWakeLock; + /* package */ ContextHubEndpointBroker( Context context, IEndpointCommunication hubInterface, @@ -158,6 +174,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mAppOpsManager = context.getSystemService(AppOpsManager.class); mAppOpsManager.startWatchingMode(AppOpsManager.OP_NONE, mPackageName, this); + + PowerManager powerManager = context.getSystemService(PowerManager.class); + mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setWorkSource(new WorkSource(mUid, mPackageName)); + mWakeLock.setReferenceCounted(true); } @Override @@ -227,6 +248,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint()); + releaseWakeLockOnExit(); } @Override @@ -302,6 +324,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) + public void onCallbackFinished() { + super.onCallbackFinished_enforcePermission(); + releaseWakeLock(); + } + /** Invoked when the underlying binder of this broker has died at the client process. */ @Override public void binderDied() { @@ -357,15 +386,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true)); } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionOpenRequest( - sessionId, initiator, serviceDescriptor); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionOpenRequest", e); - cleanupSessionResources(sessionId); - return; - } + boolean success = + invokeCallback( + (consumer) -> + consumer.onSessionOpenRequest( + sessionId, initiator, serviceDescriptor)); + if (!success) { + cleanupSessionResources(sessionId); } } @@ -374,14 +401,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Log.w(TAG, "Unknown session ID in onCloseEndpointSession: id=" + sessionId); return; } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionClosed( - sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason)); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionClosed", e); - } - } + + invokeCallback( + (consumer) -> + consumer.onSessionClosed( + sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason))); } /* package */ void onEndpointSessionOpenComplete(int sessionId) { @@ -392,16 +416,30 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } mSessionInfoMap.get(sessionId).setSessionState(SessionInfo.SessionState.ACTIVE); } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionOpenComplete(sessionId); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionClosed", e); - } - } + + invokeCallback((consumer) -> consumer.onSessionOpenComplete(sessionId)); } /* package */ void onMessageReceived(int sessionId, HubMessage message) { + byte code = onMessageReceivedInternal(sessionId, message); + if (code != ErrorCode.OK && message.isResponseRequired()) { + sendMessageDeliveryStatus( + sessionId, message.getMessageSequenceNumber(), code); + } + } + + /* package */ void onMessageDeliveryStatusReceived( + int sessionId, int sequenceNumber, byte errorCode) { + mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); + } + + /* package */ boolean hasSessionId(int sessionId) { + synchronized (mOpenSessionLock) { + return mSessionInfoMap.contains(sessionId); + } + } + + private byte onMessageReceivedInternal(int sessionId, HubMessage message) { HubEndpointInfo remote; synchronized (mOpenSessionLock) { if (!isSessionActive(sessionId)) { @@ -411,9 +449,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + sessionId + ") with message: " + message); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMANENT_ERROR); - return; + return ErrorCode.PERMANENT_ERROR; } remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo(); } @@ -435,31 +471,12 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + ". " + mPackageName + " doesn't have permission"); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMISSION_DENIED); - return; - } - - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onMessageReceived(sessionId, message); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onMessageReceived", e); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR); - } + return ErrorCode.PERMISSION_DENIED; } - } - - /* package */ void onMessageDeliveryStatusReceived( - int sessionId, int sequenceNumber, byte errorCode) { - mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); - } - /* package */ boolean hasSessionId(int sessionId) { - synchronized (mOpenSessionLock) { - return mSessionInfoMap.contains(sessionId); - } + boolean success = + invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message)); + return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR; } /** @@ -520,4 +537,63 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Collection<String> requiredPermissions = targetEndpointInfo.getRequiredPermissions(); return ContextHubServiceUtil.hasPermissions(mContext, mPid, mUid, requiredPermissions); } + + private void acquireWakeLock() { + Binder.withCleanCallingIdentity( + () -> { + if (mIsRegistered.get()) { + mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); + } + }); + } + + private void releaseWakeLock() { + Binder.withCleanCallingIdentity( + () -> { + if (mWakeLock.isHeld()) { + try { + mWakeLock.release(); + } catch (RuntimeException e) { + Log.e(TAG, "Releasing the wakelock fails - ", e); + } + } + }); + } + + private void releaseWakeLockOnExit() { + Binder.withCleanCallingIdentity( + () -> { + while (mWakeLock.isHeld()) { + try { + mWakeLock.release(); + } catch (RuntimeException e) { + Log.e( + TAG, + "Releasing the wakelock for all acquisitions fails - ", + e); + break; + } + } + }); + } + + /** + * Invokes a callback and acquires a wakelock. + * + * @param consumer The callback invoke + * @return false if the callback threw a RemoteException + */ + private boolean invokeCallback(CallbackConsumer consumer) { + if (mContextHubEndpointCallback != null) { + acquireWakeLock(); + try { + consumer.accept(mContextHubEndpointCallback); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling endpoint callback", e); + releaseWakeLock(); + return false; + } + } + return true; + } } diff --git a/services/core/java/com/android/server/location/fudger/LocationFudger.java b/services/core/java/com/android/server/location/fudger/LocationFudger.java index 27577641ad1d..28e21b71dcc9 100644 --- a/services/core/java/com/android/server/location/fudger/LocationFudger.java +++ b/services/core/java/com/android/server/location/fudger/LocationFudger.java @@ -302,6 +302,15 @@ public class LocationFudger { // requires latitude since longitudinal distances change with distance from equator. private static double metersToDegreesLongitude(double distance, double lat) { - return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat)); + // Needed to convert from longitude distance to longitude degree. + // X meters near the poles is more degrees than at the equator. + double cosLat = Math.cos(Math.toRadians(lat)); + // If we are right on top of the pole, the degree is always 0. + // We return a very small value instead to avoid divide by zero errors + // later on. + if (cosLat == 0.0) { + return 0.0001; + } + return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / cosLat; } } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 286238e7888c..0d0cdd83cc73 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -438,9 +438,9 @@ public class LockSettingsService extends ILockSettings.Stub { } LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword(newPassword); - Arrays.fill(newPasswordChars, '\u0000'); - Arrays.fill(newPassword, (byte) 0); - Arrays.fill(randomLockSeed, (byte) 0); + LockPatternUtils.zeroize(newPasswordChars); + LockPatternUtils.zeroize(newPassword); + LockPatternUtils.zeroize(randomLockSeed); return credential; } @@ -1537,7 +1537,7 @@ public class LockSettingsService extends ILockSettings.Stub { + userId); } } finally { - Arrays.fill(password, (byte) 0); + LockPatternUtils.zeroize(password); } } @@ -1570,7 +1570,7 @@ public class LockSettingsService extends ILockSettings.Stub { decryptionResult = cipher.doFinal(encryptedPassword); LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword( decryptionResult); - Arrays.fill(decryptionResult, (byte) 0); + LockPatternUtils.zeroize(decryptionResult); try { long parentSid = getGateKeeperService().getSecureUserId( mUserManager.getProfileParent(userId).id); @@ -2263,7 +2263,7 @@ public class LockSettingsService extends ILockSettings.Stub { } catch (RemoteException e) { Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId); } finally { - Arrays.fill(secret, (byte) 0); + LockPatternUtils.zeroize(secret); } } diff --git a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java index 21caf76d30d0..3d64f1890073 100644 --- a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java +++ b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java @@ -26,6 +26,7 @@ import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; import java.security.GeneralSecurityException; @@ -154,7 +155,7 @@ public class UnifiedProfilePasswordCache { } LockscreenCredential result = LockscreenCredential.createUnifiedProfilePassword(credential); - Arrays.fill(credential, (byte) 0); + LockPatternUtils.zeroize(credential); return result; } } @@ -175,7 +176,7 @@ public class UnifiedProfilePasswordCache { Slog.d(TAG, "Cannot delete key", e); } if (mEncryptedPasswords.contains(userId)) { - Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0); + LockPatternUtils.zeroize(mEncryptedPasswords.get(userId)); mEncryptedPasswords.remove(userId); } } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java index bf1b3c3f0b35..85dc811a7811 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java @@ -162,7 +162,7 @@ public class KeySyncTask implements Runnable { Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e); } finally { if (mCredential != null) { - Arrays.fill(mCredential, (byte) 0); // no longer needed. + LockPatternUtils.zeroize(mCredential); // no longer needed. } } } @@ -506,7 +506,7 @@ public class KeySyncTask implements Runnable { try { byte[] hash = MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes); - Arrays.fill(bytes, (byte) 0); + LockPatternUtils.zeroize(bytes); return hash; } catch (NoSuchAlgorithmException e) { // Impossible, SHA-256 must be supported on Android. diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java index 54303c01890a..7d8300a8148a 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java @@ -1082,7 +1082,7 @@ public class RecoverableKeyStoreManager { int keyguardCredentialsType = lockPatternUtilsToKeyguardType(savedCredentialType); try (LockscreenCredential credential = createLockscreenCredential(keyguardCredentialsType, decryptedCredentials)) { - Arrays.fill(decryptedCredentials, (byte) 0); + LockPatternUtils.zeroize(decryptedCredentials); decryptedCredentials = null; VerifyCredentialResponse verifyResponse = lockSettingsService.verifyCredential(credential, userId, 0); diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java index 0e66746f4160..f1ef333d223a 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java @@ -19,8 +19,9 @@ package com.android.server.locksettings.recoverablekeystore.storage; import android.annotation.Nullable; import android.util.SparseArray; +import com.android.internal.widget.LockPatternUtils; + import java.util.ArrayList; -import java.util.Arrays; import javax.security.auth.Destroyable; @@ -187,8 +188,8 @@ public class RecoverySessionStorage implements Destroyable { */ @Override public void destroy() { - Arrays.fill(mLskfHash, (byte) 0); - Arrays.fill(mKeyClaimant, (byte) 0); + LockPatternUtils.zeroize(mLskfHash); + LockPatternUtils.zeroize(mKeyClaimant); } } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 68e195d7f079..35bb19943a24 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -302,7 +302,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub final long token = Binder.clearCallingIdentity(); try { - mAudioService.setBluetoothA2dpOn(on); + if (!Flags.disableSetBluetoothAd2pOnCalls()) { + mAudioService.setBluetoothA2dpOn(on); + } } catch (RemoteException ex) { Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn. on=" + on); } finally { @@ -677,7 +679,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub if (DEBUG) { Slog.d(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")"); } - mAudioService.setBluetoothA2dpOn(a2dpOn); + if (!Flags.disableSetBluetoothAd2pOnCalls()) { + mAudioService.setBluetoothA2dpOn(a2dpOn); + } } } catch (RemoteException e) { Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn."); diff --git a/services/core/java/com/android/server/media/TEST_MAPPING b/services/core/java/com/android/server/media/TEST_MAPPING index 43e2afd8827d..dbf9915c6e0c 100644 --- a/services/core/java/com/android/server/media/TEST_MAPPING +++ b/services/core/java/com/android/server/media/TEST_MAPPING @@ -1,7 +1,10 @@ { "presubmit": [ { - "name": "CtsMediaBetterTogetherTestCases" + "name": "CtsMediaRouterTestCases" + }, + { + "name": "CtsMediaSessionTestCases" }, { "name": "MediaRouterServiceTests" diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 86fc732e9d04..d440d3ab3521 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.hardware.tv.mediaquality.IMediaQuality; import android.media.quality.AmbientBacklightSettings; import android.media.quality.IAmbientBacklightCallback; import android.media.quality.IMediaQualityManager; @@ -35,9 +36,11 @@ import android.media.quality.SoundProfile; import android.media.quality.SoundProfileHandle; import android.os.Binder; import android.os.Bundle; +import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; import android.util.Log; import android.util.Pair; @@ -45,6 +48,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.server.SystemService; +import com.android.server.utils.Slogf; import org.json.JSONException; import org.json.JSONObject; @@ -74,6 +78,7 @@ public class MediaQualityService extends SystemService { private final BiMap<Long, String> mSoundProfileTempIdMap; private final PackageManager mPackageManager; private final SparseArray<UserState> mUserStates = new SparseArray<>(); + private IMediaQuality mMediaQuality; public MediaQualityService(Context context) { super(context); @@ -88,6 +93,12 @@ public class MediaQualityService extends SystemService { @Override public void onStart() { + IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default"); + if (binder != null) { + Slogf.d(TAG, "binder is not null"); + mMediaQuality = IMediaQuality.Stub.asInterface(binder); + } + publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService()); } @@ -809,10 +820,29 @@ public class MediaQualityService extends SystemService { if (!hasGlobalPictureQualityServicePermission()) { //TODO: error handling } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoPqEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto picture quality", e); + } } @Override public boolean isAutoPictureQualityEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoPqEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto picture quality", e); + } return false; } @@ -821,10 +851,29 @@ public class MediaQualityService extends SystemService { if (!hasGlobalPictureQualityServicePermission()) { //TODO: error handling } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoSrEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto super resolution", e); + } } @Override public boolean isSuperResolutionEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoSrEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto super resolution", e); + } return false; } @@ -833,10 +882,29 @@ public class MediaQualityService extends SystemService { if (!hasGlobalSoundQualityServicePermission()) { //TODO: error handling } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoAqEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto audio quality", e); + } } @Override public boolean isAutoSoundQualityEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoAqEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto audio quality", e); + } return false; } diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index 4b41696a4390..e47f8ae9d3a5 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -583,6 +583,15 @@ public class GroupHelper { final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( record.getUserId(), pkgName, sectioner); + // The notification was part of a different section => trigger regrouping + final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record); + if (prevSectionKey != null && !fullAggregateGroupKey.equals(prevSectionKey)) { + if (DEBUG) { + Slog.i(TAG, "Section changed for: " + record); + } + maybeUngroupOnSectionChanged(record, prevSectionKey); + } + // This notification is already aggregated if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) { return false; @@ -652,10 +661,33 @@ public class GroupHelper { } /** + * A notification was added that was previously part of a different section and needs to trigger + * GH state cleanup. + */ + private void maybeUngroupOnSectionChanged(NotificationRecord record, + FullyQualifiedGroupKey prevSectionKey) { + maybeUngroupWithSections(record, prevSectionKey); + if (record.getGroupKey().equals(prevSectionKey.toString())) { + record.setOverrideGroupKey(null); + } + } + + /** * A notification was added that is app-grouped. */ private void maybeUngroupOnAppGrouped(NotificationRecord record) { - maybeUngroupWithSections(record, getSectionGroupKeyWithFallback(record)); + FullyQualifiedGroupKey currentSectionKey = getSectionGroupKeyWithFallback(record); + + // The notification was part of a different section => trigger regrouping + final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record); + if (prevSectionKey != null && !prevSectionKey.equals(currentSectionKey)) { + if (DEBUG) { + Slog.i(TAG, "Section changed for: " + record); + } + currentSectionKey = prevSectionKey; + } + + maybeUngroupWithSections(record, currentSectionKey); } /** diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java index 7cbbe2938fd5..5a425057ea89 100644 --- a/services/core/java/com/android/server/notification/NotificationDelegate.java +++ b/services/core/java/com/android/server/notification/NotificationDelegate.java @@ -107,4 +107,9 @@ public interface NotificationDelegate { * @param key the notification key */ void unbundleNotification(String key); + /** + * Called when the notification should be rebundled. + * @param key the notification key + */ + void rebundleNotification(String key); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index dd9741ce9ca1..341038f878d9 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1888,6 +1888,36 @@ public class NotificationManagerService extends SystemService { } } } + + @Override + public void rebundleNotification(String key) { + if (!(notificationClassification() && notificationRegroupOnClassification())) { + return; + } + synchronized (mNotificationLock) { + NotificationRecord r = mNotificationsByKey.get(key); + if (r == null) { + return; + } + + if (DBG) { + Slog.v(TAG, "rebundleNotification: " + r); + } + + if (r.getBundleType() != Adjustment.TYPE_OTHER) { + final Bundle classifBundle = new Bundle(); + classifBundle.putInt(KEY_TYPE, r.getBundleType()); + Adjustment adj = new Adjustment(r.getSbn().getPackageName(), r.getKey(), + classifBundle, "rebundle", r.getUserId()); + applyAdjustmentLocked(r, adj, /* isPosted= */ true); + mRankingHandler.requestSort(); + } else { + if (DBG) { + Slog.w(TAG, "Can't rebundle. No valid bundle type for: " + r); + } + } + } + } }; NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() { @@ -7134,6 +7164,7 @@ public class NotificationManagerService extends SystemService { adjustments.putParcelable(KEY_TYPE, newChannel); logClassificationChannelAdjustmentReceived(r, isPosted, classification); + r.setBundleType(classification); } } r.addAdjustment(adjustment); @@ -9537,7 +9568,8 @@ public class NotificationManagerService extends SystemService { || !Objects.equals(oldSbn.getNotification().getGroup(), n.getNotification().getGroup()) || oldSbn.getNotification().flags - != n.getNotification().flags) { + != n.getNotification().flags + || !old.getChannel().getId().equals(r.getChannel().getId())) { synchronized (mNotificationLock) { final String autogroupName = notificationForceGrouping() ? diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 93f512bc7e17..81af0d8a6d80 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -36,10 +36,7 @@ import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.Person; -import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ShortcutInfo; @@ -48,7 +45,6 @@ import android.media.AudioAttributes; import android.media.AudioSystem; import android.metrics.LogMaker; import android.net.Uri; -import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -226,6 +222,9 @@ public final class NotificationRecord { // lifetime extended. private boolean mCanceledAfterLifetimeExtension = false; + // type of the bundle if the notification was classified + private @Adjustment.Types int mBundleType = Adjustment.TYPE_OTHER; + public NotificationRecord(Context context, StatusBarNotification sbn, NotificationChannel channel) { this.sbn = sbn; @@ -471,6 +470,10 @@ public final class NotificationRecord { } } + if (android.service.notification.Flags.notificationClassification()) { + mBundleType = previous.mBundleType; + } + // Don't copy importance information or mGlobalSortKey, recompute them. } @@ -1493,23 +1496,14 @@ public final class NotificationRecord { final Notification notification = getNotification(); notification.visitUris((uri) -> { - if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { - visitGrantableUri(uri, false, false); - } else { - oldVisitGrantableUri(uri, false, false); - } + visitGrantableUri(uri, false, false); }); if (notification.getChannelId() != null) { NotificationChannel channel = getChannel(); if (channel != null) { - if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { - visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); - } else { - oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); - } + visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); } } } finally { @@ -1525,53 +1519,6 @@ public final class NotificationRecord { * {@link #mGrantableUris}. Otherwise, this will either log or throw * {@link SecurityException} depending on target SDK of enqueuing app. */ - private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { - if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; - - if (mGrantableUris != null && mGrantableUris.contains(uri)) { - return; // already verified this URI - } - - final int sourceUid = getSbn().getUid(); - final long ident = Binder.clearCallingIdentity(); - try { - // This will throw a SecurityException if the caller can't grant. - mUgmInternal.checkGrantUriPermission(sourceUid, null, - ContentProvider.getUriWithoutUserId(uri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); - - if (mGrantableUris == null) { - mGrantableUris = new ArraySet<>(); - } - mGrantableUris.add(uri); - } catch (SecurityException e) { - if (!userOverriddenUri) { - if (isSound) { - mSound = Settings.System.DEFAULT_NOTIFICATION_URI; - Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage()); - } else { - if (mTargetSdkVersion >= Build.VERSION_CODES.P) { - throw e; - } else { - Log.w(TAG, - "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage()); - } - } - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - /** - * Note the presence of a {@link Uri} that should have permission granted to - * whoever will be rendering it. - * <p> - * If the enqueuing app has the ability to grant access, it will be added to - * {@link #mGrantableUris}. Otherwise, this will either log or throw - * {@link SecurityException} depending on target SDK of enqueuing app. - */ private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { if (mGrantableUris != null && mGrantableUris.contains(uri)) { @@ -1689,6 +1636,14 @@ public final class NotificationRecord { mCanceledAfterLifetimeExtension = canceledAfterLifetimeExtension; } + public @Adjustment.Types int getBundleType() { + return mBundleType; + } + + public void setBundleType(@Adjustment.Types int bundleType) { + mBundleType = bundleType; + } + /** * Whether this notification is a conversation notification. */ diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 9d25d18df87c..36eabae69b22 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -1187,9 +1187,7 @@ public class PreferencesHelper implements RankingConfig { // Verify that the app has permission to read the sound Uri // Only check for new channels, as regular apps can only set sound // before creating. See: {@link NotificationChannel#setSound} - if (Flags.notificationVerifyChannelSoundUri()) { - PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); - } + PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); channel.setImportanceLockedByCriticalDeviceFunction( r.defaultAppLockedImportance || r.fixedImportance); diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 2b4d71e85dc0..c1ca9c23aef5 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -172,16 +172,6 @@ flag { } flag { - name: "notification_verify_channel_sound_uri" - namespace: "systemui" - description: "Verify Uri permission for sound when creating a notification channel" - bug: "337775777" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notification_vibration_in_sound_uri_for_channel" namespace: "systemui" description: "Enables sound uri with vibration source in notification channel" diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4c70d2347fb7..a0fbc008475c 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -125,7 +125,6 @@ import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.VerifierInfo; -import android.content.pm.dex.DexMetadataHelper; import android.content.pm.parsing.result.ParseResult; import android.content.pm.parsing.result.ParseTypeImpl; import android.net.Uri; @@ -171,7 +170,6 @@ import com.android.internal.pm.pkg.component.ParsedIntentInfo; import com.android.internal.pm.pkg.component.ParsedPermission; import com.android.internal.pm.pkg.component.ParsedPermissionGroup; import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; -import com.android.internal.security.VerityUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.server.EventLogTags; @@ -186,7 +184,6 @@ import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.SharedLibraryWrapper; import com.android.server.rollback.RollbackManagerInternal; -import com.android.server.security.FileIntegrityService; import com.android.server.utils.WatchedArrayMap; import com.android.server.utils.WatchedLongSparseArray; @@ -195,7 +192,6 @@ import dalvik.system.VMRuntime; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.security.DigestException; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -1165,11 +1161,8 @@ final class InstallPackageHelper { } try { doRenameLI(request, parsedPackage); - setUpFsVerity(parsedPackage); - } catch (Installer.InstallerException | IOException | DigestException - | NoSuchAlgorithmException | PrepareFailure e) { - request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP, - "Failed to set up verity: " + e); + } catch (PrepareFailure e) { + request.setError(e); return false; } @@ -2322,68 +2315,6 @@ final class InstallPackageHelper { } } - /** - * Set up fs-verity for the given package. For older devices that do not support fs-verity, - * this is a no-op. - */ - private void setUpFsVerity(AndroidPackage pkg) throws Installer.InstallerException, - PrepareFailure, IOException, DigestException, NoSuchAlgorithmException { - if (!PackageManagerServiceUtils.isApkVerityEnabled()) { - return; - } - - if (isIncrementalPath(pkg.getPath()) && IncrementalManager.getVersion() - < IncrementalManager.MIN_VERSION_TO_SUPPORT_FSVERITY) { - return; - } - - // Collect files we care for fs-verity setup. - ArrayMap<String, String> fsverityCandidates = new ArrayMap<>(); - fsverityCandidates.put(pkg.getBaseApkPath(), - VerityUtils.getFsveritySignatureFilePath(pkg.getBaseApkPath())); - - final String dmPath = DexMetadataHelper.buildDexMetadataPathForApk( - pkg.getBaseApkPath()); - if (new File(dmPath).exists()) { - fsverityCandidates.put(dmPath, VerityUtils.getFsveritySignatureFilePath(dmPath)); - } - - for (String path : pkg.getSplitCodePaths()) { - fsverityCandidates.put(path, VerityUtils.getFsveritySignatureFilePath(path)); - - final String splitDmPath = DexMetadataHelper.buildDexMetadataPathForApk(path); - if (new File(splitDmPath).exists()) { - fsverityCandidates.put(splitDmPath, - VerityUtils.getFsveritySignatureFilePath(splitDmPath)); - } - } - - var fis = FileIntegrityService.getService(); - for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) { - try { - final String filePath = entry.getKey(); - if (VerityUtils.hasFsverity(filePath)) { - continue; - } - - final String signaturePath = entry.getValue(); - if (new File(signaturePath).exists()) { - // If signature is provided, enable fs-verity first so that the file can be - // measured for signature check below. - VerityUtils.setUpFsverity(filePath); - - if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) { - throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, - "fs-verity signature does not verify against a known key"); - } - } - } catch (IOException e) { - throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, - "Failed to enable fs-verity: " + e); - } - } - } - private PackageFreezer freezePackageForInstall(String packageName, int userId, int installFlags, String killReason, int exitInfoReason, InstallRequest request) { if ((installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) { @@ -3060,6 +2991,8 @@ final class InstallPackageHelper { } if (succeeded) { + Slog.i(TAG, "installation completed:" + packageName); + if (Flags.aslInApkAppMetadataSource() && pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) { if (!extractAppMetadataFromApk(request.getPkg(), diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index c6760431116e..1b41c3617a05 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -26,7 +26,6 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH; import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; -import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; @@ -824,8 +823,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private File mInheritedFilesBase; - @GuardedBy("mLock") - private boolean mVerityFoundForApks; /** * Both flags should be guarded with mLock whenever changes need to be in lockstep. @@ -864,7 +861,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } else { if (DexMetadataHelper.isDexMetadataFile(file)) return false; } - if (VerityUtils.isFsveritySignatureFile(file)) return false; if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false; return true; } @@ -3565,13 +3561,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { "Missing existing base package"); } - // Default to require only if existing base apk has fs-verity signature. - mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled() - && params.mode == SessionParams.MODE_INHERIT_EXISTING - && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath()) - && (new File(VerityUtils.getFsveritySignatureFilePath( - pkgInfo.applicationInfo.getBaseCodePath()))).exists(); - final List<File> removedFiles = getRemovedFilesLocked(); final List<String> removeSplitList = new ArrayList<>(); if (!removedFiles.isEmpty()) { @@ -3972,24 +3961,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private void maybeStageFsveritySignatureLocked(File origFile, File targetFile, - boolean fsVerityRequired) throws PackageManagerException { - if (android.security.Flags.deprecateFsvSig()) { - return; - } - final File originalSignature = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (originalSignature.exists()) { - final File stagedSignature = new File( - VerityUtils.getFsveritySignatureFilePath(targetFile.getPath())); - stageFileLocked(originalSignature, stagedSignature); - } else if (fsVerityRequired) { - throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, - "Missing corresponding fs-verity signature to " + origFile); - } - } - - @GuardedBy("mLock") private void maybeStageV4SignatureLocked(File origFile, File targetFile) throws PackageManagerException { final File originalSignature = new File(origFile.getPath() + V4Signature.EXT); @@ -4015,11 +3986,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { DexMetadataHelper.buildDexMetadataPathForApk(targetFile.getName())); stageFileLocked(dexMetadataFile, targetDexMetadataFile); - - // Also stage .dm.fsv_sig. .dm may be required to install with fs-verity signature on - // supported on older devices. - maybeStageFsveritySignatureLocked(dexMetadataFile, targetDexMetadataFile, - DexMetadataHelper.isFsVerityRequired()); } @FlaggedApi(com.android.art.flags.Flags.FLAG_ART_SERVICE_V3) @@ -4105,44 +4071,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private boolean isFsVerityRequiredForApk(File origFile, File targetFile) - throws PackageManagerException { - if (mVerityFoundForApks) { - return true; - } - - // We haven't seen .fsv_sig for any APKs. Treat it as not required until we see one. - final File originalSignature = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (!originalSignature.exists()) { - return false; - } - mVerityFoundForApks = true; - - // When a signature is found, also check any previous staged APKs since they also need to - // have fs-verity signature consistently. - for (File file : mResolvedStagedFiles) { - if (!file.getName().endsWith(".apk")) { - continue; - } - // Ignore the current targeting file. - if (targetFile.getName().equals(file.getName())) { - continue; - } - throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, - "Previously staged apk is missing fs-verity signature"); - } - return true; - } - - @GuardedBy("mLock") private void resolveAndStageFileLocked(File origFile, File targetFile, String splitName, List<String> artManagedFilePaths) throws PackageManagerException { stageFileLocked(origFile, targetFile); - // Stage APK's fs-verity signature if present. - maybeStageFsveritySignatureLocked(origFile, targetFile, - isFsVerityRequiredForApk(origFile, targetFile)); // Stage APK's v4 signature if present, and fs-verity is supported. if (android.security.Flags.extendVbChainToUpdatedApk() && VerityUtils.isFsVeritySupported()) { @@ -4160,16 +4092,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private void maybeInheritFsveritySignatureLocked(File origFile) { - // Inherit the fsverity signature file if present. - final File fsveritySignatureFile = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (fsveritySignatureFile.exists()) { - mResolvedInheritedFiles.add(fsveritySignatureFile); - } - } - - @GuardedBy("mLock") private void maybeInheritV4SignatureLocked(File origFile) { // Inherit the v4 signature file if present. final File v4SignatureFile = new File(origFile.getPath() + V4Signature.EXT); @@ -4182,7 +4104,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private void inheritFileLocked(File origFile, List<String> artManagedFilePaths) { mResolvedInheritedFiles.add(origFile); - maybeInheritFsveritySignatureLocked(origFile); if (android.security.Flags.extendVbChainToUpdatedApk()) { maybeInheritV4SignatureLocked(origFile); } @@ -4193,13 +4114,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { artManagedFilePaths, origFile.getPath())) { File artManagedFile = new File(path); mResolvedInheritedFiles.add(artManagedFile); - maybeInheritFsveritySignatureLocked(artManagedFile); } } else { final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(origFile); if (dexMetadataFile != null) { mResolvedInheritedFiles.add(dexMetadataFile); - maybeInheritFsveritySignatureLocked(dexMetadataFile); } } // Inherit the digests if present. diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 7af39f74d0d6..3e376b6958ec 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -520,22 +520,6 @@ public class PackageManagerServiceUtils { } } - /** Default is to not use fs-verity since it depends on kernel support. */ - private static final int FSVERITY_DISABLED = 0; - - /** Standard fs-verity. */ - private static final int FSVERITY_ENABLED = 2; - - /** Returns true if standard APK Verity is enabled. */ - static boolean isApkVerityEnabled() { - if (android.security.Flags.deprecateFsvSig()) { - return false; - } - return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R - || SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED) - == FSVERITY_ENABLED; - } - /** * Verifies that signatures match. * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}. diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 8249d65868cd..81956fbb55e6 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -6661,7 +6661,7 @@ public class UserManagerService extends IUserManager.Stub { + userId); } new Thread(() -> { - getActivityManagerInternal().onUserRemoved(userId); + getActivityManagerInternal().onUserRemoving(userId); removeUserState(userId); }).start(); } @@ -6701,6 +6701,7 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUsersLock) { removeUserDataLU(userId); mIsUserManaged.delete(userId); + getActivityManagerInternal().onUserRemoved(userId); } synchronized (mUserStates) { mUserStates.delete(userId); diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index a6f2a3757dcb..1cf24fcd8594 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -20,7 +20,6 @@ import static android.os.Flags.adpfUseFmqChannel; import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR; import static com.android.server.power.hint.Flags.adpfSessionTag; -import static com.android.server.power.hint.Flags.cpuHeadroomAffinityCheck; import static com.android.server.power.hint.Flags.powerhintThreadCleanup; import static com.android.server.power.hint.Flags.resetOnForkEnabled; @@ -1604,8 +1603,7 @@ public final class HintManagerService extends SystemService { } } } - if (cpuHeadroomAffinityCheck() && mCheckHeadroomAffinity - && params.tids.length > 1) { + if (mCheckHeadroomAffinity && params.tids.length > 1) { checkThreadAffinityForTids(params.tids); } halParams.tids = params.tids; diff --git a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java index 17739712d65a..a75d110e3cd1 100644 --- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java +++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java @@ -88,5 +88,6 @@ public class ResourcesManagerShellCommand extends ShellCommand { out.println(" Print this help text."); out.println(" dump <PROCESS>"); out.println(" Dump the Resources objects in use as well as the history of Resources"); + } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 4ed5f90f2852..a19a3422af06 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -2199,6 +2199,19 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D }); } + /** + * Called when the notification should be rebundled. + * @param key the notification key + */ + @Override + public void rebundleNotification(String key) { + enforceStatusBarService(); + enforceValidCallingUser(); + Binder.withCleanCallingIdentity(() -> { + mNotificationDelegate.rebundleNotification(key); + }); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 3e6315635571..1fe61590a531 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -232,6 +232,7 @@ import static com.android.server.wm.IdentifierProto.USER_ID; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; @@ -2814,9 +2815,27 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A attachStartingSurfaceToAssociatedTask(); } + /** + * If the device is locked and the app does not request showWhenLocked, + * defer removing the starting window until the transition is complete. + * This prevents briefly appearing the app context and causing secure concern. + */ + void deferStartingWindowRemovalForKeyguardUnoccluding() { + if (mStartingData.mRemoveAfterTransaction != AFTER_TRANSITION_FINISH + && isKeyguardLocked() && !canShowWhenLockedInner(this) && !isVisibleRequested() + && mTransitionController.inTransition(this)) { + mStartingData.mRemoveAfterTransaction = AFTER_TRANSITION_FINISH; + } + } + void removeStartingWindow() { boolean prevEligibleForLetterboxEducation = isEligibleForLetterboxEducation(); + if (mStartingData != null + && mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) { + return; + } + if (transferSplashScreenIfNeeded()) { return; } @@ -4261,7 +4280,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } void finishRelaunching() { - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .setRelaunchingAfterRequestedOrientationChanged(false); mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this); @@ -4655,6 +4674,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A tStartingWindow.mToken = this; tStartingWindow.mActivityRecord = this; + if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) { + mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE; + } if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) { // The removal of starting window should wait for window drawn of current // activity. @@ -8125,10 +8147,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) { // We use Task here because we want to be consistent with what happens in // multi-window mode where other tasks orientations are ignored. - final ActivityRecord belowCandidate = task.getActivity( - a -> a.canDefineOrientationForActivitiesAbove() /* callback */, - this /* boundary */, false /* includeBoundary */, - true /* traverseTopToBottom */); + final ActivityRecord belowCandidate = task.getActivityBelowForDefiningOrientation(this); if (belowCandidate != null) { return belowCandidate.getRequestedConfigurationOrientation(forDisplay); } @@ -8222,7 +8241,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLastReportedConfiguration.getMergedConfiguration())) { ensureActivityConfiguration(false /* ignoreVisibility */); if (mPendingRelaunchCount > originalRelaunchingCount) { - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .setRelaunchingAfterRequestedOrientationChanged(true); } if (mTransitionController.inPlayingTransition(this)) { @@ -10217,7 +10236,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAppCompatController.getAppCompatAspectRatioOverrides() .shouldOverrideMinAspectRatio()); proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP, - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .shouldIgnoreOrientationRequestLoop()); proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP, mAppCompatController.getResizeOverrides().shouldOverrideForceResizeApp()); diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index c748264a833e..6d0e8eacd438 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -89,8 +89,8 @@ class AppCompatController { } @NonNull - AppCompatOrientationOverrides getAppCompatOrientationOverrides() { - return mAppCompatOverrides.getAppCompatOrientationOverrides(); + AppCompatOrientationOverrides getOrientationOverrides() { + return mAppCompatOverrides.getOrientationOverrides(); } @NonNull diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java index 8866e39fecc9..449458665b63 100644 --- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java @@ -154,7 +154,7 @@ class AppCompatLetterboxPolicy { @VisibleForTesting boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) { - if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + if (mActivityRecord.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged()) { return mLastShouldShowLetterboxUi; } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java index c84711d4be51..af83668f1188 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java @@ -113,7 +113,7 @@ class AppCompatOrientationOverrides { // Task to ensure that Activity Embedding is excluded. return mActivityRecord.isVisibleRequested() && mActivityRecord.getTaskFragment() != null && mActivityRecord.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN - && mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + && mActivityRecord.mAppCompatController.getOrientationOverrides() .isOverrideRespectRequestedOrientationEnabled(); } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index 16e20297dcf3..fc758ef90995 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -94,7 +94,7 @@ class AppCompatOrientationPolicy { return SCREEN_ORIENTATION_PORTRAIT; } - if (mAppCompatOverrides.getAppCompatOrientationOverrides() + if (mAppCompatOverrides.getOrientationOverrides() .isAllowOrientationOverrideOptOut()) { return candidate; } @@ -108,7 +108,7 @@ class AppCompatOrientationPolicy { } final AppCompatOrientationOverrides.OrientationOverridesState capabilityState = - mAppCompatOverrides.getAppCompatOrientationOverrides() + mAppCompatOverrides.getOrientationOverrides() .mOrientationOverridesState; if (capabilityState.mIsOverrideToReverseLandscapeOrientationEnabled @@ -170,7 +170,7 @@ class AppCompatOrientationPolicy { boolean shouldIgnoreRequestedOrientation( @ActivityInfo.ScreenOrientation int requestedOrientation) { final AppCompatOrientationOverrides orientationOverrides = - mAppCompatOverrides.getAppCompatOrientationOverrides(); + mAppCompatOverrides.getOrientationOverrides(); if (orientationOverrides.shouldEnableIgnoreOrientationRequest()) { if (orientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) { Slog.w(TAG, "Ignoring orientation update to " diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java index 9effae6aa640..9fb54db23d55 100644 --- a/services/core/java/com/android/server/wm/AppCompatOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java @@ -27,7 +27,7 @@ import com.android.server.wm.utils.OptPropFactory; public class AppCompatOverrides { @NonNull - private final AppCompatOrientationOverrides mAppCompatOrientationOverrides; + private final AppCompatOrientationOverrides mOrientationOverrides; @NonNull private final AppCompatCameraOverrides mAppCompatCameraOverrides; @NonNull @@ -48,7 +48,7 @@ public class AppCompatOverrides { @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) { mAppCompatCameraOverrides = new AppCompatCameraOverrides(activityRecord, appCompatConfiguration, optPropBuilder); - mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(activityRecord, + mOrientationOverrides = new AppCompatOrientationOverrides(activityRecord, appCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides); mReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord, appCompatConfiguration, appCompatDeviceStateQuery); @@ -64,8 +64,8 @@ public class AppCompatOverrides { } @NonNull - AppCompatOrientationOverrides getAppCompatOrientationOverrides() { - return mAppCompatOrientationOverrides; + AppCompatOrientationOverrides getOrientationOverrides() { + return mOrientationOverrides; } @NonNull diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index f40d636b522a..b932ef362aca 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -264,7 +264,7 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { // that should be respected, Check all activities in display to make sure any eligible // activity should be respected. final ActivityRecord activity = mDisplayContent.getActivity((r) -> - r.mAppCompatController.getAppCompatOrientationOverrides() + r.mAppCompatController.getOrientationOverrides() .shouldRespectRequestedOrientationDueToOverride()); return activity != null; } diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java index d49a507c9e11..5bec4424269a 100644 --- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java @@ -25,6 +25,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; +import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; import static android.window.DisplayAreaOrganizer.FEATURE_FULLSCREEN_MAGNIFICATION; import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT; @@ -151,6 +153,12 @@ public abstract class DisplayAreaPolicy { .all() .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_SECURE_SYSTEM_OVERLAY) + .build()) + .addFeature(new Feature.Builder(wmService.mPolicy, "AppZoomOut", + FEATURE_APP_ZOOM_OUT) + .all() + .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, + TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE, TYPE_WALLPAPER) .build()); } rootHierarchy diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 145c7b37fcdc..d32c31f1c1c7 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1822,7 +1822,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ private void applyFixedRotationForNonTopVisibleActivityIfNeeded(@NonNull ActivityRecord ar, @ActivityInfo.ScreenOrientation int topOrientation) { - final int orientation = ar.getRequestedOrientation(); + int orientation = ar.getRequestedOrientation(); + if (orientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) { + final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(ar); + if (nextCandidate != null) { + orientation = nextCandidate.getRequestedOrientation(); + } + } if (orientation == topOrientation || ar.inMultiWindowMode() || ar.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) { return; @@ -1864,9 +1870,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return ROTATION_UNDEFINED; } if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) { - final ActivityRecord nextCandidate = getActivity( - a -> a.canDefineOrientationForActivitiesAbove() /* callback */, - r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */); + final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(r); if (nextCandidate != null) { r = nextCandidate; activityOrientation = r.getOverrideOrientation(); @@ -2964,7 +2968,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (!handlesOrientationChangeFromDescendant(orientation)) { ActivityRecord topActivity = topRunningActivity(/* considerKeyguardState= */ true); if (topActivity != null && topActivity.mAppCompatController - .getAppCompatOrientationOverrides() + .getOrientationOverrides() .shouldUseDisplayLandscapeNaturalOrientation()) { ProtoLog.v(WM_DEBUG_ORIENTATION, "Display id=%d is ignoring orientation request for %d, return %d" @@ -4291,7 +4295,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return target; } if (android.view.inputmethod.Flags.refactorInsetsController()) { - final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked(); + final DisplayContent defaultDc = getUserMainDisplayContent(); return defaultDc.mRemoteInsetsControlTarget; } else { return getImeFallback(); @@ -4301,11 +4305,26 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp InsetsControlTarget getImeFallback() { // host is in non-default display that doesn't support system decor, default to // default display's StatusBar to control IME (when available), else let system control it. - final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked(); - WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar(); + final DisplayContent defaultDc = getUserMainDisplayContent(); + final WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar(); return statusBar != null ? statusBar : defaultDc.mRemoteInsetsControlTarget; } + private DisplayContent getUserMainDisplayContent() { + final DisplayContent defaultDc; + if (android.view.inputmethod.Flags.fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId); + defaultDc = mWmService.getUserMainDisplayContentLocked(userId); + if (defaultDc == null) { + throw new IllegalStateException( + "No default display was assigned to user " + userId); + } + } else { + defaultDc = mWmService.getDefaultDisplayContentLocked(); + } + return defaultDc; + } + /** * Returns the corresponding IME insets control target according the IME target type. * @@ -4841,8 +4860,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // The control target could be the RemoteInsetsControlTarget (if the focussed // view is on a virtual display that can not show the IME (and therefore it will // be shown on the default display) - if (isDefaultDisplay && mRemoteInsetsControlTarget != null) { - return mRemoteInsetsControlTarget; + if (android.view.inputmethod.Flags + .fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + if (isUserMainDisplay() && mRemoteInsetsControlTarget != null) { + return mRemoteInsetsControlTarget; + } + } else { + if (isDefaultDisplay && mRemoteInsetsControlTarget != null) { + return mRemoteInsetsControlTarget; + } } } return null; @@ -4858,6 +4884,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * Returns {@code true} if {@link #mDisplayId} corresponds to the user's main display. + * + * <p>Visible background users may have other than DEFAULT_DISPLAY marked as their main display. + */ + private boolean isUserMainDisplay() { + final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId); + return mDisplayId == mWmService.mUmInternal.getMainDisplayAssignedToUser(userId); + } + + /** * Computes the window the IME should be attached to. */ @VisibleForTesting diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index 3a0e41a5f9f8..b3e9244d108d 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -45,6 +45,7 @@ import android.annotation.Nullable; import android.content.ClipData; import android.content.ClipDescription; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.os.Binder; import android.os.Build; @@ -70,6 +71,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.internal.view.IDragAndDropPermissions; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.concurrent.CompletableFuture; @@ -542,10 +544,26 @@ class DragState { } } ClipDescription description = data != null ? data.getDescription() : mDataDescription; + + // Note this can be negative numbers if touch coords are left or top of the window. + PointF relativeToWindowCoords = new PointF(newWin.translateToWindowX(touchX), + newWin.translateToWindowY(touchY)); + if (Flags.enableConnectedDisplaysDnd() + && mDisplayContent.getDisplayId() != newWin.getDisplayId()) { + // Currently DRAG_STARTED coords are sent relative to the window target in **px** + // coordinates. However, this cannot be extended to connected displays scenario, + // as there's only global **dp** coordinates and no global **px** coordinates. + // Hence, the coords sent here will only try to indicate that drag started outside + // this window display, but relative distance should not be calculated or depended + // on. + relativeToWindowCoords = new PointF(-newWin.getBounds().left - 1, + -newWin.getBounds().top - 1); + } + DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED, - newWin.translateToWindowX(touchX), newWin.translateToWindowY(touchY), - description, data, false /* includeDragSurface */, - true /* includeDragFlags */, null /* dragAndDropPermission */); + relativeToWindowCoords.x, relativeToWindowCoords.y, description, data, + false /* includeDragSurface */, true /* includeDragFlags */, + null /* dragAndDropPermission */); try { newWin.mClient.dispatchDragEvent(event); // track each window that we've notified that the drag is starting diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java index 9dc3d6a81338..bc16a566bfef 100644 --- a/services/core/java/com/android/server/wm/PersisterQueue.java +++ b/services/core/java/com/android/server/wm/PersisterQueue.java @@ -86,6 +86,34 @@ class PersisterQueue { mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread"); } + /** + * Busy wait until {@link #mLazyTaskWriterThread} is in {@link Thread.State#WAITING}, or + * times out. This indicates the thread is waiting for new tasks to appear. If the wait + * succeeds, this queue waits at least {@link #mPreTaskDelayMs} milliseconds before running the + * next task. + * + * <p>This is for testing purposes only. + * + * @param timeoutMillis the maximum time of waiting in milliseconds + * @return {@code true} if the thread is in {@link Thread.State#WAITING} at return + */ + @VisibleForTesting + boolean waitUntilWritingThreadIsWaiting(long timeoutMillis) { + final long timeoutTime = SystemClock.uptimeMillis() + timeoutMillis; + do { + Thread.State state; + synchronized (this) { + state = mLazyTaskWriterThread.getState(); + } + if (state == Thread.State.WAITING) { + return true; + } + Thread.yield(); + } while (SystemClock.uptimeMillis() < timeoutTime); + + return false; + } + synchronized void startPersisting() { if (!mLazyTaskWriterThread.isAlive()) { mLazyTaskWriterThread.start(); diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java index a5454546341b..3eb13c52cca6 100644 --- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java +++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java @@ -407,10 +407,8 @@ class SnapshotPersistQueue { bitmap.recycle(); final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId); - try { - FileOutputStream fos = new FileOutputStream(file); + try (FileOutputStream fos = new FileOutputStream(file)) { swBitmap.compress(JPEG, COMPRESS_QUALITY, fos); - fos.close(); } catch (IOException e) { Slog.e(TAG, "Unable to open " + file + " for persisting.", e); return false; @@ -428,10 +426,8 @@ class SnapshotPersistQueue { swBitmap.recycle(); final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId); - try { - FileOutputStream lowResFos = new FileOutputStream(lowResFile); + try (FileOutputStream lowResFos = new FileOutputStream(lowResFile)) { lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos); - lowResFos.close(); } catch (IOException e) { Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e); return false; diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java index 7349224ddcd8..1a7a6196cf85 100644 --- a/services/core/java/com/android/server/wm/StartingData.java +++ b/services/core/java/com/android/server/wm/StartingData.java @@ -31,11 +31,18 @@ public abstract class StartingData { static final int AFTER_TRANSACTION_REMOVE_DIRECTLY = 1; /** Do copy splash screen to client after transaction done. */ static final int AFTER_TRANSACTION_COPY_TO_CLIENT = 2; + /** + * Remove the starting window after transition finish. + * Used when activity doesn't request show when locked, so the app window should never show to + * the user if device is locked. + **/ + static final int AFTER_TRANSITION_FINISH = 3; @IntDef(prefix = { "AFTER_TRANSACTION" }, value = { AFTER_TRANSACTION_IDLE, AFTER_TRANSACTION_REMOVE_DIRECTLY, AFTER_TRANSACTION_COPY_TO_CLIENT, + AFTER_TRANSITION_FINISH, }) @interface AfterTransaction {} diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d92301ba4f6f..9c1cf6e6bf62 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5255,6 +5255,10 @@ class Task extends TaskFragment { return false; } + if (!mTaskSupervisor.readyToResume()) { + return false; + } + final ActivityRecord topActivity = topRunningActivity(true /* focusableOnly */); if (topActivity == null) { // There are no activities left in this task, let's look somewhere else. diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a9646783b92d..f4a455a9c2dd 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -70,6 +70,8 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN; +import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; @@ -1374,6 +1376,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { enterAutoPip = true; } } + + if (ar.mStartingData != null && ar.mStartingData.mRemoveAfterTransaction + == AFTER_TRANSITION_FINISH + && (!ar.isVisible() || !ar.mTransitionController.inTransition(ar))) { + ar.mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE; + ar.removeStartingWindow(); + } final ChangeInfo changeInfo = mChanges.get(ar); // Due to transient-hide, there may be some activities here which weren't in the // transition. diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index aa60f939f9aa..54a3d4179e3d 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1659,6 +1659,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return ORIENTATION_UNDEFINED; } + @Nullable + ActivityRecord getActivityBelowForDefiningOrientation(ActivityRecord from) { + return getActivity(ActivityRecord::canDefineOrientationForActivitiesAbove, + from /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */); + } + /** * Calls {@link #setOrientation(int, WindowContainer)} with {@code null} to the last 2 * parameters. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 92e0931993d1..e4ef3d186bdb 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7473,6 +7473,23 @@ public class WindowManagerService extends IWindowManager.Stub return mRoot.getDisplayContent(DEFAULT_DISPLAY); } + /** + * Returns the main display content for the user passed as parameter. + * + * <p>Visible background users may have their own designated main display, distinct from the + * system default display (DEFAULT_DISPLAY). Visible background users operate independently + * with their own main displays. These secondary user main displays host the secondary home + * activities. + */ + @Nullable + DisplayContent getUserMainDisplayContentLocked(@UserIdInt int userId) { + final int userMainDisplayId = mUmInternal.getMainDisplayAssignedToUser(userId); + if (userMainDisplayId == -1) { + return null; + } + return mRoot.getDisplayContent(userMainDisplayId); + } + public void onOverlayChanged() { // Post to display thread so it can get the latest display info. mH.post(() -> { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index b43e334d6e1a..d69b06ad71ea 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -126,6 +126,7 @@ import static com.android.server.wm.IdentifierProto.USER_ID; import static com.android.server.wm.MoveAnimationSpecProto.DURATION_MS; import static com.android.server.wm.MoveAnimationSpecProto.FROM; import static com.android.server.wm.MoveAnimationSpecProto.TO; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL; @@ -1920,6 +1921,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } final ActivityRecord atoken = mActivityRecord; if (atoken != null) { + if (atoken.mStartingData != null && mAttrs.type != TYPE_APPLICATION_STARTING + && atoken.mStartingData.mRemoveAfterTransaction + == AFTER_TRANSITION_FINISH) { + // Preventing app window from visible during un-occluding animation playing due to + // alpha blending. + return false; + } final boolean isVisible = isStartingWindowAssociatedToTask() ? mStartingData.mAssociatedTask.isVisible() : atoken.isVisible(); return ((!isParentWindowHidden() && isVisible) @@ -2925,7 +2933,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int mask = FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; WindowManager.LayoutParams sa = mActivityRecord.mStartingWindow.mAttrs; + final boolean wasShowWhenLocked = (sa.flags & FLAG_SHOW_WHEN_LOCKED) != 0; + final boolean removeShowWhenLocked = (mAttrs.flags & FLAG_SHOW_WHEN_LOCKED) == 0; sa.flags = (sa.flags & ~mask) | (mAttrs.flags & mask); + if (Flags.keepAppWindowHideWhileLocked() && wasShowWhenLocked && removeShowWhenLocked) { + // Trigger unoccluding animation if needed. + mActivityRecord.checkKeyguardFlagsChanged(); + mActivityRecord.deferStartingWindowRemovalForKeyguardUnoccluding(); + } } } @@ -5424,7 +5439,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // change then delay the position update until it has redrawn to avoid any flickers. final boolean isLetterboxedAndRelaunching = activityRecord != null && activityRecord.areBoundsLetterboxed() - && activityRecord.mAppCompatController.getAppCompatOrientationOverrides() + && activityRecord.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged(); if (surfaceResizedWithoutMoveAnimation || isLetterboxedAndRelaunching) { applyWithNextDraw(mSetSurfacePositionConsumer); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index e69a7414dd76..d2d388401e23 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -8909,11 +8909,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (parent) { Preconditions.checkCallAuthorization( - isProfileOwnerOfOrganizationOwnedDevice(getCallerIdentity().getUserId())); + isProfileOwnerOfOrganizationOwnedDevice(caller.getUserId())); + // If a DPC is querying on the parent instance, make sure it's only querying the parent + // user of itself. Querying any other user is not allowed. + Preconditions.checkArgument(caller.getUserId() == userHandle); } + int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle; Boolean disallowed = mDevicePolicyEngine.getResolvedPolicy( PolicyDefinition.SCREEN_CAPTURE_DISABLED, - userHandle); + affectedUserId); return disallowed != null && disallowed; } @@ -14669,7 +14673,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled, PersistableBundle options) { - if (Flags.secondaryLockscreenApiEnabled()) { + if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) { final CallerIdentity caller = getCallerIdentity(); final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller); synchronized (getLockObject()) { @@ -14680,16 +14684,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { caller.getUserId()); } - if (mSupervisionManagerInternal != null) { - mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser( - caller.getUserId(), enabled, options); - } else { - synchronized (getLockObject()) { - DevicePolicyData policy = getUserData(caller.getUserId()); - policy.mSecondaryLockscreenEnabled = enabled; - saveSettingsLocked(caller.getUserId()); - } - } + mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser( + caller.getUserId(), enabled, options); } else { Objects.requireNonNull(who, "ComponentName is null"); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c5d42ad9f081..8e06ed8cc283 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2714,16 +2714,18 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(AuthService.class); t.traceEnd(); - if (android.security.Flags.secureLockdown()) { - t.traceBegin("StartSecureLockDeviceService.Lifecycle"); - mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class); - t.traceEnd(); - } + if (!isWatch && !isTv && !isAutomotive) { + if (android.security.Flags.secureLockdown()) { + t.traceBegin("StartSecureLockDeviceService.Lifecycle"); + mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class); + t.traceEnd(); + } - if (android.adaptiveauth.Flags.enableAdaptiveAuth()) { - t.traceBegin("StartAuthenticationPolicyService"); - mSystemServiceManager.startService(AuthenticationPolicyService.class); - t.traceEnd(); + if (android.adaptiveauth.Flags.enableAdaptiveAuth()) { + t.traceBegin("StartAuthenticationPolicyService"); + mSystemServiceManager.startService(AuthenticationPolicyService.class); + t.traceEnd(); + } } if (!isWatch) { diff --git a/services/print/java/com/android/server/print/flags.aconfig b/services/print/java/com/android/server/print/flags.aconfig index 0210791cfeda..42d142545660 100644 --- a/services/print/java/com/android/server/print/flags.aconfig +++ b/services/print/java/com/android/server/print/flags.aconfig @@ -3,7 +3,7 @@ container: "system" flag { name: "do_not_include_capabilities" - namespace: "print" + namespace: "printing" description: "Do not use the flag Context.BIND_INCLUDE_CAPABILITIES when binding to the service" bug: "291281543" } diff --git a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java index 4e9fff230bac..b80d68de0f2e 100644 --- a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java +++ b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java @@ -37,6 +37,7 @@ import static org.testng.Assert.expectThrows; import android.annotation.UserIdInt; import android.app.Application; +import android.app.backup.BackupManagerInternal; import android.app.backup.IBackupManagerMonitor; import android.app.backup.IBackupObserver; import android.app.backup.IFullBackupRestoreObserver; @@ -52,6 +53,7 @@ import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.util.SparseArray; +import com.android.server.LocalServices; import com.android.server.SystemService.TargetUser; import com.android.server.backup.testing.TransportData; import com.android.server.testing.shadows.ShadowApplicationPackageManager; @@ -229,7 +231,7 @@ public class BackupManagerServiceRoboTest { setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false); IBinder agentBinder = mock(IBinder.class); - backupManagerService.agentConnected(mUserOneId, TEST_PACKAGE, agentBinder); + backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserOneId, agentBinder); verify(mUserOneBackupAgentConnectionManager).agentConnected(TEST_PACKAGE, agentBinder); } @@ -242,7 +244,7 @@ public class BackupManagerServiceRoboTest { setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false); IBinder agentBinder = mock(IBinder.class); - backupManagerService.agentConnected(mUserTwoId, TEST_PACKAGE, agentBinder); + backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserTwoId, agentBinder); verify(mUserOneBackupAgentConnectionManager, never()).agentConnected(TEST_PACKAGE, agentBinder); @@ -1549,6 +1551,7 @@ public class BackupManagerServiceRoboTest { @Test public void testOnStart_publishesService() { BackupManagerService backupManagerService = mock(BackupManagerService.class); + LocalServices.removeServiceForTest(BackupManagerInternal.class); BackupManagerService.Lifecycle lifecycle = spy(new BackupManagerService.Lifecycle(mContext, backupManagerService)); doNothing().when(lifecycle).publishService(anyString(), any()); diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java index c4a042370de5..f1f4a0e83a79 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.when; import android.Manifest; import android.app.backup.BackupManager; +import android.app.backup.BackupManagerInternal; import android.app.backup.ISelectBackupTransportCallback; import android.app.job.JobScheduler; import android.content.ComponentName; @@ -59,6 +60,7 @@ import androidx.test.filters.SmallTest; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.backup.utils.RandomAccessFileUtils; @@ -716,7 +718,7 @@ public class BackupManagerServiceTest { // Create BMS *before* setting a main user to simulate the main user being created after // BMS, which can happen for the first ever boot of a new device. mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER)); assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER)); @@ -730,7 +732,7 @@ public class BackupManagerServiceTest { // Create BMS *before* setting a main user to simulate the main user being created after // BMS, which can happen for the first ever boot of a new device. mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER)); assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER)); @@ -754,7 +756,7 @@ public class BackupManagerServiceTest { private void createBackupManagerServiceAndUnlockSystemUser() { mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); simulateUserUnlocked(UserHandle.USER_SYSTEM); } @@ -765,7 +767,15 @@ public class BackupManagerServiceTest { private void setMockMainUserAndCreateBackupManagerService(int userId) { when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(userId)); mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); + } + + private void createBackupServiceLifecycle(Context context, BackupManagerService service) { + // Anytime we manually create the Lifecycle, we need to remove the internal BMS because + // it would've been added already at boot time and LocalServices does not allow + // overriding an existing service. + LocalServices.removeServiceForTest(BackupManagerInternal.class); + mServiceLifecycle = new BackupManagerService.Lifecycle(context, service); } private void simulateUserUnlocked(int userId) { diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java index cd94c0f6e245..e61571288ade 100644 --- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java +++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -70,8 +70,6 @@ import android.os.PerformanceHintManager; import android.os.Process; import android.os.RemoteException; import android.os.SessionCreationConfig; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -1388,7 +1386,6 @@ public class HintManagerServiceTest { @Test - @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) public void testCpuHeadroomCache() throws Exception { CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal(); CpuHeadroomParams halParams1 = new CpuHeadroomParams(); @@ -1476,8 +1473,7 @@ public class HintManagerServiceTest { } @Test - @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) - public void testGetCpuHeadroomDifferentAffinity_flagOn() throws Exception { + public void testGetCpuHeadroomDifferentAffinity() throws Exception { CountDownLatch latch = new CountDownLatch(2); int[] tids = createThreads(2, latch); CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal(); @@ -1497,28 +1493,6 @@ public class HintManagerServiceTest { verify(mIPowerMock, times(0)).getCpuHeadroom(any()); } - @Test - @DisableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) - public void testGetCpuHeadroomDifferentAffinity_flagOff() throws Exception { - CountDownLatch latch = new CountDownLatch(2); - int[] tids = createThreads(2, latch); - CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal(); - params.tids = tids; - CpuHeadroomParams halParams = new CpuHeadroomParams(); - halParams.tids = tids; - float headroom = 0.1f; - CpuHeadroomResult halRet = CpuHeadroomResult.globalHeadroom(headroom); - String ret1 = runAndWaitForCommand("taskset -p 1 " + tids[0]); - String ret2 = runAndWaitForCommand("taskset -p 3 " + tids[1]); - - HintManagerService service = createService(); - clearInvocations(mIPowerMock); - when(mIPowerMock.getCpuHeadroom(eq(halParams))).thenReturn(halRet); - assertEquals("taskset cmd return: " + ret1 + "\n" + ret2, halRet, - service.getBinderServiceInstance().getCpuHeadroom(params)); - verify(mIPowerMock, times(1)).getCpuHeadroom(any()); - } - private String runAndWaitForCommand(String command) throws Exception { java.lang.Process process = Runtime.getRuntime().exec(command); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index d6ca10a23fb9..07b18db59960 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -27,6 +27,7 @@ android_test { "servicestests-utils", "platform-test-annotations", "flag-junit", + "apct-perftests-utils", ], libs: [ @@ -64,10 +65,12 @@ android_ravenwood_test { "ravenwood-junit", "truth", "androidx.annotation_annotation", + "androidx.test.ext.junit", "androidx.test.rules", "androidx.test.uiautomator_uiautomator", "modules-utils-binary-xml", "flag-junit", + "apct-perftests-utils", ], srcs: [ "src/com/android/server/power/stats/*.java", diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java new file mode 100644 index 000000000000..cc75e9e3114f --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2025 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.power.stats; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.BatteryStatsManager; +import android.os.BatteryUsageStats; +import android.os.BatteryUsageStatsQuery; +import android.os.ParcelFileDescriptor; +import android.perftests.utils.TraceMarkParser; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.uiautomator.UiDevice; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@LargeTest +@android.platform.test.annotations.DisabledOnRavenwood(reason = "Atrace event test") +public class BatteryStatsHistoryTraceTest { + private static final String ATRACE_START = "atrace --async_start -b 1024 -c ss"; + private static final String ATRACE_STOP = "atrace --async_stop"; + private static final String ATRACE_DUMP = "atrace --async_dump"; + + @Before + public void before() throws Exception { + runShellCommand(ATRACE_START); + } + + @After + public void after() throws Exception { + runShellCommand(ATRACE_STOP); + } + + @Test + public void dumpsys() throws Exception { + runShellCommand("dumpsys batterystats --history"); + + Set<String> slices = readAtraceSlices(); + assertThat(slices).contains("BatteryStatsHistory.copy"); + assertThat(slices).contains("BatteryStatsHistory.iterate"); + } + + @Test + public void getBatteryUsageStats() throws Exception { + BatteryStatsManager batteryStatsManager = + getInstrumentation().getTargetContext().getSystemService(BatteryStatsManager.class); + BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() + .includeBatteryHistory().build(); + BatteryUsageStats batteryUsageStats = batteryStatsManager.getBatteryUsageStats(query); + assertThat(batteryUsageStats).isNotNull(); + + Set<String> slices = readAtraceSlices(); + assertThat(slices).contains("BatteryStatsHistory.copy"); + assertThat(slices).contains("BatteryStatsHistory.iterate"); + assertThat(slices).contains("BatteryStatsHistory.writeToParcel"); + } + + private String runShellCommand(String cmd) throws Exception { + return UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd); + } + + private Set<String> readAtraceSlices() throws Exception { + Set<String> keys = new HashSet<>(); + + TraceMarkParser parser = new TraceMarkParser( + line -> line.name.startsWith("BatteryStatsHistory.")); + ParcelFileDescriptor pfd = + getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_DUMP); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(pfd)))) { + String line; + while ((line = reader.readLine()) != null) { + parser.visit(line); + } + } + parser.forAllSlices((key, slices) -> keys.add(key)); + return keys; + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index fa78dfce0a17..dafe4827b2fe 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -220,6 +220,9 @@ public class AccessibilityManagerServiceTest { @Mock private ProxyManager mProxyManager; @Mock private StatusBarManagerInternal mStatusBarManagerInternal; @Mock private DevicePolicyManager mDevicePolicyManager; + @Mock + private HearingDevicePhoneCallNotificationController + mMockHearingDevicePhoneCallNotificationController; @Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback; @Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor; private IAccessibilityManager mA11yManagerServiceOnDevice; @@ -289,7 +292,8 @@ public class AccessibilityManagerServiceTest { mMockMagnificationController, mInputFilter, mProxyManager, - mFakePermissionEnforcer); + mFakePermissionEnforcer, + mMockHearingDevicePhoneCallNotificationController); mA11yms.switchUser(mTestableContext.getUserId()); mTestableLooper.processAllMessages(); diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java index 0bf419ec242c..998c1d1a23b1 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -35,6 +36,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; import android.os.Binder; import android.os.Bundle; import android.os.DropBoxManager; @@ -48,6 +50,7 @@ import android.os.Parcel; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; import android.provider.Settings; @@ -68,6 +71,7 @@ import org.junit.Test; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -150,6 +154,25 @@ public class ActivityManagerTest { } @Test + public void testRemovedUserShouldNotBeRunning() throws Exception { + final UserManager userManager = mContext.getSystemService(UserManager.class); + assertNotNull("UserManager should not be null", userManager); + final UserInfo user = userManager.createUser( + "TestUser", UserManager.USER_TYPE_FULL_SECONDARY, 0); + + mService.startUserInBackground(user.id); + assertTrue("User should be running", mService.isUserRunning(user.id, 0)); + assertTrue("User should be in running users", + Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id)); + + userManager.removeUser(user.id); + mService.startUserInBackground(user.id); + assertFalse("Removed user should not be running", mService.isUserRunning(user.id, 0)); + assertFalse("Removed user should not be in running users", + Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id)); + } + + @Test public void testServiceUnbindAndKilling() { for (int i = TEST_LOOPS; i > 0; i--) { runOnce(i); diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 6411463fe0d9..06958b81d846 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -490,29 +490,6 @@ public class UserControllerTest { mInjector.mHandler.clearAllRecordedMessages(); // Verify that continueUserSwitch worked as expected continueAndCompleteUserSwitch(userState, oldUserId, newUserId); - verify(mInjector, times(0)).dismissKeyguard(any()); - verify(mInjector, times(1)).dismissUserSwitchingDialog(any()); - continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false); - verifySystemUserVisibilityChangesNeverNotified(); - } - - @Test - public void testContinueUserSwitchDismissKeyguard() { - when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(false); - mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, - /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false, - /* backgroundUserScheduledStopTimeSecs= */ -1); - // Start user -- this will update state of mUserController - mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND); - Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); - assertNotNull(reportMsg); - UserState userState = (UserState) reportMsg.obj; - int oldUserId = reportMsg.arg1; - int newUserId = reportMsg.arg2; - mInjector.mHandler.clearAllRecordedMessages(); - // Verify that continueUserSwitch worked as expected - continueAndCompleteUserSwitch(userState, oldUserId, newUserId); - verify(mInjector, times(1)).dismissKeyguard(any()); verify(mInjector, times(1)).dismissUserSwitchingDialog(any()); continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false); verifySystemUserVisibilityChangesNeverNotified(); @@ -1923,11 +1900,6 @@ public class UserControllerTest { } @Override - protected void dismissKeyguard(Runnable runnable) { - runnable.run(); - } - - @Override void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser, String switchingFromSystemUserMessage, String switchingToSystemUserMessage, Runnable onShown) { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index 0d86d4c3fa28..776f05dfb6ea 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -402,7 +402,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPushDynamicShortcut() { + public void disabled_testPushDynamicShortcut() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5," + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1"); @@ -544,7 +544,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_10)); } - public void testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() + public void disabled_testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() throws InterruptedException { mService.updateConfigurationLocked( ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500"); @@ -627,7 +627,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(3, mManager.getRemainingCallCount()); } - public void testPublishWithNoActivity() { + public void disbabledTestPublishWithNoActivity() { // If activity is not explicitly set, use the default one. mRunningUsers.put(USER_11, true); @@ -733,7 +733,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPublishWithNoActivity_noMainActivityInPackage() { + public void disabled_testPublishWithNoActivity_noMainActivityInPackage() { mRunningUsers.put(USER_11, true); runWithCaller(CALLING_PACKAGE_2, USER_11, () -> { @@ -2557,7 +2557,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPinShortcutAndGetPinnedShortcuts_multi() { + public void disabled_testPinShortcutAndGetPinnedShortcuts_multi() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -2889,7 +2889,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() { + public void disabled_testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -3919,7 +3919,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Try save and load, also stop/start the user. */ - public void testSaveAndLoadUser() { + public void disabled_testSaveAndLoadUser() { // First, create some shortcuts and save. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16); @@ -8579,7 +8579,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testReturnedByServer() { + public void disabled_testReturnedByServer() { // Package 1 updated, with manifest shortcuts. addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index 6cb24293a7d5..fa733e85c89c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -2509,6 +2509,134 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS, + android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) + public void testRepostWithNewChannel_afterAutogrouping_isRegrouped() { + final String pkg = "package"; + final List<NotificationRecord> notificationList = new ArrayList<>(); + // Post ungrouped notifications => will be autogrouped + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, null, false); + notificationList.add(notification); + mGroupHelper.onNotificationPosted(notification, false); + } + + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + + // Post ungrouped notifications to a different section, below autogroup limit + Mockito.reset(mCallback); + // Post ungrouped notifications => will be autogrouped + final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1", + "TEST_CHANNEL_ID1", IMPORTANCE_LOW); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 4242, + String.valueOf(i + 4242), UserHandle.SYSTEM, null, false, silentChannel); + notificationList.add(notification); + mGroupHelper.onNotificationPosted(notification, false); + } + + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + + // Update a notification to a different channel that moves it to a different section + Mockito.reset(mCallback); + final NotificationRecord notifToInvalidate = notificationList.get(0); + final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate); + final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2", + "TEST_CHANNEL_ID2", IMPORTANCE_LOW); + notifToInvalidate.updateNotificationChannel(updatedChannel); + assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection); + boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false); + assertThat(needsAutogrouping).isTrue(); + + // Check that the silent section was autogrouped + final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(silentSectionGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(silentSectionGroupKey), eq(true)); + verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey())); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), + eq(expectedGroupKey), any()); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS, + android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) + public void testRepostWithNewChannel_afterForceGrouping_isRegrouped() { + final String pkg = "package"; + final String groupName = "testGroup"; + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post valid section summary notifications without children => force group + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, groupName, false); + notificationList.add(notification); + mGroupHelper.onNotificationPostedWithDelay(notification, notificationList, + summaryByGroup); + } + + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + + // Update a notification to a different channel that moves it to a different section + Mockito.reset(mCallback); + final NotificationRecord notifToInvalidate = notificationList.get(0); + final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate); + final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2", + "TEST_CHANNEL_ID2", IMPORTANCE_LOW); + notifToInvalidate.updateNotificationChannel(updatedChannel); + assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection); + boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false); + + mGroupHelper.onNotificationPostedWithDelay(notifToInvalidate, notificationList, + summaryByGroup); + + // Check that the updated notification is removed from the autogroup + assertThat(needsAutogrouping).isFalse(); + verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey())); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), + eq(expectedGroupKey), any()); + + // Post child notifications for the silent sectin => will be autogrouped + Mockito.reset(mCallback); + final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1", + "TEST_CHANNEL_ID1", IMPORTANCE_LOW); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 4242, + String.valueOf(i + 4242), UserHandle.SYSTEM, "aGroup", false, silentChannel); + notificationList.add(notification); + needsAutogrouping = mGroupHelper.onNotificationPosted(notification, false); + assertThat(needsAutogrouping).isFalse(); + mGroupHelper.onNotificationPostedWithDelay(notification, notificationList, + summaryByGroup); + } + + // Check that the silent section was autogrouped + final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(silentSectionGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(silentSectionGroupKey), eq(true)); + } + + @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testMoveAggregateGroups_updateChannel() { final String pkg = "package"; 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 7885c9b902e2..e43b28bb9404 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -6341,6 +6341,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testOnlyAutogroupIfNeeded_channelChanged_ghUpdate() { + NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, + "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false); + mService.addNotification(r); + + NotificationRecord update = generateNotificationRecord(mSilentChannel, 0, + "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false); + mService.addEnqueuedNotification(update); + + NotificationManagerService.PostNotificationRunnable runnable = + mService.new PostNotificationRunnable(update.getKey(), + update.getSbn().getPackageName(), update.getUid(), + mPostNotificationTrackerFactory.newTracker(null)); + runnable.run(); + waitForIdle(); + + verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean()); + } + + @Test public void testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate() { NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, "testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate", null, false); @@ -17901,4 +17921,63 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r1), eq(hasOriginalSummary)); } + @Test + @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + public void testRebundleNotification_restoresBundleChannel() throws Exception { + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString(), anyInt())).thenReturn(true); + + // Post a single notification + final boolean hasOriginalSummary = false; + final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); + final String keyToUnbundle = r.getKey(); + mService.addNotification(r); + + // Classify notification into the NEWS bundle + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS); + Adjustment adjustment = new Adjustment( + r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier()); + mBinderService.applyAdjustmentFromAssistant(null, adjustment); + waitForIdle(); + r.applyAdjustments(); + // Check that the NotificationRecord channel is updated + assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); + assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS); + + // Unbundle the notification + mService.mNotificationDelegate.unbundleNotification(keyToUnbundle); + + // Check that the original channel was restored + assertThat(r.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID); + assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS); + verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r), eq(hasOriginalSummary)); + + Mockito.reset(mRankingHandler); + Mockito.reset(mGroupHelper); + + // Rebundle the notification + mService.mNotificationDelegate.rebundleNotification(keyToUnbundle); + + // Actually apply the adjustments + doAnswer(invocationOnMock -> { + ((NotificationRecord) invocationOnMock.getArguments()[0]).applyAdjustments(); + ((NotificationRecord) invocationOnMock.getArguments()[0]).calculateImportance(); + return null; + }).when(mRankingHelper).extractSignals(any(NotificationRecord.class)); + mService.handleRankingSort(); + verify(handler, times(1)).scheduleSendRankingUpdate(); + + // Check that the bundle channel was restored + verify(mRankingHandler, times(1)).requestSort(); + assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); + } + } 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 8cc233b7594b..f41805d40b0d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -66,7 +66,6 @@ import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.No import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED; 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 static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI; import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA; import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER; import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE; @@ -3192,7 +3191,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() { final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri"); @@ -3212,7 +3210,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() { final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound"); @@ -3231,7 +3228,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() { final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound"); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java index 9d191cea8acb..a0727a7af87b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java @@ -335,7 +335,7 @@ public class AppCompatOrientationOverridesTest extends WindowTestsBase { } private AppCompatOrientationOverrides getTopOrientationOverrides() { - return activity().top().mAppCompatController.getAppCompatOrientationOverrides(); + return activity().top().mAppCompatController.getOrientationOverrides(); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java index a21ab5de5de2..4faa71451a4d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java @@ -601,7 +601,7 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { } private AppCompatOrientationOverrides getTopOrientationOverrides() { - return activity().top().mAppCompatController.getAppCompatOrientationOverrides(); + return activity().top().mAppCompatController.getOrientationOverrides(); } private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() { diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 5486aa34b5fa..dfd10ec86a20 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1183,6 +1183,18 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(prev, mDisplayContent.getLastOrientationSource()); // The top will use the rotation from "prev" with fixed rotation. assertTrue(top.hasFixedRotationTransform()); + + mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp(); + assertFalse(top.hasFixedRotationTransform()); + + // Assume that the requested orientation of "prev" is landscape. And the display is also + // rotated to landscape. The activities from bottom to top are TaskB{"prev, "behindTop"}, + // TaskB{"top"}. Then "behindTop" should also get landscape according to ORIENTATION_BEHIND + // instead of resolving as undefined which causes to unexpected fixed portrait rotation. + final ActivityRecord behindTop = new ActivityBuilder(mAtm).setTask(prev.getTask()) + .setOnTop(false).setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build(); + mDisplayContent.applyFixedRotationForNonTopVisibleActivityIfNeeded(behindTop); + assertFalse(behindTop.hasFixedRotationTransform()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index de4b6fac7abf..23dcb65eb30f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -34,6 +34,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -46,12 +47,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.annotation.Nullable; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; import android.content.pm.ShortcutServiceInternal; import android.graphics.PixelFormat; +import android.graphics.Rect; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -60,6 +63,7 @@ import android.os.Message; import android.os.Parcelable; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.DragEvent; import android.view.InputChannel; @@ -74,6 +78,7 @@ import androidx.test.filters.SmallTest; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; +import com.android.window.flags.Flags; import org.junit.After; import org.junit.AfterClass; @@ -141,17 +146,28 @@ public class DragDropControllerTests extends WindowTestsBase { } } + private WindowState createDropTargetWindow(String name) { + return createDropTargetWindow(name, null /* targetDisplay */); + } + /** * Creates a window state which can be used as a drop target. */ - private WindowState createDropTargetWindow(String name, int ownerId) { - final Task task = new TaskBuilder(mSupervisor).setUserId(ownerId).build(); - final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess( - mProcess).build(); + private WindowState createDropTargetWindow(String name, + @Nullable DisplayContent targetDisplay) { + final WindowState window; + if (targetDisplay == null) { + final Task task = new TaskBuilder(mSupervisor).build(); + final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess( + mProcess).build(); + window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken( + activity).setClientWindow(new TestIWindow()).build(); + } else { + window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay( + targetDisplay).setClientWindow(new TestIWindow()).build(); + } // Use a new TestIWindow so we don't collect events for other windows - final WindowState window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken( - activity).setOwnerId(ownerId).setClientWindow(new TestIWindow()).build(); InputChannel channel = new InputChannel(); window.openInputChannel(channel); window.mHasSurface = true; @@ -174,7 +190,7 @@ public class DragDropControllerTests extends WindowTestsBase { public void setUp() throws Exception { mTarget = new TestDragDropController(mWm, mWm.mH.getLooper()); mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc", TEST_PID, TEST_UID); - mWindow = createDropTargetWindow("Drag test window", 0); + mWindow = createDropTargetWindow("Drag test window"); doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0); when(mWm.mInputManager.startDragAndDrop(any(IBinder.class), any(IBinder.class))).thenReturn( true); @@ -263,8 +279,8 @@ public class DragDropControllerTests extends WindowTestsBase { @Test public void testPrivateInterceptGlobalDragDropIgnoresNonLocalWindows() { - WindowState nonLocalWindow = createDropTargetWindow("App drag test window", 0); - WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window", 0); + WindowState nonLocalWindow = createDropTargetWindow("App drag test window"); + WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window"); globalInterceptWindow.mAttrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP; // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events @@ -347,6 +363,120 @@ public class DragDropControllerTests extends WindowTestsBase { }); } + @Test + public void testDragEventCoordinates() { + int dragStartX = mWindow.getBounds().centerX(); + int dragStartY = mWindow.getBounds().centerY(); + int startOffsetPx = 10; + int dropCoordsPx = 15; + WindowState window2 = createDropTargetWindow("App drag test window"); + Rect bounds = new Rect(dragStartX + startOffsetPx, dragStartY + startOffsetPx, + mWindow.getBounds().right, mWindow.getBounds().bottom); + window2.setBounds(bounds); + window2.getFrame().set(bounds); + + // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events + // immediately after dispatching, which is a problem when using mockito arguments captor + // because it returns and modifies the same drag event. + TestIWindow iwindow = (TestIWindow) mWindow.mClient; + final ArrayList<DragEvent> dragEvents = new ArrayList<>(); + iwindow.setDragEventJournal(dragEvents); + TestIWindow iwindow2 = (TestIWindow) window2.mClient; + final ArrayList<DragEvent> dragEvents2 = new ArrayList<>(); + iwindow2.setDragEventJournal(dragEvents2); + + startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), () -> { + // Verify the start-drag event is sent as-is for the drag origin window. + final DragEvent dragEvent = dragEvents.get(0); + assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction()); + assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */); + assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */); + // Verify the start-drag event is sent relative to the window top-left. + final DragEvent dragEvent2 = dragEvents2.get(0); + assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction()); + assertEquals(-startOffsetPx, dragEvent2.getX(), 0.0 /* delta */); + assertEquals(-startOffsetPx, dragEvent2.getY(), 0.0 /* delta */); + + try { + mTarget.mDeferDragStateClosed = true; + // x, y is window-local coordinate. + mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx, + dropCoordsPx); + mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx, + dropCoordsPx); + mToken = window2.mClient.asBinder(); + // Verify only window2 received the DROP event and coords are sent as-is. + assertEquals(1, dragEvents.size()); + assertEquals(2, dragEvents2.size()); + final DragEvent dropEvent = last(dragEvents2); + assertEquals(ACTION_DROP, dropEvent.getAction()); + assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */); + assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */); + + mTarget.reportDropResult(iwindow2, true); + } finally { + mTarget.mDeferDragStateClosed = false; + } + }); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND) + public void testDragEventConnectedDisplaysCoordinates() { + final DisplayContent testDisplay = createMockSimulatedDisplay(); + int dragStartX = mWindow.getBounds().centerX(); + int dragStartY = mWindow.getBounds().centerY(); + int dropCoordsPx = 15; + WindowState window2 = createDropTargetWindow("App drag test window", testDisplay); + + // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events + // immediately after dispatching, which is a problem when using mockito arguments captor + // because it returns and modifies the same drag event. + TestIWindow iwindow = (TestIWindow) mWindow.mClient; + final ArrayList<DragEvent> dragEvents = new ArrayList<>(); + iwindow.setDragEventJournal(dragEvents); + TestIWindow iwindow2 = (TestIWindow) window2.mClient; + final ArrayList<DragEvent> dragEvents2 = new ArrayList<>(); + iwindow2.setDragEventJournal(dragEvents2); + + startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), () -> { + // Verify the start-drag event is sent as-is for the drag origin window. + final DragEvent dragEvent = dragEvents.get(0); + assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction()); + assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */); + assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */); + // Verify the start-drag event from different display is sent out of display + // bounds. + final DragEvent dragEvent2 = dragEvents2.get(0); + assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction()); + assertEquals(-window2.getBounds().left - 1, dragEvent2.getX(), 0.0 /* delta */); + assertEquals(-window2.getBounds().top - 1, dragEvent2.getY(), 0.0 /* delta */); + + try { + mTarget.mDeferDragStateClosed = true; + // x, y is window-local coordinate. + mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx, + dropCoordsPx); + mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx, + dropCoordsPx); + mToken = window2.mClient.asBinder(); + // Verify only window2 received the DROP event and coords are sent as-is + assertEquals(1, dragEvents.size()); + assertEquals(2, dragEvents2.size()); + final DragEvent dropEvent = last(dragEvents2); + assertEquals(ACTION_DROP, dropEvent.getAction()); + assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */); + assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */); + + mTarget.reportDropResult(iwindow2, true); + } finally { + mTarget.mDeferDragStateClosed = false; + } + }); + } + private DragEvent last(ArrayList<DragEvent> list) { return list.get(list.size() - 1); } @@ -503,7 +633,7 @@ public class DragDropControllerTests extends WindowTestsBase { @Test public void testRequestSurfaceForReturnAnimationFlag_dropSuccessful() { - WindowState otherWindow = createDropTargetWindow("App drag test window", 0); + WindowState otherWindow = createDropTargetWindow("App drag test window"); TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient; // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events @@ -534,7 +664,7 @@ public class DragDropControllerTests extends WindowTestsBase { @Test public void testRequestSurfaceForReturnAnimationFlag_dropUnsuccessful() { - WindowState otherWindow = createDropTargetWindow("App drag test window", 0); + WindowState otherWindow = createDropTargetWindow("App drag test window"); TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient; // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events @@ -687,6 +817,14 @@ public class DragDropControllerTests extends WindowTestsBase { * Starts a drag with the given parameters, calls Runnable `r` after drag is started. */ private void startDrag(int flag, ClipData data, Runnable r) { + startDrag(0, 0, flag, data, r); + } + + /** + * Starts a drag with the given parameters, calls Runnable `r` after drag is started. + */ + private void startDrag(float startInWindowX, float startInWindowY, int flag, ClipData data, + Runnable r) { final SurfaceSession appSession = new SurfaceSession(); try { final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName( @@ -694,8 +832,8 @@ public class DragDropControllerTests extends WindowTestsBase { PixelFormat.TRANSLUCENT).build(); assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder())); - mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0, - 0, 0, data); + mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, + startInWindowX, startInWindowY, 0, 0, data); assertNotNull(mToken); r.run(); diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java index 3e87f1f96fcd..ee9673f5ee77 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java @@ -177,15 +177,16 @@ public class PersisterQueueTests { assertTrue("Target didn't call callback enough times.", mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE)); + // Wait until writing thread is waiting, which indicates the thread is waiting for new tasks + // to appear. + assertTrue("Failed to wait until the writing thread is waiting.", + mTarget.waitUntilWritingThreadIsWaiting(TIMEOUT_ALLOWANCE)); + // Second item mFactory.setExpectedProcessedItemNumber(1); mListener.setExpectedOnPreProcessItemCallbackTimes(1); dispatchTime = SystemClock.uptimeMillis(); - // Synchronize on the instance to make sure we schedule the item after it starts to wait for - // task indefinitely. - synchronized (mTarget) { - mTarget.addItem(mFactory.createItem(), false); - } + mTarget.addItem(mFactory.createItem(), false); assertTrue("Target didn't process item enough times.", mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE)); assertEquals("Target didn't process all items.", 2, mFactory.getTotalProcessedItemCount()); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index d6080e08774e..07ee09a350d9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -185,31 +185,46 @@ public class SizeCompatTests extends WindowTestsBase { } private ActivityRecord setUpApp(DisplayContent display) { - return setUpApp(display, null /* appBuilder */); + return setUpApp(display, null /* appBuilder */, null /* taskBuilder */); } private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder appBuilder) { + return setUpApp(display, appBuilder, null /* taskBuilder */); + } + + private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder aBuilder, + TaskBuilder tBuilder) { // Use the real package name (com.android.frameworks.wmtests) so that // EnableCompatChanges/DisableCompatChanges can take effect. // Otherwise the fake WindowTestsBase.DEFAULT_COMPONENT_PACKAGE_NAME will make // PlatformCompat#isChangeEnabledByPackageName always return default value. final ComponentName componentName = ComponentName.createRelative( mContext, SizeCompatTests.class.getName()); - mTask = new TaskBuilder(mSupervisor).setDisplay(display).setComponent(componentName) + final TaskBuilder taskBuilder = tBuilder != null ? tBuilder : new TaskBuilder(mSupervisor); + mTask = taskBuilder.setDisplay(display).setComponent(componentName) .build(); - final ActivityBuilder builder = appBuilder != null ? appBuilder : new ActivityBuilder(mAtm); - mActivity = builder.setTask(mTask).setComponent(componentName).build(); + final ActivityBuilder appBuilder = aBuilder != null ? aBuilder : new ActivityBuilder(mAtm); + mActivity = appBuilder.setTask(mTask).setComponent(componentName).build(); doReturn(false).when(mActivity).isImmersiveMode(any()); return mActivity; } private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh) { - return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */); + return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, null /* taskBuilder */); } private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder) { + return setUpDisplaySizeWithApp(dw, dh, appBuilder, null /* taskBuilder */); + } + + private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, TaskBuilder taskBuilder) { + return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, taskBuilder); + } + + private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder, + TaskBuilder taskBuilder) { final TestDisplayContent.Builder builder = new TestDisplayContent.Builder(mAtm, dw, dh); - return setUpApp(builder.build(), appBuilder); + return setUpApp(builder.build(), appBuilder, taskBuilder); } private void setUpLargeScreenDisplayWithApp(int dw, int dh) { @@ -834,7 +849,7 @@ public class SizeCompatTests extends WindowTestsBase { // Change the fixed orientation. mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); assertTrue(mActivity.isRelaunching()); - assertTrue(mActivity.mAppCompatController.getAppCompatOrientationOverrides() + assertTrue(mActivity.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged()); assertFitted(); @@ -4469,6 +4484,80 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 1000, 2800), bounds); } + @Test + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) + .isUserAppAspectRatioSettingsEnabled(); + final AppCompatController appCompatController = mActivity.mAppCompatController; + final AppCompatAspectRatioOverrides aspectRatioOverrides = + appCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + // Set user aspect ratio override. + doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides) + .getUserMinAspectRatioOverrideCode(); + + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); + assertFalse(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testUserAspectRatioOverridesAppliedToNonResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) + .isUserAppAspectRatioSettingsEnabled(); + final AppCompatController appCompatController = mActivity.mAppCompatController; + final AppCompatAspectRatioOverrides aspectRatioOverrides = + appCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + // Set user aspect ratio override. + doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides) + .getUserMinAspectRatioOverrideCode(); + + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true); + assertTrue(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO, + ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE}) + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testSystemAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); + + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isAspectRatioApplied()); + } + + @Test + @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO, + ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE}) + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testSystemAspectRatioOverridesAppliedToNonResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true); + + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isAspectRatioApplied()); + } + private void assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity( float letterboxVerticalPositionMultiplier, Rect fixedOrientationLetterbox, Rect sizeCompatUnscaled, Rect sizeCompatScaled) { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index d1f5d157560b..be79160c3a09 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -76,6 +76,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.app.ActivityThread; import android.app.IApplicationThread; import android.content.pm.ActivityInfo; @@ -1522,7 +1523,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE) public void setConfigurationChangeSettingsForUser_createsFromParcel_callsSettingImpl() throws Settings.SettingNotFoundException { - final int userId = 0; + final int currentUserId = ActivityManager.getCurrentUser(); final int forcedDensity = 400; final float forcedFontScaleFactor = 1.15f; final Parcelable.Creator<ConfigurationChangeSetting> creator = @@ -1536,10 +1537,10 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.setConfigurationChangeSettingsForUser(settings, UserHandle.USER_CURRENT); - verify(mDisplayContent).setForcedDensity(forcedDensity, userId); + verify(mDisplayContent).setForcedDensity(forcedDensity, currentUserId); assertEquals(forcedFontScaleFactor, Settings.System.getFloat( mContext.getContentResolver(), Settings.System.FONT_SCALE), 0.1f /* delta */); - verify(mAtm).updateFontScaleIfNeeded(userId); + verify(mAtm).updateFontScaleIfNeeded(currentUserId); } @Test diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 7082f0028a5e..e65e4b05ef98 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -29,6 +29,7 @@ import android.annotation.SuppressAutoDoc; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -1886,6 +1887,34 @@ public class TelecomManager { } /** + * This test API determines the foreground service delegation state for a VoIP app that adds + * calls via {@link TelecomManager#addCall(CallAttributes, Executor, OutcomeReceiver, + * CallControlCallback, CallEventCallback)}. Foreground Service Delegation allows applications + * to operate in the background starting in Android 14 and is granted by Telecom via a request + * to the ActivityManager. + * + * @param handle of the voip app that is being checked + * @return true if the app has foreground service delegation. Otherwise, false. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_VOIP_CALL_MONITOR_REFACTOR) + @TestApi + public boolean hasForegroundServiceDelegation(@Nullable PhoneAccountHandle handle) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.hasForegroundServiceDelegation(handle, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, + "RemoteException calling ITelecomService#hasForegroundServiceDelegation.", + e); + } + } + return false; + } + + /** * Return the line 1 phone number for given phone account. * * <p>Requires Permission: diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index c85374e0b660..b32379ae4b1e 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -409,4 +409,10 @@ interface ITelecomService { */ void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId, String callingPackage); + + /** + * @see TelecomServiceImpl#hasForegroundServiceDelegation + */ + boolean hasForegroundServiceDelegation(in PhoneAccountHandle phoneAccountHandle, + String callingPackage); } diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt index 08b5f38a4655..75bd5d157bb2 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt @@ -17,7 +17,6 @@ package com.android.server.wm.flicker.activityembedding.open import android.graphics.Rect -import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest @@ -68,13 +67,21 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest } } - @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun navBarWindowIsVisibleAtStartAndEnd() {} - @FlakyTest(bugId = 291575593) override fun entireScreenCovered() {} + @FlakyTest(bugId = 291575593) + @Test + override fun entireScreenCovered() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarWindowIsAlwaysVisible() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarLayerPositionAtStartAndEnd() {} /** Transition begins with a split. */ @FlakyTest(bugId = 286952194) @@ -122,7 +129,6 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest /** Always expand activity is on top of the split. */ @FlakyTest(bugId = 286952194) - @Presubmit @Test fun endsWithAlwaysExpandActivityOnTop() { flicker.assertWmEnd { diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt index 0ca8f37b239b..e41364595648 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt @@ -176,12 +176,15 @@ class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBa } @Ignore("Not applicable to this CUJ.") + @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() {} @FlakyTest(bugId = 342596801) + @Test override fun entireScreenCovered() = super.entireScreenCovered() @FlakyTest(bugId = 342596801) + @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt index b8f11dcf8970..ad083fa428a9 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.junit.FlickerParametersRunnerFactory @@ -81,7 +80,6 @@ class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyF } @FlakyTest(bugId = 290767483) - @Postsubmit @Test fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() { val layerTrace = flicker.reader.readLayersTrace() ?: error("Unable to read layers trace") diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml index 914adc40194d..8d380f0d72a6 100644 --- a/tests/Input/AndroidManifest.xml +++ b/tests/Input/AndroidManifest.xml @@ -32,7 +32,7 @@ android:process=":externalProcess"> </activity> - <activity android:name="com.android.test.input.CaptureEventActivity" + <activity android:name="com.android.cts.input.CaptureEventActivity" android:label="Capture events" android:configChanges="touchscreen|uiMode|orientation|screenSize|screenLayout|keyboardHidden|uiMode|navigation|keyboard|density|fontScale|layoutDirection|locale|mcc|mnc|smallestScreenSize" android:enableOnBackInvokedCallback="false" diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index d35c9008e8cb..8c04f647fb2f 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -59,6 +59,7 @@ import junitparams.Parameters import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -1466,20 +1467,32 @@ class KeyGestureControllerTests { @Parameters(method = "customInputGesturesTestArguments") fun testCustomKeyGestures(test: TestData) { setupKeyGestureController() + val trigger = InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) val builder = InputGestureData.Builder() .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger( - InputGestureData.createKeyTrigger( - test.expectedKeys[0], - test.expectedModifierState - ) - ) + .setTrigger(trigger) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } val inputGestureData = builder.build() - keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData) + assertNull( + test.toString(), + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) + assertEquals( + test.toString(), + InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, + keyGestureController.addCustomInputGesture(0, builder.build().aidlData) + ) + assertEquals( + test.toString(), + inputGestureData.aidlData, + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) testKeyGestureInternal(test) } diff --git a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt deleted file mode 100644 index d54e3470d9c4..000000000000 --- a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.test.input - -import android.app.Activity -import android.os.Bundle -import android.view.InputEvent -import android.view.KeyEvent -import android.view.MotionEvent -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import org.junit.Assert.assertNull - -class CaptureEventActivity : Activity() { - private val events = LinkedBlockingQueue<InputEvent>() - var shouldHandleKeyEvents = true - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Set the fixed orientation if requested - if (intent.hasExtra(EXTRA_FIXED_ORIENTATION)) { - val orientation = intent.getIntExtra(EXTRA_FIXED_ORIENTATION, 0) - setRequestedOrientation(orientation) - } - - // Set the flag if requested - if (intent.hasExtra(EXTRA_WINDOW_FLAGS)) { - val flags = intent.getIntExtra(EXTRA_WINDOW_FLAGS, 0) - window.addFlags(flags) - } - } - - override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - events.add(KeyEvent(event)) - return shouldHandleKeyEvents - } - - override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - fun getInputEvent(): InputEvent? { - return events.poll(5, TimeUnit.SECONDS) - } - - fun hasReceivedEvents(): Boolean { - return !events.isEmpty() - } - - fun assertNoEvents() { - val event = events.poll(100, TimeUnit.MILLISECONDS) - assertNull("Expected no events, but received $event", event) - } - - companion object { - const val EXTRA_FIXED_ORIENTATION = "fixed_orientation" - const val EXTRA_WINDOW_FLAGS = "window_flags" - } -} diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt index 0b281d8d39e2..9e0f7347943d 100644 --- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt +++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt @@ -28,6 +28,7 @@ import android.view.InputEvent import android.view.MotionEvent import androidx.test.platform.app.InstrumentationRegistry import com.android.cts.input.BatchedEventSplitter +import com.android.cts.input.CaptureEventActivity import com.android.cts.input.InputJsonParser import com.android.cts.input.VirtualDisplayActivityScenario import com.android.cts.input.inputeventmatchers.isResampled diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java index 060133df0a40..e7e3d10c958b 100644 --- a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java +++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java @@ -81,7 +81,8 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.reportChange(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -90,7 +91,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.reportChange(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -99,7 +101,7 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChangeByPackageName(1, packageName, 0); + mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid()); } @Test @@ -108,7 +110,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChangeByPackageName(1, packageName, 0); + mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid()); } @Test @@ -133,7 +135,8 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -143,7 +146,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -152,7 +156,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -161,7 +166,7 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test @@ -171,7 +176,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test @@ -180,7 +185,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index 20315561cceb..f00a6cad6b46 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -54,7 +54,9 @@ std::string GetSafePath(StringPiece arg) { void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; @@ -65,7 +67,9 @@ void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::st void Command::AddRequiredFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; @@ -76,7 +80,9 @@ void Command::AddRequiredFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlag(StringPiece name, StringPiece description, std::optional<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; @@ -87,7 +93,9 @@ void Command::AddOptionalFlag(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; @@ -98,7 +106,9 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::unordered_set<std::string>* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - value->emplace(arg); + if (value) { + value->emplace(arg); + } return true; }; @@ -108,7 +118,9 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - *value = true; + if (value) { + *value = true; + } return true; }; diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index 2a3cb2a0c65d..ad167c979662 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -159,4 +159,22 @@ TEST(CommandTest, ShortOptions) { ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr)); } +TEST(CommandTest, OptionsWithNullptrToAcceptValues) { + TestCommand command; + command.AddRequiredFlag("--rflag", "", nullptr); + command.AddRequiredFlagList("--rlflag", "", nullptr); + command.AddOptionalFlag("--oflag", "", nullptr); + command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr); + command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr); + command.AddOptionalSwitch("--switch", "", nullptr); + + ASSERT_EQ(0, command.Execute({ + "--rflag"s, "1"s, + "--rlflag"s, "1"s, + "--oflag"s, "1"s, + "--olflag"s, "1"s, + "--olflag2"s, "1"s, + "--switch"s}, &std::cerr)); +} + } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 6c3eae11eab9..060bc5fa2242 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,9 +425,6 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { << output_format_.value()); return 1; } - if (enable_sparse_encoding_) { - table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled; - } if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 9452e588953e..98c8f5ff89c0 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -36,11 +36,9 @@ class ConvertCommand : public Command { kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &enable_sparse_encoding_); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" @@ -87,7 +85,6 @@ class ConvertCommand : public Command { std::string output_path_; std::optional<std::string> output_format_; bool verbose_ = false; - bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 232b4024abd2..eb71189ffc46 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2504,9 +2504,6 @@ int LinkCommand::Action(const std::vector<std::string>& args) { << "the --merge-only flag can be only used when building a static library"); return 1; } - if (options_.use_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } // The default build type. context.SetPackageType(PackageType::kApp); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 2f17853718ec..b5bd905c02be 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -75,7 +75,6 @@ struct LinkOptions { bool no_resource_removal = false; bool no_xml_namespaces = false; bool do_not_compress_anything = false; - bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; FeatureFlagValues feature_flag_values; @@ -163,9 +162,11 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch("--enable-sparse-encoding", - "This decreases APK size at the cost of resource retrieval performance.", - &options_.use_sparse_encoding); + AddOptionalSwitch( + "--enable-sparse-encoding", + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 762441ee1872..f218307af578 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -406,9 +406,6 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { return 1; } - if (options_.enable_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } if (options_.force_sparse_encoding) { options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 012b0f230ca2..e3af584cbbd9 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -61,9 +61,6 @@ struct OptimizeOptions { // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; - // Whether sparse encoding should be used for O+ resources. - bool enable_sparse_encoding = false; - // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; @@ -106,11 +103,9 @@ class OptimizeCommand : public Command { &kept_artifacts_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &options_.enable_sparse_encoding); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 1a82021bce71..b8ac7925d44e 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -201,7 +201,7 @@ class PackageFlattener { (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { // Sparse encode if forced or sdk version is not set in context and config. } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. + // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32). sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 0633bc81cb25..f1c4c3512ed3 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -37,8 +37,7 @@ constexpr const size_t kSparseEncodingThreshold = 60; enum class SparseEntriesMode { // Disables sparse encoding for entries. Disabled, - // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less - // than O only applies sparse encoding for resource configuration available on O+. + // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2). Enabled, // Enables sparse encoding for all entries regardless of minSdk. Forced, @@ -47,7 +46,7 @@ enum class SparseEntriesMode { struct TableFlattenerOptions { // When enabled, types for configurations with a sparse set of entries are encoded // as a sparse map of entry ID and offset to actual data. - SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; + SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled; // When true, use compact entries for simple data bool use_compact_entries = false; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 0f1168514c4a..e3d589eb078b 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); @@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt index 6da6fc6f12c3..d0807f2ecd34 100644 --- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt +++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt @@ -877,7 +877,7 @@ resource_table { } tool_fingerprint { tool: "Android Asset Packaging Tool (aapt)" - version: "2.19-SOONG BUILD NUMBER PLACEHOLDER" + version: "2.20-SOONG BUILD NUMBER PLACEHOLDER" } } xml_files { diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 8368f9d16af8..664d8412a3be 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -1,5 +1,11 @@ # Android Asset Packaging Tool 2.0 (AAPT2) release notes +## Version 2.20 +- Too many features, bug fixes, and improvements to list since the last minor version update in + 2017. This README will be updated more frequently in the future. +- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The + `--enable-sparse-encoding` flag still exists, but is a no-op. + ## Version 2.19 - Added navigation resource type. - Fixed issue with resource deduplication. (bug 64397629) diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index 3d83caf29bba..6a4dfa629394 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -227,7 +227,7 @@ std::string GetToolFingerprint() { static const char* const sMajorVersion = "2"; // Update minor version whenever a feature or flag is added. - static const char* const sMinorVersion = "19"; + static const char* const sMinorVersion = "20"; // The build id of aapt2 binary. static const std::string sBuildId = [] { |