diff options
196 files changed, 4889 insertions, 1480 deletions
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/Notification.java b/core/java/android/app/Notification.java index 5176aee9051f..252978facac0 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; @@ -6207,7 +6214,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); @@ -6807,8 +6814,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 +6989,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/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/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index f538e9ffffdd..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 @@ -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/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/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/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/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/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/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/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/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 090d1444dbef..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 @@ -112,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; @@ -1051,6 +1053,20 @@ public abstract class WMShellBaseModule { } // + // 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 // @@ -1094,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 483c8008ba65..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 @@ -1313,6 +1315,23 @@ public abstract class WMShellModule { } // + // App zoom out + // + + @WMSingleton + @Provides + 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); + } + + // // Drag and drop // 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index faf736a543dd..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 @@ -77,6 +77,8 @@ class FakeHomeStatusBarViewModel( override val iconBlockList: MutableStateFlow<List<String>> = MutableStateFlow(listOf()) + override val contentArea = MutableStateFlow(Rect(0, 0, 1, 1)) + val darkRegions = mutableListOf<Rect>() var darkIconTint = Color.BLACK 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 a70b777a25f2..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 @@ -26,10 +26,13 @@ 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 @@ -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 { 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_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/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/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/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/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/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 75117936c090..38f7c39203f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -34,6 +34,7 @@ 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 { @@ -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) 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/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/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/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/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 2541d84a5a97..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 /** @@ -120,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, ) @@ -166,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 { @@ -200,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() + } } } } 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 3f701fc56ab4..d731752ad5d5 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 @@ -43,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 @@ -130,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 @@ -185,6 +189,7 @@ constructor( ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel, animations: SystemStatusEventAnimationInteractor, + statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore, @Application coroutineScope: CoroutineScope, ) : HomeStatusBarViewModel { override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> = @@ -363,6 +368,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 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/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/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/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/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/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/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/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index 5db0d5a25d83..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 @@ -27,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 @@ -53,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/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/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 3f6484f0f58e..00d23cc6d298 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -7214,7 +7214,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/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/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 0bb3c6a067e3..81af0d8a6d80 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -222,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; @@ -467,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. } @@ -1629,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/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4c70d2347fb7..4cb776924ca4 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -3060,6 +3060,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/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 3d53078de3c3..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; } @@ -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); } 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 dd23f577e05b..acd47dad83a1 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(); 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/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/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 3a2a1ea419d4..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(); + } } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index b5777103aac8..d2d388401e23 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -14673,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()) { @@ -14684,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/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/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/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/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/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 |