diff options
252 files changed, 7321 insertions, 3635 deletions
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 4350545a1b1d..5db79fe92345 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -8288,12 +8288,12 @@ public final class ActivityThread extends ClientTransactionHandler } Context c = null; ApplicationInfo ai = info.applicationInfo; - if (context.getPackageName().equals(ai.packageName)) { + if (context != null && context.getPackageName().equals(ai.packageName)) { c = context; } else if (mInitialApplication != null && mInitialApplication.getPackageName().equals(ai.packageName)) { c = mInitialApplication; - } else { + } else if (context != null) { try { c = context.createPackageContext(ai.packageName, Context.CONTEXT_INCLUDE_CODE); diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 52c84dc0ac5d..26f919f99ee9 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -778,8 +778,18 @@ public class LauncherApps { public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) { logErrorForInvalidProfileAccess(user); try { - return convertToActivityList(mService.getLauncherActivities(mContext.getPackageName(), - packageName, user), user); + final List<LauncherActivityInfo> activityList = convertToActivityList( + mService.getLauncherActivities( + mContext.getPackageName(), + packageName, + user + ), user); + if (activityList.isEmpty()) { + // b/350144057 + Log.d(TAG, "getActivityList: No launchable activities found for" + + "packageName=" + packageName + ", user=" + user); + } + return activityList; } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java index 4ddf602c447b..b5fb0502ec2c 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java @@ -205,10 +205,6 @@ public class CameraDeviceSetupImpl extends CameraDevice.CameraDeviceSetup { */ @SuppressWarnings("AndroidFrameworkCompatChange") public static boolean isCameraDeviceSetupSupported(CameraCharacteristics chars) { - if (!Flags.featureCombinationQuery()) { - return false; - } - Integer queryVersion = chars.get( CameraCharacteristics.INFO_SESSION_CONFIGURATION_QUERY_VERSION); return queryVersion != null && queryVersion > Build.VERSION_CODES.UPSIDE_DOWN_CAKE; diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java index 9bd4860e7ccc..50c6b5b8b995 100644 --- a/core/java/android/hardware/camera2/params/SessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java @@ -165,12 +165,10 @@ public final class SessionConfiguration implements Parcelable { source.readTypedList(outConfigs, OutputConfiguration.CREATOR); // Ignore the values for hasSessionParameters and settings because we cannot reconstruct // the CaptureRequest object. - if (Flags.featureCombinationQuery()) { - boolean hasSessionParameters = source.readBoolean(); - if (hasSessionParameters) { - CameraMetadataNative settings = new CameraMetadataNative(); - settings.readFromParcel(source); - } + boolean hasSessionParameters = source.readBoolean(); + if (hasSessionParameters) { + CameraMetadataNative settings = new CameraMetadataNative(); + settings.readFromParcel(source); } if ((inputWidth > 0) && (inputHeight > 0) && (inputFormat != -1)) { @@ -212,14 +210,12 @@ public final class SessionConfiguration implements Parcelable { dest.writeBoolean(/*isMultiResolution*/ false); } dest.writeTypedList(mOutputConfigurations); - if (Flags.featureCombinationQuery()) { - if (mSessionParameters != null) { - dest.writeBoolean(/*hasSessionParameters*/true); - CameraMetadataNative metadata = mSessionParameters.getNativeCopy(); - metadata.writeToParcel(dest, /*flags*/0); - } else { - dest.writeBoolean(/*hasSessionParameters*/false); - } + if (mSessionParameters != null) { + dest.writeBoolean(/*hasSessionParameters*/true); + CameraMetadataNative metadata = mSessionParameters.getNativeCopy(); + metadata.writeToParcel(dest, /*flags*/0); + } else { + dest.writeBoolean(/*hasSessionParameters*/false); } } diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 1a309c646882..983bbc3b2774 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -119,6 +119,13 @@ flag { } flag { + namespace: "input_native" + name: "use_key_gesture_event_handler_multi_press_gestures" + description: "Use KeyGestureEvent handler APIs to control multi key press gestures" + bug: "358569822" +} + +flag { name: "keyboard_repeat_keys" namespace: "input_native" description: "Allow configurable timeout before key repeat and repeat delay rate for key repeats" diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index a4a7a983c44c..1ca4574e79b4 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -3763,7 +3763,8 @@ public class UserManager { } private static final String CACHE_KEY_IS_USER_UNLOCKED_PROPERTY = - "cache_key.is_user_unlocked"; + PropertyInvalidatedCache.createPropertyName( + PropertyInvalidatedCache.MODULE_SYSTEM, "is_user_unlocked"); private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockedCache = new PropertyInvalidatedCache<Integer, Boolean>( @@ -6694,7 +6695,9 @@ public class UserManager { } /* Cache key for anything that assumes that userIds cannot be re-used without rebooting. */ - private static final String CACHE_KEY_STATIC_USER_PROPERTIES = "cache_key.static_user_props"; + private static final String CACHE_KEY_STATIC_USER_PROPERTIES = + PropertyInvalidatedCache.createPropertyName( + PropertyInvalidatedCache.MODULE_SYSTEM, "static_user_props"); private final PropertyInvalidatedCache<Integer, String> mProfileTypeCache = new PropertyInvalidatedCache<Integer, String>(32, CACHE_KEY_STATIC_USER_PROPERTIES) { @@ -6721,7 +6724,9 @@ public class UserManager { } /* Cache key for UserProperties object. */ - private static final String CACHE_KEY_USER_PROPERTIES = "cache_key.user_properties"; + private static final String CACHE_KEY_USER_PROPERTIES = + PropertyInvalidatedCache.createPropertyName( + PropertyInvalidatedCache.MODULE_SYSTEM, "user_properties"); // TODO: It would be better to somehow have this as static, so that it can work cross-context. private final PropertyInvalidatedCache<Integer, UserProperties> mUserPropertiesCache = diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 77dde5e2b9a3..d45b24ed69be 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -1046,6 +1046,8 @@ public class ZenModeConfig implements Parcelable { DEFAULT_SUPPRESSED_VISUAL_EFFECTS); } else if (MANUAL_TAG.equals(tag)) { rt.manualRule = readRuleXml(parser); + // manualRule.enabled can never be false, but it was broken in some builds. + rt.manualRule.enabled = true; // Manual rule may be present prior to modes_ui if it were on, but in that // case it would not have a set policy, so make note of the need to set // it up later. diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index ad457ce6e18d..384add5cf929 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -70,6 +70,7 @@ import android.graphics.drawable.Drawable; import android.hardware.HardwareBuffer; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -370,6 +371,7 @@ public abstract class WallpaperService extends Service { private float mDefaultDimAmount = 0.05f; SurfaceControl mBbqSurfaceControl; BLASTBufferQueue mBlastBufferQueue; + IBinder mBbqApplyToken = new Binder(); private SurfaceControl mScreenshotSurfaceControl; private Point mScreenshotSize = new Point(); @@ -2390,6 +2392,7 @@ public abstract class WallpaperService extends Service { if (mBlastBufferQueue == null) { mBlastBufferQueue = new BLASTBufferQueue("Wallpaper", mBbqSurfaceControl, width, height, format); + mBlastBufferQueue.setApplyToken(mBbqApplyToken); // We only return the Surface the first time, as otherwise // it hasn't changed and there is no need to update. ret = mBlastBufferQueue.createSurface(); diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 3c61f4f5a33c..3846972a12e8 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -84,17 +84,6 @@ flag { } flag { - name: "fix_font_update_failure" - namespace: "text" - description: "There was a bug of updating system font from Android 13 to 14. This flag for fixing the migration failure." - is_fixed_read_only: true - bug: "331717791" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "fix_misaligned_context_menu" namespace: "text" description: "Fix the context menu misalignment and incosistent icon size." @@ -154,26 +143,6 @@ flag { } flag { - name: "portuguese_hyphenator" - namespace: "text" - description: "Portuguese taiored hyphenator" - bug: "344656282" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - name: "dont_break_email_in_nobreak_tag" - namespace: "text" - description: "Prevent line break inside email." - bug: "350691716" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "handwriting_gesture_with_transformation" namespace: "text" description: "Fix handwriting gesture is not working when view has transformation." diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index f8c97eb5fa72..53935e810913 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -1341,7 +1341,7 @@ public final class Display { public HdrCapabilities getHdrCapabilities() { synchronized (mLock) { updateDisplayInfoLocked(); - if (mDisplayInfo.hdrCapabilities == null) { + if (mDisplayInfo.hdrCapabilities == null || mDisplayInfo.isForceSdr) { return null; } int[] supportedHdrTypes; @@ -1363,6 +1363,7 @@ public final class Display { supportedHdrTypes[index++] = enabledType; } } + return new HdrCapabilities(supportedHdrTypes, mDisplayInfo.hdrCapabilities.mMaxLuminance, mDisplayInfo.hdrCapabilities.mMaxAverageLuminance, @@ -2087,6 +2088,7 @@ public final class Display { /** * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String stateToString(int state) { switch (state) { case STATE_UNKNOWN: @@ -2109,6 +2111,7 @@ public final class Display { } /** @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static String stateReasonToString(@StateReason int reason) { switch (reason) { case STATE_REASON_UNKNOWN: diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 157cec8a4d0f..cac3e3c25098 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -230,6 +230,9 @@ public final class DisplayInfo implements Parcelable { /** The formats disabled by user **/ public int[] userDisabledHdrTypes = {}; + /** When true, all HDR capabilities are disabled **/ + public boolean isForceSdr; + /** * Indicates whether the display can be switched into a mode with minimal post * processing. @@ -440,6 +443,7 @@ public final class DisplayInfo implements Parcelable { && colorMode == other.colorMode && Arrays.equals(supportedColorModes, other.supportedColorModes) && Objects.equals(hdrCapabilities, other.hdrCapabilities) + && isForceSdr == other.isForceSdr && Arrays.equals(userDisabledHdrTypes, other.userDisabledHdrTypes) && minimalPostProcessingSupported == other.minimalPostProcessingSupported && logicalDensityDpi == other.logicalDensityDpi @@ -502,6 +506,7 @@ public final class DisplayInfo implements Parcelable { supportedColorModes = Arrays.copyOf( other.supportedColorModes, other.supportedColorModes.length); hdrCapabilities = other.hdrCapabilities; + isForceSdr = other.isForceSdr; userDisabledHdrTypes = other.userDisabledHdrTypes; minimalPostProcessingSupported = other.minimalPostProcessingSupported; logicalDensityDpi = other.logicalDensityDpi; @@ -567,6 +572,7 @@ public final class DisplayInfo implements Parcelable { supportedColorModes[i] = source.readInt(); } hdrCapabilities = source.readParcelable(null, android.view.Display.HdrCapabilities.class); + isForceSdr = source.readBoolean(); minimalPostProcessingSupported = source.readBoolean(); logicalDensityDpi = source.readInt(); physicalXDpi = source.readFloat(); @@ -636,6 +642,7 @@ public final class DisplayInfo implements Parcelable { dest.writeInt(supportedColorModes[i]); } dest.writeParcelable(hdrCapabilities, flags); + dest.writeBoolean(isForceSdr); dest.writeBoolean(minimalPostProcessingSupported); dest.writeInt(logicalDensityDpi); dest.writeFloat(physicalXDpi); @@ -874,6 +881,8 @@ public final class DisplayInfo implements Parcelable { sb.append(Arrays.toString(appsSupportedModes)); sb.append(", hdrCapabilities "); sb.append(hdrCapabilities); + sb.append(", isForceSdr "); + sb.append(isForceSdr); sb.append(", userDisabledHdrTypes "); sb.append(Arrays.toString(userDisabledHdrTypes)); sb.append(", minimalPostProcessingSupported "); diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java index dd950e83dd52..b21e85aeeb6a 100644 --- a/core/java/android/view/PointerIcon.java +++ b/core/java/android/view/PointerIcon.java @@ -174,24 +174,26 @@ public final class PointerIcon implements Parcelable { @IntDef(prefix = {"POINTER_ICON_VECTOR_STYLE_FILL_"}, value = { POINTER_ICON_VECTOR_STYLE_FILL_BLACK, POINTER_ICON_VECTOR_STYLE_FILL_GREEN, - POINTER_ICON_VECTOR_STYLE_FILL_YELLOW, + POINTER_ICON_VECTOR_STYLE_FILL_RED, POINTER_ICON_VECTOR_STYLE_FILL_PINK, - POINTER_ICON_VECTOR_STYLE_FILL_BLUE + POINTER_ICON_VECTOR_STYLE_FILL_BLUE, + POINTER_ICON_VECTOR_STYLE_FILL_PURPLE }) @Retention(RetentionPolicy.SOURCE) public @interface PointerIconVectorStyleFill {} /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BLACK = 0; /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_GREEN = 1; - /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_YELLOW = 2; + /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_RED = 2; /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_PINK = 3; /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BLUE = 4; + /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_PURPLE = 5; // If adding a PointerIconVectorStyleFill, update END value for {@link SystemSettingsValidators} /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_BEGIN = POINTER_ICON_VECTOR_STYLE_FILL_BLACK; /** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_END = - POINTER_ICON_VECTOR_STYLE_FILL_BLUE; + POINTER_ICON_VECTOR_STYLE_FILL_PURPLE; /** @hide */ @IntDef(prefix = {"POINTER_ICON_VECTOR_STYLE_STROKE_"}, value = { @@ -712,12 +714,14 @@ public final class PointerIcon implements Parcelable { com.android.internal.R.style.PointerIconVectorStyleFillBlack; case POINTER_ICON_VECTOR_STYLE_FILL_GREEN -> com.android.internal.R.style.PointerIconVectorStyleFillGreen; - case POINTER_ICON_VECTOR_STYLE_FILL_YELLOW -> - com.android.internal.R.style.PointerIconVectorStyleFillYellow; + case POINTER_ICON_VECTOR_STYLE_FILL_RED -> + com.android.internal.R.style.PointerIconVectorStyleFillRed; case POINTER_ICON_VECTOR_STYLE_FILL_PINK -> com.android.internal.R.style.PointerIconVectorStyleFillPink; case POINTER_ICON_VECTOR_STYLE_FILL_BLUE -> com.android.internal.R.style.PointerIconVectorStyleFillBlue; + case POINTER_ICON_VECTOR_STYLE_FILL_PURPLE -> + com.android.internal.R.style.PointerIconVectorStyleFillPurple; default -> com.android.internal.R.style.PointerIconVectorStyleFillBlack; }; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e10cc28d0745..9ff503171a3f 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -829,6 +829,7 @@ public final class ViewRootImpl implements ViewParent, private final SurfaceControl mSurfaceControl = new SurfaceControl(); private BLASTBufferQueue mBlastBufferQueue; + private IBinder mBbqApplyToken = new Binder(); private final HdrRenderState mHdrRenderState = new HdrRenderState(this); @@ -2743,6 +2744,10 @@ public final class ViewRootImpl implements ViewParent, mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl, mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format); mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback); + // If we create and destroy BBQ without recreating the SurfaceControl, we can end up + // queuing buffers on multiple apply tokens causing out of order buffer submissions. We + // fix this by setting the same apply token on all BBQs created by this VRI. + mBlastBufferQueue.setApplyToken(mBbqApplyToken); Surface blastSurface; if (addSchandleToVriSurface()) { blastSurface = mBlastBufferQueue.createSurfaceWithHandle(); diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 1c4b16ebe891..cc5e583034a5 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -117,6 +117,13 @@ flag { } flag { + name: "enable_tile_resizing" + namespace: "lse_desktop_experience" + description: "Enables drawing a divider bar upon tiling tasks left and right in desktop mode for simultaneous resizing" + bug: "351769839" +} + +flag { name: "respect_orientation_change_for_unresizeable" namespace: "lse_desktop_experience" description: "Whether to resize task to respect requested orientation change of unresizeable activity" diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp index 70505a45fa1b..b9c3bf73f11c 100644 --- a/core/jni/android_graphics_BLASTBufferQueue.cpp +++ b/core/jni/android_graphics_BLASTBufferQueue.cpp @@ -16,16 +16,16 @@ #define LOG_TAG "BLASTBufferQueue" -#include <nativehelper/JNIHelp.h> - #include <android_runtime/AndroidRuntime.h> #include <android_runtime/android_view_Surface.h> -#include <utils/Log.h> -#include <utils/RefBase.h> - +#include <android_util_Binder.h> #include <gui/BLASTBufferQueue.h> #include <gui/Surface.h> #include <gui/SurfaceComposerClient.h> +#include <nativehelper/JNIHelp.h> +#include <utils/Log.h> +#include <utils/RefBase.h> + #include "core_jni_helpers.h" namespace android { @@ -209,6 +209,12 @@ static jobject nativeGatherPendingTransactions(JNIEnv* env, jclass clazz, jlong reinterpret_cast<jlong>(transaction)); } +static void nativeSetApplyToken(JNIEnv* env, jclass clazz, jlong ptr, jobject applyTokenObject) { + sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr); + sp<IBinder> token(ibinderForJavaObject(env, applyTokenObject)); + return queue->setApplyToken(std::move(token)); +} + static const JNINativeMethod gMethods[] = { /* name, signature, funcPtr */ // clang-format off @@ -227,6 +233,7 @@ static const JNINativeMethod gMethods[] = { {"nativeSetTransactionHangCallback", "(JLandroid/graphics/BLASTBufferQueue$TransactionHangCallback;)V", (void*)nativeSetTransactionHangCallback}, + {"nativeSetApplyToken", "(JLandroid/os/IBinder;)V", (void*)nativeSetApplyToken}, // clang-format on }; diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index dc99634ddabc..579dc91d2ca1 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -1509,7 +1509,7 @@ please see styles_device_defaults.xml. </style> <!-- @hide --> - <style name="PointerIconVectorStyleFillYellow"> + <style name="PointerIconVectorStyleFillRed"> <item name="pointerIconVectorFill">#F55E57</item> <item name="pointerIconVectorFillInverse">#F55E57</item> </style> @@ -1527,6 +1527,12 @@ please see styles_device_defaults.xml. </style> <!-- @hide --> + <style name="PointerIconVectorStyleFillPurple"> + <item name="pointerIconVectorFill">#AD72FF</item> + <item name="pointerIconVectorFillInverse">#AD72FF</item> + </style> + + <!-- @hide --> <style name="PointerIconVectorStyleStrokeWhite"> <item name="pointerIconVectorStroke">@color/white</item> <item name="pointerIconVectorStrokeInverse">@color/black</item> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 1917ecdd99b4..039665982482 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1705,9 +1705,10 @@ <java-symbol type="style" name="VectorPointer" /> <java-symbol type="style" name="PointerIconVectorStyleFillBlack" /> <java-symbol type="style" name="PointerIconVectorStyleFillGreen" /> - <java-symbol type="style" name="PointerIconVectorStyleFillYellow" /> + <java-symbol type="style" name="PointerIconVectorStyleFillRed" /> <java-symbol type="style" name="PointerIconVectorStyleFillPink" /> <java-symbol type="style" name="PointerIconVectorStyleFillBlue" /> + <java-symbol type="style" name="PointerIconVectorStyleFillPurple" /> <java-symbol type="attr" name="pointerIconVectorFill" /> <java-symbol type="style" name="PointerIconVectorStyleStrokeWhite" /> <java-symbol type="style" name="PointerIconVectorStyleStrokeBlack" /> diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java index c52f700ef4f6..90723b2f1493 100644 --- a/graphics/java/android/graphics/BLASTBufferQueue.java +++ b/graphics/java/android/graphics/BLASTBufferQueue.java @@ -17,6 +17,7 @@ package android.graphics; import android.annotation.NonNull; +import android.os.IBinder; import android.view.Surface; import android.view.SurfaceControl; @@ -47,6 +48,7 @@ public final class BLASTBufferQueue { long frameNumber); private static native void nativeSetTransactionHangCallback(long ptr, TransactionHangCallback callback); + private static native void nativeSetApplyToken(long ptr, IBinder applyToken); public interface TransactionHangCallback { void onTransactionHang(String reason); @@ -204,4 +206,8 @@ public final class BLASTBufferQueue { public void setTransactionHangCallback(TransactionHangCallback hangCallback) { nativeSetTransactionHangCallback(mNativeObject, hangCallback); } + + public void setApplyToken(IBinder applyToken) { + nativeSetApplyToken(mNativeObject, applyToken); + } } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index 9027bf34a58e..88878c6adcf2 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -40,6 +40,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; +import android.app.TaskInfo; import android.app.WindowConfiguration; import android.graphics.Rect; import android.util.ArrayMap; @@ -339,6 +340,52 @@ public class TransitionUtil { return target; } + /** + * Creates a new RemoteAnimationTarget from the provided change and leash + */ + public static RemoteAnimationTarget newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order, + boolean isTranslucent) { + int taskId; + boolean isNotInRecents; + WindowConfiguration windowConfiguration; + + if (taskInfo != null) { + taskId = taskInfo.taskId; + isNotInRecents = !taskInfo.isRunning; + windowConfiguration = taskInfo.configuration.windowConfiguration; + } else { + taskId = INVALID_TASK_ID; + isNotInRecents = true; + windowConfiguration = new WindowConfiguration(); + } + + Rect localBounds = new Rect(); + RemoteAnimationTarget target = new RemoteAnimationTarget( + taskId, + newModeToLegacyMode(mode), + // TODO: once we can properly sync transactions across process, + // then get rid of this leash. + leash, + isTranslucent, + null, + // TODO(shell-transitions): we need to send content insets? evaluate how its used. + new Rect(0, 0, 0, 0), + order, + null, + localBounds, + new Rect(), + windowConfiguration, + isNotInRecents, + null, + new Rect(), + taskInfo, + false, + INVALID_WINDOW_TYPE + ); + return target; + } + private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change, SurfaceControl leash) { return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 452d12a242c0..7e6f43458ba6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -46,7 +46,6 @@ import android.util.Log; import android.util.SparseArray; import android.view.SurfaceControl; import android.window.ITaskOrganizerController; -import android.window.ScreenCapture; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; @@ -55,7 +54,6 @@ import android.window.TaskOrganizer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FrameworkStatsLog; -import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.compatui.api.CompatUIHandler; @@ -74,7 +72,6 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.function.Consumer; /** * Unified task organizer for all components in the shell. @@ -561,19 +558,6 @@ public class ShellTaskOrganizer extends TaskOrganizer { mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskAdded(info.getTaskInfo())); } - /** - * Take a screenshot of a task. - */ - public void screenshotTask(RunningTaskInfo taskInfo, Rect crop, - Consumer<ScreenCapture.ScreenshotHardwareBuffer> consumer) { - final TaskAppearedInfo info = mTasks.get(taskInfo.taskId); - if (info == null) { - return; - } - ScreenshotUtils.captureLayer(info.getLeash(), crop, consumer); - } - - @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { synchronized (mLock) { 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 cf1d99e06d9b..b47adb43c2a6 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 @@ -489,10 +489,11 @@ public abstract class WMShellModule { @Provides static RecentsTransitionHandler provideRecentsTransitionHandler( ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, Transitions transitions, Optional<RecentTasksController> recentTasksController, HomeTransitionObserver homeTransitionObserver) { - return new RecentsTransitionHandler(shellInit, transitions, + return new RecentsTransitionHandler(shellInit, shellTaskOrganizer, transitions, recentTasksController.orElse(null), homeTransitionObserver); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index c8ffe28da79c..bd6172226cf2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -162,7 +162,7 @@ fun maximizeSizeGivenAspectRatio( fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight - if (taskInfo.appCompatTaskInfo.isTopActivityLetterboxed) { + if (taskInfo.appCompatTaskInfo.isTopActivityLetterboxed || !taskInfo.canChangeAspectRatio) { return maxOf(appLetterboxWidth, appLetterboxHeight) / minOf(appLetterboxWidth, appLetterboxHeight).toFloat() } 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 c660000e4f61..8077aeebf27f 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 @@ -20,9 +20,12 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -41,6 +44,7 @@ import android.app.PendingIntent; import android.content.Intent; import android.graphics.Color; import android.graphics.Rect; +import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -64,6 +68,7 @@ import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -79,10 +84,15 @@ import java.util.function.Consumer; * Handles the Recents (overview) animation. Only one of these can run at a time. A recents * transition must be created via {@link #startRecentsTransition}. Anything else will be ignored. */ -public class RecentsTransitionHandler implements Transitions.TransitionHandler { +public class RecentsTransitionHandler implements Transitions.TransitionHandler, + Transitions.TransitionObserver { private static final String TAG = "RecentsTransitionHandler"; + // A placeholder for a synthetic transition that isn't backed by a true system transition + public static final IBinder SYNTHETIC_TRANSITION = new Binder(); + private final Transitions mTransitions; + private final ShellTaskOrganizer mShellTaskOrganizer; private final ShellExecutor mExecutor; @Nullable private final RecentTasksController mRecentTasksController; @@ -99,19 +109,26 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { private final HomeTransitionObserver mHomeTransitionObserver; private @Nullable Color mBackgroundColor; - public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions, + public RecentsTransitionHandler( + @NonNull ShellInit shellInit, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull Transitions transitions, @Nullable RecentTasksController recentTasksController, - HomeTransitionObserver homeTransitionObserver) { + @NonNull HomeTransitionObserver homeTransitionObserver) { + mShellTaskOrganizer = shellTaskOrganizer; mTransitions = transitions; mExecutor = transitions.getMainExecutor(); mRecentTasksController = recentTasksController; mHomeTransitionObserver = homeTransitionObserver; if (!Transitions.ENABLE_SHELL_TRANSITIONS) return; if (recentTasksController == null) return; - shellInit.addInitCallback(() -> { - recentTasksController.setTransitionHandler(this); - transitions.addHandler(this); - }, this); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mRecentTasksController.setTransitionHandler(this); + mTransitions.addHandler(this); + mTransitions.registerObserver(this); } /** Register a mixer handler. {@see RecentsMixedHandler}*/ @@ -138,17 +155,59 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { mBackgroundColor = color; } + /** + * Starts a new real/synthetic recents transition. + */ @VisibleForTesting public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, IApplicationThread appThread, IRecentsAnimationRunner listener) { + // only care about latest one. + mAnimApp = appThread; + + // TODO(b/366021931): Formalize this later + final boolean isSyntheticRequest = options.containsKey("is_synthetic_recents_transition"); + if (isSyntheticRequest) { + return startSyntheticRecentsTransition(listener); + } else { + return startRealRecentsTransition(intent, fillIn, options, listener); + } + } + + /** + * Starts a synthetic recents transition that is not backed by a real WM transition. + */ + private IBinder startSyntheticRecentsTransition(@NonNull IRecentsAnimationRunner listener) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "RecentsTransitionHandler.startRecentsTransition(synthetic)"); + final RecentsController lastController = getLastController(); + if (lastController != null) { + lastController.cancel(lastController.isSyntheticTransition() + ? "existing_running_synthetic_transition" + : "existing_running_transition"); + return null; + } + + // Create a new synthetic transition and start it immediately + final RecentsController controller = new RecentsController(listener); + controller.startSyntheticTransition(); + mControllers.add(controller); + return SYNTHETIC_TRANSITION; + } + + /** + * Starts a real WM-backed recents transition. + */ + private IBinder startRealRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, + IRecentsAnimationRunner listener) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsTransitionHandler.startRecentsTransition"); - // only care about latest one. - mAnimApp = appThread; - WindowContainerTransaction wct = new WindowContainerTransaction(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.sendPendingIntent(intent, fillIn, options); - final RecentsController controller = new RecentsController(listener); + + // Find the mixed handler which should handle this request (if we are in a state where a + // mixed handler is needed). This is slightly convoluted because starting the transition + // requires the handler, but the mixed handler also needs a reference to the transition. RecentsMixedHandler mixer = null; Consumer<IBinder> setTransitionForMixer = null; for (int i = 0; i < mMixers.size(); ++i) { @@ -160,12 +219,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct, mixer == null ? this : mixer); - for (int i = 0; i < mStateListeners.size(); i++) { - mStateListeners.get(i).onTransitionStarted(transition); - } if (mixer != null) { setTransitionForMixer.accept(transition); } + + final RecentsController controller = new RecentsController(listener); if (transition != null) { controller.setTransition(transition); mControllers.add(controller); @@ -187,11 +245,28 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { return null; } - private int findController(IBinder transition) { + /** + * Returns if there is currently a pending or active recents transition. + */ + @Nullable + private RecentsController getLastController() { + return !mControllers.isEmpty() ? mControllers.getLast() : null; + } + + /** + * Finds an existing controller for the provided {@param transition}, or {@code null} if none + * exists. + */ + @Nullable + @VisibleForTesting + RecentsController findController(@NonNull IBinder transition) { for (int i = mControllers.size() - 1; i >= 0; --i) { - if (mControllers.get(i).mTransition == transition) return i; + final RecentsController controller = mControllers.get(i); + if (controller.mTransition == transition) { + return controller; + } } - return -1; + return null; } @Override @@ -199,13 +274,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction, Transitions.TransitionFinishCallback finishCallback) { - final int controllerIdx = findController(transition); - if (controllerIdx < 0) { + final RecentsController controller = findController(transition); + if (controller == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsTransitionHandler.startAnimation: no controller found"); return false; } - final RecentsController controller = mControllers.get(controllerIdx); final IApplicationThread animApp = mAnimApp; mAnimApp = null; if (!controller.start(info, startTransaction, finishTransaction, finishCallback)) { @@ -221,13 +295,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { - final int targetIdx = findController(mergeTarget); - if (targetIdx < 0) { + final RecentsController controller = findController(mergeTarget); + if (controller == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } - final RecentsController controller = mControllers.get(targetIdx); controller.merge(info, t, finishCallback); } @@ -244,8 +317,21 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } + @Override + public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + RecentsController controller = findController(SYNTHETIC_TRANSITION); + if (controller != null) { + // Cancel the existing synthetic transition if there is one + controller.cancel("incoming_transition"); + } + } + /** There is only one of these and it gets reset on finish. */ - private class RecentsController extends IRecentsAnimationController.Stub { + @VisibleForTesting + class RecentsController extends IRecentsAnimationController.Stub { + private final int mInstanceId; private IRecentsAnimationRunner mListener; @@ -307,7 +393,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { mDeathHandler = () -> { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.DeathRecipient: binder died", mInstanceId); - finish(mWillFinishToHome, false /* leaveHint */, null /* finishCb */); + finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */, + "deathRecipient"); }; try { mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */); @@ -317,6 +404,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } + /** + * Sets the started transition for this instance of the recents transition. + */ void setTransition(IBinder transition) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.setTransition: id=%s", mInstanceId, transition); @@ -330,6 +420,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } void cancel(boolean toHome, boolean withScreenshots, String reason) { + if (cancelSyntheticTransition(reason)) { + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.cancel: toHome=%b reason=%s", mInstanceId, toHome, reason); @@ -341,7 +435,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } if (mFinishCB != null) { - finishInner(toHome, false /* userLeave */, null /* finishCb */); + finishInner(toHome, false /* userLeave */, null /* finishCb */, "cancel"); } else { cleanUp(); } @@ -436,6 +530,91 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } + /** + * Starts a new transition that is not backed by a system transition. + */ + void startSyntheticTransition() { + mTransition = SYNTHETIC_TRANSITION; + + // TODO(b/366021931): Update mechanism for pulling the home task, for now add home as + // both opening and closing since there's some pre-existing + // dependencies on having a closing task + final ActivityManager.RunningTaskInfo homeTask = + mShellTaskOrganizer.getRunningTasks(DEFAULT_DISPLAY).stream() + .filter(task -> task.getActivityType() == ACTIVITY_TYPE_HOME) + .findFirst() + .get(); + final RemoteAnimationTarget openingTarget = TransitionUtil.newSyntheticTarget( + homeTask, mShellTaskOrganizer.getHomeTaskOverlayContainer(), TRANSIT_OPEN, + 0, true /* isTranslucent */); + final RemoteAnimationTarget closingTarget = TransitionUtil.newSyntheticTarget( + homeTask, mShellTaskOrganizer.getHomeTaskOverlayContainer(), TRANSIT_CLOSE, + 0, true /* isTranslucent */); + final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>(); + apps.add(openingTarget); + apps.add(closingTarget); + try { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.start: calling onAnimationStart with %d apps", + mInstanceId, apps.size()); + mListener.onAnimationStart(this, + apps.toArray(new RemoteAnimationTarget[apps.size()]), + new RemoteAnimationTarget[0], + new Rect(0, 0, 0, 0), new Rect(), new Bundle()); + for (int i = 0; i < mStateListeners.size(); i++) { + mStateListeners.get(i).onAnimationStateChanged(true); + } + } catch (RemoteException e) { + Slog.e(TAG, "Error starting recents animation", e); + cancel("startSynthetricTransition() failed"); + } + } + + /** + * Returns whether this transition is backed by a real system transition or not. + */ + boolean isSyntheticTransition() { + return mTransition == SYNTHETIC_TRANSITION; + } + + /** + * Called when a synthetic transition is canceled. + */ + boolean cancelSyntheticTransition(String reason) { + if (!isSyntheticTransition()) { + return false; + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.cancelSyntheticTransition reason=%s", + mInstanceId, reason); + try { + // TODO(b/366021931): Notify the correct tasks once we build actual targets, and + // clean up leashes accordingly + mListener.onAnimationCanceled(new int[0], new TaskSnapshot[0]); + } catch (RemoteException e) { + Slog.e(TAG, "Error canceling previous recents animation", e); + } + cleanUp(); + return true; + } + + /** + * Called when a synthetic transition is finished. + * @return + */ + boolean finishSyntheticTransition() { + if (!isSyntheticTransition()) { + return false; + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.finishSyntheticTransition", mInstanceId); + // TODO(b/366021931): Clean up leashes accordingly + cleanUp(); + return true; + } + boolean start(TransitionInfo info, SurfaceControl.Transaction t, SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCB) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -662,7 +841,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { // Set the callback once again so we can finish correctly. mFinishCB = finishCB; finishInner(true /* toHome */, false /* userLeave */, - null /* finishCb */); + null /* finishCb */, "takeOverAnimation"); }, updatedStates); }); } @@ -810,7 +989,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { sendCancelWithSnapshots(); mExecutor.executeDelayed( () -> finishInner(true /* toHome */, false /* userLeaveHint */, - null /* finishCb */), 0); + null /* finishCb */, "merge"), 0); return; } if (recentsOpening != null) { @@ -1005,7 +1184,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { return; } final int displayId = mInfo.getRootCount() > 0 ? mInfo.getRoot(0).getDisplayId() - : Display.DEFAULT_DISPLAY; + : DEFAULT_DISPLAY; // transient launches don't receive focus automatically. Since we are taking over // the gesture now, take focus explicitly. // This also moves recents back to top if the user gestured before a switch @@ -1038,11 +1217,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { @Override @SuppressLint("NewApi") public void finish(boolean toHome, boolean sendUserLeaveHint, IResultReceiver finishCb) { - mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint, finishCb)); + mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint, finishCb, + "requested")); } private void finishInner(boolean toHome, boolean sendUserLeaveHint, - IResultReceiver runnerFinishCb) { + IResultReceiver runnerFinishCb, String reason) { + if (finishSyntheticTransition()) { + return; + } + if (mFinishCB == null) { Slog.e(TAG, "Duplicate call to finish"); return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java index e8733ebd8f03..95874c8193c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java @@ -24,7 +24,4 @@ public interface RecentsTransitionStateListener { /** Notifies whether the recents animation is running. */ default void onAnimationStateChanged(boolean running) { } - - /** Notifies that a recents shell transition has started. */ - default void onTransitionStarted(IBinder transition) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index 6013a1ea1d9d..dec28fefd789 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -16,7 +16,7 @@ package com.android.wm.shell.transition; -import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived; import android.annotation.NonNull; import android.annotation.Nullable; @@ -257,7 +257,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Override public Transitions.TransitionHandler getHandlerForTakeover( @NonNull IBinder transition, @NonNull TransitionInfo info) { - if (!returnAnimationFrameworkLibrary()) { + if (!returnAnimationFrameworkLongLived()) { return null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index aba8b61af306..d03832d3e85e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -38,7 +38,7 @@ import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; -import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived; import static com.android.window.flags.Flags.ensureWallpaperInTransitions; import static com.android.window.flags.Flags.migratePredictiveBackTransition; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; @@ -1252,7 +1252,7 @@ public class Transitions implements RemoteCallable<Transitions>, @Nullable public TransitionHandler getHandlerForTakeover( @NonNull IBinder transition, @NonNull TransitionInfo info) { - if (!returnAnimationFrameworkLibrary()) { + if (!returnAnimationFrameworkLongLived()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "Trying to get a handler for takeover but the flag is disabled"); return null; @@ -1501,16 +1501,16 @@ public class Transitions implements RemoteCallable<Transitions>, * transition animation. The Transition system will apply it when * finishCallback is called by the transition handler. */ - void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, + default void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction); + @NonNull SurfaceControl.Transaction finishTransaction) {} /** * Called when the transition is starting to play. It isn't called for merged transitions. * * @param transition the unique token of this transition */ - void onTransitionStarting(@NonNull IBinder transition); + default void onTransitionStarting(@NonNull IBinder transition) {} /** * Called when a transition is merged into another transition. There won't be any following @@ -1519,7 +1519,7 @@ public class Transitions implements RemoteCallable<Transitions>, * @param merged the unique token of the transition that's merged to another one * @param playing the unique token of the transition that accepts the merge */ - void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing); + default void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {} /** * Called when the transition is finished. This isn't called for merged transitions. @@ -1527,7 +1527,7 @@ public class Transitions implements RemoteCallable<Transitions>, * @param transition the unique token of this transition * @param aborted {@code true} if this transition is aborted; {@code false} otherwise. */ - void onTransitionFinished(@NonNull IBinder transition, boolean aborted); + default void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {} } @BinderThread 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 d43ee4425e6b..b1fc55f604d2 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 @@ -753,9 +753,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final ActivityInfo activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */); final IconProvider provider = new IconProvider(mContext); final Drawable appIconDrawable = provider.getIcon(activityInfo); + final Drawable badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, + UserHandle.of(mTaskInfo.userId)); final BaseIconFactory headerIconFactory = createIconFactory(mContext, R.dimen.desktop_mode_caption_icon_radius); - mAppIconBitmap = headerIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT); + mAppIconBitmap = headerIconFactory.createIconBitmap(badgedAppIconDrawable, + 1f /* scale */); final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext, R.dimen.desktop_mode_resize_veil_icon_size); diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt new file mode 100644 index 000000000000..f08e50e0d4ee --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper.Corners.LEFT_TOP +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionResizeAndDrag { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + + private val targetApp = CalculatorAppHelper(instrumentation) + private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(0) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun startMediaProjectionAndResize() { + mediaProjectionAppHelper.startSingleAppMediaProjection(wmHelper, targetApp) + + with(DesktopModeAppHelper(targetApp)) { + val windowRect = wmHelper.getWindowRegion(this).bounds + // Set start x-coordinate as center of app header. + val startX = windowRect.centerX() + val startY = windowRect.top + + dragWindow(startX, startY, endX = startX + 150, endY = startY + 150, wmHelper, device) + cornerResize(wmHelper, device, LEFT_TOP, -200, -200) + } + } + + @After + fun teardown() { + testApp.exit(wmHelper) + targetApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt new file mode 100644 index 000000000000..717ea306eb77 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionWithMaxDesktopWindows { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val targetApp = CalculatorAppHelper(instrumentation) + private val mailApp = MailAppHelper(instrumentation) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + private val simpleApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(0) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun startMediaProjection() { + // TODO(b/366455106) - handle max task Limit + mediaProjectionAppHelper.startSingleAppMediaProjection(wmHelper, targetApp) + mailApp.launchViaIntent(wmHelper) + simpleApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @After + fun teardown() { + mailApp.exit(wmHelper) + simpleApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + targetApp.exit(wmHelper) + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt new file mode 100644 index 000000000000..005195296c62 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartScreenMediaProjectionWithMaxDesktopWindows { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + private val simpleApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun startMediaProjection() { + mediaProjectionAppHelper.startEntireScreenMediaProjection(wmHelper) + simpleApp.launchViaIntent(wmHelper) + mailApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + simpleApp.exit(wmHelper) + mailApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp new file mode 100644 index 000000000000..85e6a8d1d865 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp @@ -0,0 +1,38 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "WMShellFlickerTestsMediaProjection", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + test_config_template: "AndroidTestTemplate.xml", + srcs: ["src/**/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellScenariosMediaProjection", + "WMShellTestUtils", + ], + data: ["trace_config/*"], +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml new file mode 100644 index 000000000000..74b0daf3a2aa --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml @@ -0,0 +1,85 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.wm.shell.flicker"> + + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> + <!-- Read and write traces from external storage --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <!-- Allow the test to write directly to /sdcard/ --> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> + <!-- Write secure settings --> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <!-- Capture screen contents --> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <!-- Enable / Disable tracing !--> + <uses-permission android:name="android.permission.DUMP" /> + <!-- Run layers trace --> + <uses-permission android:name="android.permission.HARDWARE_TEST"/> + <!-- Capture screen recording --> + <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/> + <!-- Workaround grant runtime permission exception from b/152733071 --> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> + <uses-permission android:name="android.permission.READ_LOGS"/> + <!-- Force-stop test apps --> + <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/> + <!-- Control test app's media session --> + <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> + <!-- ATM.removeRootTasksWithActivityTypes() --> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Enable bubble notification--> + <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> + <!-- Allow the test to connect to perfetto trace processor --> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> + <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> + + <!-- Allow the test to write directly to /sdcard/ and connect to trace processor --> + <application android:requestLegacyExternalStorage="true" + android:networkSecurityConfig="@xml/network_security_config" + android:largeHeap="true"> + <uses-library android:name="android.test.runner"/> + + <service android:name=".NotificationListener" + android:exported="true" + android:label="WMShellTestsNotificationListenerService" + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> + <intent-filter> + <action android:name="android.service.notification.NotificationListenerService" /> + </intent-filter> + </service> + + <service android:name="com.android.wm.shell.flicker.utils.MediaProjectionService" + android:foregroundServiceType="mediaProjection" + android:label="WMShellTestsMediaProjectionService" + android:enabled="true"> + </service> + + <!-- (b/197936012) Remove startup provider due to test timeout issue --> + <provider + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + tools:node="remove" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.wm.shell.flicker" + android:label="WindowManager Shell Flicker Tests"> + </instrumentation> +</manifest> diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml new file mode 100644 index 000000000000..40dbbac32c7f --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<configuration description="Runs WindowManager Shell Flicker Tests {MODULE}"> + <option name="test-tag" value="FlickerTests"/> + <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> + <option name="isolated-storage" value="false"/> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> + <!-- keeps the screen on during tests --> + <option name="screen-always-on" value="on"/> + <!-- prevents the phone from restarting --> + <option name="force-skip-system-props" value="true"/> + <!-- set WM tracing verbose level to all --> + <option name="run-command" value="cmd window tracing level all"/> + <!-- set WM tracing to frame (avoid incomplete states) --> + <option name="run-command" value="cmd window tracing frame"/> + <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> + <option name="run-command" value="pm disable com.google.android.internal.betterbug"/> + <!-- ensure lock screen mode is swipe --> + <option name="run-command" value="locksettings set-disabled false"/> + <!-- restart launcher to activate TAPL --> + <option name="run-command" + value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/> + <!-- Increase trace size: 20mb for WM and 80mb for SF --> + <option name="run-command" value="cmd window tracing size 20480"/> + <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="test-user-token" value="%TEST_USER%"/> + <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> + <option name="run-command" value="settings put system show_touches 1"/> + <option name="run-command" value="settings put system pointer_location 1"/> + <option name="teardown-command" + value="settings delete secure show_ime_with_hard_keyboard"/> + <option name="teardown-command" value="settings delete system show_touches"/> + <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" + value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="{MODULE}.apk"/> + <option name="test-file-name" value="FlickerTestApp.apk"/> + </target_preparer> + + <!-- Needed for pushing the trace config file --> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="push-file" + key="trace_config.textproto" + value="/data/misc/perfetto-traces/trace_config.textproto" + /> + <!--Install the content provider automatically when we push some file in sdcard folder.--> + <!--Needed to avoid the installation during the test suite.--> + <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto"/> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="{PACKAGE}"/> + <option name="shell-timeout" value="6600s"/> + <option name="test-timeout" value="6000s"/> + <option name="hidden-api-checks" value="false"/> + <option name="device-listeners" value="android.device.collectors.PerfettoListener"/> + <!-- PerfettoListener related arguments --> + <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/> + <option name="instrumentation-arg" + key="perfetto_config_file" + value="trace_config.textproto" + /> + <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> + </test> + <!-- Needed for pulling the collected trace config on to the host --> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="perfetto_file_path"/> + <option name="directory-keys" + value="/data/user/0/com.android.wm.shell.flicker/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> +</configuration> diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml new file mode 100644 index 000000000000..4bd9ca049f55 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config> diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto new file mode 100644 index 000000000000..9f2e49755fec --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto @@ -0,0 +1,71 @@ +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# proto-message: TraceConfig + +# Enable periodic flushing of the trace buffer into the output file. +write_into_file: true + +# Writes the userspace buffer into the file every 1s. +file_write_period_ms: 2500 + +# See b/126487238 - we need to guarantee ordering of events. +flush_period_ms: 30000 + +# The trace buffers needs to be big enough to hold |file_write_period_ms| of +# trace data. The trace buffer sizing depends on the number of trace categories +# enabled and the device activity. + +# RSS events +buffers: { + size_kb: 63488 + fill_policy: RING_BUFFER +} + +data_sources { + config { + name: "linux.process_stats" + target_buffer: 0 + # polled per-process memory counters and process/thread names. + # If you don't want the polled counters, remove the "process_stats_config" + # section, but keep the data source itself as it still provides on-demand + # thread/process naming for ftrace data below. + process_stats_config { + scan_all_processes_on_start: true + } + } +} + +data_sources: { + config { + name: "linux.ftrace" + ftrace_config { + ftrace_events: "ftrace/print" + ftrace_events: "task/task_newtask" + ftrace_events: "task/task_rename" + atrace_categories: "ss" + atrace_categories: "wm" + atrace_categories: "am" + atrace_categories: "aidl" + atrace_categories: "input" + atrace_categories: "binder_driver" + atrace_categories: "sched_process_exit" + atrace_apps: "com.android.server.wm.flicker.testapp" + atrace_apps: "com.android.systemui" + atrace_apps: "com.android.wm.shell.flicker.service" + atrace_apps: "com.google.android.apps.nexuslauncher" + } + } +} + diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/Android.bp b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/Android.bp new file mode 100644 index 000000000000..997a0af68d1a --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/Android.bp @@ -0,0 +1,46 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "WMShellScenariosMediaProjection", + platform_apis: true, + optimize: { + enabled: false, + }, + srcs: ["src/**/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellTestUtils", + "wm-shell-flicker-utils", + "androidx.test.ext.junit", + "flickertestapplib", + "flickerlib-helpers", + "flickerlib-trace_processor_shell", + "platform-test-annotations", + "wm-flicker-common-app-helpers", + "launcher-helper-lib", + "launcher-aosp-tapl", + ], +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt new file mode 100644 index 000000000000..1573b58853da --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionWithDisplayRotations { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val targetApp = CalculatorAppHelper(instrumentation) + private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + tapl.setExpectedRotation(initialRotation.value) + testApp.launchViaIntent(wmHelper) + } + + @Test + open fun startMediaProjectionAndRotate() { + mediaProjectionAppHelper.startSingleAppMediaProjection(wmHelper, targetApp) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_90) + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_270) + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_0) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt new file mode 100644 index 000000000000..e80a895c1aa6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartScreenMediaProjectionWithDisplayRotations { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + testApp.launchViaIntent(wmHelper) + } + + @Test + open fun startMediaProjectionAndRotate() { + mediaProjectionAppHelper.startEntireScreenMediaProjection(wmHelper) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_90) + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_270) + ChangeDisplayOrientationRule.setRotation(initialRotation) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionService.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionService.kt new file mode 100644 index 000000000000..aa4e216f01a2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionService.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.utils + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.Icon +import android.os.IBinder +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import android.util.Log + +class MediaProjectionService : Service() { + + private var mTestBitmap: Bitmap? = null + + private val notificationId: Int = 1 + private val notificationChannelId: String = "MediaProjectionFlickerTest" + private val notificationChannelName = "FlickerMediaProjectionService" + + var mMessenger: Messenger? = null + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + mMessenger = intent.extras?.getParcelable( + MediaProjectionUtils.EXTRA_MESSENGER, Messenger::class.java) + startForeground() + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + mTestBitmap?.recycle() + mTestBitmap = null + sendMessage(MediaProjectionUtils.MSG_SERVICE_DESTROYED) + super.onDestroy() + } + + private fun createNotificationIcon(): Icon { + Log.d(TAG, "createNotification") + + mTestBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888) + val canvas = Canvas(mTestBitmap!!) + canvas.drawColor(Color.BLUE) + return Icon.createWithBitmap(mTestBitmap) + } + + private fun startForeground() { + Log.d(TAG, "startForeground") + val channel = NotificationChannel( + notificationChannelId, + notificationChannelName, NotificationManager.IMPORTANCE_NONE + ) + channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + + val notificationManager: NotificationManager = + getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + + val notificationBuilder: Notification.Builder = + Notification.Builder(this, notificationChannelId) + + val notification = notificationBuilder.setOngoing(true) + .setContentTitle("App is running") + .setSmallIcon(createNotificationIcon()) + .setCategory(Notification.CATEGORY_SERVICE) + .setContentText("Context") + .build() + + startForeground(notificationId, notification) + sendMessage(MediaProjectionUtils.MSG_START_FOREGROUND_DONE) + } + + fun sendMessage(what: Int) { + Log.d(TAG, "sendMessage") + with(Message.obtain()) { + this.what = what + try { + mMessenger!!.send(this) + } catch (e: RemoteException) { + Log.d(TAG, "Unable to send message", e) + } + } + } + + companion object { + private const val TAG: String = "FlickerMediaProjectionService" + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractorKosmos.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt index e3beff727ae3..f9706969ff11 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractorKosmos.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt @@ -14,8 +14,11 @@ * limitations under the License. */ -package com.android.systemui.qs.panels.domain.interactor +package com.android.wm.shell.flicker.utils -import com.android.systemui.kosmos.Kosmos - -val Kosmos.noopGridConsistencyInteractor by Kosmos.Fixture { NoopGridConsistencyInteractor() } +object MediaProjectionUtils { + const val REQUEST_CODE: Int = 99 + const val MSG_START_FOREGROUND_DONE: Int = 1 + const val MSG_SERVICE_DESTROYED: Int = 2 + const val EXTRA_MESSENGER: String = "messenger" +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 712c30641200..e610ebd6bfab 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -18,7 +18,9 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RecentTaskInfo import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityOptions import android.app.KeyguardManager +import android.app.PendingIntent import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -44,6 +46,7 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY +import android.view.DragEvent import android.view.Gravity import android.view.SurfaceControl import android.view.WindowManager @@ -108,6 +111,7 @@ import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS import com.android.wm.shell.transition.Transitions.TransitionHandler import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import java.util.function.Consumer import java.util.Optional import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue @@ -132,8 +136,8 @@ import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.atLeastOnce -import org.mockito.kotlin.eq import org.mockito.kotlin.capture +import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -2991,6 +2995,8 @@ class DesktopTasksControllerTest : ShellTestCase() { screenOrientation = SCREEN_ORIENTATION_LANDSCAPE configuration.windowConfiguration.appBounds = bounds } + appCompatTaskInfo.topActivityLetterboxAppWidth = bounds.width() + appCompatTaskInfo.topActivityLetterboxAppHeight = bounds.height() isResizeable = false } @@ -3085,6 +3091,95 @@ class DesktopTasksControllerTest : ShellTestCase() { assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() } + + @Test + fun onUnhandledDrag_newFreeformIntent() { + testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, + PointF(1200f, 700f), + Rect(240, 700, 2160, 1900)) + } + + @Test + fun onUnhandledDrag_newFreeformIntentSplitLeft() { + testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + PointF(50f, 700f), + Rect(0, 0, 500, 1000)) + } + + @Test + fun onUnhandledDrag_newFreeformIntentSplitRight() { + testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, + PointF(2500f, 700f), + Rect(500, 0, 1000, 1000)) + } + + @Test + fun onUnhandledDrag_newFullscreenIntent() { + testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + PointF(1200f, 50f), + Rect()) + } + + /** + * Assert that an unhandled drag event launches a PendingIntent with the + * windowing mode and bounds we are expecting. + */ + private fun testOnUnhandledDrag( + indicatorType: DesktopModeVisualIndicator.IndicatorType, + inputCoordinate: PointF, + expectedBounds: Rect + ) { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + markTaskVisible(task) + task.isFocused = true + val runningTasks = ArrayList<RunningTaskInfo>() + runningTasks.add(task) + val spyController = spy(controller) + val mockPendingIntent = mock(PendingIntent::class.java) + val mockDragEvent = mock(DragEvent::class.java) + val mockCallback = mock(Consumer::class.java) + val b = SurfaceControl.Builder() + b.setName("test surface") + val dragSurface = b.build() + whenever(shellTaskOrganizer.runningTasks).thenReturn(runningTasks) + whenever(mockDragEvent.dragSurface).thenReturn(dragSurface) + whenever(mockDragEvent.x).thenReturn(inputCoordinate.x) + whenever(mockDragEvent.y).thenReturn(inputCoordinate.y) + whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + doReturn(indicatorType) + .whenever(spyController).updateVisualIndicator( + eq(task), + anyOrNull(), + anyOrNull(), + anyOrNull(), + eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) + ) + + spyController.onUnhandledDrag( + mockPendingIntent, + mockDragEvent, + mockCallback as Consumer<Boolean> + ) + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + var expectedWindowingMode: Int + if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) { + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN + // Fullscreen launches currently use default transitions + verify(transitions).startTransition(any(), capture(arg), anyOrNull()) + } else { + expectedWindowingMode = WINDOWING_MODE_FREEFORM + // All other launches use a special handler. + verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + } + assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + .launchWindowingMode).isEqualTo(expectedWindowingMode) + assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + .launchBounds).isEqualTo(expectedBounds) + } + private val desktopWallpaperIntent: Intent get() = Intent(context, DesktopWallpaperActivity::class.java) @@ -3149,6 +3244,18 @@ class DesktopTasksControllerTest : ShellTestCase() { appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride + if (deviceOrientation == ORIENTATION_LANDSCAPE) { + configuration.windowConfiguration.appBounds = + Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) + appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_LONG + appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_SHORT + } else { + configuration.windowConfiguration.appBounds = + Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) + appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_SHORT + appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_LONG + } + if (shouldLetterbox) { appCompatTaskInfo.setHasMinAspectRatioOverride(aspectRatioOverrideApplied) if (deviceOrientation == ORIENTATION_LANDSCAPE && @@ -3165,14 +3272,6 @@ class DesktopTasksControllerTest : ShellTestCase() { appCompatTaskInfo.topActivityLetterboxAppHeight = 1200 } } - - if (deviceOrientation == ORIENTATION_LANDSCAPE) { - configuration.windowConfiguration.appBounds = - Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) - } else { - configuration.windowConfiguration.appBounds = - Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) - } } whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) runningTasks.add(task) 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 new file mode 100644 index 000000000000..769acf7fdfde --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.recents; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityTaskManager; +import android.app.IApplicationThread; +import android.app.KeyguardManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.HomeTransitionObserver; +import com.android.wm.shell.transition.Transitions; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.quality.Strictness; + +import java.util.Optional; + +/** + * Tests for {@link RecentTasksController} + * + * Usage: atest WMShellUnitTests:RecentsTransitionHandlerTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class RecentsTransitionHandlerTest extends ShellTestCase { + + @Mock + private Context mContext; + @Mock + private TaskStackListenerImpl mTaskStackListener; + @Mock + private ShellCommandHandler mShellCommandHandler; + @Mock + private DesktopModeTaskRepository mDesktopModeTaskRepository; + @Mock + private ActivityTaskManager mActivityTaskManager; + @Mock + private DisplayInsetsController mDisplayInsetsController; + @Mock + private IRecentTasksListener mRecentTasksListener; + @Mock + private TaskStackTransitionObserver mTaskStackTransitionObserver; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private ShellTaskOrganizer mShellTaskOrganizer; + private RecentTasksController mRecentTasksController; + private RecentTasksController mRecentTasksControllerReal; + private RecentsTransitionHandler mRecentsTransitionHandler; + private ShellInit mShellInit; + private ShellController mShellController; + private TestShellExecutor mMainExecutor; + private static StaticMockitoSession sMockitoSession; + + @Before + public void setUp() { + sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus.class).startMocking(); + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mMainExecutor = new TestShellExecutor(); + when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); + when(mContext.getSystemService(KeyguardManager.class)) + .thenReturn(mock(KeyguardManager.class)); + mShellInit = spy(new ShellInit(mMainExecutor)); + mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, + mDisplayInsetsController, mMainExecutor)); + mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit, + mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager, + Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver, + mMainExecutor); + mRecentTasksController = spy(mRecentTasksControllerReal); + mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler, + null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController), + mMainExecutor); + + final Transitions transitions = mock(Transitions.class); + doReturn(mMainExecutor).when(transitions).getMainExecutor(); + mRecentsTransitionHandler = new RecentsTransitionHandler(mShellInit, mShellTaskOrganizer, + transitions, mRecentTasksController, mock(HomeTransitionObserver.class)); + + mShellInit.init(); + } + + @After + public void tearDown() { + sMockitoSession.finishMocking(); + } + + @Test + public void testStartSyntheticRecentsTransition_callsOnAnimationStart() throws Exception { + final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class); + doReturn(new Binder()).when(runner).asBinder(); + Bundle options = new Bundle(); + options.putBoolean("is_synthetic_recents_transition", true); + IBinder transition = mRecentsTransitionHandler.startRecentsTransition( + mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class), + runner); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + + // Finish and verify no transition remains + mRecentsTransitionHandler.findController(transition).finish(true /* toHome */, + false /* sendUserLeaveHint */, null /* finishCb */); + mMainExecutor.flushAll(); + assertNull(mRecentsTransitionHandler.findController(transition)); + } + + @Test + public void testStartSyntheticRecentsTransition_callsOnAnimationCancel() throws Exception { + final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class); + doReturn(new Binder()).when(runner).asBinder(); + Bundle options = new Bundle(); + options.putBoolean("is_synthetic_recents_transition", true); + IBinder transition = mRecentsTransitionHandler.startRecentsTransition( + mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class), + runner); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + + mRecentsTransitionHandler.findController(transition).cancel("test"); + mMainExecutor.flushAll(); + verify(runner).onAnimationCanceled(any(), any()); + assertNull(mRecentsTransitionHandler.findController(transition)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 7937a843b90a..fec9e3ebd1ef 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -557,7 +557,7 @@ public class ShellTransitionTests extends ShellTestCase { mMainExecutor.flushAll(); // Takeover shouldn't happen when the flag is disabled. - setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY); + setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED); IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); @@ -572,7 +572,7 @@ public class ShellTransitionTests extends ShellTestCase { verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any()); // Takeover should happen when the flag is enabled. - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY); + setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); info = new TransitionInfoBuilder(TRANSIT_OPEN) @@ -1211,7 +1211,7 @@ public class ShellTransitionTests extends ShellTestCase { mTransactionPool, createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); final RecentsTransitionHandler recentsHandler = - new RecentsTransitionHandler(shellInit, transitions, + new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions, mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); transitions.replaceDefaultHandlerForTest(mDefaultHandler); shellInit.init(); diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index 25c063d6ccd8..202535d45191 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -273,6 +273,7 @@ LIBANDROID { ASurfaceTransaction_fromJava; # introduced=34 ASurfaceTransaction_reparent; # introduced=29 ASurfaceTransaction_setBuffer; # introduced=29 + ASurfaceTransaction_setBufferWithRelease; # introduced=36 ASurfaceTransaction_setBufferAlpha; # introduced=29 ASurfaceTransaction_setBufferDataSpace; # introduced=29 ASurfaceTransaction_setBufferTransparency; # introduced=29 diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 6ce83cd7b765..e46db6bb3727 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -416,6 +416,35 @@ void ASurfaceTransaction_setBuffer(ASurfaceTransaction* aSurfaceTransaction, transaction->setBuffer(surfaceControl, graphic_buffer, fence); } +void ASurfaceTransaction_setBufferWithRelease( + ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl, + AHardwareBuffer* buffer, int acquire_fence_fd, void* _Null_unspecified context, + ASurfaceTransaction_OnBufferRelease aReleaseCallback) { + CHECK_NOT_NULL(aSurfaceTransaction); + CHECK_NOT_NULL(aSurfaceControl); + CHECK_NOT_NULL(aReleaseCallback); + + sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl); + Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction); + + sp<GraphicBuffer> graphic_buffer(GraphicBuffer::fromAHardwareBuffer(buffer)); + + std::optional<sp<Fence>> fence = std::nullopt; + if (acquire_fence_fd != -1) { + fence = new Fence(acquire_fence_fd); + } + + ReleaseBufferCallback releaseBufferCallback = + [context, + aReleaseCallback](const ReleaseCallbackId&, const sp<Fence>& releaseFence, + std::optional<uint32_t> /* currentMaxAcquiredBufferCount */) { + (*aReleaseCallback)(context, (releaseFence) ? releaseFence->dup() : -1); + }; + + transaction->setBuffer(surfaceControl, graphic_buffer, fence, /* frameNumber */ std::nullopt, + /* producerId */ 0, releaseBufferCallback); +} + void ASurfaceTransaction_setGeometry(ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl, const ARect& source, const ARect& destination, int32_t transform) { diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index feee89a51e7c..0b094a2f70e2 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1409,6 +1409,8 @@ <string name="media_transfer_this_device_name">This phone</string> <!-- Name of the tablet device. [CHAR LIMIT=30] --> <string name="media_transfer_this_device_name_tablet">This tablet</string> + <!-- Name of the internal speaker. [CHAR LIMIT=30] --> + <string name="media_transfer_this_device_name_desktop">This computer (internal)</string> <!-- Name of the default media output of the TV. [CHAR LIMIT=30] --> <string name="media_transfer_this_device_name_tv">@string/tv_media_transfer_default</string> <!-- Name of the internal mic. [CHAR LIMIT=30] --> @@ -1639,6 +1641,12 @@ <!-- Name of the 3.5mm and usb audio device. [CHAR LIMIT=50] --> <string name="media_transfer_wired_usb_device_name">Wired headphone</string> + <!-- Name of the 3.5mm headphone, used in desktop devices. [CHAR LIMIT=50] --> + <string name="media_transfer_headphone_name">Headphone</string> + + <!-- Name of the usb audio device speaker, used in desktop devices. [CHAR LIMIT=50] --> + <string name="media_transfer_usb_speaker_name">USB speaker</string> + <!-- Name of the 3.5mm audio device mic. [CHAR LIMIT=50] --> <string name="media_transfer_wired_device_mic_name">Mic jack</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java index 548eb3fd4b8f..874e03012ae2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java @@ -57,7 +57,7 @@ public final class InputRouteManager { } }; - /* package */ InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) { + public InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) { mContext = context; mAudioManager = audioManager; Handler handler = new Handler(context.getMainLooper()); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index 9eaf8d3838d8..116de567a7da 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -72,6 +72,8 @@ public class PhoneMediaDevice extends MediaDevice { return context.getString(R.string.media_transfer_this_device_name_tv); } else if (isTablet()) { return context.getString(R.string.media_transfer_this_device_name_tablet); + } else if (inputRoutingEnabledAndIsDesktop()) { + return context.getString(R.string.media_transfer_this_device_name_desktop); } else { return context.getString(R.string.media_transfer_this_device_name); } @@ -85,10 +87,18 @@ public class PhoneMediaDevice extends MediaDevice { switch (routeInfo.getType()) { case TYPE_WIRED_HEADSET: case TYPE_WIRED_HEADPHONES: + name = + inputRoutingEnabledAndIsDesktop() + ? context.getString(R.string.media_transfer_headphone_name) + : context.getString(R.string.media_transfer_wired_usb_device_name); + break; case TYPE_USB_DEVICE: case TYPE_USB_HEADSET: case TYPE_USB_ACCESSORY: - name = context.getString(R.string.media_transfer_wired_usb_device_name); + name = + inputRoutingEnabledAndIsDesktop() + ? context.getString(R.string.media_transfer_usb_speaker_name) + : context.getString(R.string.media_transfer_wired_usb_device_name); break; case TYPE_DOCK: name = context.getString(R.string.media_transfer_dock_speaker_device_name); @@ -139,6 +149,16 @@ public class PhoneMediaDevice extends MediaDevice { .contains("tablet"); } + static boolean isDesktop() { + return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(",")) + .contains("desktop"); + } + + static boolean inputRoutingEnabledAndIsDesktop() { + return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl() + && isDesktop(); + } + // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") @Override diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java index e2d58d660fd5..23cfc01b07b8 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java @@ -47,6 +47,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowSystemProperties; @RunWith(RobolectricTestRunner.class) public class PhoneMediaDeviceTest { @@ -114,6 +115,31 @@ public class PhoneMediaDeviceTest { when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); + assertThat(mPhoneMediaDevice.getName()).isEqualTo(getMediaTransferThisDeviceName(mContext)); + } + + @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) + @Test + public void getName_returnCorrectName_desktop() { + ShadowSystemProperties.override("ro.build.characteristics", "desktop"); + + when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES); + + assertThat(mPhoneMediaDevice.getName()) + .isEqualTo(mContext.getString(R.string.media_transfer_headphone_name)); + + when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADSET); + + assertThat(mPhoneMediaDevice.getName()) + .isEqualTo(mContext.getString(R.string.media_transfer_headphone_name)); + + when(mInfo.getType()).thenReturn(TYPE_USB_DEVICE); + + assertThat(mPhoneMediaDevice.getName()) + .isEqualTo(mContext.getString(R.string.media_transfer_usb_speaker_name)); + + when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); + assertThat(mPhoneMediaDevice.getName()) .isEqualTo(getMediaTransferThisDeviceName(mContext)); } diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp index c107ff5a34ce..1a99d25786ff 100644 --- a/packages/SettingsProvider/Android.bp +++ b/packages/SettingsProvider/Android.bp @@ -36,6 +36,7 @@ android_library { "aconfig_new_storage_flags_lib", "aconfigd_java_utils", "aconfig_demo_flags_java_lib", + "configinfra_framework_flags_java_lib", "device_config_service_flags_java", "libaconfig_java_proto_lite", "SettingsLibDeviceStateRotationLock", diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java index c9ad5a5e3b90..fbce6ca07b3e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java @@ -99,13 +99,23 @@ public final class DeviceConfigService extends Binder { @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.print("SyncDisabledForTests: "); - MyShellCommand.getSyncDisabledForTests(pw, pw); - - pw.print("Is mainline: "); - pw.println(UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()); + if (android.provider.flags.Flags.dumpImprovements()) { + pw.print("SyncDisabledForTests: "); + MyShellCommand.getSyncDisabledForTests(pw, pw); + + pw.print("UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService(): "); + pw.println(UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()); + + pw.println("DeviceConfig provider: "); + try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(fd)) { + DeviceConfig.dump(pfd, pw, /* prefix= */ " ", args); + } catch (IOException e) { + pw.print("IOException creating ParcelFileDescriptor: "); + pw.println(e); + } + } - final IContentProvider iprovider = mProvider.getIContentProvider(); + IContentProvider iprovider = mProvider.getIContentProvider(); pw.println("DeviceConfig flags:"); for (String line : MyShellCommand.listAll(iprovider)) { pw.println(line); @@ -251,22 +261,13 @@ public final class DeviceConfigService extends Binder { public static HashMap<String, String> getAllFlags(IContentProvider provider) { HashMap<String, String> allFlags = new HashMap<String, String>(); - try { - Bundle args = new Bundle(); - args.putInt(Settings.CALL_METHOD_USER_KEY, - ActivityManager.getService().getCurrentUser().id); - Bundle b = provider.call(new AttributionSource(Process.myUid(), - resolveCallingPackage(), null), Settings.AUTHORITY, - Settings.CALL_METHOD_LIST_CONFIG, null, args); - if (b != null) { - Map<String, String> flagsToValues = - (HashMap) b.getSerializable(Settings.NameValueTable.VALUE); - allFlags.putAll(flagsToValues); + for (DeviceConfig.Properties properties : DeviceConfig.getAllProperties()) { + List<String> keys = new ArrayList<>(properties.getKeyset()); + for (String flagName : properties.getKeyset()) { + String fullName = properties.getNamespace() + "/" + flagName; + allFlags.put(fullName, properties.getString(flagName, null)); } - } catch (RemoteException e) { - throw new RuntimeException("Failed in IPC", e); } - return allFlags; } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index f59eab001be9..cd16af76d4b8 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -452,7 +452,7 @@ filegroup { "tests/src/**/systemui/clipboardoverlay/ClipboardListenerTest.java", "tests/src/**/systemui/doze/DozeScreenStateTest.java", "tests/src/**/systemui/keyguard/WorkLockActivityControllerTest.java", - "tests/src/**/systemui/media/dialog/MediaOutputControllerTest.java", + "tests/src/**/systemui/media/dialog/MediaSwitchingControllerTest.java", "tests/src/**/systemui/navigationbar/views/NavigationBarTest.java", "tests/src/**/systemui/power/PowerNotificationWarningsTest.java", "tests/src/**/systemui/power/PowerUITest.java", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 7974f9222a0c..98c491b82e2e 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -149,6 +149,16 @@ flag { } flag { + name: "modes_dialog_single_rows" + namespace: "systemui" + description: "[Experiment] Display one entry per grid row in the Modes Dialog." + bug: "366034002" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "pss_app_selector_recents_split_screen" namespace: "systemui" description: "Allows recent apps selected for partial screenshare to be launched in split screen mode" @@ -528,6 +538,13 @@ flag { } flag { + name: "status_bar_connected_displays" + namespace: "systemui" + description: "Shows the status bar on connected displays" + bug: "362720336" +} + +flag { name: "status_bar_switch_to_spn_from_data_spn" namespace: "systemui" description: "Fix usage of the SPN broadcast extras" @@ -538,13 +555,6 @@ flag { } flag { - name: "haptic_volume_slider" - namespace: "systemui" - description: "Adds haptic feedback to the volume slider." - bug: "316953430" -} - -flag { name: "new_volume_panel" namespace: "systemui" description: "Switches to the new volume panel (without Slices)." @@ -668,13 +678,6 @@ flag { } flag { - name: "compose_lockscreen" - namespace: "systemui" - description: "Enables the compose version of lockscreen that runs standalone, outside of Flexiglass." - bug: "301968149" -} - -flag { name: "enable_contextual_tip_for_power_off" namespace: "systemui" description: "Enables on-screen contextual tip about how to power off or restart phone" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index 9d0b095ad4cc..d02527531a53 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -57,6 +57,7 @@ import com.android.systemui.Flags.activityTransitionUseLargestWindow import com.android.systemui.Flags.translucentOccludingActivityFix import com.android.systemui.animation.TransitionAnimator.Companion.toTransitionState import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary +import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived import com.android.wm.shell.shared.IShellTransitions import com.android.wm.shell.shared.ShellTransitions import java.util.concurrent.Executor @@ -607,8 +608,8 @@ constructor( * this registration. */ fun register(controller: Controller) { - check(returnAnimationFrameworkLibrary()) { - "Long-lived registrations cannot be used when the returnAnimationFrameworkLibrary " + + check(returnAnimationFrameworkLongLived()) { + "Long-lived registrations cannot be used when the returnAnimationFrameworkLongLived " + "flag is disabled" } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt index 163b35596c1b..8321238b28b1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt @@ -16,7 +16,6 @@ package com.android.systemui.bouncer.ui.composable -import android.view.HapticFeedbackConstants import androidx.annotation.VisibleForTesting import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D @@ -133,10 +132,7 @@ fun PatternBouncer( // Perform haptic feedback, but only if the current dot is not null, so we don't perform it // when the UI first shows up or when the user lifts their pointer/finger. if (currentDot != null) { - view.performHapticFeedback( - HapticFeedbackConstants.VIRTUAL_KEY, - HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING, - ) + viewModel.performDotFeedback(view) } if (!isAnimationEnabled) { @@ -206,10 +202,7 @@ fun PatternBouncer( // Show the failure animation if the user entered the wrong input. LaunchedEffect(animateFailure) { if (animateFailure) { - showFailureAnimation( - dots = dots, - scalingAnimatables = dotScalingAnimatables, - ) + showFailureAnimation(dots = dots, scalingAnimatables = dotScalingAnimatables) viewModel.onFailureAnimationShown() } } @@ -358,15 +351,10 @@ fun PatternBouncer( (1 - checkNotNull(dotAppearMoveUpAnimatables[dot]).value) * initialOffset drawCircle( center = - pixelOffset( - dot, - spacing, - horizontalOffset, - verticalOffset + appearOffset, - ), + pixelOffset(dot, spacing, horizontalOffset, verticalOffset + appearOffset), color = dotColor.copy(alpha = checkNotNull(dotAppearFadeInAnimatables[dot]).value), - radius = dotRadius * checkNotNull(dotScalingAnimatables[dot]).value + radius = dotRadius * checkNotNull(dotScalingAnimatables[dot]).value, ) } } @@ -387,7 +375,7 @@ private suspend fun showEntryAnimation( delayMillis = 33 * dot.y, durationMillis = 450, easing = Easings.LegacyDecelerate, - ) + ), ) } } @@ -400,7 +388,7 @@ private suspend fun showEntryAnimation( delayMillis = 0, durationMillis = 450 + (33 * dot.y), easing = Easings.StandardDecelerate, - ) + ), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt index 489e24e8b328..0830c9b359a4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -16,8 +16,8 @@ package com.android.systemui.bouncer.ui.composable -import android.view.HapticFeedbackConstants import android.view.MotionEvent +import android.view.View import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec @@ -72,11 +72,7 @@ import kotlinx.coroutines.launch /** Renders the PIN button pad. */ @Composable -fun PinPad( - viewModel: PinBouncerViewModel, - verticalSpacing: Dp, - modifier: Modifier = Modifier, -) { +fun PinPad(viewModel: PinBouncerViewModel, verticalSpacing: Dp, modifier: Modifier = Modifier) { DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsStateWithLifecycle() @@ -104,7 +100,7 @@ fun PinPad( columns = columns, verticalSpacing = verticalSpacing, horizontalSpacing = calculateHorizontalSpacingBetweenColumns(gridWidth = 300.dp), - modifier = modifier.focusRequester(focusRequester).sysuiResTag("pin_pad_grid") + modifier = modifier.focusRequester(focusRequester).sysuiResTag("pin_pad_grid"), ) { repeat(9) { index -> DigitButton( @@ -126,10 +122,11 @@ fun PinPad( ), isInputEnabled = isInputEnabled, onClicked = viewModel::onBackspaceButtonClicked, + onPointerDown = viewModel::onBackspaceButtonPressed, onLongPressed = viewModel::onBackspaceButtonLongPressed, appearance = backspaceButtonAppearance, scaling = buttonScaleAnimatables[9]::value, - elementId = "delete_button" + elementId = "delete_button", ) DigitButton( @@ -138,7 +135,7 @@ fun PinPad( onClicked = viewModel::onPinButtonClicked, scaling = buttonScaleAnimatables[10]::value, isAnimationEnabled = isDigitButtonAnimationEnabled, - onPointerDown = viewModel::onDigitButtonDown + onPointerDown = viewModel::onDigitButtonDown, ) ActionButton( @@ -152,7 +149,7 @@ fun PinPad( onClicked = viewModel::onAuthenticateButtonClicked, appearance = confirmButtonAppearance, scaling = buttonScaleAnimatables[11]::value, - elementId = "key_enter" + elementId = "key_enter", ) } } @@ -162,7 +159,7 @@ private fun DigitButton( digit: Int, isInputEnabled: Boolean, onClicked: (Int) -> Unit, - onPointerDown: () -> Unit, + onPointerDown: (View?) -> Unit, scaling: () -> Float, isAnimationEnabled: Boolean, ) { @@ -178,7 +175,7 @@ private fun DigitButton( val scale = if (isAnimationEnabled) scaling() else 1f scaleX = scale scaleY = scale - } + }, ) { contentColor -> // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes // it into Text, use that here, to animate more efficiently. @@ -197,6 +194,7 @@ private fun ActionButton( onClicked: () -> Unit, elementId: String, onLongPressed: (() -> Unit)? = null, + onPointerDown: ((View?) -> Unit)? = null, appearance: ActionButtonAppearance, scaling: () -> Float, ) { @@ -222,18 +220,16 @@ private fun ActionButton( foregroundColor = foregroundColor, isAnimationEnabled = true, elementId = elementId, + onPointerDown = onPointerDown, modifier = Modifier.graphicsLayer { alpha = hiddenAlpha val scale = scaling() scaleX = scale scaleY = scale - } + }, ) { contentColor -> - Icon( - icon = icon, - tint = contentColor(), - ) + Icon(icon = icon, tint = contentColor()) } } @@ -247,22 +243,13 @@ private fun PinPadButton( modifier: Modifier = Modifier, elementId: String? = null, onLongPressed: (() -> Unit)? = null, - onPointerDown: (() -> Unit)? = null, + onPointerDown: ((View?) -> Unit)? = null, content: @Composable (contentColor: () -> Color) -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val indication = LocalIndication.current.takeUnless { isPressed } - val view = LocalView.current - LaunchedEffect(isPressed) { - if (isPressed) { - view.performHapticFeedback( - HapticFeedbackConstants.VIRTUAL_KEY, - HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING, - ) - } - } // Pin button animation specification is asymmetric: fast animation to the pressed state, and a // slow animation upon release. Note that isPressed is guaranteed to be true for at least the @@ -277,7 +264,7 @@ private fun PinPadButton( animateDpAsState( if (isAnimationEnabled && isPressed) 24.dp else pinButtonMaxSize / 2, label = "PinButton round corners", - animationSpec = tween(animDurationMillis, easing = animEasing) + animationSpec = tween(animDurationMillis, easing = animEasing), ) val colorAnimationSpec: AnimationSpec<Color> = tween(animDurationMillis, easing = animEasing) val containerColor: Color by @@ -287,7 +274,7 @@ private fun PinPadButton( else -> backgroundColor }, label = "Pin button container color", - animationSpec = colorAnimationSpec + animationSpec = colorAnimationSpec, ) val contentColor = animateColorAsState( @@ -296,7 +283,7 @@ private fun PinPadButton( else -> foregroundColor }, label = "Pin button container color", - animationSpec = colorAnimationSpec + animationSpec = colorAnimationSpec, ) Box( @@ -319,11 +306,11 @@ private fun PinPadButton( interactionSource = interactionSource, indication = indication, onClick = onClicked, - onLongClick = onLongPressed + onLongClick = onLongPressed, ) .pointerInteropFilter { motionEvent -> if (motionEvent.action == MotionEvent.ACTION_DOWN) { - onPointerDown?.let { it() } + onPointerDown?.let { it(view) } } false } @@ -353,10 +340,7 @@ private suspend fun showFailureAnimation( animatable.animateTo( targetValue = 1f, animationSpec = - tween( - durationMillis = pinButtonErrorRevertMs, - easing = Easings.Legacy, - ), + tween(durationMillis = pinButtonErrorRevertMs, easing = Easings.Legacy), ) } } @@ -364,9 +348,7 @@ private suspend fun showFailureAnimation( } /** Returns the amount of horizontal spacing between columns, in dips. */ -private fun calculateHorizontalSpacingBetweenColumns( - gridWidth: Dp, -): Dp { +private fun calculateHorizontalSpacingBetweenColumns(gridWidth: Dp): Dp { return (gridWidth - (pinButtonMaxSize * columns)) / (columns - 1) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt index 296fc27ac0ff..dcf32b2bcda4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt @@ -16,15 +16,10 @@ package com.android.systemui.common.ui.compose.windowinsets -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.displayCutout -import androidx.compose.foundation.layout.systemBars import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -36,9 +31,6 @@ val LocalDisplayCutout = staticCompositionLocalOf { DisplayCutout() } /** The corner radius in px of the current display. */ val LocalScreenCornerRadius = staticCompositionLocalOf { 0.dp } -/** The screen height in px without accounting for any screen insets (cutouts, status/nav bars) */ -val LocalRawScreenHeight = staticCompositionLocalOf { 0f } - @Composable fun ScreenDecorProvider( displayCutout: StateFlow<DisplayCutout>, @@ -48,22 +40,9 @@ fun ScreenDecorProvider( val cutout by displayCutout.collectAsStateWithLifecycle() val screenCornerRadiusDp = with(LocalDensity.current) { screenCornerRadius.toDp() } - val density = LocalDensity.current - val navBarHeight = - with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() } - val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding() - val displayCutoutHeight = WindowInsets.displayCutout.asPaddingValues().calculateTopPadding() - val screenHeight = - with(density) { - (LocalConfiguration.current.screenHeightDp.dp + - maxOf(statusBarHeight, displayCutoutHeight)) - .toPx() - } + navBarHeight - CompositionLocalProvider( LocalScreenCornerRadius provides screenCornerRadiusDp, LocalDisplayCutout provides cutout, - LocalRawScreenHeight provides screenHeight, ) { content() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt index 897a8613263f..a2ae8bbf66e4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt @@ -24,9 +24,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import com.android.compose.nestedscroll.PriorityNestedScrollConnection -import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import kotlin.math.max import kotlin.math.roundToInt import kotlin.math.tanh @@ -36,9 +38,10 @@ import kotlinx.coroutines.launch @Composable fun Modifier.stackVerticalOverscroll( coroutineScope: CoroutineScope, - canScrollForward: () -> Boolean + canScrollForward: () -> Boolean, ): Modifier { - val screenHeight = LocalRawScreenHeight.current + val screenHeight = + with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() } val overscrollOffset = remember { Animatable(0f) } val stackNestedScrollConnection = remember { NotificationStackNestedScrollConnection( @@ -60,10 +63,10 @@ fun Modifier.stackVerticalOverscroll( overscrollOffset.animateTo( targetValue = 0f, initialVelocity = velocityAvailable, - animationSpec = tween() + animationSpec = tween(), ) } - } + }, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 91ecfc18a76e..1b99a9644575 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -19,6 +19,7 @@ package com.android.systemui.notifications.ui.composable import android.util.Log import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.tween import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background @@ -29,6 +30,8 @@ import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.absoluteOffset @@ -36,9 +39,11 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imeAnimationTarget import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme @@ -68,6 +73,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp @@ -81,7 +87,6 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.NestedScrollBehavior import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.thenIf -import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession @@ -96,6 +101,7 @@ import com.android.systemui.statusbar.notification.stack.ui.viewmodel.Notificati import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch object Notifications { @@ -171,7 +177,7 @@ fun SceneScope.SnoozeableHeadsUpNotificationSpace( setCurrent = { scrollOffset = it }, min = minScrollOffset, max = maxScrollOffset, - delta + delta, ) } @@ -209,8 +215,8 @@ fun SceneScope.SnoozeableHeadsUpNotificationSpace( calculateHeadsUpPlaceholderYOffset( scrollOffset.roundToInt(), minScrollOffset.roundToInt(), - stackScrollView.topHeadsUpHeight - ) + stackScrollView.topHeadsUpHeight, + ), ) } .thenIf(isHeadsUp) { @@ -218,11 +224,8 @@ fun SceneScope.SnoozeableHeadsUpNotificationSpace( bottomBehavior = NestedScrollBehavior.EdgeAlways ) .nestedScroll(nestedScrollConnection) - .scrollable( - orientation = Orientation.Vertical, - state = scrollableState, - ) - } + .scrollable(orientation = Orientation.Vertical, state = scrollableState) + }, ) } @@ -259,6 +262,7 @@ fun SceneScope.ConstrainedNotificationStack( * Adds the space where notification stack should appear in the scene, with a scrim and nested * scrolling. */ +@OptIn(ExperimentalLayoutApi::class) @Composable fun SceneScope.NotificationScrollingStack( shadeSession: SaveableSession, @@ -291,7 +295,7 @@ fun SceneScope.NotificationScrollingStack( val navBarHeight = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() val bottomPadding = if (shouldReserveSpaceForNavBar) navBarHeight else 0.dp - val screenHeight = LocalRawScreenHeight.current + val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } /** * The height in px of the contents of notification stack. Depending on the number of @@ -325,6 +329,14 @@ fun SceneScope.NotificationScrollingStack( screenHeight - maxScrimTop() - with(density) { navBarHeight.toPx() } } + val isRemoteInputActive by viewModel.isRemoteInputActive.collectAsStateWithLifecycle(false) + + // The bottom Y bound of the currently focused remote input notification. + val remoteInputRowBottom by viewModel.remoteInputRowBottomBound.collectAsStateWithLifecycle(0f) + + // The top y bound of the IME. + val imeTop = remember { mutableFloatStateOf(0f) } + // we are not scrolled to the top unless the scrim is at its maximum offset. LaunchedEffect(viewModel, scrimOffset) { snapshotFlow { scrimOffset.value >= 0f } @@ -342,15 +354,34 @@ fun SceneScope.NotificationScrollingStack( LaunchedEffect(syntheticScroll, scrimOffset, scrollState) { snapshotFlow { syntheticScroll.value } .collect { delta -> - val minOffset = minScrimOffset() - if (scrimOffset.value > minOffset) { - val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f) - scrimOffset.snapTo((scrimOffset.value - delta).coerceAtLeast(minOffset)) - if (remainingDelta > 0f) { - scrollState.scrollBy(remainingDelta) - } - } else { - scrollState.scrollTo(delta.roundToInt()) + scrollNotificationStack( + scope = coroutineScope, + delta = delta, + animate = false, + scrimOffset = scrimOffset, + minScrimOffset = minScrimOffset, + scrollState = scrollState, + ) + } + } + + // if remote input state changes, compare the row and IME's overlap and offset the scrim and + // placeholder accordingly. + LaunchedEffect(isRemoteInputActive, remoteInputRowBottom, imeTop) { + imeTop.floatValue = 0f + snapshotFlow { imeTop.floatValue } + .collect { imeTopValue -> + // only scroll the stack if ime value has been populated (ime placeholder has been + // composed at least once), and our remote input row overlaps with the ime bounds. + if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) { + scrollNotificationStack( + scope = coroutineScope, + delta = remoteInputRowBottom - imeTopValue, + animate = true, + scrimOffset = scrimOffset, + minScrimOffset = minScrimOffset, + scrollState = scrollState, + ) } } } @@ -394,12 +425,12 @@ fun SceneScope.NotificationScrollingStack( scrimOffset.value < 0 && layoutState.isTransitioning( from = Scenes.Shade, - to = Scenes.QuickSettings + to = Scenes.QuickSettings, ) ) { IntOffset( x = 0, - y = (scrimOffset.value * (1 - shadeToQsFraction)).roundToInt() + y = (scrimOffset.value * (1 - shadeToQsFraction)).roundToInt(), ) } else { IntOffset(x = 0, y = scrimOffset.value.roundToInt()) @@ -458,13 +489,11 @@ fun SceneScope.NotificationScrollingStack( .thenIf(shouldFillMaxSize) { Modifier.fillMaxSize() } .debugBackground(viewModel, DEBUG_BOX_COLOR) ) { - NotificationPlaceholder( - stackScrollView = stackScrollView, - viewModel = viewModel, + Column( modifier = Modifier.verticalNestedScrollToScene( topBehavior = NestedScrollBehavior.EdgeWithPreview, - isExternalOverscrollGesture = { isCurrentGestureOverscroll.value } + isExternalOverscrollGesture = { isCurrentGestureOverscroll.value }, ) .thenIf(shadeMode == ShadeMode.Single) { Modifier.nestedScroll(scrimNestedScrollConnection) @@ -473,18 +502,31 @@ fun SceneScope.NotificationScrollingStack( .verticalScroll(scrollState) .padding(top = topPadding) .fillMaxWidth() - .notificationStackHeight( - view = stackScrollView, - totalVerticalPadding = topPadding + bottomPadding, - ) - .onSizeChanged { size -> stackHeight.intValue = size.height }, - ) + ) { + NotificationPlaceholder( + stackScrollView = stackScrollView, + viewModel = viewModel, + modifier = + Modifier.notificationStackHeight( + view = stackScrollView, + totalVerticalPadding = topPadding + bottomPadding, + ) + .onSizeChanged { size -> stackHeight.intValue = size.height }, + ) + Spacer( + modifier = + Modifier.windowInsetsBottomHeight(WindowInsets.imeAnimationTarget) + .onGloballyPositioned { coordinates: LayoutCoordinates -> + imeTop.floatValue = screenHeight - coordinates.size.height + } + ) + } } if (shouldIncludeHeadsUpSpace) { HeadsUpNotificationSpace( stackScrollView = stackScrollView, viewModel = viewModel, - modifier = Modifier.padding(top = topPadding) + modifier = Modifier.padding(top = topPadding), ) } } @@ -572,6 +614,42 @@ private fun SceneScope.NotificationPlaceholder( ) } +private suspend fun scrollNotificationStack( + scope: CoroutineScope, + delta: Float, + animate: Boolean, + scrimOffset: Animatable<Float, AnimationVector1D>, + minScrimOffset: () -> Float, + scrollState: ScrollState, +) { + val minOffset = minScrimOffset() + if (scrimOffset.value > minOffset) { + val remainingDelta = + (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f).roundToInt() + if (remainingDelta > 0) { + if (animate) { + // launch a new coroutine for the remainder animation so that it doesn't suspend the + // scrim animation, allowing both to play simultaneously. + scope.launch { scrollState.animateScrollTo(remainingDelta) } + } else { + scrollState.scrollTo(remainingDelta) + } + } + val newScrimOffset = (scrimOffset.value - delta).coerceAtLeast(minOffset) + if (animate) { + scrimOffset.animateTo(newScrimOffset) + } else { + scrimOffset.snapTo(newScrimOffset) + } + } else { + if (animate) { + scrollState.animateScrollBy(delta) + } else { + scrollState.scrollBy(delta) + } + } +} + private fun calculateCornerRadius( scrimCornerRadius: Dp, screenCornerRadius: Dp, @@ -618,7 +696,7 @@ private fun consumeDeltaWithinRange( setCurrent: (Float) -> Unit, min: Float, max: Float, - delta: Float + delta: Float, ): Float { return if (delta < 0 && current > min) { val remainder = (current + delta - min).coerceAtMost(0f) @@ -631,10 +709,7 @@ private fun consumeDeltaWithinRange( } else 0f } -private inline fun debugLog( - viewModel: NotificationsPlaceholderViewModel, - msg: () -> Any, -) { +private inline fun debugLog(viewModel: NotificationsPlaceholderViewModel, msg: () -> Any) { if (viewModel.isDebugLoggingEnabled) { Log.d(TAG, msg().toString()) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index fa92bef34f38..0c1c16522567 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -61,6 +61,7 @@ import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource @@ -79,7 +80,6 @@ import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout -import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable @@ -229,17 +229,16 @@ private fun SceneScope.QuickSettingsScene( } .thenIf(cutoutLocation != CutoutLocation.CENTER) { Modifier.displayCutoutPadding() } ) { + val density = LocalDensity.current val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsStateWithLifecycle() val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle() val customizingAnimationDuration by viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsStateWithLifecycle() - val screenHeight = LocalRawScreenHeight.current + val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } BackHandler(enabled = isCustomizing) { viewModel.qsSceneAdapter.requestCloseCustomizer() } - val collapsedHeaderHeight = - with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() } val lifecycleOwner = LocalLifecycleOwner.current val footerActionsViewModel = remember(lifecycleOwner, viewModel) { @@ -268,7 +267,6 @@ private fun SceneScope.QuickSettingsScene( val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - val density = LocalDensity.current val bottomPadding by animateDpAsState( targetValue = if (isCustomizing) 0.dp else navBarBottomHeight, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index b85523bc1694..6c4edf49fd83 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade.ui.composable +import android.view.HapticFeedbackConstants import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -39,17 +40,20 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.SceneScope import com.android.compose.windowsizeclass.LocalWindowSizeClass +import com.android.systemui.scene.shared.model.Scenes /** Renders a lightweight shade UI container, as an overlay. */ @Composable @@ -58,6 +62,13 @@ fun SceneScope.OverlayShade( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { + val view = LocalView.current + LaunchedEffect(Unit) { + if (layoutState.currentTransition?.fromContent == Scenes.Gone) { + view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START) + } + } + Box(modifier) { Scrim(onClicked = onScrimClicked) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index fb9dde345251..0bb1d928c2b4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.Velocity import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.fastSumBy import com.android.compose.ui.util.SpaceVectorConverter @@ -234,8 +235,15 @@ internal class MultiPointerDraggableNode( pointersDown == 0 -> { startedPosition = null - val lastPointerUp = changes.single { it.id == velocityPointerId } - velocityTracker.addPointerInputChange(lastPointerUp) + // In case of multiple events with 0 pointers down (not pressed) we may have + // already removed the velocityPointer + val lastPointerUp = changes.fastFilter { it.id == velocityPointerId } + check(lastPointerUp.isEmpty() || lastPointerUp.size == 1) { + "There are ${lastPointerUp.size} pointers up: $lastPointerUp" + } + if (lastPointerUp.size == 1) { + velocityTracker.addPointerInputChange(lastPointerUp.first()) + } } // The first pointer down, startedPosition was not set. diff --git a/packages/SystemUI/docs/scene.md b/packages/SystemUI/docs/scene.md index 0ac15c583b29..234c7a032d2e 100644 --- a/packages/SystemUI/docs/scene.md +++ b/packages/SystemUI/docs/scene.md @@ -68,15 +68,13 @@ file evalutes to `true`. 1. Set a collection of **aconfig flags** to `true` by running the following commands: ```console - $ adb shell device_config override systemui com.android.systemui.scene_container true - $ adb shell device_config override systemui com.android.systemui.compose_lockscreen true $ adb shell device_config override systemui com.android.systemui.keyguard_bottom_area_refactor true $ adb shell device_config override systemui com.android.systemui.keyguard_wm_state_refactor true - $ adb shell device_config override systemui com.android.systemui.media_in_scene_container true $ adb shell device_config override systemui com.android.systemui.migrate_clocks_to_blueprint true - $ adb shell device_config override systemui com.android.systemui.notifications_heads_up_refactor true + $ adb shell device_config override systemui com.android.systemui.notification_avalanche_throttle_hun true $ adb shell device_config override systemui com.android.systemui.predictive_back_sysui true $ adb shell device_config override systemui com.android.systemui.device_entry_udfps_refactor true + $ adb shell device_config override systemui com.android.systemui.scene_container true ``` 2. **Restart** System UI by issuing the following command: ```console diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 156e06843d15..312e62d0b624 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -196,6 +196,28 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { } @Test + public void testUserSwitcherToOneHandedRemovesViews() { + // Can happen when a SIM is inserted into a large screen device + initMode(MODE_USER_SWITCHER); + { + View view1 = mKeyguardSecurityContainer.findViewById( + R.id.keyguard_bouncer_user_switcher); + View view2 = mKeyguardSecurityContainer.findViewById(R.id.user_switcher_header); + assertThat(view1).isNotNull(); + assertThat(view2).isNotNull(); + } + + initMode(MODE_ONE_HANDED); + { + View view1 = mKeyguardSecurityContainer.findViewById( + R.id.keyguard_bouncer_user_switcher); + View view2 = mKeyguardSecurityContainer.findViewById(R.id.user_switcher_header); + assertThat(view1).isNull(); + assertThat(view2).isNull(); + } + } + + @Test public void updatePosition_movesKeyguard() { setupForUpdateKeyguardPosition(/* oneHandedMode= */ true); mKeyguardSecurityContainer.updatePositionByTouchX( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index deef65218c4b..9552564cf1a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -16,18 +16,26 @@ package com.android.systemui.bouncer.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.keyguard.AuthInteractionProperties +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.haptics.msdl.bouncerHapticPlayer +import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.testKosmos +import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -39,11 +47,15 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope + private val msdlPlayer = kosmos.fakeMSDLPlayer + private val bouncerHapticPlayer = kosmos.bouncerHapticPlayer + private val authInteractionProperties = AuthInteractionProperties() private val underTest = kosmos.pinBouncerViewModelFactory.create( isInputEnabled = MutableStateFlow(true), onIntentionalUserInput = {}, authenticationMethod = AuthenticationMethodModel.Pin, + bouncerHapticPlayer = bouncerHapticPlayer, ) @Before @@ -77,4 +89,42 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateButtonClicked() assertThat(animateFailure).isFalse() } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun onAuthenticationResult_playUnlockTokenIfSuccessful() = + testScope.runTest { + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + // Correct PIN: + FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> + underTest.onPinButtonClicked(digit) + } + underTest.onAuthenticateButtonClicked() + runCurrent() + + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun onAuthenticationResult_playFailureTokenIfFailure() = + testScope.runTest { + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + // Wrong PIN: + FakeAuthenticationRepository.DEFAULT_PIN.drop(2).forEach { digit -> + underTest.onPinButtonClicked(digit) + } + underTest.onAuthenticateButtonClicked() + runCurrent() + + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 7c773a902367..c163c6fc0a30 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -16,9 +16,11 @@ package com.android.systemui.bouncer.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.authenticationRepository @@ -27,12 +29,16 @@ import com.android.systemui.authentication.domain.interactor.authenticationInter import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.haptics.msdl.FakeMSDLPlayer +import com.android.systemui.haptics.msdl.bouncerHapticPlayer +import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos +import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,10 +61,13 @@ class PatternBouncerViewModelTest : SysuiTestCase() { private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val bouncerViewModel by lazy { kosmos.bouncerSceneContentViewModel } + private val msdlPlayer: FakeMSDLPlayer = kosmos.fakeMSDLPlayer + private val bouncerHapticHelper = kosmos.bouncerHapticPlayer private val underTest = kosmos.patternBouncerViewModelFactory.create( isInputEnabled = MutableStateFlow(true).asStateFlow(), onIntentionalUserInput = {}, + bouncerHapticPlayer = bouncerHapticHelper, ) private val containerSize = 90 // px @@ -115,10 +124,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { .that(selectedDots) .isEqualTo( CORRECT_PATTERN.subList(0, index + 1).map { - PatternDotViewModel( - x = it.x, - y = it.y, - ) + PatternDotViewModel(x = it.x, y = it.y) } ) assertWithMessage("Wrong current dot for index $index") @@ -174,7 +180,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { listOf( PatternDotViewModel(0, 0), PatternDotViewModel(1, 0), - PatternDotViewModel(2, 0) + PatternDotViewModel(2, 0), ) ) } @@ -200,7 +206,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { listOf( PatternDotViewModel(1, 0), PatternDotViewModel(1, 1), - PatternDotViewModel(1, 2) + PatternDotViewModel(1, 2), ) ) } @@ -228,7 +234,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { listOf( PatternDotViewModel(2, 0), PatternDotViewModel(1, 1), - PatternDotViewModel(0, 2) + PatternDotViewModel(0, 2), ) ) } @@ -300,10 +306,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { val attempts = FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT + 1 repeat(attempts) { attempt -> underTest.onDragStart() - CORRECT_PATTERN.subList( - 0, - kosmos.authenticationRepository.minPatternLength - 1, - ) + CORRECT_PATTERN.subList(0, kosmos.authenticationRepository.minPatternLength - 1) .forEach { coordinate -> underTest.onDrag( xPx = 30f * coordinate.x + 15, @@ -341,6 +344,16 @@ class PatternBouncerViewModelTest : SysuiTestCase() { assertThat(authResult).isTrue() } + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun performDotFeedback_deliversDragToken() = + testScope.runTest { + underTest.performDotFeedback(null) + + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR) + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + private fun dragOverCoordinates(vararg coordinatesDragged: Point) { underTest.onDragStart() coordinatesDragged.forEach(::dragToCoordinate) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 2ee4aee2abab..af5f2acb444d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository @@ -35,12 +36,15 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository import com.android.systemui.classifier.fakeFalsingCollector import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.haptics.msdl.bouncerHapticPlayer +import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos +import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import kotlin.random.Random import kotlin.random.nextInt @@ -64,11 +68,14 @@ class PinBouncerViewModelTest : SysuiTestCase() { private val testScope = kosmos.testScope private val sceneInteractor by lazy { kosmos.sceneInteractor } private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val msdlPlayer = kosmos.fakeMSDLPlayer + private val bouncerHapticPlayer = kosmos.bouncerHapticPlayer private val underTest by lazy { kosmos.pinBouncerViewModelFactory.create( isInputEnabled = MutableStateFlow(true), onIntentionalUserInput = {}, authenticationMethod = AuthenticationMethodModel.Pin, + bouncerHapticPlayer = bouncerHapticPlayer, ) } @@ -97,6 +104,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true), onIntentionalUserInput = {}, authenticationMethod = AuthenticationMethodModel.Sim, + bouncerHapticPlayer = bouncerHapticPlayer, ) assertThat(underTest.isSimAreaVisible).isTrue() @@ -122,6 +130,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true), onIntentionalUserInput = {}, authenticationMethod = AuthenticationMethodModel.Pin, + bouncerHapticPlayer = bouncerHapticPlayer, ) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val hintedPinLength by collectLastValue(underTest.hintedPinLength) @@ -487,11 +496,39 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { lockDeviceAndOpenPinBouncer() - underTest.onDigitButtonDown() + underTest.onDigitButtonDown(null) assertTrue(kosmos.fakeFalsingCollector.wasLastGestureAvoided()) } + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun onDigiButtonDown_deliversKeyStandardToken() = + testScope.runTest { + underTest.onDigitButtonDown(null) + + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.KEYPRESS_STANDARD) + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun onBackspaceButtonPressed_deliversKeyDeleteToken() { + underTest.onBackspaceButtonPressed(null) + + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.KEYPRESS_DELETE) + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun onBackspaceButtonLongPressed_deliversLongPressToken() { + underTest.onBackspaceButtonLongPressed() + + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.LONG_PRESS) + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt index c2acc5ff6689..160865d625f5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt @@ -31,8 +31,11 @@ import com.android.systemui.flags.fakeSystemPropertiesHelper import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeTrustRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AuthenticationFlags +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest @@ -211,7 +214,7 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) kosmos.fakeUserRepository.setSelectedUserInfo( primaryUser, - SelectionStatus.SELECTION_COMPLETE + SelectionStatus.SELECTION_COMPLETE, ) kosmos.fakeTrustRepository.setCurrentUserTrusted(true) @@ -240,6 +243,49 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { } @Test + fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleepInAod() = + testScope.runTest { + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope = this, + ) + kosmos.powerInteractor.setAsleepForTest() + runCurrent() + + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + } + + @Test + fun deviceUnlockStatus_staysLocked_whenFingerprintUnlocked_whileDeviceAsleep() = + testScope.runTest { + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + assertThat(kosmos.keyguardTransitionInteractor.getCurrentState()) + .isEqualTo(KeyguardState.LOCKSCREEN) + + kosmos.powerInteractor.setAsleepForTest() + runCurrent() + + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + } + + @Test fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() = testScope.runTest { kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) @@ -273,7 +319,7 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to DeviceEntryRestrictionReason.UserLockdown, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to - DeviceEntryRestrictionReason.PolicyLockdown + DeviceEntryRestrictionReason.PolicyLockdown, ) } @@ -285,7 +331,7 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) kosmos.fakeSystemPropertiesHelper.set( DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, - "not mainline reboot" + "not mainline reboot", ) runCurrent() @@ -321,7 +367,7 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) kosmos.fakeSystemPropertiesHelper.set( DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, - "not mainline reboot" + "not mainline reboot", ) runCurrent() @@ -358,7 +404,7 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) kosmos.fakeSystemPropertiesHelper.set( DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, - "not mainline reboot" + "not mainline reboot", ) runCurrent() @@ -394,12 +440,12 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { collectLastValue(underTest.deviceEntryRestrictionReason) kosmos.fakeSystemPropertiesHelper.set( DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, - DeviceUnlockedInteractor.REBOOT_MAINLINE_UPDATE + DeviceUnlockedInteractor.REBOOT_MAINLINE_UPDATE, ) kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( AuthenticationFlags( userId = 1, - flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT + flag = LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT, ) ) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt index 50b727c3fed9..9cfd328a9484 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.coroutines.collectLastValue import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel +import com.android.systemui.education.domain.interactor.mockEduInputManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope @@ -62,7 +63,13 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { // Create TestContext here because TemporaryFolder.create() is called in @Before. It is // needed before calling TemporaryFolder.newFolder(). val testContext = TestContext(context, tmpFolder.newFolder()) - underTest = UserContextualEducationRepository(testContext, dsScopeProvider) + underTest = + UserContextualEducationRepository( + testContext, + dsScopeProvider, + kosmos.mockEduInputManager, + kosmos.testDispatcher + ) underTest.setUser(testUserId) } @@ -99,7 +106,8 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(), lastEducationTime = kosmos.fakeEduClock.instant(), usageSessionStartTime = kosmos.fakeEduClock.instant(), - userId = testUserId + userId = testUserId, + gestureType = BACK ) underTest.updateGestureEduModel(BACK) { newModel } val model by collectLastValue(underTest.readGestureEduModelFlow(BACK)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 64915fbf551f..8201bbe4dc47 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -17,15 +17,13 @@ package com.android.systemui.education.domain.interactor import android.content.pm.UserInfo -import android.hardware.input.InputManager -import android.hardware.input.KeyGestureEvent -import android.view.KeyEvent -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK +import com.android.systemui.contextualeducation.GestureType.HOME +import com.android.systemui.contextualeducation.GestureType.OVERVIEW import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.education.data.model.GestureEduModel @@ -40,20 +38,21 @@ import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.kotlin.any -import org.mockito.kotlin.verify +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @kotlinx.coroutines.ExperimentalCoroutinesApi -class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { +class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val contextualEduInteractor = kosmos.contextualEducationInteractor @@ -71,21 +70,27 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { underTest.start() contextualEduInteractor.start() userRepository.setUserInfos(USER_INFOS) + testScope.launch { + contextualEduInteractor.updateKeyboardFirstConnectionTime() + contextualEduInteractor.updateTouchpadFirstConnectionTime() + } } @Test fun newEducationInfoOnMaxSignalCountReached() = testScope.runTest { - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) val model by collectLastValue(underTest.educationTriggered) - assertThat(model?.gestureType).isEqualTo(BACK) + + assertThat(model?.gestureType).isEqualTo(gestureType) } @Test fun newEducationToastOn1stEducation() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) + assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast) } @@ -93,12 +98,12 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun newEducationNotificationOn2ndEducation() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) // runCurrent() to trigger 1st education runCurrent() eduClock.offset(minDurationForNextEdu) - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification) } @@ -106,7 +111,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { @Test fun noEducationInfoBeforeMaxSignalCountReached() = testScope.runTest { - contextualEduInteractor.incrementSignalCount(BACK) + contextualEduInteractor.incrementSignalCount(gestureType) val model by collectLastValue(underTest.educationTriggered) assertThat(model).isNull() } @@ -115,8 +120,8 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun noEducationInfoWhenShortcutTriggeredPreviously() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) - contextualEduInteractor.updateShortcutTriggerTime(BACK) - triggerMaxEducationSignals(BACK) + contextualEduInteractor.updateShortcutTriggerTime(gestureType) + triggerMaxEducationSignals(gestureType) assertThat(model).isNull() } @@ -124,12 +129,12 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun no2ndEducationBeforeMinEduIntervalReached() = testScope.runTest { val models by collectValues(underTest.educationTriggered) - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) runCurrent() // Offset a duration that is less than the required education interval eduClock.offset(1.seconds) - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) runCurrent() assertThat(models.filterNotNull().size).isEqualTo(1) @@ -140,15 +145,15 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { testScope.runTest { val models by collectValues(underTest.educationTriggered) // Trigger 2 educations - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) runCurrent() eduClock.offset(minDurationForNextEdu) - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) runCurrent() // Try triggering 3rd education eduClock.offset(minDurationForNextEdu) - triggerMaxEducationSignals(BACK) + triggerMaxEducationSignals(gestureType) assertThat(models.filterNotNull().size).isEqualTo(2) } @@ -157,18 +162,21 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() = testScope.runTest { val model by - collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) - contextualEduInteractor.incrementSignalCount(BACK) + collectLastValue( + kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType) + ) + contextualEduInteractor.incrementSignalCount(gestureType) eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds)) val secondSignalReceivedTime = eduClock.instant() - contextualEduInteractor.incrementSignalCount(BACK) + contextualEduInteractor.incrementSignalCount(gestureType) assertThat(model) .isEqualTo( GestureEduModel( signalCount = 1, usageSessionStartTime = secondSignalReceivedTime, - userId = 0 + userId = 0, + gestureType = gestureType ) ) } @@ -252,22 +260,9 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { @Test fun updateShortcutTimeOnKeyboardShortcutTriggered() = testScope.runTest { - // runCurrent() to trigger inputManager#registerKeyGestureEventListener in the - // interactor - runCurrent() - val listenerCaptor = - ArgumentCaptor.forClass(InputManager.KeyGestureEventListener::class.java) - verify(kosmos.mockEduInputManager) - .registerKeyGestureEventListener(any(), listenerCaptor.capture()) - - val allAppsKeyGestureEvent = - KeyGestureEvent.Builder() - .setDeviceId(1) - .setModifierState(KeyEvent.META_META_ON) - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) - .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) - .build() - listenerCaptor.value.onKeyGestureEvent(allAppsKeyGestureEvent) + // Only All Apps needs to update the keyboard shortcut + assumeTrue(gestureType == ALL_APPS) + kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS) val model by collectLastValue( @@ -293,10 +288,18 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { runCurrent() } + private suspend fun setUpForDeviceConnection() { + contextualEduInteractor.updateKeyboardFirstConnectionTime() + contextualEduInteractor.updateTouchpadFirstConnectionTime() + } + companion object { - private val USER_INFOS = - listOf( - UserInfo(101, "Second User", 0), - ) + private val USER_INFOS = listOf(UserInfo(101, "Second User", 0)) + + @JvmStatic + @Parameters(name = "{0}") + fun getGestureTypes(): List<GestureType> { + return listOf(BACK, HOME, OVERVIEW, ALL_APPS) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt index c4ac585f7e4a..ab33269ec954 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -69,6 +70,11 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { @Before fun setUp() { + testScope.launch { + interactor.updateKeyboardFirstConnectionTime() + interactor.updateTouchpadFirstConnectionTime() + } + val viewModel = ContextualEduViewModel( kosmos.applicationContext.resources, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt index 686b518b56e0..366b55db4f20 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.fakeVibratorHelper import com.android.systemui.kosmos.testScope import com.android.systemui.log.core.FakeLogBuffer @@ -68,11 +69,13 @@ class QSLongPressEffectTest : SysuiTestCase() { vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_SPIN] = spinDuration whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(true) + kosmos.falsingManager.setFalseLongTap(false) longPressEffect = QSLongPressEffect( vibratorHelper, kosmos.keyguardStateController, + kosmos.falsingManager, FakeLogBuffer.Factory.create(), ) longPressEffect.callback = callback @@ -180,11 +183,7 @@ class QSLongPressEffectTest : SysuiTestCase() { // THEN the expected texture is played val reverseHaptics = - LongPressHapticBuilder.createReversedEffect( - progress, - lowTickDuration, - effectDuration, - ) + LongPressHapticBuilder.createReversedEffect(progress, lowTickDuration, effectDuration) assertThat(reverseHaptics).isNotNull() assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue() } @@ -224,6 +223,20 @@ class QSLongPressEffectTest : SysuiTestCase() { } @Test + fun onAnimationComplete_isFalseLongClick_effectEndsInIdleWithReset() = + testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { + // GIVEN that the long-click is false + kosmos.falsingManager.setFalseLongTap(true) + + // GIVEN that the animation completes + longPressEffect.handleAnimationComplete() + + // THEN the long-press effect ends in the idle state and the properties are reset + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + verify(callback, times(1)).onResetProperties() + } + + @Test fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversingAndClick() = testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP) { // GIVEN that the animation completes diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt index 0c716137f434..639737b37efd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt @@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger import com.android.systemui.inputdevice.tutorial.domain.interactor.KeyboardTouchpadConnectionInteractor import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD @@ -53,6 +54,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock +import org.mockito.kotlin.mock @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -81,6 +83,7 @@ class KeyboardTouchpadTutorialViewModelTest : SysuiTestCase() { Optional.of(kosmos.touchpadGesturesInteractor), KeyboardTouchpadConnectionInteractor(keyboardRepo, touchpadRepo), hasTouchpadTutorialScreens, + mock<InputDeviceTutorialLogger>(), SavedStateHandle(mapOf(INTENT_TUTORIAL_TYPE_KEY to startingPeripheral)) ) lifecycle.addObserver(viewModel) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index 6c3c7ef0162d..fcf4662be145 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -16,7 +16,10 @@ */ package com.android.systemui.keyguard.data.quickaffordance +import android.app.Flags import android.net.Uri +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.provider.Settings import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS import android.provider.Settings.Global.ZEN_MODE_OFF @@ -25,6 +28,7 @@ import android.provider.Settings.Secure.ZEN_DURATION_PROMPT import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.EnableZenModeDialog +import com.android.settingslib.notification.modes.TestModeBuilder import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -35,7 +39,11 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.settings.data.repository.secureSettingsRepository import com.android.systemui.statusbar.policy.ZenModeController +import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository +import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq @@ -43,6 +51,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat +import java.time.Duration import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -66,8 +75,13 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { private val kosmos = testKosmos() private val testDispatcher = kosmos.testDispatcher private val testScope = kosmos.testScope + private val settings = kosmos.fakeSettings + private val zenModeRepository = kosmos.fakeZenModeRepository + private val deviceProvisioningRepository = kosmos.fakeDeviceProvisioningRepository + private val secureSettingsRepository = kosmos.secureSettingsRepository + @Mock private lateinit var zenModeController: ZenModeController @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var conditionUri: Uri @@ -85,17 +99,36 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { DoNotDisturbQuickAffordanceConfig( context, zenModeController, + kosmos.zenModeInteractor, settings, userTracker, testDispatcher, + testScope.backgroundScope, conditionUri, enableZenModeDialog, ) } @Test + @EnableFlags(Flags.FLAG_MODES_UI) fun dndNotAvailable_pickerStateHidden() = testScope.runTest { + deviceProvisioningRepository.setDeviceProvisioned(false) + runCurrent() + + val result = underTest.getPickerScreenState() + runCurrent() + + assertEquals( + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, + result, + ) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + fun controllerDndNotAvailable_pickerStateHidden() = + testScope.runTest { // given whenever(zenModeController.isZenAvailable).thenReturn(false) @@ -105,13 +138,33 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { // then assertEquals( KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, - result + result, ) } @Test + @EnableFlags(Flags.FLAG_MODES_UI) fun dndAvailable_pickerStateVisible() = testScope.runTest { + deviceProvisioningRepository.setDeviceProvisioned(true) + runCurrent() + + val result = underTest.getPickerScreenState() + runCurrent() + + assertThat(result) + .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java) + val defaultPickerState = + result as KeyguardQuickAffordanceConfig.PickerScreenState.Default + assertThat(defaultPickerState.configureIntent).isNotNull() + assertThat(defaultPickerState.configureIntent?.action) + .isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + fun controllerDndAvailable_pickerStateVisible() = + testScope.runTest { // given whenever(zenModeController.isZenAvailable).thenReturn(true) @@ -129,7 +182,27 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { } @Test - fun onTriggered_dndModeIsNotZEN_MODE_OFF_setToZEN_MODE_OFF() = + @EnableFlags(Flags.FLAG_MODES_UI) + fun onTriggered_dndModeIsNotOff_setToOff() = + testScope.runTest { + val currentModes by collectLastValue(zenModeRepository.modes) + + zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2) + collectLastValue(underTest.lockScreenState) + runCurrent() + + val result = underTest.onTriggered(null) + runCurrent() + + val dndMode = currentModes!!.single() + assertThat(dndMode.isActive).isFalse() + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + fun onTriggered_controllerDndModeIsNotZEN_MODE_OFF_setToZEN_MODE_OFF() = testScope.runTest { // given whenever(zenModeController.isZenAvailable).thenReturn(true) @@ -140,11 +213,12 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { // when val result = underTest.onTriggered(null) + verify(zenModeController) .setZen( spyZenMode.capture(), spyConditionId.capture(), - eq(DoNotDisturbQuickAffordanceConfig.TAG) + eq(DoNotDisturbQuickAffordanceConfig.TAG), ) // then @@ -154,7 +228,28 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { } @Test - fun onTriggered_dndModeIsZEN_MODE_OFF_settingFOREVER_setZenWithoutCondition() = + @EnableFlags(Flags.FLAG_MODES_UI) + fun onTriggered_dndModeIsOff_settingFOREVER_setZenWithoutCondition() = + testScope.runTest { + val currentModes by collectLastValue(zenModeRepository.modes) + + zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) + secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER) + collectLastValue(underTest.lockScreenState) + runCurrent() + + val result = underTest.onTriggered(null) + runCurrent() + + val dndMode = currentModes!!.single() + assertThat(dndMode.isActive).isTrue() + assertThat(zenModeRepository.getModeActiveDuration(dndMode.id)).isNull() + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + fun onTriggered_controllerDndModeIsZEN_MODE_OFF_settingFOREVER_setZenWithoutCondition() = testScope.runTest { // given whenever(zenModeController.isZenAvailable).thenReturn(true) @@ -169,7 +264,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { .setZen( spyZenMode.capture(), spyConditionId.capture(), - eq(DoNotDisturbQuickAffordanceConfig.TAG) + eq(DoNotDisturbQuickAffordanceConfig.TAG), ) // then @@ -179,7 +274,27 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { } @Test - fun onTriggered_dndZEN_MODE_OFF_settingNotFOREVERorPROMPT_zenWithCondition() = + @EnableFlags(Flags.FLAG_MODES_UI) + fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() = + testScope.runTest { + val currentModes by collectLastValue(zenModeRepository.modes) + zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) + secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900) + runCurrent() + + val result = underTest.onTriggered(null) + runCurrent() + + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + val dndMode = currentModes!!.single() + assertThat(dndMode.isActive).isTrue() + assertThat(zenModeRepository.getModeActiveDuration(dndMode.id)) + .isEqualTo(Duration.ofMinutes(-900)) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + fun onTriggered_controllerDndZEN_MODE_OFF_settingNotFOREVERorPROMPT_zenWithCondition() = testScope.runTest { // given whenever(zenModeController.isZenAvailable).thenReturn(true) @@ -194,7 +309,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { .setZen( spyZenMode.capture(), spyConditionId.capture(), - eq(DoNotDisturbQuickAffordanceConfig.TAG) + eq(DoNotDisturbQuickAffordanceConfig.TAG), ) // then @@ -204,7 +319,28 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { } @Test - fun onTriggered_dndModeIsZEN_MODE_OFF_settingIsPROMPT_showDialog() = + @EnableFlags(Flags.FLAG_MODES_UI) + fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() = + testScope.runTest { + val expandable: Expandable = mock() + zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) + secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT) + whenever(enableZenModeDialog.createDialog()).thenReturn(mock()) + collectLastValue(underTest.lockScreenState) + runCurrent() + + val result = underTest.onTriggered(expandable) + + assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog) + assertEquals( + expandable, + (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable, + ) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + fun onTriggered_controllerDndModeIsZEN_MODE_OFF_settingIsPROMPT_showDialog() = testScope.runTest { // given val expandable: Expandable = mock() @@ -222,13 +358,31 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog) assertEquals( expandable, - (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable + (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable, ) } @Test + @EnableFlags(Flags.FLAG_MODES_UI) fun lockScreenState_dndAvailableStartsAsTrue_changeToFalse_StateIsHidden() = testScope.runTest { + deviceProvisioningRepository.setDeviceProvisioned(true) + val valueSnapshot = collectLastValue(underTest.lockScreenState) + val secondLastValue = valueSnapshot() + runCurrent() + + deviceProvisioningRepository.setDeviceProvisioned(false) + runCurrent() + val lastValue = valueSnapshot() + + assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) + assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + fun lockScreenState_controllerDndAvailableStartsAsTrue_changeToFalse_StateIsHidden() = + testScope.runTest { // given whenever(zenModeController.isZenAvailable).thenReturn(true) val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor() @@ -246,7 +400,44 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { } @Test - fun lockScreenState_dndModeStartsAsZEN_MODE_OFF_changeToNotOFF_StateVisible() = + @EnableFlags(Flags.FLAG_MODES_UI) + fun lockScreenState_dndModeStartsAsOff_changeToOn_StateVisible() = + testScope.runTest { + val lockScreenState by collectLastValue(underTest.lockScreenState) + + zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) + runCurrent() + + assertThat(lockScreenState) + .isEqualTo( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + Icon.Resource( + R.drawable.qs_dnd_icon_off, + ContentDescription.Resource(R.string.dnd_is_off), + ), + ActivationState.Inactive, + ) + ) + + zenModeRepository.removeMode(TestModeBuilder.MANUAL_DND_INACTIVE.id) + zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + runCurrent() + + assertThat(lockScreenState) + .isEqualTo( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + Icon.Resource( + R.drawable.qs_dnd_icon_on, + ContentDescription.Resource(R.string.dnd_is_on), + ), + ActivationState.Active, + ) + ) + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + fun lockScreenState_controllerDndModeStartsAsZEN_MODE_OFF_changeToNotOFF_StateVisible() = testScope.runTest { // given whenever(zenModeController.isZenAvailable).thenReturn(true) @@ -265,9 +456,9 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( R.drawable.qs_dnd_icon_off, - ContentDescription.Resource(R.string.dnd_is_off) + ContentDescription.Resource(R.string.dnd_is_off), ), - ActivationState.Inactive + ActivationState.Inactive, ), secondLastValue, ) @@ -275,9 +466,9 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( R.drawable.qs_dnd_icon_on, - ContentDescription.Resource(R.string.dnd_is_on) + ContentDescription.Resource(R.string.dnd_is_on), ), - ActivationState.Active + ActivationState.Active, ), lastValue, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt index 59f16d70fab5..84b7f5c28265 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt @@ -17,17 +17,16 @@ package com.android.systemui.keyguard.domain.interactor -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint @@ -59,8 +58,8 @@ import org.mockito.MockitoAnnotations class KeyguardBlueprintInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val underTest = kosmos.keyguardBlueprintInteractor - private val keyguardBlueprintRepository = kosmos.keyguardBlueprintRepository + private val underTest by lazy { kosmos.keyguardBlueprintInteractor } + private val keyguardBlueprintRepository by lazy { kosmos.keyguardBlueprintRepository } private val clockRepository by lazy { kosmos.fakeKeyguardClockRepository } private val configurationRepository by lazy { kosmos.fakeConfigurationRepository } private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository } @@ -75,7 +74,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { sensorId = 1, strength = SensorStrength.STRONG, sensorType = FingerprintSensorType.POWER_BUTTON, - sensorLocations = mapOf() + sensorLocations = mapOf(), ) } @@ -93,7 +92,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN) + @DisableSceneContainer fun testAppliesSplitShadeBlueprint() { testScope.runTest { val blueprintId by collectLastValue(underTest.blueprintId) @@ -107,7 +106,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN) + @EnableSceneContainer fun testDoesNotApplySplitShadeBlueprint() { testScope.runTest { val blueprintId by collectLastValue(underTest.blueprintId) @@ -122,7 +121,7 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN) + @DisableSceneContainer fun fingerprintPropertyInitialized_updatesBlueprint() { testScope.runTest { underTest.start() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt index 41c5b7332a4f..ff6ea3a14ff2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt @@ -25,6 +25,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.BurnInInteractor @@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Answers @@ -71,10 +74,8 @@ class AodBurnInViewModelTest : SysuiTestCase() { private val burnInFlow = MutableStateFlow(BurnInModel()) @Before - @DisableFlags( - AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, - AConfigFlags.FLAG_COMPOSE_LOCKSCREEN - ) + @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) + @DisableSceneContainer fun setUp() { MockitoAnnotations.initMocks(this) whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow) @@ -112,18 +113,13 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN, value = 1f, - transitionState = TransitionState.FINISHED + transitionState = TransitionState.FINISHED, ), validateStep = false, ) // Trigger a change to the burn-in model - burnInFlow.value = - BurnInModel( - translationX = 20, - translationY = 30, - scale = 0.5f, - ) + burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f) assertThat(movement?.translationX).isEqualTo(0) assertThat(movement?.translationY).isEqualTo(0) @@ -143,17 +139,12 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, value = 1f, - transitionState = TransitionState.FINISHED + transitionState = TransitionState.FINISHED, ), validateStep = false, ) // Trigger a change to the burn-in model - burnInFlow.value = - BurnInModel( - translationX = 20, - translationY = 30, - scale = 0.5f, - ) + burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f) assertThat(movement?.translationX).isEqualTo(20) assertThat(movement?.translationY).isEqualTo(30) @@ -166,7 +157,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, value = 0f, - transitionState = TransitionState.STARTED + transitionState = TransitionState.STARTED, ), validateStep = false, ) @@ -180,11 +171,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() = testScope.runTest { - burnInParameters = - burnInParameters.copy( - minViewY = 100, - topInset = 80, - ) + burnInParameters = burnInParameters.copy(minViewY = 100, topInset = 80) val movement by collectLastValue(underTest.movement(burnInParameters)) // Set to dozing (on AOD) @@ -193,18 +180,13 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, value = 1f, - transitionState = TransitionState.FINISHED + transitionState = TransitionState.FINISHED, ), validateStep = false, ) // Trigger a change to the burn-in model - burnInFlow.value = - BurnInModel( - translationX = 20, - translationY = -30, - scale = 0.5f, - ) + burnInFlow.value = BurnInModel(translationX = 20, translationY = -30, scale = 0.5f) assertThat(movement?.translationX).isEqualTo(20) // -20 instead of -30, due to inset of 80 assertThat(movement?.translationY).isEqualTo(-20) @@ -217,7 +199,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, value = 0f, - transitionState = TransitionState.STARTED + transitionState = TransitionState.STARTED, ), validateStep = false, ) @@ -231,11 +213,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() = testScope.runTest { - burnInParameters = - burnInParameters.copy( - minViewY = 100, - topInset = 80, - ) + burnInParameters = burnInParameters.copy(minViewY = 100, topInset = 80) val movement by collectLastValue(underTest.movement(burnInParameters)) // Set to dozing (on AOD) @@ -244,18 +222,13 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, value = 1f, - transitionState = TransitionState.FINISHED + transitionState = TransitionState.FINISHED, ), validateStep = false, ) // Trigger a change to the burn-in model - burnInFlow.value = - BurnInModel( - translationX = 20, - translationY = -30, - scale = 0.5f, - ) + burnInFlow.value = BurnInModel(translationX = 20, translationY = -30, scale = 0.5f) assertThat(movement?.translationX).isEqualTo(20) // -20 instead of -30, due to inset of 80 assertThat(movement?.translationY).isEqualTo(-20) @@ -268,7 +241,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, value = 0f, - transitionState = TransitionState.STARTED + transitionState = TransitionState.STARTED, ), validateStep = false, ) @@ -291,18 +264,13 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.GONE, to = KeyguardState.AOD, value = 1f, - transitionState = TransitionState.FINISHED + transitionState = TransitionState.FINISHED, ), validateStep = false, ) // Trigger a change to the burn-in model - burnInFlow.value = - BurnInModel( - translationX = 20, - translationY = 30, - scale = 0.5f, - ) + burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f) assertThat(movement?.translationX).isEqualTo(20) assertThat(movement?.translationY).isEqualTo(30) @@ -311,9 +279,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + @DisableSceneContainer @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun translationAndScale_composeFlagOff_weatherLargeClock() = + fun translationAndScale_sceneContainerOff_weatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, isWeatherClock = true, @@ -321,9 +289,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + @DisableSceneContainer @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun translationAndScale_composeFlagOff_weatherSmallClock() = + fun translationAndScale_sceneContainerOff_weatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, isWeatherClock = true, @@ -331,9 +299,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + @DisableSceneContainer @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun translationAndScale_composeFlagOff_nonWeatherLargeClock() = + fun translationAndScale_sceneContainerOff_nonWeatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, isWeatherClock = false, @@ -341,9 +309,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + @DisableSceneContainer @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun translationAndScale_composeFlagOff_nonWeatherSmallClock() = + fun translationAndScale_sceneContainerOff_nonWeatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, isWeatherClock = false, @@ -351,11 +319,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags( - AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, - AConfigFlags.FLAG_COMPOSE_LOCKSCREEN - ) - fun translationAndScale_composeFlagOn_weatherLargeClock() = + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) + @EnableSceneContainer + fun translationAndScale_sceneContainerOn_weatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, isWeatherClock = true, @@ -363,11 +329,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags( - AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, - AConfigFlags.FLAG_COMPOSE_LOCKSCREEN - ) - fun translationAndScale_composeFlagOn_weatherSmallClock() = + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) + @EnableSceneContainer + fun translationAndScale_sceneContainerOn_weatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, isWeatherClock = true, @@ -375,11 +339,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags( - AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, - AConfigFlags.FLAG_COMPOSE_LOCKSCREEN - ) - fun translationAndScale_composeFlagOn_nonWeatherLargeClock() = + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) + @EnableSceneContainer + fun translationAndScale_sceneContainerOn_nonWeatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, isWeatherClock = false, @@ -387,11 +349,10 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags( - AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, - AConfigFlags.FLAG_COMPOSE_LOCKSCREEN - ) - fun translationAndScale_composeFlagOn_nonWeatherSmallClock() = + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) + @EnableSceneContainer + @Ignore("b/367659687") + fun translationAndScale_sceneContainerOn_nonWeatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, isWeatherClock = false, @@ -421,18 +382,13 @@ class AodBurnInViewModelTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, value = 1f, - transitionState = TransitionState.FINISHED + transitionState = TransitionState.FINISHED, ), validateStep = false, ) // Trigger a change to the burn-in model - burnInFlow.value = - BurnInModel( - translationX = 20, - translationY = 30, - scale = 0.5f, - ) + burnInFlow.value = BurnInModel(translationX = 20, translationY = 30, scale = 0.5f) assertThat(movement?.translationX).isEqualTo(20) assertThat(movement?.translationY).isEqualTo(30) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt index 17e1b53a3ba9..05a6b8785daf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt @@ -16,14 +16,13 @@ package com.android.systemui.keyguard.ui.viewmodel -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.BrokenWithSceneContainer +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardClockRepository @@ -229,8 +228,8 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN) - fun testSmallClockTop_splitShade_composeLockscreenOn() = + @EnableSceneContainer + fun testSmallClockTop_splitShade_sceneContainerOn() = testScope.runTest { with(kosmos) { shadeRepository.setShadeLayoutWide(true) @@ -244,8 +243,8 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN) - fun testSmallClockTop_splitShade_composeLockscreenOff() = + @DisableSceneContainer + fun testSmallClockTop_splitShade_sceneContainerOff() = testScope.runTest { with(kosmos) { shadeRepository.setShadeLayoutWide(true) @@ -257,8 +256,8 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - @EnableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN) - fun testSmallClockTop_nonSplitShade_composeLockscreenOn() = + @EnableSceneContainer + fun testSmallClockTop_nonSplitShade_sceneContainerOn() = testScope.runTest { with(kosmos) { shadeRepository.setShadeLayoutWide(false) @@ -270,8 +269,8 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN) - fun testSmallClockTop_nonSplitShade_composeLockscreenOff() = + @DisableSceneContainer + fun testSmallClockTop_nonSplitShade_sceneContainerOff() = testScope.runTest { with(kosmos) { shadeRepository.setShadeLayoutWide(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt deleted file mode 100644 index 42db96e917ee..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.interactor - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.kosmos.testScope -import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository -import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository -import com.android.systemui.qs.panels.data.repository.gridLayoutTypeRepository -import com.android.systemui.qs.panels.shared.model.GridLayoutType -import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType -import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository -import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor -import com.android.systemui.qs.pipeline.shared.TileSpec -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidJUnit4::class) -class GridConsistencyInteractorTest : SysuiTestCase() { - - data object NoopGridLayoutType : GridLayoutType - - private val kosmos = - testKosmos().apply { - defaultLargeTilesRepository = - object : DefaultLargeTilesRepository { - override val defaultLargeTiles = - setOf( - TileSpec.create("largeA"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("largeD"), - ) - } - gridConsistencyInteractorsMap = - mapOf( - Pair(NoopGridLayoutType, noopGridConsistencyInteractor), - Pair(InfiniteGridLayoutType, infiniteGridConsistencyInteractor) - ) - } - - private val underTest = with(kosmos) { gridConsistencyInteractor } - - @Before - fun setUp() { - // Mostly testing InfiniteGridConsistencyInteractor because it reorders tiles - with(kosmos) { gridLayoutTypeRepository.setLayout(InfiniteGridLayoutType) } - underTest.start() - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun changeLayoutType_usesCorrectGridConsistencyInteractor() = - with(kosmos) { - testScope.runTest { - // Using the no-op grid consistency interactor - gridLayoutTypeRepository.setLayout(NoopGridLayoutType) - - // Setting an invalid layout with holes - // [ Large A ] [ sa ] - // [ Large B ] [ Large C ] - // [ sb ] [ Large D ] - val newTiles = - listOf( - TileSpec.create("largeA"), - TileSpec.create("smallA"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("smallB"), - TileSpec.create("largeD"), - ) - tileSpecRepository.setTiles(0, newTiles) - - runCurrent() - - val tiles = currentTilesInteractor.currentTiles.value - val tileSpecs = tiles.map { it.spec } - - // Saved tiles should be unchanged - assertThat(tileSpecs).isEqualTo(newTiles) - } - } - - @Test - fun validTilesWithInfiniteGridConsistencyInteractor_unchangedList() = - with(kosmos) { - testScope.runTest { - // Setting a valid layout with holes - // [ Large A ] [ sa ][ sb ] - // [ Large B ] [ Large C ] - // [ Large D ] - val newTiles = - listOf( - TileSpec.create("largeA"), - TileSpec.create("smallA"), - TileSpec.create("smallB"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("largeD"), - ) - tileSpecRepository.setTiles(0, newTiles) - - runCurrent() - - val tiles = currentTilesInteractor.currentTiles.value - val tileSpecs = tiles.map { it.spec } - - // Saved tiles should be unchanged - assertThat(tileSpecs).isEqualTo(newTiles) - } - } - - @Test - fun invalidTilesWithInfiniteGridConsistencyInteractor_savesNewList() = - with(kosmos) { - testScope.runTest { - // Setting an invalid layout with holes - // [ sa ] [ Large A ] - // [ Large B ] [ sb ] [ sc ] - // [ sd ] [ se ] [ Large C ] - val newTiles = - listOf( - TileSpec.create("smallA"), - TileSpec.create("largeA"), - TileSpec.create("largeB"), - TileSpec.create("smallB"), - TileSpec.create("smallC"), - TileSpec.create("smallD"), - TileSpec.create("smallE"), - TileSpec.create("largeC"), - ) - tileSpecRepository.setTiles(0, newTiles) - - runCurrent() - - val tiles = currentTilesInteractor.currentTiles.value - val tileSpecs = tiles.map { it.spec } - - // Expected grid - // [ sa ] [ Large A ] [ sb ] - // [ Large B ] [ sc ] [ sd ] - // [ se ] [ Large C ] - val expectedTiles = - listOf( - TileSpec.create("smallA"), - TileSpec.create("largeA"), - TileSpec.create("smallB"), - TileSpec.create("largeB"), - TileSpec.create("smallC"), - TileSpec.create("smallD"), - TileSpec.create("smallE"), - TileSpec.create("largeC"), - ) - - // Saved tiles should be unchanged - assertThat(tileSpecs).isEqualTo(expectedTiles) - } - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt deleted file mode 100644 index ea51398e6256..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.interactor - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.kosmos.testScope -import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository -import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository -import com.android.systemui.qs.pipeline.shared.TileSpec -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class InfiniteGridConsistencyInteractorTest : SysuiTestCase() { - - private val kosmos = - testKosmos().apply { - defaultLargeTilesRepository = - object : DefaultLargeTilesRepository { - override val defaultLargeTiles: Set<TileSpec> = - setOf( - TileSpec.create("largeA"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("largeD"), - ) - } - } - private val underTest = with(kosmos) { infiniteGridConsistencyInteractor } - - @Test - fun validTiles_returnsUnchangedList() = - with(kosmos) { - testScope.runTest { - // Original grid - // [ Large A ] [ sa ][ sb ] - // [ Large B ] [ Large C ] - // [ Large D ] - val tiles = - listOf( - TileSpec.create("largeA"), - TileSpec.create("smallA"), - TileSpec.create("smallB"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("largeD"), - ) - - val newTiles = underTest.reconcileTiles(tiles) - - assertThat(newTiles).isEqualTo(tiles) - } - } - - @Test - fun invalidTiles_moveIconTileForward() = - with(kosmos) { - testScope.runTest { - // Original grid - // [ Large A ] [ sa ] - // [ Large B ] [ Large C ] - // [ sb ] [ Large D ] - val tiles = - listOf( - TileSpec.create("largeA"), - TileSpec.create("smallA"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("smallB"), - TileSpec.create("largeD"), - ) - // Expected grid - // [ Large A ] [ sa ][ sb ] - // [ Large B ] [ Large C ] - // [ Large D ] - val expectedTiles = - listOf( - TileSpec.create("largeA"), - TileSpec.create("smallA"), - TileSpec.create("smallB"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("largeD"), - ) - - val newTiles = underTest.reconcileTiles(tiles) - - assertThat(newTiles).isEqualTo(expectedTiles) - } - } - - @Test - fun invalidTiles_moveIconTileBack() = - with(kosmos) { - testScope.runTest { - // Original grid - // [ sa ] [ Large A ] - // [ Large B ] [ Large C ] - // [ Large D ] - val tiles = - listOf( - TileSpec.create("smallA"), - TileSpec.create("largeA"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("largeD"), - ) - // Expected grid - // [ Large A ] [ Large B ] - // [ Large C ] [ Large D ] - // [ sa ] - val expectedTiles = - listOf( - TileSpec.create("largeA"), - TileSpec.create("largeB"), - TileSpec.create("largeC"), - TileSpec.create("largeD"), - TileSpec.create("smallA"), - ) - - val newTiles = underTest.reconcileTiles(tiles) - - assertThat(newTiles).isEqualTo(expectedTiles) - } - } - - @Test - fun invalidTiles_multipleCorrections() = - with(kosmos) { - testScope.runTest { - // Original grid - // [ sa ] [ Large A ] - // [ Large B ] [ sb ] [ sc ] - // [ sd ] [ se ] [ Large C ] - val tiles = - listOf( - TileSpec.create("smallA"), - TileSpec.create("largeA"), - TileSpec.create("largeB"), - TileSpec.create("smallB"), - TileSpec.create("smallC"), - TileSpec.create("smallD"), - TileSpec.create("smallE"), - TileSpec.create("largeC"), - ) - // Expected grid - // [ sa ] [ Large A ] [ sb ] - // [ Large B ] [ sc ] [ sd ] - // [ se ] [ Large C ] - val expectedTiles = - listOf( - TileSpec.create("smallA"), - TileSpec.create("largeA"), - TileSpec.create("smallB"), - TileSpec.create("largeB"), - TileSpec.create("smallC"), - TileSpec.create("smallD"), - TileSpec.create("smallE"), - TileSpec.create("largeC"), - ) - - val newTiles = underTest.reconcileTiles(tiles) - - assertThat(newTiles).isEqualTo(expectedTiles) - } - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt index 53384afb66be..9e90090549dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testScope import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository +import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel @@ -44,12 +45,7 @@ class InfiniteGridLayoutTest : SysuiTestCase() { } private val underTest = - with(kosmos) { - InfiniteGridLayout( - iconTilesViewModel, - fixedColumnsSizeViewModel, - ) - } + with(kosmos) { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) } @Test fun correctPagination_underOnePage_sameOrder() = @@ -65,7 +61,7 @@ class InfiniteGridLayoutTest : SysuiTestCase() { smallTile(), largeTile(), largeTile(), - smallTile() + smallTile(), ) val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 763a1a943bf8..385089122fc4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -27,6 +27,7 @@ import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.internal.logging.uiEventLoggerFake import com.android.internal.policy.IKeyguardDismissCallback @@ -88,9 +89,11 @@ import com.android.systemui.scene.data.model.asIterable import com.android.systemui.scene.data.repository.Transition import com.android.systemui.scene.domain.interactor.sceneBackInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shared.system.QuickStepContract import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor @@ -161,6 +164,7 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @DisableFlags(DualShade.FLAG_NAME) fun hydrateVisibility() = testScope.runTest { val currentDesiredSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -221,6 +225,87 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(DualShade.FLAG_NAME) + fun hydrateVisibility_dualShade() = + testScope.runTest { + val currentDesiredSceneKey by collectLastValue(sceneInteractor.currentScene) + val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays) + val isVisible by collectLastValue(sceneInteractor.isVisible) + val transitionStateFlow = + prepareState( + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = true, + initialSceneKey = Scenes.Gone, + ) + assertThat(currentDesiredSceneKey).isEqualTo(Scenes.Gone) + assertThat(currentDesiredOverlays).isEmpty() + assertThat(isVisible).isTrue() + + underTest.start() + assertThat(isVisible).isFalse() + + // Expand the notifications shade. + fakeSceneDataSource.pause() + sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason") + transitionStateFlow.value = + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = Overlays.NotificationsShade, + fromContent = Scenes.Gone, + toContent = Overlays.NotificationsShade, + currentScene = Scenes.Gone, + currentOverlays = flowOf(emptySet()), + progress = flowOf(0.5f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + assertThat(isVisible).isTrue() + fakeSceneDataSource.unpause(expectedScene = Scenes.Gone) + transitionStateFlow.value = + ObservableTransitionState.Idle( + currentScene = Scenes.Gone, + currentOverlays = setOf(Overlays.NotificationsShade), + ) + assertThat(isVisible).isTrue() + + // Collapse the notifications shade. + fakeSceneDataSource.pause() + sceneInteractor.hideOverlay(Overlays.NotificationsShade, "reason") + transitionStateFlow.value = + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = Overlays.NotificationsShade, + fromContent = Overlays.NotificationsShade, + toContent = Scenes.Gone, + currentScene = Scenes.Gone, + currentOverlays = flowOf(setOf(Overlays.NotificationsShade)), + progress = flowOf(0.5f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + assertThat(isVisible).isTrue() + fakeSceneDataSource.unpause(expectedScene = Scenes.Gone) + transitionStateFlow.value = + ObservableTransitionState.Idle( + currentScene = Scenes.Gone, + currentOverlays = emptySet(), + ) + assertThat(isVisible).isFalse() + + kosmos.headsUpNotificationRepository.setNotifications( + buildNotificationRows(isPinned = true) + ) + assertThat(isVisible).isTrue() + + kosmos.headsUpNotificationRepository.setNotifications( + buildNotificationRows(isPinned = false) + ) + assertThat(isVisible).isFalse() + } + + @Test fun hydrateVisibility_basedOnDeviceProvisioning() = testScope.runTest { val isVisible by collectLastValue(sceneInteractor.isVisible) @@ -1621,6 +1706,7 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @DisableFlags(DualShade.FLAG_NAME) fun hydrateInteractionState_whileLocked() = testScope.runTest { val transitionStateFlow = prepareState(initialSceneKey = Scenes.Lockscreen) @@ -1707,6 +1793,7 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @DisableFlags(DualShade.FLAG_NAME) fun hydrateInteractionState_whileUnlocked() = testScope.runTest { val transitionStateFlow = @@ -1795,6 +1882,186 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(DualShade.FLAG_NAME) + fun hydrateInteractionState_dualShade_whileLocked() = + testScope.runTest { + val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays) + val transitionStateFlow = prepareState(initialSceneKey = Scenes.Lockscreen) + underTest.start() + runCurrent() + verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true) + assertThat(currentDesiredOverlays).isEmpty() + + clearInvocations(centralSurfaces) + emulateSceneTransition( + transitionStateFlow = transitionStateFlow, + toScene = Scenes.Bouncer, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces) + .setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false) + }, + ) + + clearInvocations(centralSurfaces) + emulateSceneTransition( + transitionStateFlow = transitionStateFlow, + toScene = Scenes.Lockscreen, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true) + }, + ) + + clearInvocations(centralSurfaces) + emulateOverlayTransition( + transitionStateFlow = transitionStateFlow, + toOverlay = Overlays.NotificationsShade, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces) + .setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false) + }, + ) + + clearInvocations(centralSurfaces) + emulateSceneTransition( + transitionStateFlow = transitionStateFlow, + toScene = Scenes.Lockscreen, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces).setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true) + }, + ) + + clearInvocations(centralSurfaces) + emulateOverlayTransition( + transitionStateFlow = transitionStateFlow, + toOverlay = Overlays.QuickSettingsShade, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + ) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun hydrateInteractionState_dualShade_whileUnlocked() = + testScope.runTest { + val currentDesiredOverlays by collectLastValue(sceneInteractor.currentOverlays) + val transitionStateFlow = + prepareState( + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = true, + initialSceneKey = Scenes.Gone, + ) + underTest.start() + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + assertThat(currentDesiredOverlays).isEmpty() + + clearInvocations(centralSurfaces) + emulateSceneTransition( + transitionStateFlow = transitionStateFlow, + toScene = Scenes.Bouncer, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + ) + + clearInvocations(centralSurfaces) + emulateSceneTransition( + transitionStateFlow = transitionStateFlow, + toScene = Scenes.Lockscreen, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + ) + + clearInvocations(centralSurfaces) + emulateSceneTransition( + transitionStateFlow = transitionStateFlow, + toScene = Scenes.Shade, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + ) + + clearInvocations(centralSurfaces) + emulateSceneTransition( + transitionStateFlow = transitionStateFlow, + toScene = Scenes.Lockscreen, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + ) + + clearInvocations(centralSurfaces) + emulateSceneTransition( + transitionStateFlow = transitionStateFlow, + toScene = Scenes.QuickSettings, + verifyBeforeTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyDuringTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + verifyAfterTransition = { + verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean()) + }, + ) + } + + @Test fun respondToFalsingDetections() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) @@ -2131,19 +2398,40 @@ class SceneContainerStartableTest : SysuiTestCase() { verifyAfterTransition: (() -> Unit)? = null, ) { val fromScene = sceneInteractor.currentScene.value + val fromOverlays = sceneInteractor.currentOverlays.value sceneInteractor.changeScene(toScene, "reason") runCurrent() verifyBeforeTransition?.invoke() transitionStateFlow.value = - ObservableTransitionState.Transition( - fromScene = fromScene, - toScene = toScene, - currentScene = flowOf(fromScene), - progress = flowOf(0.5f), - isInitiatedByUserInput = true, - isUserInputOngoing = flowOf(true), - ) + if (fromOverlays.isEmpty()) { + // Regular scene-to-scene transition. + ObservableTransitionState.Transition.ChangeScene( + fromScene = fromScene, + toScene = toScene, + currentScene = flowOf(fromScene), + currentOverlays = fromOverlays, + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + } else { + // An overlay is present; hide it. + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = fromOverlays.first(), + fromContent = fromOverlays.first(), + toContent = toScene, + currentScene = fromScene, + currentOverlays = sceneInteractor.currentOverlays, + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + } runCurrent() verifyDuringTransition?.invoke() @@ -2152,6 +2440,60 @@ class SceneContainerStartableTest : SysuiTestCase() { verifyAfterTransition?.invoke() } + private fun TestScope.emulateOverlayTransition( + transitionStateFlow: MutableStateFlow<ObservableTransitionState>, + toOverlay: OverlayKey, + verifyBeforeTransition: (() -> Unit)? = null, + verifyDuringTransition: (() -> Unit)? = null, + verifyAfterTransition: (() -> Unit)? = null, + ) { + val fromScene = sceneInteractor.currentScene.value + val fromOverlays = sceneInteractor.currentOverlays.value + sceneInteractor.showOverlay(toOverlay, "reason") + runCurrent() + verifyBeforeTransition?.invoke() + + transitionStateFlow.value = + if (fromOverlays.isEmpty()) { + // Show a new overlay. + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = toOverlay, + fromContent = fromScene, + toContent = toOverlay, + currentScene = fromScene, + currentOverlays = sceneInteractor.currentOverlays, + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + } else { + // Overlay-to-overlay transition. + ObservableTransitionState.Transition.ReplaceOverlay( + fromOverlay = fromOverlays.first(), + toOverlay = toOverlay, + currentScene = fromScene, + currentOverlays = sceneInteractor.currentOverlays, + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + } + runCurrent() + verifyDuringTransition?.invoke() + + transitionStateFlow.value = + ObservableTransitionState.Idle( + currentScene = fromScene, + currentOverlays = setOf(toOverlay), + ) + runCurrent() + verifyAfterTransition?.invoke() + } + private fun TestScope.prepareState( isDeviceUnlocked: Boolean = false, isBypassEnabled: Boolean = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt index 4d69f0ddc4b7..f86337ec63dc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt @@ -19,8 +19,8 @@ package com.android.systemui.scene.shared.flag import android.platform.test.flag.junit.FlagsParameterization import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_COMPOSE_LOCKSCREEN import com.android.systemui.Flags.FLAG_EXAMPLE_FLAG +import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase import com.android.systemui.flags.andSceneContainer @@ -66,7 +66,7 @@ internal class SceneContainerFlagParameterizationTest : SysuiTestCase() { @Test fun oneDependencyAndSceneContainer() { - val dependentFlag = FLAG_COMPOSE_LOCKSCREEN + val dependentFlag = FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR val result = FlagsParameterization.allCombinationsOf(dependentFlag).andSceneContainer() Truth.assertThat(result).hasSize(3) Truth.assertThat(result[0].mOverrides[dependentFlag]).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index fb32855ee2b7..0f6dc0723f42 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -17,7 +17,9 @@ package com.android.systemui.statusbar.policy.domain.interactor import android.app.AutomaticZenRule +import android.app.Flags import android.app.NotificationManager.Policy +import android.platform.test.annotations.EnableFlags import android.provider.Settings import android.provider.Settings.Secure.ZEN_DURATION import android.provider.Settings.Secure.ZEN_DURATION_FOREVER @@ -32,6 +34,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.shared.settings.data.repository.secureSettingsRepository +import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -50,10 +53,31 @@ class ZenModeInteractorTest : SysuiTestCase() { private val testScope = kosmos.testScope private val zenModeRepository = kosmos.fakeZenModeRepository private val settingsRepository = kosmos.secureSettingsRepository + private val deviceProvisioningRepository = kosmos.fakeDeviceProvisioningRepository private val underTest = kosmos.zenModeInteractor @Test + fun isZenAvailable_off() = + testScope.runTest { + val isZenAvailable by collectLastValue(underTest.isZenAvailable) + deviceProvisioningRepository.setDeviceProvisioned(false) + runCurrent() + + assertThat(isZenAvailable).isFalse() + } + + @Test + fun isZenAvailable_on() = + testScope.runTest { + val isZenAvailable by collectLastValue(underTest.isZenAvailable) + deviceProvisioningRepository.setDeviceProvisioned(true) + runCurrent() + + assertThat(isZenAvailable).isTrue() + } + + @Test fun isZenModeEnabled_off() = testScope.runTest { val enabled by collectLastValue(underTest.isZenModeEnabled) @@ -337,4 +361,22 @@ class ZenModeInteractorTest : SysuiTestCase() { runCurrent() assertThat(mainActiveMode).isNull() } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + fun dndMode_flows() = + testScope.runTest { + val dndMode by collectLastValue(underTest.dndMode) + + zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) + runCurrent() + + assertThat(dndMode!!.isActive).isFalse() + + zenModeRepository.removeMode(TestModeBuilder.MANUAL_DND_INACTIVE.id) + zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + runCurrent() + + assertThat(dndMode!!.isActive).isTrue() + } } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index f9c2aef5f070..ba3822bd3c23 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3114,6 +3114,10 @@ <string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string> <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] --> <string name="media_output_group_title_suggested_device">Suggested Devices</string> + <!-- Title for input device group. [CHAR LIMIT=NONE] --> + <string name="media_input_group_title">Input</string> + <!-- Title for output device group. [CHAR LIMIT=NONE] --> + <string name="media_output_group_title">Output</string> <!-- Summary for end session dialog. [CHAR LIMIT=NONE] --> <string name="media_output_end_session_dialog_summary">Stop your shared session to move media to another device</string> <!-- Button text for stopping session [CHAR LIMIT=60] --> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 7efe2dde1320..ffbc85ca530f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -968,6 +968,15 @@ public class KeyguardSecurityContainer extends ConstraintLayout { constraintSet.constrainWidth(mViewFlipper.getId(), MATCH_CONSTRAINT); constraintSet.applyTo(mView); } + + @Override + public void onDestroy() { + if (mView == null) return; + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(mView); + constraintSet.clear(mViewFlipper.getId()); + constraintSet.applyTo(mView); + } } /** @@ -1043,12 +1052,20 @@ public class KeyguardSecurityContainer extends ConstraintLayout { @Override public void onDensityOrFontScaleChanged() { mView.removeView(mUserSwitcherViewGroup); + mView.removeView(mUserSwitcher); inflateUserSwitcher(); } @Override public void onDestroy() { - mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback); + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(mView); + constraintSet.clear(mUserSwitcherViewGroup.getId()); + constraintSet.clear(mViewFlipper.getId()); + constraintSet.applyTo(mView); + + mView.removeView(mUserSwitcherViewGroup); + mView.removeView(mUserSwitcher); } private Drawable findLargeUserIcon(int userId) { @@ -1344,5 +1361,13 @@ public class KeyguardSecurityContainer extends ConstraintLayout { constraintSet.constrainPercentWidth(mViewFlipper.getId(), 0.5f); constraintSet.applyTo(mView); } + + @Override + public void onDestroy() { + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(mView); + constraintSet.clear(mViewFlipper.getId()); + constraintSet.applyTo(mView); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt index 19e7537007bf..b8c30fe9d4a8 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt @@ -76,11 +76,7 @@ class BouncerHapticPlayer @Inject constructor(private val msdlPlayer: dagger.Laz /** Deliver MSDL feedback when the delete key of the pin bouncer is pressed */ fun playDeleteKeyPressFeedback() = msdlPlayer.get().playToken(MSDLToken.KEYPRESS_DELETE) - /** - * Deliver MSDL feedback when the delete key of the pin bouncer is long-pressed - * - * @return whether MSDL feedback is allowed to play. - */ + /** Deliver MSDL feedback when the delete key of the pin bouncer is long-pressed. */ fun playDeleteKeyLongPressedFeedback() = msdlPlayer.get().playToken(MSDLToken.LONG_PRESS) /** Deliver MSDL feedback when a numpad key is pressed on the pin bouncer */ diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index c67b35424cc9..873d1b3cc03d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -21,6 +21,7 @@ import com.android.app.tracing.coroutines.flow.collectLatest import com.android.systemui.authentication.domain.interactor.AuthenticationResult import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer import com.android.systemui.lifecycle.ExclusiveActivatable import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.Channel @@ -42,6 +43,7 @@ sealed class AuthMethodBouncerViewModel( /** Name to use for performance tracing purposes. */ val traceName: String, + protected val bouncerHapticPlayer: BouncerHapticPlayer? = null, ) : ExclusiveActivatable() { private val _animateFailure = MutableStateFlow(false) @@ -80,6 +82,8 @@ sealed class AuthMethodBouncerViewModel( return@collectLatest } + performAuthenticationHapticFeedback(authenticationResult) + _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED clearInput() } @@ -112,20 +116,23 @@ sealed class AuthMethodBouncerViewModel( /** Returns the input entered so far. */ protected abstract fun getInput(): List<Any> + /** Perform authentication result haptics */ + private fun performAuthenticationHapticFeedback(result: AuthenticationResult) { + if (result == AuthenticationResult.SKIPPED) return + + bouncerHapticPlayer?.playAuthenticationFeedback( + authenticationSucceeded = result == AuthenticationResult.SUCCEEDED + ) + } + /** * Attempts to authenticate the user using the current input value. * * @see BouncerInteractor.authenticate */ - protected fun tryAuthenticate( - input: List<Any> = getInput(), - useAutoConfirm: Boolean = false, - ) { + protected fun tryAuthenticate(input: List<Any> = getInput(), useAutoConfirm: Boolean = false) { authenticationRequests.trySend(AuthenticationRequest(input, useAutoConfirm)) } - private data class AuthenticationRequest( - val input: List<Any>, - val useAutoConfirm: Boolean, - ) + private data class AuthenticationRequest(val input: List<Any>, val useAutoConfirm: Boolean) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt index 0aada06a7eb7..0bcb58dee934 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt @@ -30,6 +30,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationWipeModel import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel +import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.qualifiers.Application @@ -61,6 +62,7 @@ constructor( private val pinViewModelFactory: PinBouncerViewModel.Factory, private val patternViewModelFactory: PatternBouncerViewModel.Factory, private val passwordViewModelFactory: PasswordBouncerViewModel.Factory, + private val bouncerHapticPlayer: BouncerHapticPlayer, ) : ExclusiveActivatable() { private val _selectedUserImage = MutableStateFlow<Bitmap?>(null) val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow() @@ -162,10 +164,7 @@ constructor( } launch { - combine( - userSwitcher.users, - userSwitcher.menu, - ) { users, actions -> + combine(userSwitcher.users, userSwitcher.menu) { users, actions -> users.map { user -> UserSwitcherDropdownItemViewModel( icon = Icon.Loaded(user.image, contentDescription = null), @@ -178,7 +177,7 @@ constructor( icon = Icon.Resource( action.iconResourceId, - contentDescription = null + contentDescription = null, ), text = Text.Resource(action.textResourceId), onClick = action.onClicked, @@ -226,7 +225,7 @@ constructor( } private fun getChildViewModel( - authenticationMethod: AuthenticationMethodModel, + authenticationMethod: AuthenticationMethodModel ): AuthMethodBouncerViewModel? { // If the current child view-model matches the authentication method, reuse it instead of // creating a new instance. @@ -241,12 +240,14 @@ constructor( authenticationMethod = authenticationMethod, onIntentionalUserInput = ::onIntentionalUserInput, isInputEnabled = isInputEnabled, + bouncerHapticPlayer = bouncerHapticPlayer, ) is AuthenticationMethodModel.Sim -> pinViewModelFactory.create( authenticationMethod = authenticationMethod, onIntentionalUserInput = ::onIntentionalUserInput, isInputEnabled = isInputEnabled, + bouncerHapticPlayer = bouncerHapticPlayer, ) is AuthenticationMethodModel.Password -> passwordViewModelFactory.create( @@ -257,6 +258,7 @@ constructor( patternViewModelFactory.create( onIntentionalUserInput = ::onIntentionalUserInput, isInputEnabled = isInputEnabled, + bouncerHapticPlayer = bouncerHapticPlayer, ) else -> null } @@ -317,10 +319,7 @@ constructor( return when { // The wipe dialog takes priority over the lockout dialog. wipeText != null -> - DialogViewModel( - text = wipeText, - onDismiss = { wipeDialogMessage.value = null }, - ) + DialogViewModel(text = wipeText, onDismiss = { wipeDialogMessage.value = null }) lockoutText != null -> DialogViewModel( text = lockoutText, @@ -338,7 +337,7 @@ constructor( fun onKeyEvent(keyEvent: KeyEvent): Boolean { return (authMethodViewModel.value as? PinBouncerViewModel)?.onKeyEvent( keyEvent.type, - keyEvent.nativeKeyEvent.keyCode + keyEvent.nativeKeyEvent.keyCode, ) ?: false } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt index 0a866b43429f..158f102ccdb3 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -18,9 +18,11 @@ package com.android.systemui.bouncer.ui.viewmodel import android.content.Context import android.util.TypedValue +import android.view.View import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer import com.android.systemui.res.R import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -35,7 +37,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch /** Holds UI state and handles user input for the pattern bouncer UI. */ @@ -44,6 +45,7 @@ class PatternBouncerViewModel constructor( private val applicationContext: Context, interactor: BouncerInteractor, + @Assisted bouncerHapticPlayer: BouncerHapticPlayer, @Assisted isInputEnabled: StateFlow<Boolean>, @Assisted private val onIntentionalUserInput: () -> Unit, ) : @@ -51,6 +53,7 @@ constructor( interactor = interactor, isInputEnabled = isInputEnabled, traceName = "PatternBouncerViewModel", + bouncerHapticPlayer = bouncerHapticPlayer, ) { /** The number of columns in the dot grid. */ @@ -190,14 +193,7 @@ constructor( private fun defaultDots(): List<PatternDotViewModel> { return buildList { (0 until columnCount).forEach { x -> - (0 until rowCount).forEach { y -> - add( - PatternDotViewModel( - x = x, - y = y, - ) - ) - } + (0 until rowCount).forEach { y -> add(PatternDotViewModel(x = x, y = y)) } } } } @@ -207,14 +203,17 @@ constructor( applicationContext.resources.getValue( com.android.internal.R.dimen.lock_pattern_dot_hit_factor, outValue, - true + true, ) max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR) } + fun performDotFeedback(view: View?) = bouncerHapticPlayer?.playPatternDotFeedback(view) + @AssistedFactory interface Factory { fun create( + bouncerHapticPlayer: BouncerHapticPlayer, isInputEnabled: StateFlow<Boolean>, onIntentionalUserInput: () -> Unit, ): PatternBouncerViewModel @@ -231,7 +230,7 @@ constructor( */ private fun PatternDotViewModel.isOnLineSegment( first: PatternDotViewModel, - second: PatternDotViewModel + second: PatternDotViewModel, ): Boolean { val anotherPoint = this // No need to consider any points outside the bounds of two end points @@ -253,14 +252,8 @@ private fun Int.isBetween(a: Int, b: Int): Boolean { return (this in a..b) || (this in b..a) } -data class PatternDotViewModel( - val x: Int, - val y: Int, -) { +data class PatternDotViewModel(val x: Int, val y: Int) { fun toCoordinate(): AuthenticationPatternCoordinate { - return AuthenticationPatternCoordinate( - x = x, - y = y, - ) + return AuthenticationPatternCoordinate(x = x, y = y) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index da29c6230cd8..0cb4260e4d7f 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -19,12 +19,14 @@ package com.android.systemui.bouncer.ui.viewmodel import android.content.Context +import android.view.HapticFeedbackConstants import android.view.KeyEvent.KEYCODE_0 import android.view.KeyEvent.KEYCODE_9 import android.view.KeyEvent.KEYCODE_DEL import android.view.KeyEvent.KEYCODE_NUMPAD_0 import android.view.KeyEvent.KEYCODE_NUMPAD_9 import android.view.KeyEvent.isConfirmKey +import android.view.View import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType import com.android.keyguard.PinShapeAdapter @@ -32,6 +34,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags +import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer import com.android.systemui.res.R import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -56,6 +59,7 @@ constructor( applicationContext: Context, interactor: BouncerInteractor, private val simBouncerInteractor: SimBouncerInteractor, + @Assisted bouncerHapticPlayer: BouncerHapticPlayer, @Assisted isInputEnabled: StateFlow<Boolean>, @Assisted private val onIntentionalUserInput: () -> Unit, @Assisted override val authenticationMethod: AuthenticationMethodModel, @@ -64,6 +68,7 @@ constructor( interactor = interactor, isInputEnabled = isInputEnabled, traceName = "PinBouncerViewModel", + bouncerHapticPlayer = bouncerHapticPlayer, ) { /** * Whether the sim-related UI in the pin view is showing. @@ -126,10 +131,9 @@ constructor( .collect { _hintedPinLength.value = it } } launch { - combine( - mutablePinInput, - interactor.isAutoConfirmEnabled, - ) { mutablePinEntries, isAutoConfirmEnabled -> + combine(mutablePinInput, interactor.isAutoConfirmEnabled) { + mutablePinEntries, + isAutoConfirmEnabled -> computeBackspaceButtonAppearance( pinInput = mutablePinEntries, isAutoConfirmEnabled = isAutoConfirmEnabled, @@ -183,8 +187,22 @@ constructor( mutablePinInput.value = mutablePinInput.value.deleteLast() } + fun onBackspaceButtonPressed(view: View?) { + if (bouncerHapticPlayer?.isEnabled == true) { + bouncerHapticPlayer.playDeleteKeyPressFeedback() + } else { + view?.performHapticFeedback( + HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING, + ) + } + } + /** Notifies that the user long-pressed the backspace button. */ fun onBackspaceButtonLongPressed() { + if (bouncerHapticPlayer?.isEnabled == true) { + bouncerHapticPlayer.playDeleteKeyLongPressedFeedback() + } clearInput() } @@ -266,13 +284,24 @@ constructor( } } - /** Notifies that the user has pressed down on a digit button. */ - fun onDigitButtonDown() { + /** + * Notifies that the user has pressed down on a digit button. This function also performs haptic + * feedback on the view. + */ + fun onDigitButtonDown(view: View?) { if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) { // Current PIN bouncer informs FalsingInteractor#avoidGesture() upon every Pin button // touch. super.onDown() } + if (bouncerHapticPlayer?.isEnabled == true) { + bouncerHapticPlayer.playNumpadKeyFeedback() + } else { + view?.performHapticFeedback( + HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING, + ) + } } @AssistedFactory @@ -281,6 +310,7 @@ constructor( isInputEnabled: StateFlow<Boolean>, onIntentionalUserInput: () -> Unit, authenticationMethod: AuthenticationMethodModel, + bouncerHapticPlayer: BouncerHapticPlayer, ): PinBouncerViewModel } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index c7a47b18f467..1ada56dea45a 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -30,6 +30,7 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.Build; +import android.os.UserHandle; import android.provider.Settings; import android.util.Log; @@ -37,6 +38,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.user.utils.UserScopedService; import javax.inject.Inject; import javax.inject.Provider; @@ -67,13 +69,13 @@ public class ClipboardListener implements public ClipboardListener(Context context, Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, ClipboardToast clipboardToast, - ClipboardManager clipboardManager, + UserScopedService<ClipboardManager> clipboardManager, KeyguardManager keyguardManager, UiEventLogger uiEventLogger) { mContext = context; mOverlayProvider = clipboardOverlayControllerProvider; mClipboardToast = clipboardToast; - mClipboardManager = clipboardManager; + mClipboardManager = clipboardManager.forUser(UserHandle.CURRENT); mKeyguardManager = keyguardManager; mUiEventLogger = uiEventLogger; } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 8818c3af4916..8f913ff01337 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -733,9 +733,8 @@ public class FrameworkServicesModule { } @Provides - @Singleton - static ClipboardManager provideClipboardManager(Context context) { - return context.getSystemService(ClipboardManager.class); + static UserScopedService<ClipboardManager> provideClipboardManager(Context context) { + return new UserScopedServiceImpl<>(context, ClipboardManager.class); } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt index e17e530a03d9..5259c5dca39f 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt @@ -26,7 +26,9 @@ import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReaso import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus import com.android.systemui.flags.SystemPropertiesHelper +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.TrustInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject @@ -36,6 +38,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -57,6 +60,7 @@ constructor( private val powerInteractor: PowerInteractor, private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, private val systemPropertiesHelper: SystemPropertiesHelper, + keyguardTransitionInteractor: KeyguardTransitionInteractor, ) { private val deviceUnlockSource = @@ -74,7 +78,7 @@ constructor( trustInteractor.isTrusted.filter { it }.map { DeviceUnlockSource.TrustAgent }, authenticationInteractor.onAuthenticationResult .filter { it } - .map { DeviceUnlockSource.BouncerInput } + .map { DeviceUnlockSource.BouncerInput }, ) private val faceEnrolledAndEnabled = biometricSettingsInteractor.isFaceAuthEnrolledAndEnabled @@ -170,10 +174,20 @@ constructor( combine( powerInteractor.isAsleep, isInLockdown, - ::Pair, + keyguardTransitionInteractor + .transitionValue(KeyguardState.AOD) + .map { it == 1f } + .distinctUntilChanged(), + ::Triple, ) - .flatMapLatestConflated { (isAsleep, isInLockdown) -> - if (isAsleep || isInLockdown) { + .flatMapLatestConflated { (isAsleep, isInLockdown, isAod) -> + val isForceLocked = + when { + isAsleep && !isAod -> true + isInLockdown -> true + else -> false + } + if (isForceLocked) { flowOf(DeviceUnlockStatus(false, null)) } else { deviceUnlockSource.map { DeviceUnlockStatus(true, it) } diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt index 1daaa1128ba0..500c5b387ac6 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.education.data.model +import com.android.systemui.contextualeducation.GestureType import java.time.Instant /** @@ -23,6 +24,7 @@ import java.time.Instant * gesture stores its own model separately. */ data class GestureEduModel( + val gestureType: GestureType, val signalCount: Int = 0, val educationShownCount: Int = 0, val lastShortcutTriggeredTime: Instant? = null, diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt index 01f838ff1ea7..29785959de18 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt @@ -17,6 +17,8 @@ package com.android.systemui.education.data.repository import android.content.Context +import android.hardware.input.InputManager +import android.hardware.input.KeyGestureEvent import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.PreferenceDataStoreFactory @@ -25,23 +27,31 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStoreFile +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Instant +import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Provider import kotlin.properties.Delegates.notNull +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map /** @@ -64,6 +74,8 @@ interface ContextualEducationRepository { suspend fun updateEduDeviceConnectionTime( transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime ) + + val keyboardShortcutTriggered: Flow<GestureType> } /** @@ -75,9 +87,13 @@ class UserContextualEducationRepository @Inject constructor( @Application private val applicationContext: Context, - @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope> + @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>, + private val inputManager: InputManager, + @Background private val backgroundDispatcher: CoroutineDispatcher, ) : ContextualEducationRepository { companion object { + const val TAG = "UserContextualEducationRepository" + const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT" const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN" const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME" @@ -98,6 +114,30 @@ constructor( @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data } + override val keyboardShortcutTriggered: Flow<GestureType> = + conflatedCallbackFlow { + val listener = + InputManager.KeyGestureEventListener { event -> + // Only store keyboard shortcut time for gestures providing keyboard + // education + val shortcutType = + when (event.keyGestureType) { + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS + + else -> null + } + + if (shortcutType != null) { + trySendWithFailureLogging(shortcutType, TAG) + } + } + + inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener) + awaitClose { inputManager.unregisterKeyGestureEventListener(listener) } + } + .flowOn(backgroundDispatcher) + override fun setUser(userId: Int) { dataStoreScope?.cancel() val newDsScope = dataStoreScopeProvider.get() @@ -136,7 +176,8 @@ constructor( preferences[getLastEducationTimeKey(gestureType)]?.let { Instant.ofEpochSecond(it) }, - userId = userId + userId = userId, + gestureType = gestureType, ) } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt index 10be26e1ce0f..c88b36495ac2 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt @@ -18,7 +18,10 @@ package com.android.systemui.education.domain.interactor import com.android.systemui.CoreStartable import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK +import com.android.systemui.contextualeducation.GestureType.HOME +import com.android.systemui.contextualeducation.GestureType.OVERVIEW import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduClock @@ -53,6 +56,13 @@ constructor( ) : CoreStartable { val backGestureModelFlow = readEduModelsOnSignalCountChanged(BACK) + val homeGestureModelFlow = readEduModelsOnSignalCountChanged(HOME) + val overviewGestureModelFlow = readEduModelsOnSignalCountChanged(OVERVIEW) + val allAppsGestureModelFlow = readEduModelsOnSignalCountChanged(ALL_APPS) + val eduDeviceConnectionTimeFlow = + repository.readEduDeviceConnectionTime().distinctUntilChanged() + + val keyboardShortcutTriggered = repository.keyboardShortcutTriggered override fun start() { backgroundScope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index 43855d971a2a..faee32694964 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -16,15 +16,8 @@ package com.android.systemui.education.domain.interactor -import android.hardware.input.InputManager -import android.hardware.input.InputManager.KeyGestureEventListener -import android.hardware.input.KeyGestureEvent import android.os.SystemProperties import com.android.systemui.CoreStartable -import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.contextualeducation.GestureType -import com.android.systemui.contextualeducation.GestureType.ALL_APPS -import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduClock @@ -32,19 +25,19 @@ import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.shared.model.EducationInfo import com.android.systemui.education.shared.model.EducationUiType import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository -import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Clock -import java.util.concurrent.Executor import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.DurationUnit import kotlin.time.toDuration import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch /** Allow listening to new contextual education triggered */ @@ -55,7 +48,6 @@ constructor( @Background private val backgroundScope: CoroutineScope, private val contextualEducationInteractor: ContextualEducationInteractor, private val userInputDeviceRepository: UserInputDeviceRepository, - private val inputManager: InputManager, @EduClock private val clock: Clock, ) : CoreStartable { @@ -82,34 +74,32 @@ constructor( private val _educationTriggered = MutableStateFlow<EducationInfo?>(null) val educationTriggered = _educationTriggered.asStateFlow() - private val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow { - val listener = KeyGestureEventListener { event -> - // Only store keyboard shortcut time for gestures providing keyboard education - val shortcutType = - when (event.keyGestureType) { - KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS - else -> null - } - - if (shortcutType != null) { - trySendWithFailureLogging(shortcutType, TAG) - } - } - - inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener) - awaitClose { inputManager.unregisterKeyGestureEventListener(listener) } - } - + @OptIn(ExperimentalCoroutinesApi::class) override fun start() { backgroundScope.launch { - contextualEducationInteractor.backGestureModelFlow.collect { - if (isUsageSessionExpired(it)) { - contextualEducationInteractor.startNewUsageSession(BACK) - } else if (isEducationNeeded(it)) { - _educationTriggered.value = EducationInfo(BACK, getEduType(it), it.userId) - contextualEducationInteractor.updateOnEduTriggered(BACK) + contextualEducationInteractor.eduDeviceConnectionTimeFlow + .flatMapLatest { + val gestureFlows = mutableListOf<Flow<GestureEduModel>>() + if (it.touchpadFirstConnectionTime != null) { + gestureFlows.add(contextualEducationInteractor.backGestureModelFlow) + gestureFlows.add(contextualEducationInteractor.homeGestureModelFlow) + gestureFlows.add(contextualEducationInteractor.overviewGestureModelFlow) + } + + if (it.keyboardFirstConnectionTime != null) { + gestureFlows.add(contextualEducationInteractor.allAppsGestureModelFlow) + } + gestureFlows.merge() + } + .collect { + if (isUsageSessionExpired(it)) { + contextualEducationInteractor.startNewUsageSession(it.gestureType) + } else if (isEducationNeeded(it)) { + _educationTriggered.value = + EducationInfo(it.gestureType, getEduType(it), it.userId) + contextualEducationInteractor.updateOnEduTriggered(it.gestureType) + } } - } } backgroundScope.launch { @@ -139,7 +129,7 @@ constructor( } backgroundScope.launch { - keyboardShortcutTriggered.collect { + contextualEducationInteractor.keyboardShortcutTriggered.collect { contextualEducationInteractor.updateShortcutTriggerTime(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 6318dc000c21..0b775ab486bd 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -31,9 +31,7 @@ import com.android.systemui.Flags.statusBarCallChipNotificationIcon import com.android.systemui.Flags.statusBarScreenSharingChips import com.android.systemui.Flags.statusBarUseReposForCallChip import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.MigrateClocksToBlueprint -import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag @@ -62,10 +60,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // SceneContainer dependencies SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta } - // ComposeLockscreen dependencies - ComposeLockscreen.token dependsOn KeyguardBottomAreaRefactor.token - ComposeLockscreen.token dependsOn MigrateClocksToBlueprint.token - // CommunalHub dependencies communalHub dependsOn MigrateClocksToBlueprint.token @@ -99,7 +93,7 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha get() = FlagToken( FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, - statusBarCallChipNotificationIcon() + statusBarCallChipNotificationIcon(), ) private inline val statusBarScreenSharingChipsToken diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt index e50c05c7f6ed..e09e1987698d 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -29,6 +29,7 @@ import com.android.systemui.animation.Expandable import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.log.dagger.QSLog +import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController @@ -44,12 +45,12 @@ import javax.inject.Inject * @property[vibratorHelper] The [VibratorHelper] to deliver haptic effects. * @property[effectDuration] The duration of the effect in ms. */ -// TODO(b/332902869): In addition from being injectable, we can consider making it a singleton class QSLongPressEffect @Inject constructor( private val vibratorHelper: VibratorHelper?, private val keyguardStateController: KeyguardStateController, + private val falsingManager: FalsingManager, @QSLog private val logBuffer: LogBuffer, ) { @@ -72,7 +73,7 @@ constructor( private val durations = vibratorHelper?.getPrimitiveDurations( VibrationEffect.Composition.PRIMITIVE_LOW_TICK, - VibrationEffect.Composition.PRIMITIVE_SPIN + VibrationEffect.Composition.PRIMITIVE_SPIN, ) private var longPressHint: VibrationEffect? = null @@ -152,15 +153,27 @@ constructor( logEvent(qsTile?.tileSpec, state, "animation completed") when (state) { State.RUNNING_FORWARD -> { - vibrate(snapEffect) - if (keyguardStateController.isUnlocked) { + val wasFalseLongTap = falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY) + if (wasFalseLongTap) { + callback?.onResetProperties() + setState(State.IDLE) + logEvent(qsTile?.tileSpec, state, "false long click. No action triggered") + } else if (keyguardStateController.isUnlocked) { + vibrate(snapEffect) setState(State.LONG_CLICKED) + qsTile?.longClick(expandable) + logEvent(qsTile?.tileSpec, state, "long click action triggered") } else { + vibrate(snapEffect) callback?.onResetProperties() setState(State.IDLE) + qsTile?.longClick(expandable) + logEvent( + qsTile?.tileSpec, + state, + "properties reset and long click action triggered", + ) } - logEvent(qsTile?.tileSpec, state, "long click action triggered") - qsTile?.longClick(expandable) } State.RUNNING_BACKWARDS_FROM_UP -> { callback?.onEffectFinishedReversing() @@ -236,7 +249,7 @@ constructor( LongPressHapticBuilder.createLongPressHint( durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION, durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION, - effectDuration + effectDuration, ) setState(State.IDLE) return true @@ -265,7 +278,7 @@ constructor( } override fun dialogTransitionController( - cuj: DialogCuj?, + cuj: DialogCuj? ): DialogTransitionAnimator.Controller? = DialogTransitionAnimator.Controller.fromView(view, cuj) } @@ -298,7 +311,7 @@ constructor( str2 = event str3 = state.name }, - { "[long-press effect on $str1 tile] $str2 on state: $str3" } + { "[long-press effect on $str1 tile] $str2 on state: $str3" }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt index 95251749132d..48f5cb6dc219 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/InputDeviceTutorialLogger.kt @@ -16,27 +16,27 @@ package com.android.systemui.inputdevice.tutorial +import com.android.systemui.inputdevice.tutorial.domain.interactor.ConnectionState +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen as KeyboardTouchpadTutorialScreen +import com.android.systemui.log.ConstantStringsLogger +import com.android.systemui.log.ConstantStringsLoggerImpl import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.core.MessageInitializer +import com.android.systemui.log.core.MessagePrinter import com.android.systemui.log.dagger.InputDeviceTutorialLog -import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen -import com.google.errorprone.annotations.CompileTimeConstant +import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen as TouchpadTutorialScreen import javax.inject.Inject private const val TAG = "InputDeviceTutorial" class InputDeviceTutorialLogger @Inject -constructor(@InputDeviceTutorialLog private val buffer: LogBuffer) { +constructor(@InputDeviceTutorialLog private val buffer: LogBuffer) : + ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) { - fun log(@CompileTimeConstant s: String) { - buffer.log(TAG, LogLevel.INFO, message = s) - } - - fun logGoingToScreen(screen: Screen, context: TutorialContext) { - buffer.log( - TAG, - LogLevel.INFO, + fun logGoingToScreen(screen: TouchpadTutorialScreen, context: TutorialContext) { + logInfo( { str1 = screen.toString() str2 = context.string @@ -46,7 +46,58 @@ constructor(@InputDeviceTutorialLog private val buffer: LogBuffer) { } fun logCloseTutorial(context: TutorialContext) { - buffer.log(TAG, LogLevel.INFO, { str1 = context.string }, { "Closing $str1" }) + logInfo({ str1 = context.string }, { "Closing $str1" }) + } + + fun logOpenTutorial(context: TutorialContext) { + logInfo({ str1 = context.string }, { "Opening $str1" }) + } + + fun logNextScreenMissingHardware(nextScreen: KeyboardTouchpadTutorialScreen) { + buffer.log( + TAG, + LogLevel.WARNING, + { str1 = nextScreen.toString() }, + { "next screen should be $str1 but required hardware is missing" } + ) + } + + fun logNextScreen(nextScreen: KeyboardTouchpadTutorialScreen) { + logInfo({ str1 = nextScreen.toString() }, { "going to $str1 screen" }) + } + + fun logNewConnectionState(connectionState: ConnectionState) { + logInfo( + { + bool1 = connectionState.touchpadConnected + bool2 = connectionState.keyboardConnected + }, + { "Received connection state: touchpad connected: $bool1 keyboard connected: $bool2" } + ) + } + + fun logMovingBetweenScreens( + previousScreen: KeyboardTouchpadTutorialScreen?, + currentScreen: KeyboardTouchpadTutorialScreen + ) { + logInfo( + { + str1 = previousScreen?.toString() ?: "NO_SCREEN" + str2 = currentScreen.toString() + }, + { "Moving from $str1 screen to $str2 screen" } + ) + } + + fun logGoingBack(previousScreen: KeyboardTouchpadTutorialScreen) { + logInfo({ str1 = previousScreen.toString() }, { "Going back to $str1 screen" }) + } + + private inline fun logInfo( + messageInitializer: MessageInitializer, + noinline messagePrinter: MessagePrinter + ) { + buffer.log(TAG, LogLevel.INFO, messageInitializer, messagePrinter) } enum class TutorialContext(val string: String) { diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt index 1adc285e6bb5..c130c6c7fe12 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt @@ -28,6 +28,8 @@ import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import com.android.compose.theme.PlatformTheme +import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger +import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel @@ -48,6 +50,7 @@ class KeyboardTouchpadTutorialActivity constructor( private val viewModelFactoryAssistedProvider: ViewModelFactoryAssistedProvider, private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, + private val logger: InputDeviceTutorialLogger, ) : ComponentActivity() { companion object { @@ -74,6 +77,7 @@ constructor( lifecycleScope.launch { vm.closeActivity.collect { finish -> if (finish) { + logger.logCloseTutorial(TutorialContext.KEYBOARD_TOUCHPAD_TUTORIAL) finish() } } @@ -81,6 +85,9 @@ constructor( setContent { PlatformTheme { KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) } } + if (savedInstanceState == null) { + logger.logOpenTutorial(TutorialContext.KEYBOARD_TOUCHPAD_TUTORIAL) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt index 315c102e94d0..5cf19677a98e 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger import com.android.systemui.inputdevice.tutorial.domain.interactor.ConnectionState import com.android.systemui.inputdevice.tutorial.domain.interactor.KeyboardTouchpadConnectionInteractor import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY @@ -47,6 +48,7 @@ class KeyboardTouchpadTutorialViewModel( private val gesturesInteractor: Optional<TouchpadGesturesInteractor>, private val keyboardTouchpadConnectionInteractor: KeyboardTouchpadConnectionInteractor, private val hasTouchpadTutorialScreens: Boolean, + private val logger: InputDeviceTutorialLogger, handle: SavedStateHandle ) : ViewModel(), DefaultLifecycleObserver { @@ -68,7 +70,10 @@ class KeyboardTouchpadTutorialViewModel( init { viewModelScope.launch { - keyboardTouchpadConnectionInteractor.connectionState.collect { connectionState = it } + keyboardTouchpadConnectionInteractor.connectionState.collect { + logger.logNewConnectionState(connectionState) + connectionState = it + } } viewModelScope.launch { @@ -89,7 +94,14 @@ class KeyboardTouchpadTutorialViewModel( viewModelScope.launch { // close activity if screen requires touchpad but we don't have it. This can only happen // when current sysui build doesn't contain touchpad module dependency - _screen.filterNot { it.canBeShown() }.collect { _closeActivity.value = true } + _screen + .filterNot { it.canBeShown() } + .collect { + logger.e( + "Touchpad is connected but touchpad module is missing, something went wrong" + ) + _closeActivity.value = true + } } } @@ -114,11 +126,14 @@ class KeyboardTouchpadTutorialViewModel( if (requiredHardwarePresent(nextScreen)) { break } + logger.logNextScreenMissingHardware(nextScreen) nextScreen = nextScreen.next() } if (nextScreen == null) { + logger.d("Final screen reached, closing tutorial") _closeActivity.value = true } else { + logger.logNextScreen(nextScreen) _screen.value = nextScreen screensBackStack.add(nextScreen) } @@ -127,6 +142,7 @@ class KeyboardTouchpadTutorialViewModel( private fun Screen.canBeShown() = requiredHardware != TOUCHPAD || hasTouchpadTutorialScreens private fun setupDeviceState(previousScreen: Screen?, currentScreen: Screen) { + logger.logMovingBetweenScreens(previousScreen, currentScreen) if (previousScreen?.requiredHardware == currentScreen.requiredHardware) return previousScreen?.let { clearDeviceStateForScreen(it) } when (currentScreen.requiredHardware) { @@ -153,6 +169,7 @@ class KeyboardTouchpadTutorialViewModel( _closeActivity.value = true } else { screensBackStack.removeLast() + logger.logGoingBack(screensBackStack.last()) _screen.value = screensBackStack.last() } } @@ -162,6 +179,7 @@ class KeyboardTouchpadTutorialViewModel( constructor( private val gesturesInteractor: Optional<TouchpadGesturesInteractor>, private val keyboardTouchpadConnected: KeyboardTouchpadConnectionInteractor, + private val logger: InputDeviceTutorialLogger, @Assisted private val hasTouchpadTutorialScreens: Boolean, ) : AbstractSavedStateViewModelFactory() { @@ -180,6 +198,7 @@ class KeyboardTouchpadTutorialViewModel( gesturesInteractor, keyboardTouchpadConnected, hasTouchpadTutorialScreens, + logger, handle ) as T diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index df0f10acac1f..416eabae78eb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -24,12 +24,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.ConstraintSet.BOTTOM -import androidx.constraintlayout.widget.ConstraintSet.END -import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import androidx.constraintlayout.widget.ConstraintSet.START -import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayout @@ -47,7 +41,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor -import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder @@ -128,7 +121,7 @@ constructor( keyguardStatusViewComponentFactory.build( LayoutInflater.from(context).inflate(R.layout.keyguard_status_view, null) as KeyguardStatusView, - context.display + context.display, ) val controller = statusViewComponent.keyguardStatusViewController controller.init() @@ -143,29 +136,12 @@ constructor( initializeViews() if (!SceneContainerFlag.isEnabled) { - if (ComposeLockscreen.isEnabled) { - val composeView = - createLockscreen( - context = context, - viewModelFactory = lockscreenContentViewModelFactory, - blueprints = lockscreenSceneBlueprintsLazy.get(), - ) - composeView.id = View.generateViewId() - val cs = ConstraintSet() - cs.clone(keyguardRootView) - cs.connect(composeView.id, START, PARENT_ID, START) - cs.connect(composeView.id, END, PARENT_ID, END) - cs.connect(composeView.id, TOP, PARENT_ID, TOP) - cs.connect(composeView.id, BOTTOM, PARENT_ID, BOTTOM) - keyguardRootView.addView(composeView) - } else { - KeyguardBlueprintViewBinder.bind( - keyguardRootView, - keyguardBlueprintViewModel, - keyguardClockViewModel, - smartspaceViewModel, - ) - } + KeyguardBlueprintViewBinder.bind( + keyguardRootView, + keyguardBlueprintViewModel, + keyguardClockViewModel, + smartspaceViewModel, + ) } if (deviceEntryUnlockTrackerViewBinder.isPresent) { deviceEntryUnlockTrackerViewBinder.get().bind(keyguardRootView) @@ -247,7 +223,7 @@ constructor( LockscreenContent( viewModelFactory = viewModelFactory, blueprints = sceneBlueprints, - clockInteractor = clockInteractor + clockInteractor = clockInteractor, ) ) { Content(modifier = Modifier.fillMaxSize()) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt index 406b9f6e6a0d..be873344b719 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt @@ -25,7 +25,9 @@ import android.provider.Settings.Global.ZEN_MODE_OFF import android.provider.Settings.Secure.ZEN_DURATION_FOREVER import android.provider.Settings.Secure.ZEN_DURATION_PROMPT import android.service.notification.ZenModeConfig +import android.util.Log import com.android.settingslib.notification.modes.EnableZenModeDialog +import com.android.settingslib.notification.modes.ZenMode import com.android.settingslib.notification.modes.ZenModeDialogMetricsLogger import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging @@ -35,30 +37,38 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.modes.shared.ModesUi import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.ZenModeController +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn @SysUISingleton class DoNotDisturbQuickAffordanceConfig constructor( private val context: Context, private val controller: ZenModeController, + private val interactor: ZenModeInteractor, private val secureSettings: SecureSettings, private val userTracker: UserTracker, @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundScope: CoroutineScope, private val testConditionId: Uri?, testDialog: EnableZenModeDialog?, ) : KeyguardQuickAffordanceConfig { @@ -67,15 +77,45 @@ constructor( constructor( context: Context, controller: ZenModeController, + interactor: ZenModeInteractor, secureSettings: SecureSettings, userTracker: UserTracker, @Background backgroundDispatcher: CoroutineDispatcher, - ) : this(context, controller, secureSettings, userTracker, backgroundDispatcher, null, null) + @Background backgroundScope: CoroutineScope, + ) : this( + context, + controller, + interactor, + secureSettings, + userTracker, + backgroundDispatcher, + backgroundScope, + null, + null, + ) - private var dndMode: Int = 0 - private var isAvailable = false + private var zenMode: Int = 0 + private var oldIsAvailable = false private var settingsValue: Int = 0 + private val dndMode: StateFlow<ZenMode?> by lazy { + ModesUi.assertInNewMode() + interactor.dndMode.stateIn( + scope = backgroundScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + } + + private val isAvailable: StateFlow<Boolean> by lazy { + ModesUi.assertInNewMode() + interactor.isZenAvailable.stateIn( + scope = backgroundScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + } + private val conditionUri: Uri get() = testConditionId @@ -104,42 +144,68 @@ constructor( override val pickerIconResourceId: Int = R.drawable.ic_do_not_disturb override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = - combine( - conflatedCallbackFlow { - val callback = - object : ZenModeController.Callback { - override fun onZenChanged(zen: Int) { - dndMode = zen - trySendWithFailureLogging(updateState(), TAG) - } + if (ModesUi.isEnabled) { + combine(isAvailable, dndMode) { isAvailable, dndMode -> + if (!isAvailable) { + KeyguardQuickAffordanceConfig.LockScreenState.Hidden + } else if (dndMode?.isActive == true) { + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + Icon.Resource( + R.drawable.qs_dnd_icon_on, + ContentDescription.Resource(R.string.dnd_is_on), + ), + ActivationState.Active, + ) + } else { + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + Icon.Resource( + R.drawable.qs_dnd_icon_off, + ContentDescription.Resource(R.string.dnd_is_off), + ), + ActivationState.Inactive, + ) + } + } + } else { + combine( + conflatedCallbackFlow { + val callback = + object : ZenModeController.Callback { + override fun onZenChanged(zen: Int) { + zenMode = zen + trySendWithFailureLogging(updateState(), TAG) + } - override fun onZenAvailableChanged(available: Boolean) { - isAvailable = available - trySendWithFailureLogging(updateState(), TAG) + override fun onZenAvailableChanged(available: Boolean) { + oldIsAvailable = available + trySendWithFailureLogging(updateState(), TAG) + } } - } - dndMode = controller.zen - isAvailable = controller.isZenAvailable - trySendWithFailureLogging(updateState(), TAG) - - controller.addCallback(callback) - - awaitClose { controller.removeCallback(callback) } - }, - secureSettings - .observerFlow(userTracker.userId, Settings.Secure.ZEN_DURATION) - .onStart { emit(Unit) } - .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) } - .flowOn(backgroundDispatcher) - .distinctUntilChanged() - .onEach { settingsValue = it } - ) { callbackFlowValue, _ -> - callbackFlowValue + zenMode = controller.zen + oldIsAvailable = controller.isZenAvailable + trySendWithFailureLogging(updateState(), TAG) + + controller.addCallback(callback) + + awaitClose { controller.removeCallback(callback) } + }, + secureSettings + .observerFlow(userTracker.userId, Settings.Secure.ZEN_DURATION) + .onStart { emit(Unit) } + .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) } + .flowOn(backgroundDispatcher) + .distinctUntilChanged() + .onEach { settingsValue = it }, + ) { callbackFlowValue, _ -> + callbackFlowValue + } } override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState { - return if (controller.isZenAvailable) { + val isZenAvailable = if (ModesUi.isEnabled) isAvailable.value else controller.isZenAvailable + + return if (isZenAvailable) { KeyguardQuickAffordanceConfig.PickerScreenState.Default( configureIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) ) @@ -151,32 +217,63 @@ constructor( override fun onTriggered( expandable: Expandable? ): KeyguardQuickAffordanceConfig.OnTriggeredResult { - return when { - !isAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled - dndMode != ZEN_MODE_OFF -> { - controller.setZen(ZEN_MODE_OFF, null, TAG) - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled - } - settingsValue == ZEN_DURATION_PROMPT -> - KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog( - dialog.createDialog(), - expandable - ) - settingsValue == ZEN_DURATION_FOREVER -> { - controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG) + return if (ModesUi.isEnabled) { + if (!isAvailable.value) { KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } else { + val dnd = dndMode.value + if (dnd == null) { + Log.wtf(TAG, "Triggered DND but it's null!?") + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } + if (dnd.isActive) { + interactor.deactivateMode(dnd) + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } else { + if (interactor.shouldAskForZenDuration(dnd)) { + // NOTE: The dialog handles turning on the mode itself. + return KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog( + dialog.createDialog(), + expandable, + ) + } else { + interactor.activateMode(dnd) + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } + } } - else -> { - controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionUri, TAG) - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } else { + when { + !oldIsAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + zenMode != ZEN_MODE_OFF -> { + controller.setZen(ZEN_MODE_OFF, null, TAG) + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } + + settingsValue == ZEN_DURATION_PROMPT -> + KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog( + dialog.createDialog(), + expandable, + ) + + settingsValue == ZEN_DURATION_FOREVER -> { + controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG) + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } + + else -> { + controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionUri, TAG) + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } } } } private fun updateState(): KeyguardQuickAffordanceConfig.LockScreenState { - return if (!isAvailable) { + ModesUi.assertInLegacyMode() + return if (!oldIsAvailable) { KeyguardQuickAffordanceConfig.LockScreenState.Hidden - } else if (dndMode == ZEN_MODE_OFF) { + } else if (zenMode == ZEN_MODE_OFF) { KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( R.drawable.qs_dnd_icon_off, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt index 7afc7596a994..6932eb51e141 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt @@ -25,13 +25,13 @@ import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository -import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.KeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -64,7 +64,7 @@ constructor( /** Current BlueprintId */ val blueprintId = shadeInteractor.isShadeLayoutWide.map { isShadeLayoutWide -> - val useSplitShade = isShadeLayoutWide && !ComposeLockscreen.isEnabled + val useSplitShade = isShadeLayoutWide && !SceneContainerFlag.isEnabled when { useSplitShade -> SplitShadeKeyguardBlueprint.ID else -> DefaultKeyguardBlueprint.DEFAULT diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt deleted file mode 100644 index 601fbfaf1b64..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/ComposeLockscreen.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.shared - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the compose lockscreen flag state. */ -@Suppress("NOTHING_TO_INLINE") -object ComposeLockscreen { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_COMPOSE_LOCKSCREEN - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.composeLockscreen() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index ed82159e6160..deb0b2d8f848 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -59,7 +59,6 @@ import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor -import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters @@ -72,6 +71,7 @@ import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.VibratorHelper @@ -241,7 +241,7 @@ object KeyguardRootViewBinder { disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { - if (ComposeLockscreen.isEnabled) { + if (SceneContainerFlag.isEnabled) { view.setViewTreeOnBackPressedDispatcherOwner( object : OnBackPressedDispatcherOwner { override val onBackPressedDispatcher = @@ -261,10 +261,7 @@ object KeyguardRootViewBinder { -> if (biometricMessage?.message != null) { chipbarCoordinator!!.displayView( - createChipbarInfo( - biometricMessage.message, - R.drawable.ic_lock, - ) + createChipbarInfo(biometricMessage.message, R.drawable.ic_lock) ) } else { chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull") @@ -327,12 +324,16 @@ object KeyguardRootViewBinder { .getDimensionPixelSize(R.dimen.shelf_appear_translation) .stateIn(this) viewModel.isNotifIconContainerVisible.collect { isVisible -> - childViews[aodNotificationIconContainerId] - ?.setAodNotifIconContainerIsVisible( - isVisible, - iconsAppearTranslationPx.value, - screenOffAnimationController, - ) + if (isVisible.value) { + blueprintViewModel.refreshBlueprint() + } else { + childViews[aodNotificationIconContainerId] + ?.setAodNotifIconContainerIsVisible( + isVisible, + iconsAppearTranslationPx.value, + screenOffAnimationController, + ) + } } } @@ -382,7 +383,7 @@ object KeyguardRootViewBinder { if (msdlFeedback()) { msdlPlayer?.playToken( MSDLToken.UNLOCK, - authInteractionProperties + authInteractionProperties, ) } else { vibratorHelper.performHapticFeedback( @@ -398,7 +399,7 @@ object KeyguardRootViewBinder { if (msdlFeedback()) { msdlPlayer?.playToken( MSDLToken.FAILURE, - authInteractionProperties + authInteractionProperties, ) } else { vibratorHelper.performHapticFeedback( @@ -425,7 +426,7 @@ object KeyguardRootViewBinder { blueprintViewModel, clockViewModel, childViews, - burnInParams + burnInParams, ) ) @@ -464,11 +465,7 @@ object KeyguardRootViewBinder { */ private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo { return ChipbarInfo( - startIcon = - TintedIcon( - Icon.Resource(icon, null), - ChipbarInfo.DEFAULT_ICON_TINT, - ), + startIcon = TintedIcon(Icon.Resource(icon, null), ChipbarInfo.DEFAULT_ICON_TINT), text = Text.Loaded(message), endItem = null, vibrationEffect = null, @@ -499,7 +496,7 @@ object KeyguardRootViewBinder { oldLeft: Int, oldTop: Int, oldRight: Int, - oldBottom: Int + oldBottom: Int, ) { // After layout, ensure the notifications are positioned correctly childViews[nsslPlaceholderId]?.let { notificationListPlaceholder -> @@ -515,7 +512,7 @@ object KeyguardRootViewBinder { viewModel.onNotificationContainerBoundsChanged( notificationListPlaceholder.top.toFloat(), notificationListPlaceholder.bottom.toFloat(), - animate = shouldAnimate + animate = shouldAnimate, ) } @@ -531,7 +528,7 @@ object KeyguardRootViewBinder { Int.MAX_VALUE } else { view.getTop() - } + }, ) } } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt index c6efcfad8da7..4cf3c4e7f6d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt @@ -25,20 +25,18 @@ import androidx.constraintlayout.widget.ConstraintLayout import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -data class TransitionData( - val config: Config, - val start: Long = System.currentTimeMillis(), -) +data class TransitionData(val config: Config, val start: Long = System.currentTimeMillis()) class KeyguardBlueprintViewModel @Inject constructor( @Main private val handler: Handler, - keyguardBlueprintInteractor: KeyguardBlueprintInteractor, + private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor, ) { val blueprint = keyguardBlueprintInteractor.blueprint val blueprintId = keyguardBlueprintInteractor.blueprintId @@ -76,6 +74,9 @@ constructor( } } + fun refreshBlueprint(type: Type = Type.NoTransition) = + keyguardBlueprintInteractor.refreshBlueprint(type) + fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) { runningTransitions.mutate() @@ -95,7 +96,7 @@ constructor( Log.w( TAG, "runTransition: skipping ${transition::class.simpleName}: " + - "currentPriority=$currentPriority; config=$config" + "currentPriority=$currentPriority; config=$config", ) } apply() @@ -106,7 +107,7 @@ constructor( Log.i( TAG, "runTransition: running ${transition::class.simpleName}: " + - "currentPriority=$currentPriority; config=$config" + "currentPriority=$currentPriority; config=$config", ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index 73028c5cf496..36f684ee4759 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -25,10 +25,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor -import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel import com.android.systemui.statusbar.ui.SystemBarUtilsProxy @@ -56,10 +56,9 @@ constructor( var burnInLayer: Layer? = null val clockSize: StateFlow<ClockSize> = - combine( - keyguardClockInteractor.selectedClockSize, - keyguardClockInteractor.clockSize, - ) { selectedSize, clockSize -> + combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) { + selectedSize, + clockSize -> if (selectedSize == ClockSizeSetting.SMALL) ClockSize.SMALL else clockSize } .stateIn( @@ -80,10 +79,7 @@ constructor( val currentClock = keyguardClockInteractor.currentClock val hasCustomWeatherDataDisplay = - combine( - isLargeClockVisible, - currentClock, - ) { isLargeClock, currentClock -> + combine(isLargeClockVisible, currentClock) { isLargeClock, currentClock -> currentClock?.let { clock -> val face = if (isLargeClock) clock.largeClock else clock.smallClock face.config.hasCustomWeatherDataDisplay @@ -93,14 +89,14 @@ constructor( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = - currentClock.value?.largeClock?.config?.hasCustomWeatherDataDisplay ?: false + currentClock.value?.largeClock?.config?.hasCustomWeatherDataDisplay ?: false, ) val clockShouldBeCentered: StateFlow<Boolean> = keyguardClockInteractor.clockShouldBeCentered.stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = false + initialValue = false, ) // To translate elements below smartspace in weather clock to avoid overlapping between date @@ -111,7 +107,7 @@ constructor( .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = false + initialValue = false, ) val currentClockLayout: StateFlow<ClockLayout> = @@ -145,7 +141,7 @@ constructor( .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = ClockLayout.SMALL_CLOCK + initialValue = ClockLayout.SMALL_CLOCK, ) val hasCustomPositionUpdatedAnimation: StateFlow<Boolean> = @@ -156,7 +152,7 @@ constructor( .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = false + initialValue = false, ) /** Calculates the top margin for the small clock. */ @@ -164,10 +160,10 @@ constructor( val statusBarHeight = systemBarUtils.getStatusBarHeaderHeightKeyguard() return if (shadeInteractor.isShadeLayoutWide.value) { resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin) - - if (ComposeLockscreen.isEnabled) statusBarHeight else 0 + if (SceneContainerFlag.isEnabled) statusBarHeight else 0 } else { resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + - if (!ComposeLockscreen.isEnabled) statusBarHeight else 0 + if (!SceneContainerFlag.isEnabled) statusBarHeight else 0 } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index ff9495daaa22..2961d05b2e51 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -57,7 +57,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { private static final float DEVICE_CONNECTED_ALPHA = 1f; protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); - public MediaOutputAdapter(MediaOutputController controller) { + public MediaOutputAdapter(MediaSwitchingController controller) { super(controller); setHasStableIds(true); } @@ -531,8 +531,10 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { @RequiresApi(34) private static class Api34Impl { @DoNotInline - static View.OnClickListener getClickListenerBasedOnSelectionBehavior(MediaDevice device, - MediaOutputController controller, View.OnClickListener defaultTransferListener) { + static View.OnClickListener getClickListenerBasedOnSelectionBehavior( + MediaDevice device, + MediaSwitchingController controller, + View.OnClickListener defaultTransferListener) { switch (device.getSelectionBehavior()) { case SELECTION_BEHAVIOR_NONE: return null; diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 5958b0a24a5e..63a7e013022a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -63,7 +63,7 @@ public abstract class MediaOutputBaseAdapter extends static final int CUSTOMIZED_ITEM_GROUP = 2; static final int CUSTOMIZED_ITEM_DYNAMIC_GROUP = 3; - protected final MediaOutputController mController; + protected final MediaSwitchingController mController; private static final int UNMUTE_DEFAULT_VOLUME = 2; @@ -73,7 +73,7 @@ public abstract class MediaOutputBaseAdapter extends int mCurrentActivePosition; private boolean mIsInitVolumeFirstTime; - public MediaOutputBaseAdapter(MediaOutputController controller) { + public MediaOutputBaseAdapter(MediaSwitchingController controller) { mController = controller; mIsDragging = false; mCurrentActivePosition = -1; @@ -127,7 +127,7 @@ public abstract class MediaOutputBaseAdapter extends return mCurrentActivePosition; } - public MediaOutputController getController() { + public MediaSwitchingController getController() { return mController; } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 6cc4dcbaa1ea..6bc995f3437c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -65,11 +65,9 @@ import com.android.systemui.statusbar.phone.SystemUIDialog; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -/** - * Base dialog for media output UI - */ -public abstract class MediaOutputBaseDialog extends SystemUIDialog implements - MediaOutputController.Callback, Window.Callback { +/** Base dialog for media output UI */ +public abstract class MediaOutputBaseDialog extends SystemUIDialog + implements MediaSwitchingController.Callback, Window.Callback { private static final String TAG = "MediaOutputDialog"; private static final String EMPTY_TITLE = " "; @@ -82,7 +80,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements private final RecyclerView.LayoutManager mLayoutManager; final Context mContext; - final MediaOutputController mMediaOutputController; + final MediaSwitchingController mMediaSwitchingController; final BroadcastSender mBroadcastSender; /** @@ -212,22 +210,22 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements @Override public void onLayoutCompleted(RecyclerView.State state) { super.onLayoutCompleted(state); - mMediaOutputController.setRefreshing(false); - mMediaOutputController.refreshDataSetIfNeeded(); + mMediaSwitchingController.setRefreshing(false); + mMediaSwitchingController.refreshDataSetIfNeeded(); } } public MediaOutputBaseDialog( Context context, BroadcastSender broadcastSender, - MediaOutputController mediaOutputController, + MediaSwitchingController mediaSwitchingController, boolean includePlaybackAndAppMetadata) { super(context, R.style.Theme_SystemUI_Dialog_Media); // Save the context that is wrapped with our theme. mContext = getContext(); mBroadcastSender = broadcastSender; - mMediaOutputController = mediaOutputController; + mMediaSwitchingController = mediaSwitchingController; mLayoutManager = new LayoutManagerWrapper(mContext); mListMaxHeight = context.getResources().getDimensionPixelSize( R.dimen.media_output_dialog_list_max_height); @@ -279,9 +277,9 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements // Init bottom buttons mDoneButton.setOnClickListener(v -> dismiss()); mStopButton.setOnClickListener(v -> onStopButtonClick()); - mAppButton.setOnClickListener(mMediaOutputController::tryToLaunchMediaApplication); + mAppButton.setOnClickListener(mMediaSwitchingController::tryToLaunchMediaApplication); mMediaMetadataSectionLayout.setOnClickListener( - mMediaOutputController::tryToLaunchMediaApplication); + mMediaSwitchingController::tryToLaunchMediaApplication); mDismissing = false; } @@ -298,10 +296,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements @Override public void start() { - mMediaOutputController.start(this); + mMediaSwitchingController.start(this); if (isBroadcastSupported() && !mIsLeBroadcastCallbackRegistered) { - mMediaOutputController.registerLeBroadcastServiceCallback(mExecutor, - mBroadcastCallback); + mMediaSwitchingController.registerLeBroadcastServiceCallback( + mExecutor, mBroadcastCallback); mIsLeBroadcastCallbackRegistered = true; } } @@ -311,11 +309,11 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements // unregister broadcast callback should only depend on profile and registered flag // rather than remote device or broadcast state // otherwise it might have risks of leaking registered callback handle - if (mMediaOutputController.isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) { - mMediaOutputController.unregisterLeBroadcastServiceCallback(mBroadcastCallback); + if (mMediaSwitchingController.isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) { + mMediaSwitchingController.unregisterLeBroadcastServiceCallback(mBroadcastCallback); mIsLeBroadcastCallbackRegistered = false; } - mMediaOutputController.stop(); + mMediaSwitchingController.stop(); } @VisibleForTesting @@ -326,18 +324,17 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements void refresh(boolean deviceSetChanged) { // TODO(287191450): remove binder calls in this method from the UI thread. // If the dialog is going away or is already refreshing, do nothing. - if (mDismissing || mMediaOutputController.isRefreshing()) { + if (mDismissing || mMediaSwitchingController.isRefreshing()) { return; } - mMediaOutputController.setRefreshing(true); + mMediaSwitchingController.setRefreshing(true); // Update header icon final int iconRes = getHeaderIconRes(); final IconCompat headerIcon = getHeaderIcon(); final IconCompat appSourceIcon = getAppSourceIcon(); boolean colorSetUpdated = false; mCastAppLayout.setVisibility( - mMediaOutputController.shouldShowLaunchSection() - ? View.VISIBLE : View.GONE); + mMediaSwitchingController.shouldShowLaunchSection() ? View.VISIBLE : View.GONE); if (iconRes != 0) { mHeaderIcon.setVisibility(View.VISIBLE); mHeaderIcon.setImageResource(iconRes); @@ -371,10 +368,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements mAppResourceIcon.setVisibility(View.GONE); } else if (appSourceIcon != null) { Icon appIcon = appSourceIcon.toIcon(mContext); - mAppResourceIcon.setColorFilter(mMediaOutputController.getColorItemContent()); + mAppResourceIcon.setColorFilter(mMediaSwitchingController.getColorItemContent()); mAppResourceIcon.setImageIcon(appIcon); } else { - Drawable appIconDrawable = mMediaOutputController.getAppSourceIconFromPackage(); + Drawable appIconDrawable = mMediaSwitchingController.getAppSourceIconFromPackage(); if (appIconDrawable != null) { mAppResourceIcon.setImageDrawable(appIconDrawable); } else { @@ -387,7 +384,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements R.dimen.media_output_dialog_header_icon_padding); mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size)); } - mAppButton.setText(mMediaOutputController.getAppSourceName()); + mAppButton.setText(mMediaSwitchingController.getAppSourceName()); if (!mIncludePlaybackAndAppMetadata) { mHeaderTitle.setVisibility(View.GONE); @@ -424,23 +421,26 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements mAdapter.updateItems(); } } else { - mMediaOutputController.setRefreshing(false); - mMediaOutputController.refreshDataSetIfNeeded(); + mMediaSwitchingController.setRefreshing(false); + mMediaSwitchingController.refreshDataSetIfNeeded(); } } private void updateButtonBackgroundColorFilter() { - ColorFilter buttonColorFilter = new PorterDuffColorFilter( - mMediaOutputController.getColorButtonBackground(), - PorterDuff.Mode.SRC_IN); + ColorFilter buttonColorFilter = + new PorterDuffColorFilter( + mMediaSwitchingController.getColorButtonBackground(), + PorterDuff.Mode.SRC_IN); mDoneButton.getBackground().setColorFilter(buttonColorFilter); mStopButton.getBackground().setColorFilter(buttonColorFilter); - mDoneButton.setTextColor(mMediaOutputController.getColorPositiveButtonText()); + mDoneButton.setTextColor(mMediaSwitchingController.getColorPositiveButtonText()); } private void updateDialogBackgroundColor() { - getDialogView().getBackground().setTint(mMediaOutputController.getColorDialogBackground()); - mDeviceListLayout.setBackgroundColor(mMediaOutputController.getColorDialogBackground()); + getDialogView() + .getBackground() + .setTint(mMediaSwitchingController.getColorDialogBackground()); + mDeviceListLayout.setBackgroundColor(mMediaSwitchingController.getColorDialogBackground()); } private Drawable resizeDrawable(Drawable drawable, int size) { @@ -499,7 +499,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements protected void startLeBroadcast() { mStopButton.setText(R.string.media_output_broadcast_starting); mStopButton.setEnabled(false); - if (!mMediaOutputController.startBluetoothLeBroadcast()) { + if (!mMediaSwitchingController.startBluetoothLeBroadcast()) { // If the system can't execute "broadcast start", then UI shows the error. handleLeBroadcastStartFailed(); } @@ -512,9 +512,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements && sharedPref.getBoolean(PREF_IS_LE_BROADCAST_FIRST_LAUNCH, true)) { Log.d(TAG, "PREF_IS_LE_BROADCAST_FIRST_LAUNCH: true"); - mMediaOutputController.launchLeBroadcastNotifyDialog(mDialogView, + mMediaSwitchingController.launchLeBroadcastNotifyDialog( + mDialogView, mBroadcastSender, - MediaOutputController.BroadcastNotifyDialog.ACTION_FIRST_LAUNCH, + MediaSwitchingController.BroadcastNotifyDialog.ACTION_FIRST_LAUNCH, (d, w) -> { startLeBroadcast(); }); @@ -527,14 +528,13 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements } protected void startLeBroadcastDialog() { - mMediaOutputController.launchMediaOutputBroadcastDialog(mDialogView, - mBroadcastSender); + mMediaSwitchingController.launchMediaOutputBroadcastDialog(mDialogView, mBroadcastSender); refresh(); } protected void stopLeBroadcast() { mStopButton.setEnabled(false); - if (!mMediaOutputController.stopBluetoothLeBroadcast()) { + if (!mMediaSwitchingController.stopBluetoothLeBroadcast()) { // If the system can't execute "broadcast stop", then UI does refresh. mMainThreadHandler.post(() -> refresh()); } @@ -559,7 +559,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements } public void onStopButtonClick() { - mMediaOutputController.releaseSession(); + mMediaSwitchingController.releaseSession(); dismiss(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java index 1e317554859c..9b5b872a00db 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java @@ -235,14 +235,17 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } }; - MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar, - BroadcastSender broadcastSender, MediaOutputController mediaOutputController) { + MediaOutputBroadcastDialog( + Context context, + boolean aboveStatusbar, + BroadcastSender broadcastSender, + MediaSwitchingController mediaSwitchingController) { super( context, broadcastSender, - mediaOutputController, /* includePlaybackAndAppMetadata */ + mediaSwitchingController, /* includePlaybackAndAppMetadata */ true); - mAdapter = new MediaOutputAdapter(mMediaOutputController); + mAdapter = new MediaOutputAdapter(mMediaSwitchingController); // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class // that extends MediaOutputBaseDialog if (!aboveStatusbar) { @@ -262,8 +265,8 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { super.start(); if (!mIsLeBroadcastAssistantCallbackRegistered) { mIsLeBroadcastAssistantCallbackRegistered = true; - mMediaOutputController.registerLeBroadcastAssistantServiceCallback(mExecutor, - mBroadcastAssistantCallback); + mMediaSwitchingController.registerLeBroadcastAssistantServiceCallback( + mExecutor, mBroadcastAssistantCallback); } /* Add local source broadcast to connected capable devices that may be possible receivers * of stream. @@ -276,7 +279,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { super.stop(); if (mIsLeBroadcastAssistantCallbackRegistered) { mIsLeBroadcastAssistantCallbackRegistered = false; - mMediaOutputController.unregisterLeBroadcastAssistantServiceCallback( + mMediaSwitchingController.unregisterLeBroadcastAssistantServiceCallback( mBroadcastAssistantCallback); } } @@ -288,7 +291,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { @Override IconCompat getHeaderIcon() { - return mMediaOutputController.getHeaderIcon(); + return mMediaSwitchingController.getHeaderIcon(); } @Override @@ -299,17 +302,17 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { @Override CharSequence getHeaderText() { - return mMediaOutputController.getHeaderTitle(); + return mMediaSwitchingController.getHeaderTitle(); } @Override CharSequence getHeaderSubtitle() { - return mMediaOutputController.getHeaderSubTitle(); + return mMediaSwitchingController.getHeaderSubTitle(); } @Override IconCompat getAppSourceIcon() { - return mMediaOutputController.getNotificationSmallIcon(); + return mMediaSwitchingController.getNotificationSmallIcon(); } @Override @@ -319,16 +322,16 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { @Override public void onStopButtonClick() { - mMediaOutputController.stopBluetoothLeBroadcast(); + mMediaSwitchingController.stopBluetoothLeBroadcast(); dismiss(); } private String getBroadcastMetadataInfo(int metadata) { switch (metadata) { case METADATA_BROADCAST_NAME: - return mMediaOutputController.getBroadcastName(); + return mMediaSwitchingController.getBroadcastName(); case METADATA_BROADCAST_CODE: - return mMediaOutputController.getBroadcastCode(); + return mMediaSwitchingController.getBroadcastCode(); default: return ""; } @@ -342,13 +345,15 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { mBroadcastQrCodeView = getDialogView().requireViewById(R.id.qrcode_view); mBroadcastNotify = getDialogView().requireViewById(R.id.broadcast_info); - mBroadcastNotify.setOnClickListener(v -> { - mMediaOutputController.launchLeBroadcastNotifyDialog( - /* view= */ null, - /* broadcastSender= */ null, - MediaOutputController.BroadcastNotifyDialog.ACTION_BROADCAST_INFO_ICON, - /* onClickListener= */ null); - }); + mBroadcastNotify.setOnClickListener( + v -> { + mMediaSwitchingController.launchLeBroadcastNotifyDialog( + /* mediaOutputDialog= */ null, + /* broadcastSender= */ null, + MediaSwitchingController.BroadcastNotifyDialog + .ACTION_BROADCAST_INFO_ICON, + /* listener= */ null); + }); mBroadcastName = getDialogView().requireViewById(R.id.broadcast_name_summary); mBroadcastNameEdit = getDialogView().requireViewById(R.id.broadcast_name_edit); mBroadcastNameEdit.setOnClickListener(v -> { @@ -409,16 +414,16 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { return; } - for (BluetoothDevice sink : mMediaOutputController.getConnectedBroadcastSinkDevices()) { + for (BluetoothDevice sink : mMediaSwitchingController.getConnectedBroadcastSinkDevices()) { Log.d(TAG, "The broadcastMetadata broadcastId: " + broadcastMetadata.getBroadcastId() + ", the device: " + sink.getAnonymizedAddress()); - if (mMediaOutputController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) { + if (mMediaSwitchingController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) { Log.d(TAG, "The sink device has the broadcast source now."); return; } - if (!mMediaOutputController.addSourceIntoSinkDeviceWithBluetoothLeAssistant(sink, - broadcastMetadata, /*isGroupOp=*/ false)) { + if (!mMediaSwitchingController.addSourceIntoSinkDeviceWithBluetoothLeAssistant( + sink, broadcastMetadata, /* isGroupOp= */ false)) { Log.e(TAG, "Error: Source add failed"); } } @@ -457,11 +462,11 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } private String getLocalBroadcastMetadataQrCodeString() { - return mMediaOutputController.getLocalBroadcastMetadataQrCodeString(); + return mMediaSwitchingController.getLocalBroadcastMetadataQrCodeString(); } private BluetoothLeBroadcastMetadata getBroadcastMetadata() { - return mMediaOutputController.getBroadcastMetadata(); + return mMediaSwitchingController.getBroadcastMetadata(); } @VisibleForTesting @@ -476,8 +481,8 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { * stopped then used the new Broadcast code to start the Broadcast. */ mIsStopbyUpdateBroadcastCode = true; - mMediaOutputController.setBroadcastCode(updatedString); - if (!mMediaOutputController.stopBluetoothLeBroadcast()) { + mMediaSwitchingController.setBroadcastCode(updatedString); + if (!mMediaSwitchingController.stopBluetoothLeBroadcast()) { handleLeBroadcastStopFailed(); return; } @@ -485,8 +490,8 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { /* If the user wants to update the Broadcast Name, we don't need to stop the Broadcast * session. Only use the new Broadcast name to update the broadcast session. */ - mMediaOutputController.setBroadcastName(updatedString); - if (!mMediaOutputController.updateBluetoothLeBroadcast()) { + mMediaSwitchingController.setBroadcastName(updatedString); + if (!mMediaSwitchingController.updateBluetoothLeBroadcast()) { handleLeBroadcastUpdateFailed(); } } @@ -496,12 +501,13 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { public boolean isBroadcastSupported() { if (!legacyLeAudioSharing()) return false; boolean isBluetoothLeDevice = false; - if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) { - isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice( - mMediaOutputController.getCurrentConnectedMediaDevice()); + if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) { + isBluetoothLeDevice = + mMediaSwitchingController.isBluetoothLeDevice( + mMediaSwitchingController.getCurrentConnectedMediaDevice()); } - return mMediaOutputController.isBroadcastSupported() && isBluetoothLeDevice; + return mMediaSwitchingController.isBroadcastSupported() && isBluetoothLeDevice; } @Override @@ -515,7 +521,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { @Override public void handleLeBroadcastStartFailed() { - mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode); + mMediaSwitchingController.setBroadcastCode(mCurrentBroadcastCode); mRetryCount++; handleUpdateFailedUi(); @@ -538,8 +544,8 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { @Override public void handleLeBroadcastUpdateFailed() { - //Change the value in shared preferences back to it original value - mMediaOutputController.setBroadcastName(mCurrentBroadcastName); + // Change the value in shared preferences back to it original value + mMediaSwitchingController.setBroadcastName(mCurrentBroadcastName); mRetryCount++; handleUpdateFailedUi(); @@ -550,7 +556,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { if (mIsStopbyUpdateBroadcastCode) { mIsStopbyUpdateBroadcastCode = false; mRetryCount = 0; - if (!mMediaOutputController.startBluetoothLeBroadcast()) { + if (!mMediaSwitchingController.startBluetoothLeBroadcast()) { handleLeBroadcastStartFailed(); return; } @@ -561,8 +567,8 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { @Override public void handleLeBroadcastStopFailed() { - //Change the value in shared preferences back to it original value - mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode); + // Change the value in shared preferences back to it original value + mMediaSwitchingController.setBroadcastCode(mCurrentBroadcastCode); mRetryCount++; handleUpdateFailedUi(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt index 6ef9ea36882b..2e7e66f5b384 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt @@ -29,7 +29,7 @@ constructor( private val context: Context, private val broadcastSender: BroadcastSender, private val dialogTransitionAnimator: DialogTransitionAnimator, - private val mediaOutputControllerFactory: MediaOutputController.Factory + private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory ) { var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null @@ -41,7 +41,7 @@ constructor( // TODO: b/321969740 - Populate the userHandle parameter. The user handle is necessary to // disambiguate the same package running on different users. val controller = - mediaOutputControllerFactory.create( + mediaSwitchingControllerFactory.create( packageName, /* userHandle= */ null, /* token */ null, diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index eb6a32023eb7..c9af7b322811 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -46,14 +46,14 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { Context context, boolean aboveStatusbar, BroadcastSender broadcastSender, - MediaOutputController mediaOutputController, + MediaSwitchingController mediaSwitchingController, DialogTransitionAnimator dialogTransitionAnimator, UiEventLogger uiEventLogger, boolean includePlaybackAndAppMetadata) { - super(context, broadcastSender, mediaOutputController, includePlaybackAndAppMetadata); + super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata); mDialogTransitionAnimator = dialogTransitionAnimator; mUiEventLogger = uiEventLogger; - mAdapter = new MediaOutputAdapter(mMediaOutputController); + mAdapter = new MediaOutputAdapter(mMediaSwitchingController); if (!aboveStatusbar) { getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } @@ -72,7 +72,7 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { @Override IconCompat getHeaderIcon() { - return mMediaOutputController.getHeaderIcon(); + return mMediaSwitchingController.getHeaderIcon(); } @Override @@ -83,27 +83,29 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { @Override CharSequence getHeaderText() { - return mMediaOutputController.getHeaderTitle(); + return mMediaSwitchingController.getHeaderTitle(); } @Override CharSequence getHeaderSubtitle() { - return mMediaOutputController.getHeaderSubTitle(); + return mMediaSwitchingController.getHeaderSubTitle(); } @Override IconCompat getAppSourceIcon() { - return mMediaOutputController.getNotificationSmallIcon(); + return mMediaSwitchingController.getNotificationSmallIcon(); } @Override int getStopButtonVisibility() { boolean isActiveRemoteDevice = false; - if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) { - isActiveRemoteDevice = mMediaOutputController.isActiveRemoteDevice( - mMediaOutputController.getCurrentConnectedMediaDevice()); + if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) { + isActiveRemoteDevice = + mMediaSwitchingController.isActiveRemoteDevice( + mMediaSwitchingController.getCurrentConnectedMediaDevice()); } - boolean showBroadcastButton = isBroadcastSupported() && mMediaOutputController.isPlaying(); + boolean showBroadcastButton = + isBroadcastSupported() && mMediaSwitchingController.isPlaying(); return (isActiveRemoteDevice || showBroadcastButton) ? View.VISIBLE : View.GONE; } @@ -115,13 +117,14 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { boolean isBroadcastEnabled = false; if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST)) { - if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) { - isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice( - mMediaOutputController.getCurrentConnectedMediaDevice()); + if (mMediaSwitchingController.getCurrentConnectedMediaDevice() != null) { + isBluetoothLeDevice = + mMediaSwitchingController.isBluetoothLeDevice( + mMediaSwitchingController.getCurrentConnectedMediaDevice()); // if broadcast is active, broadcast should be considered as supported // there could be a valid case that broadcast is ongoing // without active LEA device connected - isBroadcastEnabled = mMediaOutputController.isBluetoothLeBroadcastEnabled(); + isBroadcastEnabled = mMediaSwitchingController.isBluetoothLeBroadcastEnabled(); } } else { // To decouple LE Audio Broadcast and Unicast, it always displays the button when there @@ -129,15 +132,16 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { isBluetoothLeDevice = true; } - return mMediaOutputController.isBroadcastSupported() + return mMediaSwitchingController.isBroadcastSupported() && (isBluetoothLeDevice || isBroadcastEnabled); } @Override public CharSequence getStopButtonText() { int resId = R.string.media_output_dialog_button_stop_casting; - if (isBroadcastSupported() && mMediaOutputController.isPlaying() - && !mMediaOutputController.isBluetoothLeBroadcastEnabled()) { + if (isBroadcastSupported() + && mMediaSwitchingController.isPlaying() + && !mMediaSwitchingController.isBluetoothLeBroadcastEnabled()) { resId = R.string.media_output_broadcast; } return mContext.getText(resId); @@ -145,8 +149,8 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { @Override public void onStopButtonClick() { - if (isBroadcastSupported() && mMediaOutputController.isPlaying()) { - if (!mMediaOutputController.isBluetoothLeBroadcastEnabled()) { + if (isBroadcastSupported() && mMediaSwitchingController.isPlaying()) { + if (!mMediaSwitchingController.isBluetoothLeBroadcastEnabled()) { if (startLeBroadcastDialogForFirstTime()) { return; } @@ -155,7 +159,7 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { stopLeBroadcast(); } } else { - mMediaOutputController.releaseSession(); + mMediaSwitchingController.releaseSession(); mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); dismiss(); } @@ -163,8 +167,9 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { @Override public int getBroadcastIconVisibility() { - return (isBroadcastSupported() && mMediaOutputController.isBluetoothLeBroadcastEnabled()) - ? View.VISIBLE : View.GONE; + return (isBroadcastSupported() && mMediaSwitchingController.isBluetoothLeBroadcastEnabled()) + ? View.VISIBLE + : View.GONE; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt index 47e069102035..4e9451a838ad 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt @@ -35,7 +35,7 @@ constructor( private val broadcastSender: BroadcastSender, private val uiEventLogger: UiEventLogger, private val dialogTransitionAnimator: DialogTransitionAnimator, - private val mediaOutputControllerFactory: MediaOutputController.Factory, + private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory, ) { companion object { const val INTERACTION_JANK_TAG = "media_output" @@ -118,7 +118,7 @@ constructor( // Dismiss the previous dialog, if any. mediaOutputDialog?.dismiss() - val controller = mediaOutputControllerFactory.create(packageName, userHandle, token) + val controller = mediaSwitchingControllerFactory.create(packageName, userHandle, token) val mediaOutputDialog = MediaOutputDialog( diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 875e505db1c6..2cbc75755cfd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -77,6 +77,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.InfoMediaManager; +import com.android.settingslib.media.InputRouteManager; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.flags.Flags; @@ -116,12 +117,13 @@ import java.util.function.Function; import java.util.stream.Collectors; /** - * Controller for media output dialog + * Controller for a dialog that allows users to switch media output and input devices, control + * volume, connect to new devices, etc. */ -public class MediaOutputController implements LocalMediaManager.DeviceCallback, - INearbyMediaDevicesUpdateCallback { +public class MediaSwitchingController + implements LocalMediaManager.DeviceCallback, INearbyMediaDevicesUpdateCallback { - private static final String TAG = "MediaOutputController"; + private static final String TAG = "MediaSwitchingController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final String PAGE_CONNECTED_DEVICES_KEY = "top_level_connected_devices"; @@ -137,10 +139,12 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private final DialogTransitionAnimator mDialogTransitionAnimator; private final CommonNotifCollection mNotifCollection; protected final Object mMediaDevicesLock = new Object(); + protected final Object mInputMediaDevicesLock = new Object(); @VisibleForTesting final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>(); - private final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); + private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>(); + private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>(); private final AudioManager mAudioManager; private final PowerExemptionManager mPowerExemptionManager; private final KeyguardManager mKeyGuardManager; @@ -153,6 +157,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, @VisibleForTesting boolean mNeedRefresh = false; private MediaController mMediaController; + private InputRouteManager mInputRouteManager; @VisibleForTesting Callback mCallback; @VisibleForTesting @@ -181,8 +186,20 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, ACTION_BROADCAST_INFO_ICON } + @VisibleForTesting + final InputRouteManager.InputDeviceCallback mInputDeviceCallback = + new InputRouteManager.InputDeviceCallback() { + @Override + public void onInputDeviceListUpdated(@NonNull List<MediaDevice> devices) { + synchronized (mInputMediaDevicesLock) { + buildInputMediaItems(devices); + mCallback.onDeviceListChanged(); + } + } + }; + @AssistedInject - public MediaOutputController( + public MediaSwitchingController( Context context, @Assisted String packageName, @Assisted @Nullable UserHandle userHandle, @@ -241,19 +258,23 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, R.dimen.media_output_dialog_default_margin_end); mItemMarginEndSelectable = (int) mContext.getResources().getDimension( R.dimen.media_output_dialog_selectable_margin_end); + + if (enableInputRouting()) { + mInputRouteManager = new InputRouteManager(mContext, audioManager); + } } @AssistedFactory public interface Factory { - /** Construct a MediaOutputController */ - MediaOutputController create( + /** Construct a MediaSwitchingController */ + MediaSwitchingController create( String packageName, UserHandle userHandle, MediaSession.Token token); } protected void start(@NonNull Callback cb) { synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mMediaItemList.clear(); + mOutputMediaItemList.clear(); } mNearbyDeviceInfoMap.clear(); if (mNearbyMediaDevicesManager != null) { @@ -277,6 +298,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mCallback = cb; mLocalMediaManager.registerCallback(this); mLocalMediaManager.startScan(); + + if (enableInputRouting()) { + mInputRouteManager.registerCallback(mInputDeviceCallback); + } } boolean shouldShowLaunchSection() { @@ -300,12 +325,19 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mLocalMediaManager.stopScan(); synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mMediaItemList.clear(); + mOutputMediaItemList.clear(); } if (mNearbyMediaDevicesManager != null) { mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this); } mNearbyDeviceInfoMap.clear(); + + if (enableInputRouting()) { + mInputRouteManager.unregisterCallback(mInputDeviceCallback); + synchronized (mInputMediaDevicesLock) { + mInputMediaItemList.clear(); + } + } } private MediaController getMediaController() { @@ -335,7 +367,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, @Override public void onDeviceListUpdate(List<MediaDevice> devices) { - boolean isListEmpty = mMediaItemList.isEmpty(); + boolean isListEmpty = mOutputMediaItemList.isEmpty(); if (isListEmpty || !mIsRefreshing) { buildMediaItems(devices); mCallback.onDeviceListChanged(); @@ -352,7 +384,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, public void onSelectedDeviceStateChanged(MediaDevice device, @LocalMediaManager.MediaDeviceState int state) { mCallback.onRouteChanged(); - mMetricLogger.logOutputItemSuccess(device.toString(), new ArrayList<>(mMediaItemList)); + mMetricLogger.logOutputItemSuccess( + device.toString(), new ArrayList<>(mOutputMediaItemList)); } @Override @@ -363,7 +396,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, @Override public void onRequestFailed(int reason) { mCallback.onRouteChanged(); - mMetricLogger.logOutputItemFailure(new ArrayList<>(mMediaItemList), reason); + mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason); } /** @@ -382,7 +415,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } try { synchronized (mMediaDevicesLock) { - mMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); + mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); } mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice()); } catch (Exception e) { @@ -638,9 +671,9 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private void buildMediaItems(List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { - List<MediaItem> updatedMediaItems = buildMediaItems(mMediaItemList, devices); - mMediaItemList.clear(); - mMediaItemList.addAll(updatedMediaItems); + List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices); + mOutputMediaItemList.clear(); + mOutputMediaItemList.addAll(updatedMediaItems); } } @@ -714,6 +747,19 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } } + private boolean enableInputRouting() { + return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl(); + } + + private void buildInputMediaItems(List<MediaDevice> devices) { + synchronized (mInputMediaDevicesLock) { + List<MediaItem> updatedInputMediaItems = + devices.stream().map(MediaItem::createDeviceMediaItem).toList(); + mInputMediaItemList.clear(); + mInputMediaItemList.addAll(updatedInputMediaItems); + } + } + /** * Initial categorization of current devices, will not be called for updates to the devices * list. @@ -778,7 +824,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mediaDevice.setRangeZone(mNearbyDeviceInfoMap.get(mediaDevice.getId())); } } - } boolean isCurrentConnectedDeviceRemote() { @@ -837,8 +882,31 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, }); } + private void addInputDevices(List<MediaItem> mediaItems) { + mediaItems.add( + MediaItem.createGroupDividerMediaItem( + mContext.getString(R.string.media_input_group_title))); + mediaItems.addAll(mInputMediaItemList); + } + + private void addOutputDevices(List<MediaItem> mediaItems) { + mediaItems.add( + MediaItem.createGroupDividerMediaItem( + mContext.getString(R.string.media_output_group_title))); + mediaItems.addAll(mOutputMediaItemList); + } + public List<MediaItem> getMediaItemList() { - return mMediaItemList; + // If input routing is not enabled, only return output media items. + if (!enableInputRouting()) { + return mOutputMediaItemList; + } + + // If input routing is enabled, return both output and input media items. + List<MediaItem> mediaItems = new ArrayList<>(); + addOutputDevices(mediaItems); + addInputDevices(mediaItems); + return mediaItems; } public MediaDevice getCurrentConnectedMediaDevice() { @@ -921,7 +989,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, public boolean isAnyDeviceTransferring() { synchronized (mMediaDevicesLock) { - for (MediaItem mediaItem : mMediaItemList) { + for (MediaItem mediaItem : mOutputMediaItemList) { if (mediaItem.getMediaDevice().isPresent() && mediaItem.getMediaDevice().get().getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { @@ -986,8 +1054,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } void launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender) { - MediaOutputController controller = - new MediaOutputController( + MediaSwitchingController controller = + new MediaSwitchingController( mContext, mPackageName, mUserHandle, diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index d59658947771..4251b81226b3 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -53,7 +53,6 @@ import android.text.TextPaint; import android.text.TextUtils; import android.util.Log; import android.view.Window; -import android.view.WindowManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; @@ -309,9 +308,6 @@ public class MediaProjectionPermissionActivity extends Activity { private void setUpDialog(AlertDialog dialog) { SystemUIDialog.registerDismissListener(dialog); SystemUIDialog.applyFlags(dialog, /* showWhenLocked= */ false); - - final Window w = dialog.getWindow(); - w.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); SystemUIDialog.setDialogSize(dialog); dialog.setOnCancelListener(this::onDialogDismissedOrCancelled); @@ -319,6 +315,7 @@ public class MediaProjectionPermissionActivity extends Activity { dialog.create(); dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); + final Window w = dialog.getWindow(); w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt index 072d322d69a6..1fe54e46fee1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt @@ -23,17 +23,14 @@ import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepositor import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepositoryImpl import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepository import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepositoryImpl -import com.android.systemui.qs.panels.domain.interactor.GridTypeConsistencyInteractor -import com.android.systemui.qs.panels.domain.interactor.InfiniteGridConsistencyInteractor -import com.android.systemui.qs.panels.domain.interactor.NoopGridConsistencyInteractor import com.android.systemui.qs.panels.shared.model.GridLayoutType import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.panels.ui.compose.GridLayout -import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout +import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModelImpl import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModel @@ -56,11 +53,6 @@ interface PanelsModule { @Binds fun bindGridLayoutTypeRepository(impl: GridLayoutTypeRepositoryImpl): GridLayoutTypeRepository - @Binds - fun bindDefaultGridConsistencyInteractor( - impl: NoopGridConsistencyInteractor - ): GridTypeConsistencyInteractor - @Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel @Binds fun bindGridSizeViewModel(impl: FixedColumnsSizeViewModelImpl): FixedColumnsSizeViewModel @@ -74,12 +66,6 @@ interface PanelsModule { @PaginatedBaseLayoutType fun bindPaginatedBaseGridLayout(impl: InfiniteGridLayout): PaginatableGridLayout - @Binds - @PaginatedBaseLayoutType - fun bindPaginatedBaseConsistencyInteractor( - impl: NoopGridConsistencyInteractor - ): GridTypeConsistencyInteractor - @Binds @Named("Default") fun bindDefaultGridLayout(impl: PaginatedGridLayout): GridLayout companion object { @@ -117,28 +103,5 @@ interface PanelsModule { ): Set<GridLayoutType> { return entries.map { it.first }.toSet() } - - @Provides - @IntoSet - fun provideGridConsistencyInteractor( - consistencyInteractor: InfiniteGridConsistencyInteractor - ): Pair<GridLayoutType, GridTypeConsistencyInteractor> { - return Pair(InfiniteGridLayoutType, consistencyInteractor) - } - - @Provides - @IntoSet - fun providePaginatedGridConsistencyInteractor( - @PaginatedBaseLayoutType consistencyInteractor: GridTypeConsistencyInteractor, - ): Pair<GridLayoutType, GridTypeConsistencyInteractor> { - return Pair(PaginatedGridLayoutType, consistencyInteractor) - } - - @Provides - fun provideGridConsistencyInteractorMap( - entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridTypeConsistencyInteractor>> - ): Map<GridLayoutType, GridTypeConsistencyInteractor> { - return entries.toMap() - } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt deleted file mode 100644 index a2e7ea6fe797..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractor.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.interactor - -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.core.LogLevel -import com.android.systemui.qs.panels.shared.model.GridLayoutType -import com.android.systemui.qs.panels.shared.model.PanelsLog -import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor -import com.android.systemui.qs.pipeline.shared.TileSpec -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch - -@SysUISingleton -class GridConsistencyInteractor -@Inject -constructor( - private val gridLayoutTypeInteractor: GridLayoutTypeInteractor, - private val currentTilesInteractor: CurrentTilesInteractor, - private val consistencyInteractors: - Map<GridLayoutType, @JvmSuppressWildcards GridTypeConsistencyInteractor>, - private val defaultConsistencyInteractor: GridTypeConsistencyInteractor, - @PanelsLog private val logBuffer: LogBuffer, - @Application private val applicationScope: CoroutineScope, -) { - fun start() { - applicationScope.launch { - gridLayoutTypeInteractor.layout.collectLatest { type -> - val consistencyInteractor = - consistencyInteractors[type] ?: defaultConsistencyInteractor - currentTilesInteractor.currentTiles - .map { tiles -> tiles.map { it.spec } } - .collectLatest { tiles -> - val newTiles = consistencyInteractor.reconcileTiles(tiles) - if (newTiles != tiles) { - currentTilesInteractor.setTiles(newTiles) - logChange(newTiles) - } - } - } - } - } - - private fun logChange(tiles: List<TileSpec>) { - logBuffer.log( - LOG_BUFFER_CURRENT_TILES_CHANGE_TAG, - LogLevel.DEBUG, - { str1 = tiles.toString() }, - { "Tiles reordered: $str1" } - ) - } - - private companion object { - const val LOG_BUFFER_CURRENT_TILES_CHANGE_TAG = "GridConsistencyTilesChange" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt deleted file mode 100644 index 4cdabaedc49e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridTypeConsistencyInteractor.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.interactor - -import com.android.systemui.qs.pipeline.shared.TileSpec - -interface GridTypeConsistencyInteractor { - /** - * Given a list of tiles, return the best list of the same tiles (preserving as much order as - * possible, such that it's consistent with the current layout. - */ - fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt deleted file mode 100644 index 874b3b0a4636..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.interactor - -import android.util.Log -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.qs.panels.shared.model.SizedTile -import com.android.systemui.qs.panels.shared.model.SizedTileImpl -import com.android.systemui.qs.panels.shared.model.TileRow -import com.android.systemui.qs.pipeline.shared.TileSpec -import javax.inject.Inject - -@SysUISingleton -class InfiniteGridConsistencyInteractor -@Inject -constructor( - private val iconTilesInteractor: IconTilesInteractor, - private val gridSizeInteractor: FixedColumnsSizeInteractor -) : GridTypeConsistencyInteractor { - - /** - * Tries to fill in every columns of all rows (except the last row), potentially reordering - * tiles. - */ - override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> { - val newTiles: MutableList<TileSpec> = mutableListOf() - val row = TileRow<TileSpec>(columns = gridSizeInteractor.columns.value) - val tilesQueue: ArrayDeque<SizedTile<TileSpec>> = - ArrayDeque( - tiles.map { - SizedTileImpl( - it, - if (iconTilesInteractor.isIconTile(it)) 1 else 2, - ) - } - ) - - while (tilesQueue.isNotEmpty()) { - if (row.isFull()) { - newTiles.addAll(row.tiles.map { it.tile }) - row.clear() - } - - val tile = tilesQueue.removeFirst() - - // If the tile fits in the row, add it. - if (!row.maybeAddTile(tile)) { - // If the tile does not fit the row, find an icon tile to move. - // We'll try to either add an icon tile from the queue to complete the row, or - // remove an icon tile from the current row to free up space. - - val iconTile: SizedTile<TileSpec>? = tilesQueue.firstOrNull { it.width == 1 } - if (iconTile != null) { - tilesQueue.remove(iconTile) - tilesQueue.addFirst(tile) - row.maybeAddTile(iconTile) - } else { - val tileToRemove: SizedTile<TileSpec>? = row.findLastIconTile() - if (tileToRemove != null) { - row.removeTile(tileToRemove) - row.maybeAddTile(tile) - - // Moving the icon tile to the end because there's no other - // icon tiles in the queue. - tilesQueue.addLast(tileToRemove) - } else { - // If the row does not have an icon tile, add the incomplete row. - // Note: this shouldn't happen because an icon tile is guaranteed to be in a - // row that doesn't have enough space for a large tile. - val tileSpecs = row.tiles.map { it.tile } - Log.wtf(TAG, "Uneven row does not have an icon tile to remove: $tileSpecs") - newTiles.addAll(tileSpecs) - row.clear() - tilesQueue.addFirst(tile) - } - } - } - } - - // Add last row that might be incomplete - newTiles.addAll(row.tiles.map { it.tile }) - - return newTiles.toList() - } - - private companion object { - const val TAG = "InfiniteGridConsistencyInteractor" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt deleted file mode 100644 index 0386a6ab20d6..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopGridConsistencyInteractor.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.interactor - -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.qs.pipeline.shared.TileSpec -import javax.inject.Inject - -/** [GridTypeConsistencyInteractor] implementation that doesn't do any changes to tiles. */ -@SysUISingleton -class NoopGridConsistencyInteractor @Inject constructor() : GridTypeConsistencyInteractor { - override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> = tiles -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt index 9a2315be29a2..1f8a24a1da67 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt @@ -161,9 +161,9 @@ private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean { @Composable fun Modifier.dragAndDropTileSource( sizedTile: SizedTile<EditTileViewModel>, + dragAndDropState: DragAndDropState, onTap: (TileSpec) -> Unit, - onDoubleTap: (TileSpec) -> Unit, - dragAndDropState: DragAndDropState + onDoubleTap: (TileSpec) -> Unit = {}, ): Modifier { val state by rememberUpdatedState(dragAndDropState) return dragAndDropSource { @@ -181,11 +181,11 @@ fun Modifier.dragAndDropTileSource( ClipData( QsDragAndDrop.CLIPDATA_LABEL, arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE), - ClipData.Item(sizedTile.tile.tileSpec.spec) + ClipData.Item(sizedTile.tile.tileSpec.spec), ) ) ) - } + }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index fde40da2e5bc..f4acbec1063c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile +import com.android.systemui.qs.panels.ui.compose.infinitegrid.TileLazyGrid import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel @Composable diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt deleted file mode 100644 index afd47a7f7758..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +++ /dev/null @@ -1,926 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalFoundationApi::class) - -package com.android.systemui.qs.panels.ui.compose - -import android.content.res.Resources -import android.graphics.drawable.Animatable -import android.service.quicksettings.Tile.STATE_ACTIVE -import android.service.quicksettings.Tile.STATE_INACTIVE -import android.text.TextUtils -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi -import androidx.compose.animation.graphics.res.animatedVectorResource -import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter -import androidx.compose.animation.graphics.vector.AnimatedImageVector -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.LocalOverscrollConfiguration -import androidx.compose.foundation.background -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.border -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Arrangement.spacedBy -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyGridItemScope -import androidx.compose.foundation.lazy.grid.LazyGridScope -import androidx.compose.foundation.lazy.grid.LazyGridState -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInRoot -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.onClick -import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription -import androidx.compose.ui.semantics.toggleableState -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.util.fastMap -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.compose.animation.Expandable -import com.android.compose.modifiers.background -import com.android.compose.modifiers.thenIf -import com.android.systemui.animation.Expandable -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.common.ui.compose.Icon -import com.android.systemui.common.ui.compose.load -import com.android.systemui.compose.modifiers.sysuiResTag -import com.android.systemui.plugins.qs.QSTile -import com.android.systemui.qs.panels.shared.model.SizedTile -import com.android.systemui.qs.panels.shared.model.SizedTileImpl -import com.android.systemui.qs.panels.ui.compose.TileDefaults.longPressLabel -import com.android.systemui.qs.panels.ui.model.GridCell -import com.android.systemui.qs.panels.ui.model.SpacerGridCell -import com.android.systemui.qs.panels.ui.model.TileGridCell -import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState -import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel -import com.android.systemui.qs.panels.ui.viewmodel.TileUiState -import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel -import com.android.systemui.qs.panels.ui.viewmodel.toUiState -import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor -import com.android.systemui.qs.pipeline.shared.TileSpec -import com.android.systemui.qs.shared.model.groupAndSort -import com.android.systemui.qs.tileimpl.QSTileImpl -import com.android.systemui.res.R -import java.util.function.Supplier -import kotlinx.coroutines.delay - -object TileType - -private const val TEST_TAG_SMALL = "qs_tile_small" -private const val TEST_TAG_LARGE = "qs_tile_large" -private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target" - -@Composable -fun Tile(tile: TileViewModel, iconOnly: Boolean, showLabels: Boolean = false, modifier: Modifier) { - val state by tile.state.collectAsStateWithLifecycle(tile.currentState) - val resources = resources() - val uiState = remember(state, resources) { state.toUiState(resources) } - val colors = TileDefaults.getColorForState(uiState) - - // TODO(b/361789146): Draw the shapes instead of clipping - val tileShape = TileDefaults.animateTileShape(uiState.state) - - TileContainer( - colors = colors, - showLabels = showLabels, - label = uiState.label, - iconOnly = iconOnly, - shape = tileShape, - clickEnabled = true, - onClick = tile::onClick, - onLongClick = tile::onLongClick, - modifier = modifier.height(tileHeight()), - uiState = uiState, - ) { - val icon = getTileIcon(icon = uiState.icon) - if (iconOnly) { - TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center)) - } else { - val iconShape = TileDefaults.animateIconShape(uiState.state) - LargeTileContent( - label = uiState.label, - secondaryLabel = uiState.secondaryLabel, - icon = icon, - colors = colors, - iconShape = iconShape, - toggleClickSupported = state.handlesSecondaryClick, - onClick = { - if (state.handlesSecondaryClick) { - tile.onSecondaryClick() - } - }, - onLongClick = { tile.onLongClick(it) }, - accessibilityUiState = uiState.accessibilityUiState, - ) - } - } -} - -@Composable -private fun TileContainer( - colors: TileColors, - showLabels: Boolean, - label: String, - iconOnly: Boolean, - shape: Shape, - clickEnabled: Boolean = false, - onClick: (Expandable) -> Unit = {}, - onLongClick: (Expandable) -> Unit = {}, - modifier: Modifier = Modifier, - uiState: TileUiState? = null, - content: @Composable BoxScope.(Expandable) -> Unit, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = - spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin), Alignment.Top), - modifier = modifier, - ) { - val backgroundColor = - if (iconOnly || uiState?.handlesSecondaryClick != true) { - colors.iconBackground - } else { - colors.background - } - Expandable( - color = backgroundColor, - shape = shape, - modifier = Modifier.height(tileHeight()).clip(shape), - ) { - val longPressLabel = longPressLabel() - Box( - modifier = - Modifier.fillMaxSize() - .thenIf(clickEnabled) { - Modifier.combinedClickable( - onClick = { onClick(it) }, - onLongClick = { onLongClick(it) }, - onClickLabel = uiState?.accessibilityUiState?.clickLabel, - onLongClickLabel = longPressLabel, - ) - } - .thenIf(uiState != null) { - uiState as TileUiState - Modifier.semantics { - role = uiState.accessibilityUiState.accessibilityRole - if ( - uiState.accessibilityUiState.accessibilityRole == - Role.Switch - ) { - uiState.accessibilityUiState.toggleableState?.let { - toggleableState = it - } - } - stateDescription = uiState.accessibilityUiState.stateDescription - } - .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE) - .thenIf(iconOnly) { - Modifier.semantics { - contentDescription = - uiState.accessibilityUiState.contentDescription - } - } - } - .tilePadding() - ) { - content(it) - } - } - - if (showLabels && iconOnly) { - Text( - label, - maxLines = 2, - color = colors.label, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - ) - } - } -} - -@Composable -private fun LargeTileContent( - label: String, - secondaryLabel: String?, - icon: Icon, - colors: TileColors, - iconShape: Shape, - accessibilityUiState: AccessibilityUiState? = null, - toggleClickSupported: Boolean = false, - onClick: () -> Unit = {}, - onLongClick: () -> Unit = {}, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = tileHorizontalArrangement(), - ) { - // Icon - val longPressLabel = longPressLabel() - Box( - modifier = - Modifier.size(TileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) { - Modifier.clip(iconShape) - .background(colors.iconBackground, { 1f }) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - onLongClickLabel = longPressLabel, - ) - .thenIf(accessibilityUiState != null) { - accessibilityUiState as AccessibilityUiState - Modifier.semantics { - contentDescription = accessibilityUiState.contentDescription - stateDescription = accessibilityUiState.stateDescription - accessibilityUiState.toggleableState?.let { - toggleableState = it - } - role = Role.Switch - } - .sysuiResTag(TEST_TAG_TOGGLE) - } - } - ) { - TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center)) - } - - // Labels - Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) { - Text(label, color = colors.label, modifier = Modifier.tileMarquee()) - if (!TextUtils.isEmpty(secondaryLabel)) { - Text( - secondaryLabel ?: "", - color = colors.secondaryLabel, - modifier = - Modifier.tileMarquee().thenIf( - accessibilityUiState - ?.stateDescription - ?.contains(secondaryLabel ?: "") == true - ) { - Modifier.clearAndSetSemantics {} - }, - ) - } - } - } -} - -private fun Modifier.tileMarquee(): Modifier { - return basicMarquee(iterations = 1, initialDelayMillis = 200) -} - -@Composable -fun TileLazyGrid( - modifier: Modifier = Modifier, - state: LazyGridState = rememberLazyGridState(), - columns: GridCells, - content: LazyGridScope.() -> Unit, -) { - LazyVerticalGrid( - state = state, - columns = columns, - verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)), - horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)), - modifier = modifier, - content = content, - ) -} - -@Composable -fun DefaultEditTileGrid( - currentListState: EditTileListState, - otherTiles: List<SizedTile<EditTileViewModel>>, - columns: Int, - modifier: Modifier, - onAddTile: (TileSpec, Int) -> Unit, - onRemoveTile: (TileSpec) -> Unit, - onSetTiles: (List<TileSpec>) -> Unit, - onResize: (TileSpec) -> Unit, -) { - val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState { - onAddTile(it, CurrentTilesInteractor.POSITION_AT_END) - } - val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical) - - CompositionLocalProvider(LocalOverscrollConfiguration provides null) { - Column( - verticalArrangement = - spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), - modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()), - ) { - AnimatedContent( - targetState = currentListState.dragInProgress, - modifier = Modifier.wrapContentSize(), - ) { dragIsInProgress -> - EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) { - if (dragIsInProgress) { - RemoveTileTarget() - } else { - Text(text = "Hold and drag to rearrange tiles.") - } - } - } - - CurrentTilesGrid( - currentListState, - columns, - tilePadding, - onRemoveTile, - onResize, - onSetTiles, - ) - - // Hide available tiles when dragging - AnimatedVisibility( - visible = !currentListState.dragInProgress, - enter = fadeIn(), - exit = fadeOut(), - ) { - Column( - verticalArrangement = - spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), - modifier = modifier.fillMaxSize(), - ) { - EditGridHeader { Text(text = "Hold and drag to add tiles.") } - - AvailableTileGrid( - otherTiles, - columns, - tilePadding, - addTileToEnd, - currentListState, - ) - } - } - - // Drop zone to remove tiles dragged out of the tile grid - Spacer( - modifier = - Modifier.fillMaxWidth() - .weight(1f) - .dragAndDropRemoveZone(currentListState, onRemoveTile) - ) - } - } -} - -@Composable -private fun EditGridHeader( - modifier: Modifier = Modifier, - content: @Composable BoxScope.() -> Unit, -) { - CompositionLocalProvider( - LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight), - ) { - content() - } - } -} - -@Composable -private fun RemoveTileTarget() { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = tileHorizontalArrangement(), - modifier = - Modifier.fillMaxHeight() - .border(1.dp, LocalContentColor.current, shape = CircleShape) - .padding(10.dp), - ) { - Icon(imageVector = Icons.Default.Clear, contentDescription = null) - Text(text = "Remove") - } -} - -@Composable -private fun CurrentTilesContainer(content: @Composable () -> Unit) { - Box( - Modifier.fillMaxWidth() - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f), - shape = RoundedCornerShape(48.dp), - ) - .padding(dimensionResource(R.dimen.qs_tile_margin_vertical)) - ) { - content() - } -} - -@Composable -private fun CurrentTilesGrid( - listState: EditTileListState, - columns: Int, - tilePadding: Dp, - onClick: (TileSpec) -> Unit, - onResize: (TileSpec) -> Unit, - onSetTiles: (List<TileSpec>) -> Unit, -) { - val currentListState by rememberUpdatedState(listState) - - CurrentTilesContainer { - val tileHeight = tileHeight() - val totalRows = listState.tiles.lastOrNull()?.row ?: 0 - val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding) - val gridState = rememberLazyGridState() - var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) } - - TileLazyGrid( - state = gridState, - modifier = - Modifier.height(totalHeight) - .dragAndDropTileList(gridState, gridContentOffset, listState) { - onSetTiles(currentListState.tileSpecs()) - } - .onGloballyPositioned { coordinates -> - gridContentOffset = coordinates.positionInRoot() - } - .testTag(CURRENT_TILES_GRID_TEST_TAG), - columns = GridCells.Fixed(columns), - ) { - editTiles( - listState.tiles, - ClickAction.REMOVE, - onClick, - listState, - onResize = onResize, - indicatePosition = true, - ) - } - } -} - -@Composable -private fun AvailableTileGrid( - tiles: List<SizedTile<EditTileViewModel>>, - columns: Int, - tilePadding: Dp, - onClick: (TileSpec) -> Unit, - dragAndDropState: DragAndDropState, -) { - val availableTileHeight = tileHeight(true) - val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding) - - // Available tiles aren't visible during drag and drop, so the row isn't needed - val groupedTiles = - remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) { - groupAndSort(tiles.fastMap { TileGridCell(it, 0) }) - } - val labelColors = TileDefaults.inactiveTileColors() - // Available tiles - TileLazyGrid( - modifier = Modifier.height(availableGridHeight).testTag(AVAILABLE_TILES_GRID_TEST_TAG), - columns = GridCells.Fixed(columns), - ) { - groupedTiles.forEach { category, tiles -> - stickyHeader { - Text( - text = category.label.load() ?: "", - fontSize = 20.sp, - color = labelColors.label, - modifier = - Modifier.background(Color.Black) - .padding(start = 16.dp, bottom = 8.dp, top = 8.dp), - ) - } - editTiles( - tiles, - ClickAction.ADD, - onClick, - dragAndDropState = dragAndDropState, - showLabels = true, - ) - } - } -} - -fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp { - val rows = (nTiles + columns - 1) / columns - return gridHeight(rows, tileHeight, padding) -} - -fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp { - return ((tileHeight + padding) * rows) - padding -} - -private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any { - return if (this is TileGridCell && !dragAndDropState.isMoving(tile.tileSpec)) { - key - } else { - index - } -} - -fun LazyGridScope.editTiles( - cells: List<GridCell>, - clickAction: ClickAction, - onClick: (TileSpec) -> Unit, - dragAndDropState: DragAndDropState, - onResize: (TileSpec) -> Unit = {}, - showLabels: Boolean = false, - indicatePosition: Boolean = false, -) { - items( - count = cells.size, - key = { cells[it].key(it, dragAndDropState) }, - span = { cells[it].span }, - contentType = { TileType }, - ) { index -> - when (val cell = cells[index]) { - is TileGridCell -> - if (dragAndDropState.isMoving(cell.tile.tileSpec)) { - // If the tile is being moved, replace it with a visible spacer - SpacerGridCell( - Modifier.background( - color = MaterialTheme.colorScheme.secondary, - alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA }, - shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius), - ) - .animateItem() - ) - } else { - TileGridCell( - cell = cell, - index = index, - dragAndDropState = dragAndDropState, - clickAction = clickAction, - onClick = onClick, - onResize = onResize, - showLabels = showLabels, - indicatePosition = indicatePosition, - ) - } - is SpacerGridCell -> SpacerGridCell() - } - } -} - -@Composable -private fun LazyGridItemScope.TileGridCell( - cell: TileGridCell, - index: Int, - dragAndDropState: DragAndDropState, - clickAction: ClickAction, - onClick: (TileSpec) -> Unit, - onResize: (TileSpec) -> Unit = {}, - showLabels: Boolean = false, - indicatePosition: Boolean = false, -) { - val tileHeight = tileHeight(cell.isIcon && showLabels) - val onClickActionName = - when (clickAction) { - ClickAction.ADD -> stringResource(id = R.string.accessibility_qs_edit_tile_add_action) - ClickAction.REMOVE -> - stringResource(id = R.string.accessibility_qs_edit_remove_tile_action) - } - val stateDescription = - if (indicatePosition) { - stringResource(id = R.string.accessibility_qs_edit_position, index + 1) - } else { - "" - } - EditTile( - tileViewModel = cell.tile, - iconOnly = cell.isIcon, - showLabels = showLabels, - modifier = - Modifier.height(tileHeight) - .animateItem() - .semantics(mergeDescendants = true) { - onClick(onClickActionName) { false } - this.stateDescription = stateDescription - } - .dragAndDropTileSource( - SizedTileImpl(cell.tile, cell.width), - onClick, - onResize, - dragAndDropState, - ), - ) -} - -@Composable -private fun SpacerGridCell(modifier: Modifier = Modifier) { - // By default, spacers are invisible and exist purely to catch drag movements - Box(modifier.height(tileHeight()).fillMaxWidth().tilePadding()) -} - -@Composable -fun EditTile( - tileViewModel: EditTileViewModel, - iconOnly: Boolean, - showLabels: Boolean, - modifier: Modifier = Modifier, -) { - val label = tileViewModel.label.text - val colors = TileDefaults.inactiveTileColors() - - TileContainer( - colors = colors, - showLabels = showLabels, - label = label, - iconOnly = iconOnly, - shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius), - modifier = modifier, - ) { - if (iconOnly) { - TileIcon( - icon = tileViewModel.icon, - color = colors.icon, - modifier = Modifier.align(Alignment.Center), - ) - } else { - LargeTileContent( - label = label, - secondaryLabel = tileViewModel.appName?.text, - icon = tileViewModel.icon, - colors = colors, - iconShape = RoundedCornerShape(TileDefaults.InactiveCornerRadius), - ) - } - } -} - -enum class ClickAction { - ADD, - REMOVE, -} - -@Composable -private fun getTileIcon(icon: Supplier<QSTile.Icon?>): Icon { - val context = LocalContext.current - return icon.get()?.let { - if (it is QSTileImpl.ResourceIcon) { - Icon.Resource(it.resId, null) - } else { - Icon.Loaded(it.getDrawable(context), null) - } - } ?: Icon.Resource(R.drawable.ic_error_outline, null) -} - -@OptIn(ExperimentalAnimationGraphicsApi::class) -@Composable -private fun TileIcon( - icon: Icon, - color: Color, - animateToEnd: Boolean = false, - modifier: Modifier = Modifier, -) { - val iconModifier = modifier.size(TileDefaults.IconSize) - val context = LocalContext.current - val loadedDrawable = - remember(icon, context) { - when (icon) { - is Icon.Loaded -> icon.drawable - is Icon.Resource -> context.getDrawable(icon.res) - } - } - if (loadedDrawable !is Animatable) { - Icon(icon = icon, tint = color, modifier = iconModifier) - } else if (icon is Icon.Resource) { - val image = AnimatedImageVector.animatedVectorResource(id = icon.res) - val painter = - if (animateToEnd) { - rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true) - } else { - var atEnd by remember(icon.res) { mutableStateOf(false) } - LaunchedEffect(key1 = icon.res) { - delay(350) - atEnd = true - } - rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd) - } - Image( - painter = painter, - contentDescription = icon.contentDescription?.load(), - colorFilter = ColorFilter.tint(color = color), - modifier = iconModifier, - ) - } -} - -private fun Modifier.tilePadding(): Modifier { - return padding(TileDefaults.TilePadding) -} - -private fun tileHorizontalArrangement(): Arrangement.Horizontal { - return spacedBy(space = TileDefaults.TileArrangementPadding, alignment = Alignment.Start) -} - -@Composable -fun tileHeight(iconWithLabel: Boolean = false): Dp { - return if (iconWithLabel) { - TileDefaults.IconTileWithLabelHeight - } else { - TileDefaults.TileHeight - } -} - -private data class TileColors( - val background: Color, - val iconBackground: Color, - val label: Color, - val secondaryLabel: Color, - val icon: Color, -) - -private object EditModeTileDefaults { - const val PLACEHOLDER_ALPHA = .3f - val EditGridHeaderHeight = 60.dp -} - -private object TileDefaults { - val InactiveCornerRadius = 50.dp - val ActiveIconCornerRadius = 16.dp - val ActiveTileCornerRadius = 24.dp - - val ToggleTargetSize = 56.dp - val IconSize = 24.dp - - val TilePadding = 8.dp - val TileArrangementPadding = 6.dp - - val TileHeight = 72.dp - val IconTileWithLabelHeight = 140.dp - - @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile) - - /** An active tile without dual target uses the active color as background */ - @Composable - fun activeTileColors(): TileColors = - TileColors( - background = MaterialTheme.colorScheme.primary, - iconBackground = MaterialTheme.colorScheme.primary, - label = MaterialTheme.colorScheme.onPrimary, - secondaryLabel = MaterialTheme.colorScheme.onPrimary, - icon = MaterialTheme.colorScheme.onPrimary, - ) - - /** An active tile with dual target only show the active color on the icon */ - @Composable - fun activeDualTargetTileColors(): TileColors = - TileColors( - background = MaterialTheme.colorScheme.surfaceVariant, - iconBackground = MaterialTheme.colorScheme.primary, - label = MaterialTheme.colorScheme.onSurfaceVariant, - secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant, - icon = MaterialTheme.colorScheme.onPrimary, - ) - - @Composable - fun inactiveTileColors(): TileColors = - TileColors( - background = MaterialTheme.colorScheme.surfaceVariant, - iconBackground = MaterialTheme.colorScheme.surfaceVariant, - label = MaterialTheme.colorScheme.onSurfaceVariant, - secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant, - icon = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - @Composable - fun unavailableTileColors(): TileColors = - TileColors( - background = MaterialTheme.colorScheme.surface, - iconBackground = MaterialTheme.colorScheme.surface, - label = MaterialTheme.colorScheme.onSurface, - secondaryLabel = MaterialTheme.colorScheme.onSurface, - icon = MaterialTheme.colorScheme.onSurface, - ) - - @Composable - fun getColorForState(uiState: TileUiState): TileColors { - return when (uiState.state) { - STATE_ACTIVE -> { - if (uiState.handlesSecondaryClick) { - activeDualTargetTileColors() - } else { - activeTileColors() - } - } - STATE_INACTIVE -> inactiveTileColors() - else -> unavailableTileColors() - } - } - - @Composable - fun animateIconShape(state: Int): Shape { - return animateShape( - state = state, - activeCornerRadius = ActiveIconCornerRadius, - label = "QSTileCornerRadius", - ) - } - - @Composable - fun animateTileShape(state: Int): Shape { - return animateShape( - state = state, - activeCornerRadius = ActiveTileCornerRadius, - label = "QSTileIconCornerRadius", - ) - } - - @Composable - fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape { - val animatedCornerRadius by - animateDpAsState( - targetValue = - if (state == STATE_ACTIVE) { - activeCornerRadius - } else { - InactiveCornerRadius - }, - label = label, - ) - return RoundedCornerShape(animatedCornerRadius) - } -} - -private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid" -private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid" - -/** - * A composable function that returns the [Resources]. It will be recomposed when [Configuration] - * gets updated. - */ -@Composable -@ReadOnlyComposable -private fun resources(): Resources { - LocalConfiguration.current - return LocalContext.current.resources -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt new file mode 100644 index 000000000000..aeb6031d7b72 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -0,0 +1,204 @@ +/* + * 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.qs.panels.ui.compose.infinitegrid + +import android.graphics.drawable.Animatable +import android.text.TextUtils +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.Image +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.background +import com.android.compose.modifiers.thenIf +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.common.ui.compose.load +import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel +import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState +import com.android.systemui.res.R +import kotlinx.coroutines.delay + +private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target" + +@Composable +fun LargeTileContent( + label: String, + secondaryLabel: String?, + icon: Icon, + colors: TileColors, + accessibilityUiState: AccessibilityUiState? = null, + toggleClickSupported: Boolean = false, + iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), + onClick: () -> Unit = {}, + onLongClick: () -> Unit = {}, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = tileHorizontalArrangement(), + ) { + // Icon + val longPressLabel = longPressLabel() + Box( + modifier = + Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) { + Modifier.clip(iconShape) + .background(colors.iconBackground, { 1f }) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = longPressLabel, + ) + .thenIf(accessibilityUiState != null) { + Modifier.semantics { + accessibilityUiState as AccessibilityUiState + contentDescription = accessibilityUiState.contentDescription + stateDescription = accessibilityUiState.stateDescription + accessibilityUiState.toggleableState?.let { + toggleableState = it + } + role = Role.Switch + } + .sysuiResTag(TEST_TAG_TOGGLE) + } + } + ) { + SmallTileContent( + icon = icon, + color = colors.icon, + modifier = Modifier.align(Alignment.Center), + ) + } + + // Labels + LargeTileLabels( + label = label, + secondaryLabel = secondaryLabel, + colors = colors, + accessibilityUiState = accessibilityUiState, + ) + } +} + +@Composable +private fun LargeTileLabels( + label: String, + secondaryLabel: String?, + colors: TileColors, + accessibilityUiState: AccessibilityUiState? = null, +) { + Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) { + Text(label, color = colors.label, modifier = Modifier.tileMarquee()) + if (!TextUtils.isEmpty(secondaryLabel)) { + Text( + secondaryLabel ?: "", + color = colors.secondaryLabel, + modifier = + Modifier.tileMarquee().thenIf( + accessibilityUiState?.stateDescription?.contains(secondaryLabel ?: "") == + true + ) { + Modifier.clearAndSetSemantics {} + }, + ) + } + } +} + +@OptIn(ExperimentalAnimationGraphicsApi::class) +@Composable +fun SmallTileContent( + modifier: Modifier = Modifier, + icon: Icon, + color: Color, + animateToEnd: Boolean = false, +) { + val iconModifier = modifier.size(CommonTileDefaults.IconSize) + val context = LocalContext.current + val loadedDrawable = + remember(icon, context) { + when (icon) { + is Icon.Loaded -> icon.drawable + is Icon.Resource -> context.getDrawable(icon.res) + } + } + if (loadedDrawable !is Animatable) { + Icon(icon = icon, tint = color, modifier = iconModifier) + } else if (icon is Icon.Resource) { + val image = AnimatedImageVector.animatedVectorResource(id = icon.res) + val painter = + if (animateToEnd) { + rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true) + } else { + var atEnd by remember(icon.res) { mutableStateOf(false) } + LaunchedEffect(key1 = icon.res) { + delay(350) + atEnd = true + } + rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd) + } + Image( + painter = painter, + contentDescription = icon.contentDescription?.load(), + colorFilter = ColorFilter.tint(color = color), + modifier = iconModifier, + ) + } +} + +object CommonTileDefaults { + val IconSize = 24.dp + val ToggleTargetSize = 56.dp + val TileHeight = 72.dp + val TilePadding = 8.dp + val TileArrangementPadding = 6.dp + val InactiveCornerRadius = 50.dp + + @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt new file mode 100644 index 000000000000..a43b8807b211 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -0,0 +1,503 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalFoundationApi::class) + +package com.android.systemui.qs.panels.ui.compose.infinitegrid + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridItemScope +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastMap +import com.android.compose.modifiers.background +import com.android.systemui.common.ui.compose.load +import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.SizedTileImpl +import com.android.systemui.qs.panels.ui.compose.DragAndDropState +import com.android.systemui.qs.panels.ui.compose.EditTileListState +import com.android.systemui.qs.panels.ui.compose.dragAndDropRemoveZone +import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList +import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius +import com.android.systemui.qs.panels.ui.model.GridCell +import com.android.systemui.qs.panels.ui.model.SpacerGridCell +import com.android.systemui.qs.panels.ui.model.TileGridCell +import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.shared.model.groupAndSort +import com.android.systemui.res.R + +object TileType + +@Composable +fun DefaultEditTileGrid( + currentListState: EditTileListState, + otherTiles: List<SizedTile<EditTileViewModel>>, + columns: Int, + modifier: Modifier, + onAddTile: (TileSpec, Int) -> Unit, + onRemoveTile: (TileSpec) -> Unit, + onSetTiles: (List<TileSpec>) -> Unit, + onResize: (TileSpec) -> Unit, +) { + val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState { + onAddTile(it, CurrentTilesInteractor.POSITION_AT_END) + } + + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + Column( + verticalArrangement = + spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), + modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()), + ) { + AnimatedContent( + targetState = currentListState.dragInProgress, + modifier = Modifier.wrapContentSize(), + label = "", + ) { dragIsInProgress -> + EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) { + if (dragIsInProgress) { + RemoveTileTarget() + } else { + Text(text = "Hold and drag to rearrange tiles.") + } + } + } + + CurrentTilesGrid(currentListState, columns, onRemoveTile, onResize, onSetTiles) + + // Hide available tiles when dragging + AnimatedVisibility( + visible = !currentListState.dragInProgress, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column( + verticalArrangement = + spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), + modifier = modifier.fillMaxSize(), + ) { + EditGridHeader { Text(text = "Hold and drag to add tiles.") } + + AvailableTileGrid(otherTiles, columns, addTileToEnd, currentListState) + } + } + + // Drop zone to remove tiles dragged out of the tile grid + Spacer( + modifier = + Modifier.fillMaxWidth() + .weight(1f) + .dragAndDropRemoveZone(currentListState, onRemoveTile) + ) + } + } +} + +@Composable +private fun EditGridHeader( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight), + ) { + content() + } + } +} + +@Composable +private fun RemoveTileTarget() { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = tileHorizontalArrangement(), + modifier = + Modifier.fillMaxHeight() + .border(1.dp, LocalContentColor.current, shape = CircleShape) + .padding(10.dp), + ) { + Icon(imageVector = Icons.Default.Clear, contentDescription = null) + Text(text = "Remove") + } +} + +@Composable +private fun CurrentTilesContainer(content: @Composable () -> Unit) { + Box( + Modifier.fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f), + shape = RoundedCornerShape(48.dp), + ) + .padding(dimensionResource(R.dimen.qs_tile_margin_vertical)) + ) { + content() + } +} + +@Composable +private fun CurrentTilesGrid( + listState: EditTileListState, + columns: Int, + onClick: (TileSpec) -> Unit, + onResize: (TileSpec) -> Unit, + onSetTiles: (List<TileSpec>) -> Unit, +) { + val currentListState by rememberUpdatedState(listState) + val tilePadding = CommonTileDefaults.TileArrangementPadding + + CurrentTilesContainer { + val tileHeight = CommonTileDefaults.TileHeight + val totalRows = listState.tiles.lastOrNull()?.row ?: 0 + val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding) + val gridState = rememberLazyGridState() + var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) } + + TileLazyGrid( + state = gridState, + modifier = + Modifier.height(totalHeight) + .dragAndDropTileList(gridState, gridContentOffset, listState) { + onSetTiles(currentListState.tileSpecs()) + } + .onGloballyPositioned { coordinates -> + gridContentOffset = coordinates.positionInRoot() + } + .testTag(CURRENT_TILES_GRID_TEST_TAG), + columns = GridCells.Fixed(columns), + ) { + EditTiles(listState.tiles, onClick, listState, onResize = onResize) + } + } +} + +@Composable +private fun AvailableTileGrid( + tiles: List<SizedTile<EditTileViewModel>>, + columns: Int, + onClick: (TileSpec) -> Unit, + dragAndDropState: DragAndDropState, +) { + // Available tiles aren't visible during drag and drop, so the row isn't needed + val groupedTiles = + remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) { + groupAndSort(tiles.fastMap { TileGridCell(it, 0) }) + } + val labelColors = EditModeTileDefaults.editTileColors() + + // Available tiles + Column( + verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding), + horizontalAlignment = Alignment.Start, + modifier = + Modifier.fillMaxWidth().wrapContentHeight().testTag(AVAILABLE_TILES_GRID_TEST_TAG), + ) { + groupedTiles.forEach { (category, tiles) -> + Text( + text = category.label.load() ?: "", + fontSize = 20.sp, + color = labelColors.label, + modifier = + Modifier.fillMaxWidth() + .background(Color.Black) + .padding(start = 16.dp, bottom = 8.dp, top = 8.dp), + ) + tiles.chunked(columns).forEach { row -> + Row( + horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding), + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max), + ) { + row.forEachIndexed { index, tileGridCell -> + AvailableTileGridCell( + cell = tileGridCell, + index = index, + dragAndDropState = dragAndDropState, + onClick = onClick, + modifier = Modifier.weight(1f).fillMaxHeight(), + ) + } + + // Spacers for incomplete rows + repeat(columns - row.size) { Spacer(modifier = Modifier.weight(1f)) } + } + } + } + } +} + +fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp { + return ((tileHeight + padding) * rows) - padding +} + +private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any { + return when (this) { + is TileGridCell -> { + if (dragAndDropState.isMoving(tile.tileSpec)) index else key + } + is SpacerGridCell -> index + } +} + +fun LazyGridScope.EditTiles( + cells: List<GridCell>, + onClick: (TileSpec) -> Unit, + dragAndDropState: DragAndDropState, + onResize: (TileSpec) -> Unit = {}, +) { + items( + count = cells.size, + key = { cells[it].key(it, dragAndDropState) }, + span = { cells[it].span }, + contentType = { TileType }, + ) { index -> + when (val cell = cells[index]) { + is TileGridCell -> + if (dragAndDropState.isMoving(cell.tile.tileSpec)) { + // If the tile is being moved, replace it with a visible spacer + SpacerGridCell( + Modifier.background( + color = MaterialTheme.colorScheme.secondary, + alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA }, + shape = RoundedCornerShape(InactiveCornerRadius), + ) + .animateItem() + ) + } else { + TileGridCell( + cell = cell, + index = index, + dragAndDropState = dragAndDropState, + onClick = onClick, + onResize = onResize, + ) + } + is SpacerGridCell -> SpacerGridCell() + } + } +} + +@Composable +private fun LazyGridItemScope.TileGridCell( + cell: TileGridCell, + index: Int, + dragAndDropState: DragAndDropState, + onClick: (TileSpec) -> Unit, + onResize: (TileSpec) -> Unit = {}, +) { + val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_remove_tile_action) + val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) + + EditTile( + tileViewModel = cell.tile, + iconOnly = cell.isIcon, + modifier = + Modifier.animateItem() + .semantics(mergeDescendants = true) { + onClick(onClickActionName) { false } + this.stateDescription = stateDescription + } + .dragAndDropTileSource( + SizedTileImpl(cell.tile, cell.width), + dragAndDropState, + onClick, + onResize, + ), + ) +} + +@Composable +private fun AvailableTileGridCell( + cell: TileGridCell, + index: Int, + dragAndDropState: DragAndDropState, + modifier: Modifier = Modifier, + onClick: (TileSpec) -> Unit, +) { + val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action) + val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) + val colors = EditModeTileDefaults.editTileColors() + + // Displays the tile as an icon tile with the label underneath + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top), + modifier = modifier, + ) { + EditTile( + tileViewModel = cell.tile, + iconOnly = true, + colors = colors, + modifier = + Modifier.semantics(mergeDescendants = true) { + onClick(onClickActionName) { false } + this.stateDescription = stateDescription + } + .dragAndDropTileSource( + SizedTileImpl(cell.tile, cell.width), + dragAndDropState, + onTap = onClick, + ), + ) + Box(Modifier.fillMaxSize()) { + Text( + cell.tile.label.text, + maxLines = 2, + color = colors.label, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.Center), + ) + } + } +} + +@Composable +private fun SpacerGridCell(modifier: Modifier = Modifier) { + // By default, spacers are invisible and exist purely to catch drag movements + Box(modifier.height(CommonTileDefaults.TileHeight).fillMaxWidth().tilePadding()) +} + +@Composable +fun EditTile( + tileViewModel: EditTileViewModel, + iconOnly: Boolean, + modifier: Modifier = Modifier, + colors: TileColors = EditModeTileDefaults.editTileColors(), +) { + EditTileContainer(colors = colors, modifier = modifier) { + if (iconOnly) { + SmallTileContent( + icon = tileViewModel.icon, + color = colors.icon, + modifier = Modifier.align(Alignment.Center), + ) + } else { + LargeTileContent( + label = tileViewModel.label.text, + secondaryLabel = tileViewModel.appName?.text, + icon = tileViewModel.icon, + colors = colors, + ) + } + } +} + +@Composable +private fun EditTileContainer( + colors: TileColors, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + Box( + modifier = + modifier + .height(CommonTileDefaults.TileHeight) + .fillMaxWidth() + .drawBehind { + drawRoundRect( + SolidColor(colors.background), + cornerRadius = CornerRadius(InactiveCornerRadius.toPx()), + ) + } + .tilePadding(), + content = content, + ) +} + +private object EditModeTileDefaults { + const val PLACEHOLDER_ALPHA = .3f + val EditGridHeaderHeight = 60.dp + + @Composable + fun editTileColors(): TileColors = + TileColors( + background = MaterialTheme.colorScheme.surfaceVariant, + iconBackground = MaterialTheme.colorScheme.surfaceVariant, + label = MaterialTheme.colorScheme.onSurfaceVariant, + secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant, + icon = MaterialTheme.colorScheme.onSurfaceVariant, + ) +} + +private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid" +private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid" diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index c75b601ab4a8..f96c27dc3ef6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.panels.ui.compose +package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -26,6 +26,8 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.shared.model.SizedTileImpl +import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout +import com.android.systemui.qs.panels.ui.compose.rememberEditListState import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel @@ -61,7 +63,7 @@ constructor( Tile( tile = sizedTiles[index].tile, iconOnly = iconTilesViewModel.isIconTile(sizedTiles[index].tile.spec), - modifier = Modifier + modifier = Modifier, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt new file mode 100644 index 000000000000..aa6c08eecd76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -0,0 +1,329 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalFoundationApi::class) + +package com.android.systemui.qs.panels.ui.compose.infinitegrid + +import android.content.res.Resources +import android.service.quicksettings.Tile.STATE_ACTIVE +import android.service.quicksettings.Tile.STATE_INACTIVE +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.Expandable +import com.android.compose.modifiers.thenIf +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel +import com.android.systemui.qs.panels.ui.viewmodel.TileUiState +import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel +import com.android.systemui.qs.panels.ui.viewmodel.toUiState +import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.res.R +import java.util.function.Supplier + +private const val TEST_TAG_SMALL = "qs_tile_small" +private const val TEST_TAG_LARGE = "qs_tile_large" + +@Composable +fun TileLazyGrid( + columns: GridCells, + modifier: Modifier = Modifier, + state: LazyGridState = rememberLazyGridState(), + content: LazyGridScope.() -> Unit, +) { + LazyVerticalGrid( + state = state, + columns = columns, + verticalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding), + horizontalArrangement = spacedBy(CommonTileDefaults.TileArrangementPadding), + modifier = modifier, + content = content, + ) +} + +@Composable +fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) { + val state by tile.state.collectAsStateWithLifecycle(tile.currentState) + val resources = resources() + val uiState = remember(state, resources) { state.toUiState(resources) } + val colors = TileDefaults.getColorForState(uiState) + + // TODO(b/361789146): Draw the shapes instead of clipping + val tileShape = TileDefaults.animateTileShape(uiState.state) + + TileContainer( + color = + if (iconOnly || !uiState.handlesSecondaryClick) { + colors.iconBackground + } else { + colors.background + }, + shape = tileShape, + iconOnly = iconOnly, + onClick = tile::onClick, + onLongClick = tile::onLongClick, + uiState = uiState, + modifier = modifier, + ) { expandable -> + val icon = getTileIcon(icon = uiState.icon) + if (iconOnly) { + SmallTileContent( + icon = icon, + color = colors.icon, + modifier = Modifier.align(Alignment.Center), + ) + } else { + val iconShape = TileDefaults.animateIconShape(uiState.state) + LargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + icon = icon, + colors = colors, + iconShape = iconShape, + toggleClickSupported = state.handlesSecondaryClick, + onClick = { + if (state.handlesSecondaryClick) { + tile.onSecondaryClick() + } + }, + onLongClick = { tile.onLongClick(expandable) }, + ) + } + } +} + +@Composable +private fun TileContainer( + color: Color, + shape: Shape, + iconOnly: Boolean, + uiState: TileUiState, + modifier: Modifier = Modifier, + onClick: (Expandable) -> Unit = {}, + onLongClick: (Expandable) -> Unit = {}, + content: @Composable BoxScope.(Expandable) -> Unit, +) { + Expandable(color = color, shape = shape, modifier = modifier.clip(shape)) { + val longPressLabel = longPressLabel() + Box( + modifier = + Modifier.height(CommonTileDefaults.TileHeight) + .fillMaxWidth() + .combinedClickable( + onClick = { onClick(it) }, + onLongClick = { onLongClick(it) }, + onClickLabel = uiState.accessibilityUiState.clickLabel, + onLongClickLabel = longPressLabel, + ) + .semantics { + role = uiState.accessibilityUiState.accessibilityRole + if (uiState.accessibilityUiState.accessibilityRole == Role.Switch) { + uiState.accessibilityUiState.toggleableState?.let { + toggleableState = it + } + } + stateDescription = uiState.accessibilityUiState.stateDescription + } + .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE) + .thenIf(iconOnly) { + Modifier.semantics { + contentDescription = uiState.accessibilityUiState.contentDescription + } + } + .tilePadding() + ) { + content(it) + } + } +} + +@Composable +private fun getTileIcon(icon: Supplier<QSTile.Icon?>): Icon { + val context = LocalContext.current + return icon.get()?.let { + if (it is QSTileImpl.ResourceIcon) { + Icon.Resource(it.resId, null) + } else { + Icon.Loaded(it.getDrawable(context), null) + } + } ?: Icon.Resource(R.drawable.ic_error_outline, null) +} + +fun tileHorizontalArrangement(): Arrangement.Horizontal { + return spacedBy(space = CommonTileDefaults.TileArrangementPadding, alignment = Alignment.Start) +} + +fun Modifier.tileMarquee(): Modifier { + return basicMarquee(iterations = 1, initialDelayMillis = 200) +} + +fun Modifier.tilePadding(): Modifier { + return padding(CommonTileDefaults.TilePadding) +} + +data class TileColors( + val background: Color, + val iconBackground: Color, + val label: Color, + val secondaryLabel: Color, + val icon: Color, +) + +private object TileDefaults { + val ActiveIconCornerRadius = 16.dp + val ActiveTileCornerRadius = 24.dp + + /** An active tile without dual target uses the active color as background */ + @Composable + fun activeTileColors(): TileColors = + TileColors( + background = MaterialTheme.colorScheme.primary, + iconBackground = MaterialTheme.colorScheme.primary, + label = MaterialTheme.colorScheme.onPrimary, + secondaryLabel = MaterialTheme.colorScheme.onPrimary, + icon = MaterialTheme.colorScheme.onPrimary, + ) + + /** An active tile with dual target only show the active color on the icon */ + @Composable + fun activeDualTargetTileColors(): TileColors = + TileColors( + background = MaterialTheme.colorScheme.surfaceVariant, + iconBackground = MaterialTheme.colorScheme.primary, + label = MaterialTheme.colorScheme.onSurfaceVariant, + secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant, + icon = MaterialTheme.colorScheme.onPrimary, + ) + + @Composable + fun inactiveTileColors(): TileColors = + TileColors( + background = MaterialTheme.colorScheme.surfaceVariant, + iconBackground = MaterialTheme.colorScheme.surfaceVariant, + label = MaterialTheme.colorScheme.onSurfaceVariant, + secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant, + icon = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + @Composable + fun unavailableTileColors(): TileColors = + TileColors( + background = MaterialTheme.colorScheme.surface, + iconBackground = MaterialTheme.colorScheme.surface, + label = MaterialTheme.colorScheme.onSurface, + secondaryLabel = MaterialTheme.colorScheme.onSurface, + icon = MaterialTheme.colorScheme.onSurface, + ) + + @Composable + fun getColorForState(uiState: TileUiState): TileColors { + return when (uiState.state) { + STATE_ACTIVE -> { + if (uiState.handlesSecondaryClick) { + activeDualTargetTileColors() + } else { + activeTileColors() + } + } + STATE_INACTIVE -> inactiveTileColors() + else -> unavailableTileColors() + } + } + + @Composable + fun animateIconShape(state: Int): Shape { + return animateShape( + state = state, + activeCornerRadius = ActiveIconCornerRadius, + label = "QSTileCornerRadius", + ) + } + + @Composable + fun animateTileShape(state: Int): Shape { + return animateShape( + state = state, + activeCornerRadius = ActiveTileCornerRadius, + label = "QSTileIconCornerRadius", + ) + } + + @Composable + fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape { + val animatedCornerRadius by + animateDpAsState( + targetValue = + if (state == STATE_ACTIVE) { + activeCornerRadius + } else { + InactiveCornerRadius + }, + label = label, + ) + return RoundedCornerShape(animatedCornerRadius) + } +} + +/** + * A composable function that returns the [Resources]. It will be recomposed when [Configuration] + * gets updated. + */ +@Composable +@ReadOnlyComposable +private fun resources(): Resources { + LocalConfiguration.current + return LocalContext.current.resources +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt index 08ee856a0ec6..b16a7075607f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt @@ -24,7 +24,7 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.shared.model.CategoryAndName /** Represents an item from a grid associated with a row and a span */ -interface GridCell { +sealed interface GridCell { val row: Int val span: GridItemSpan } @@ -38,30 +38,26 @@ data class TileGridCell( override val tile: EditTileViewModel, override val row: Int, override val width: Int, - override val span: GridItemSpan = GridItemSpan(width) + override val span: GridItemSpan = GridItemSpan(width), ) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile { val key: String = "${tile.tileSpec.spec}-$row" constructor( sizedTile: SizedTile<EditTileViewModel>, - row: Int - ) : this( - tile = sizedTile.tile, - row = row, - width = sizedTile.width, - ) + row: Int, + ) : this(tile = sizedTile.tile, row = row, width = sizedTile.width) } /** Represents an empty space used to fill incomplete rows. Will always display as a 1x1 tile */ @Immutable data class SpacerGridCell( override val row: Int, - override val span: GridItemSpan = GridItemSpan(1) + override val span: GridItemSpan = GridItemSpan(1), ) : GridCell fun List<SizedTile<EditTileViewModel>>.toGridCells( columns: Int, - includeSpacers: Boolean = false + includeSpacers: Boolean = false, ): List<GridCell> { return splitInRowsSequence(this, columns) .flatMapIndexed { rowIndex, sizedTiles -> diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt index 0bcb6b7e7874..9677d47b38e8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt @@ -18,8 +18,6 @@ package com.android.systemui.qs.pipeline.domain.startable import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.qs.flags.NewQsUI -import com.android.systemui.qs.panels.domain.interactor.GridConsistencyInteractor import com.android.systemui.qs.pipeline.domain.interactor.AccessibilityTilesInteractor import com.android.systemui.qs.pipeline.domain.interactor.AutoAddInteractor import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor @@ -36,16 +34,11 @@ constructor( private val autoAddInteractor: AutoAddInteractor, private val featureFlags: QSPipelineFlagsRepository, private val restoreReconciliationInteractor: RestoreReconciliationInteractor, - private val gridConsistencyInteractor: GridConsistencyInteractor, ) : CoreStartable { override fun start() { accessibilityTilesInteractor.init(currentTilesInteractor) autoAddInteractor.init(currentTilesInteractor) restoreReconciliationInteractor.start() - - if (NewQsUI.isEnabled) { - gridConsistencyInteractor.start() - } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index e11ffccb0be3..b7e2cf23e3a8 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -61,6 +61,7 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.session.shared.SessionStorage import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.logger.SceneLogger +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.NotificationShadeWindowController @@ -228,8 +229,10 @@ constructor( is ObservableTransitionState.Idle -> { if (state.currentScene != Scenes.Gone) { true to "scene is not Gone" + } else if (state.currentOverlays.isNotEmpty()) { + true to "overlay is shown" } else { - false to "scene is Gone" + false to "scene is Gone and no overlays are shown" } } is ObservableTransitionState.Transition -> { @@ -712,19 +715,21 @@ constructor( if (isDeviceLocked) { sceneInteractor.transitionState .mapNotNull { it as? ObservableTransitionState.Idle } - .map { it.currentScene } + .map { it.currentScene to it.currentOverlays } .distinctUntilChanged() - .map { sceneKey -> - when (sceneKey) { + .map { (sceneKey, currentOverlays) -> + when { // When locked, showing the lockscreen scene should be reported // as "interacting" while showing other scenes should report as // "not interacting". // // This is done here in order to match the legacy // implementation. The real reason why is lost to lore and myth. - Scenes.Lockscreen -> true - Scenes.Bouncer -> false - Scenes.Shade -> false + Overlays.NotificationsShade in currentOverlays -> false + Overlays.QuickSettingsShade in currentOverlays -> null + sceneKey == Scenes.Lockscreen -> true + sceneKey == Scenes.Bouncer -> false + sceneKey == Scenes.Shade -> false else -> null } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt index 751448fe607e..7b6b0f614cc2 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt @@ -26,7 +26,6 @@ import com.android.systemui.flags.RefactorFlagUtils import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.MigrateClocksToBlueprint -import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.phone.PredictiveBackSysUiFlag @@ -39,7 +38,6 @@ object SceneContainerFlag { inline val isEnabled get() = sceneContainer() && // mainAconfigFlag - ComposeLockscreen.isEnabled && KeyguardBottomAreaRefactor.isEnabled && KeyguardWmStateRefactor.isEnabled && MigrateClocksToBlueprint.isEnabled && @@ -55,7 +53,6 @@ object SceneContainerFlag { /** The set of secondary flags which must be enabled for scene container to work properly */ inline fun getSecondaryFlags(): Sequence<FlagToken> = sequenceOf( - ComposeLockscreen.token, KeyguardBottomAreaRefactor.token, KeyguardWmStateRefactor.token, MigrateClocksToBlueprint.token, diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 42499f043457..f76c5fd4ca83 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -137,7 +137,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver; -import com.android.systemui.keyguard.shared.ComposeLockscreen; import com.android.systemui.keyguard.shared.model.Edge; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -186,6 +185,7 @@ import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.ConversationNotificationManager; import com.android.systemui.statusbar.notification.DynamicPrivacyController; +import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.ViewGroupFadeHelper; @@ -207,7 +207,6 @@ import com.android.systemui.statusbar.phone.BounceInterpolator; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; -import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.KeyguardBottomAreaView; import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -2511,11 +2510,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return 0; } - if (ComposeLockscreen.isEnabled()) { - return (int) mKeyguardInteractor.getNotificationContainerBounds() - .getValue().getTop(); - } - if (!mKeyguardBypassController.getBypassEnabled()) { if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) { return (int) mKeyguardInteractor.getNotificationContainerBounds() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt index 4056e7b89c2c..c6c303ebb0d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt @@ -16,9 +16,11 @@ package com.android.systemui.statusbar.core import android.app.Fragment -import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.fragments.FragmentHostManager +import com.android.systemui.res.R +import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener +import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewUpdatedListener import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions import com.android.systemui.statusbar.phone.PhoneStatusBarView import com.android.systemui.statusbar.phone.PhoneStatusBarViewController @@ -33,50 +35,16 @@ import javax.inject.Provider * Responsible for creating the status bar window and initializing the root components of that * window (see [CollapsedStatusBarFragment]) */ -@SysUISingleton -class StatusBarInitializer @Inject constructor( - private val windowController: StatusBarWindowController, - private val collapsedStatusBarFragmentProvider: Provider<CollapsedStatusBarFragment>, - private val creationListeners: Set<@JvmSuppressWildcards OnStatusBarViewInitializedListener>, -) { +interface StatusBarInitializer { - var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null + var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? /** * Creates the status bar window and root views, and initializes the component. * * TODO(b/277764509): Initialize the status bar via [CoreStartable#start]. */ - fun initializeStatusBar() { - windowController.fragmentHostManager.addTagListener( - CollapsedStatusBarFragment.TAG, - object : FragmentHostManager.FragmentListener { - override fun onFragmentViewCreated(tag: String, fragment: Fragment) { - val statusBarFragmentComponent = (fragment as CollapsedStatusBarFragment) - .statusBarFragmentComponent ?: throw IllegalStateException() - statusBarViewUpdatedListener?.onStatusBarViewUpdated( - statusBarFragmentComponent.phoneStatusBarView, - statusBarFragmentComponent.phoneStatusBarViewController, - statusBarFragmentComponent.phoneStatusBarTransitions - ) - creationListeners.forEach { listener -> - listener.onStatusBarViewInitialized(statusBarFragmentComponent) - } - } - - override fun onFragmentViewDestroyed(tag: String?, fragment: Fragment?) { - // nop - } - } - ).fragmentManager - .beginTransaction() - .replace( - R.id.status_bar_container, - collapsedStatusBarFragmentProvider.get(), - CollapsedStatusBarFragment.TAG - ) - .commit() - } + fun initializeStatusBar() interface OnStatusBarViewInitializedListener { @@ -84,7 +52,8 @@ class StatusBarInitializer @Inject constructor( * The status bar view has been initialized. * * @param component Dagger component that is created when the status bar view is created. - * Can be used to retrieve dependencies from that scope, including the status bar root view. + * Can be used to retrieve dependencies from that scope, including the status bar root + * view. */ fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) } @@ -93,7 +62,53 @@ class StatusBarInitializer @Inject constructor( fun onStatusBarViewUpdated( statusBarView: PhoneStatusBarView, statusBarViewController: PhoneStatusBarViewController, - statusBarTransitions: PhoneStatusBarTransitions + statusBarTransitions: PhoneStatusBarTransitions, ) } } + +@SysUISingleton +class StatusBarInitializerImpl +@Inject +constructor( + private val windowController: StatusBarWindowController, + private val collapsedStatusBarFragmentProvider: Provider<CollapsedStatusBarFragment>, + private val creationListeners: Set<@JvmSuppressWildcards OnStatusBarViewInitializedListener>, +) : StatusBarInitializer { + + override var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null + + override fun initializeStatusBar() { + windowController.fragmentHostManager + .addTagListener( + CollapsedStatusBarFragment.TAG, + object : FragmentHostManager.FragmentListener { + override fun onFragmentViewCreated(tag: String, fragment: Fragment) { + val statusBarFragmentComponent = + (fragment as CollapsedStatusBarFragment).statusBarFragmentComponent + ?: throw IllegalStateException() + statusBarViewUpdatedListener?.onStatusBarViewUpdated( + statusBarFragmentComponent.phoneStatusBarView, + statusBarFragmentComponent.phoneStatusBarViewController, + statusBarFragmentComponent.phoneStatusBarTransitions, + ) + creationListeners.forEach { listener -> + listener.onStatusBarViewInitialized(statusBarFragmentComponent) + } + } + + override fun onFragmentViewDestroyed(tag: String?, fragment: Fragment?) { + // nop + } + }, + ) + .fragmentManager + .beginTransaction() + .replace( + R.id.status_bar_container, + collapsedStatusBarFragmentProvider.get(), + CollapsedStatusBarFragment.TAG, + ) + .commit() + } +} 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 406a66449f82..526c64c15696 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -20,12 +20,16 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory +import com.android.systemui.statusbar.core.StatusBarInitializer +import com.android.systemui.statusbar.core.StatusBarInitializerImpl import com.android.systemui.statusbar.data.StatusBarDataLayerModule import com.android.systemui.statusbar.phone.LightBarController import com.android.systemui.statusbar.phone.StatusBarSignalPolicy import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.systemui.statusbar.window.StatusBarWindowControllerImpl import dagger.Binds import dagger.Module import dagger.Provides @@ -57,6 +61,13 @@ abstract class StatusBarModule { @ClassKey(StatusBarSignalPolicy::class) abstract fun bindStatusBarSignalPolicy(impl: StatusBarSignalPolicy): CoreStartable + @Binds abstract fun statusBarInitializer(impl: StatusBarInitializerImpl): StatusBarInitializer + + @Binds + abstract fun statusBarWindowController( + impl: StatusBarWindowControllerImpl + ): StatusBarWindowController + companion object { @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt index c0302bc348b6..9af4b8c18c86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt @@ -25,6 +25,7 @@ import dagger.Module import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow /** * Repository used for tracking the state of notification remote input (e.g. when the user presses @@ -33,14 +34,21 @@ import kotlinx.coroutines.flow.Flow interface RemoteInputRepository { /** Whether remote input is currently active for any notification. */ val isRemoteInputActive: Flow<Boolean> + + /** + * The bottom bound of the currently focused remote input notification row, or null if there + * isn't one. + */ + val remoteInputRowBottomBound: Flow<Float?> + + fun setRemoteInputRowBottomBound(bottom: Float?) } @SysUISingleton class RemoteInputRepositoryImpl @Inject -constructor( - private val notificationRemoteInputManager: NotificationRemoteInputManager, -) : RemoteInputRepository { +constructor(private val notificationRemoteInputManager: NotificationRemoteInputManager) : + RemoteInputRepository { override val isRemoteInputActive: Flow<Boolean> = conflatedCallbackFlow { trySend(false) // initial value is false val callback = @@ -52,6 +60,12 @@ constructor( notificationRemoteInputManager.addControllerCallback(callback) awaitClose { notificationRemoteInputManager.removeControllerCallback(callback) } } + + override val remoteInputRowBottomBound = MutableStateFlow<Float?>(null) + + override fun setRemoteInputRowBottomBound(bottom: Float?) { + remoteInputRowBottomBound.value = bottom + } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt index 68f727b046c0..b83b0cc8d2c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt @@ -20,13 +20,24 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.data.repository.RemoteInputRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapNotNull /** * Interactor used for business logic pertaining to the notification remote input (e.g. when the * user presses "reply" on a notification and the keyboard opens). */ @SysUISingleton -class RemoteInputInteractor @Inject constructor(remoteInputRepository: RemoteInputRepository) { +class RemoteInputInteractor +@Inject +constructor(private val remoteInputRepository: RemoteInputRepository) { /** Is remote input currently active for a notification? */ val isRemoteInputActive: Flow<Boolean> = remoteInputRepository.isRemoteInputActive + + /** The bottom bound of the currently focused remote input notification row. */ + val remoteInputRowBottomBound: Flow<Float> = + remoteInputRepository.remoteInputRowBottomBound.mapNotNull { it } + + fun setRemoteInputRowBottomBound(bottom: Float?) { + remoteInputRepository.setRemoteInputRowBottomBound(bottom) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index cb3e26b9f8ea..5003a6af5c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -21,6 +21,7 @@ import static android.service.notification.NotificationListenerService.REASON_CA import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; +import static com.android.systemui.statusbar.policy.RemoteInputView.FOCUS_ANIMATION_MIN_SCALE; import static com.android.systemui.util.ColorUtilKt.hexColorString; import android.animation.Animator; @@ -83,6 +84,7 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarIconView; @@ -118,6 +120,7 @@ import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; +import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.statusbar.policy.SmartReplyConstants; import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent; import com.android.systemui.util.Compile; @@ -830,6 +833,20 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPrivateLayout.setRemoteInputController(r); } + /** + * Return the cumulative y-value that the actions container expands via its scale animator when + * remote input is activated. + */ + public float getRemoteInputActionsContainerExpandedOffset() { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f; + RemoteInputView expandedRemoteInput = mPrivateLayout.getExpandedRemoteInput(); + if (expandedRemoteInput == null) return 0f; + View actionsContainerLayout = expandedRemoteInput.getActionsContainerLayout(); + if (actionsContainerLayout == null) return 0f; + + return actionsContainerLayout.getHeight() * (1 - FOCUS_ANIMATION_MIN_SCALE) * 0.5f; + } + public void addChildNotification(ExpandableNotificationRow row) { addChildNotification(row, -1); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 7543f3b48e48..e7c67f93eb78 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -99,6 +99,7 @@ import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.FakeShadowView; +import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; @@ -120,7 +121,6 @@ import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrim import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape; import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; -import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.ScrollAdapter; @@ -740,6 +740,15 @@ public class NotificationStackScrollLayout updateFooter(); } + void sendRemoteInputRowBottomBound(Float bottom) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + if (bottom != null) { + bottom += getResources().getDimensionPixelSize( + com.android.internal.R.dimen.notification_content_margin); + } + mScrollViewFields.sendRemoteInputRowBottomBound(bottom); + } + /** Setter for filtered notifs, to be removed with the FooterViewRefactor flag. */ public void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) { FooterViewRefactor.assertInLegacyMode(); @@ -1274,6 +1283,11 @@ public class NotificationStackScrollLayout } @Override + public void setRemoteInputRowBottomBoundConsumer(@Nullable Consumer<Float> consumer) { + mScrollViewFields.setRemoteInputRowBottomBoundConsumer(consumer); + } + + @Override public void setHeadsUpHeightConsumer(@Nullable Consumer<Float> consumer) { mScrollViewFields.setHeadsUpHeightConsumer(consumer); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index e5f63c1cb480..dad6894a43ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -98,6 +98,9 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.DynamicPrivacyController; +import com.android.systemui.statusbar.notification.HeadsUpNotificationViewControllerEmptyImpl; +import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; +import com.android.systemui.statusbar.notification.HeadsUpTouchHelper.HeadsUpNotificationViewController; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; @@ -129,9 +132,6 @@ import com.android.systemui.statusbar.notification.row.NotificationSnooze; import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; -import com.android.systemui.statusbar.notification.HeadsUpNotificationViewControllerEmptyImpl; -import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; -import com.android.systemui.statusbar.notification.HeadsUpTouchHelper.HeadsUpNotificationViewController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -1605,6 +1605,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { return new RemoteInputController.Delegate() { public void setRemoteInputActive(NotificationEntry entry, boolean remoteInputActive) { + if (SceneContainerFlag.isEnabled()) { + sendRemoteInputRowBottomBound(entry, remoteInputActive); + } mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive); entry.notifyHeightChanged(true /* needsAnimation */); if (!FooterViewRefactor.isEnabled()) { @@ -1620,6 +1623,15 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.requestDisallowLongPress(); mView.requestDisallowDismiss(); } + + private void sendRemoteInputRowBottomBound(NotificationEntry entry, + boolean remoteInputActive) { + ExpandableNotificationRow row = entry.getRow(); + float top = row.getTranslationY(); + int height = row.getActualHeight(); + float bottom = top + height + row.getRemoteInputActionsContainerExpandedOffset(); + mView.sendRemoteInputRowBottomBound(remoteInputActive ? bottom : null); + } }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt index aa3953987c10..c08ed6120832 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt @@ -57,6 +57,13 @@ class ScrollViewFields { * guts off of this gesture, we can notify the placeholder through here. */ var currentGestureInGutsConsumer: Consumer<Boolean>? = null + + /** + * When a notification begins remote input, its bottom Y bound is sent to the placeholder + * through here in order to adjust to accommodate the IME. + */ + var remoteInputRowBottomBoundConsumer: Consumer<Float?>? = null + /** * Any time the heads up height is recalculated, it should be updated here to be used by the * placeholder @@ -75,6 +82,10 @@ class ScrollViewFields { fun sendCurrentGestureInGuts(isCurrentGestureInGuts: Boolean) = currentGestureInGutsConsumer?.accept(isCurrentGestureInGuts) + /** send [bottomY] to the [remoteInputRowBottomBoundConsumer], if present. */ + fun sendRemoteInputRowBottomBound(bottomY: Float?) = + remoteInputRowBottomBoundConsumer?.accept(bottomY) + /** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */ fun sendHeadsUpHeight(headsUpHeight: Float) = headsUpHeightConsumer?.accept(headsUpHeight) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt index 235b4da3f029..41c02934efa6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt @@ -74,6 +74,9 @@ interface NotificationScrollView { /** Set a consumer for current gesture in guts events */ fun setCurrentGestureInGutsConsumer(consumer: Consumer<Boolean>?) + /** Set a consumer for current remote input notification row bottom bound events */ + fun setRemoteInputRowBottomBoundConsumer(consumer: Consumer<Float?>?) + /** Set a consumer for heads up height changed events */ fun setHeadsUpHeightConsumer(consumer: Consumer<Float>?) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index 6d5553fec6b4..2e37dead8787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -108,10 +108,14 @@ constructor( view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer) view.setCurrentGestureInGutsConsumer(viewModel.currentGestureInGutsConsumer) + view.setRemoteInputRowBottomBoundConsumer( + viewModel.remoteInputRowBottomBoundConsumer + ) DisposableHandle { view.setSyntheticScrollConsumer(null) view.setCurrentGestureOverscrollConsumer(null) view.setCurrentGestureInGutsConsumer(null) + view.setRemoteInputRowBottomBoundConsumer(null) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 8d7007b2fba4..5b2e02d446cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -31,6 +31,7 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape @@ -56,6 +57,7 @@ constructor( dumpManager: DumpManager, stackAppearanceInteractor: NotificationStackAppearanceInteractor, shadeInteractor: ShadeInteractor, + private val remoteInputInteractor: RemoteInputInteractor, private val sceneInteractor: SceneInteractor, // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released - // while the flag is off, creating this object too early results in a crash @@ -240,6 +242,10 @@ constructor( val currentGestureInGutsConsumer: (Boolean) -> Unit = stackAppearanceInteractor::setCurrentGestureInGuts + /** Receives the bottom bound of the currently focused remote input notification row. */ + val remoteInputRowBottomBoundConsumer: (Float?) -> Unit = + remoteInputInteractor::setRemoteInputRowBottomBound + /** Whether the notification stack is scrollable or not. */ val isScrollable: Flow<Boolean> = combine(sceneInteractor.currentScene, sceneInteractor.currentOverlays) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 69c1bf3b61b7..c8e83581e831 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -24,6 +24,7 @@ import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds @@ -49,6 +50,7 @@ constructor( private val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, + remoteInputInteractor: RemoteInputInteractor, featureFlags: FeatureFlagsClassic, dumpManager: DumpManager, ) : @@ -132,6 +134,12 @@ constructor( val isCurrentGestureOverscroll: Flow<Boolean> = interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll") + /** Whether remote input is currently active for any notification. */ + val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive + + /** The bottom bound of the currently focused remote input notification row. */ + val remoteInputRowBottomBound = remoteInputInteractor.remoteInputRowBottomBound + /** Sets whether the notification stack is scrolled to the top. */ fun setScrolledToTop(scrolledToTop: Boolean) { interactor.setScrolledToTop(scrolledToTop) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 31776cf5ad1b..16d5f8d30593 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -106,7 +106,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private static final long FOCUS_ANIMATION_CROSSFADE_DURATION = 50; private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33; private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83; - private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; + public static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120; private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index dbeaa59cd219..ba45942177a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -27,7 +27,10 @@ import com.android.settingslib.notification.modes.ZenIcon import com.android.settingslib.notification.modes.ZenIconLoader import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.modes.shared.ModesUi import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository +import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository +import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo import java.time.Duration @@ -51,7 +54,17 @@ constructor( private val notificationSettingsRepository: NotificationSettingsRepository, @Background private val bgDispatcher: CoroutineDispatcher, private val iconLoader: ZenIconLoader, + private val deviceProvisioningRepository: DeviceProvisioningRepository, + private val userSetupRepository: UserSetupRepository, ) { + val isZenAvailable: Flow<Boolean> = + combine( + deviceProvisioningRepository.isDeviceProvisioned, + userSetupRepository.isUserSetUp, + ) { isDeviceProvisioned, isUserSetUp -> + isDeviceProvisioned && isUserSetUp + } + val isZenModeEnabled: Flow<Boolean> = zenModeRepository.globalZenMode .map { @@ -80,6 +93,18 @@ constructor( val modes: Flow<List<ZenMode>> = zenModeRepository.modes + /** + * Returns the special "manual DND" mode. + * + * This is only meant as a temporary solution for "legacy" UI pieces that handle DND + * specifically; any new or migrated features should use modes more generally, through [modes] + * or [activeModes]. + */ + val dndMode: Flow<ZenMode?> by lazy { + ModesUi.assertInNewMode() + zenModeRepository.modes.map { modes -> modes.singleOrNull { it.isManualDnd } } + } + /** Flow returning the currently active mode(s), if any. */ val activeModes: Flow<ActiveZenModes> = modes @@ -113,10 +138,11 @@ constructor( Log.e( TAG, "Interactor cannot handle showing the zen duration prompt. " + - "Please use EnableZenModeDialog when this setting is active." + "Please use EnableZenModeDialog when this setting is active.", ) null } + ZEN_DURATION_FOREVER -> null else -> Duration.ofMinutes(zenDuration.toLong()) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt index af93880bad51..27bc6d36c1e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -59,32 +59,26 @@ fun ModeTile(viewModel: ModeTileViewModel) { ) CompositionLocalProvider(LocalContentColor provides contentColor) { - Surface( - color = tileColor, - shape = RoundedCornerShape(16.dp), - ) { + Surface(color = tileColor, shape = RoundedCornerShape(16.dp)) { Row( modifier = Modifier.combinedClickable( onClick = viewModel.onClick, onLongClick = viewModel.onLongClick, - onLongClickLabel = viewModel.onLongClickLabel + onLongClickLabel = viewModel.onLongClickLabel, ) - .padding(20.dp) + .padding(16.dp) .semantics { stateDescription = viewModel.stateDescription }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = - Arrangement.spacedBy( - space = 10.dp, - alignment = Alignment.Start, - ), + Arrangement.spacedBy(space = 8.dp, alignment = Alignment.Start), ) { Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp)) Column { Text( viewModel.text, fontWeight = FontWeight.W500, - modifier = Modifier.tileMarquee().testTag("name") + modifier = Modifier.tileMarquee().testTag("name"), ) Text( viewModel.subtext, @@ -94,7 +88,7 @@ fun ModeTile(viewModel: ModeTileViewModel) { .testTag(if (viewModel.enabled) "stateOn" else "stateOff") .clearAndSetSemantics { contentDescription = viewModel.subtextDescription - } + }, ) } } @@ -103,8 +97,5 @@ fun ModeTile(viewModel: ModeTileViewModel) { } private fun Modifier.tileMarquee(): Modifier { - return this.basicMarquee( - iterations = 1, - initialDelayMillis = 200, - ) + return this.basicMarquee(iterations = 1) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt index 73d361f69eac..5953ea598929 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy.ui.dialog.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable @@ -27,23 +26,20 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.Flags import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel @Composable fun ModeTileGrid(viewModel: ModesDialogViewModel) { val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList()) - // TODO(b/346519570): Handle what happens when we have more than a few modes. LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp), + columns = GridCells.Fixed(if (Flags.modesDialogSingleRows()) 1 else 2), + modifier = Modifier.fillMaxWidth().heightIn(max = 300.dp), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - items( - tiles.size, - key = { index -> tiles[index].id }, - ) { index -> + items(tiles.size, key = { index -> tiles[index].id }) { index -> ModeTile(viewModel = tiles[index]) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt new file mode 100644 index 000000000000..421e5c45bbfe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.window + +import android.view.View +import android.view.ViewGroup +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.fragments.FragmentHostManager +import java.util.Optional + +/** Encapsulates all logic for the status bar window state management. */ +interface StatusBarWindowController { + val statusBarHeight: Int + + /** Rereads the status bar height and reapplies the current state if the height is different. */ + fun refreshStatusBarHeight() + + /** Adds the status bar view to the window manager. */ + fun attach() + + /** Adds the given view to the status bar window view. */ + fun addViewToWindow(view: View, layoutParams: ViewGroup.LayoutParams) + + /** Returns the status bar window's background view. */ + val backgroundView: View + + /** Returns a fragment host manager for the status bar window view. */ + val fragmentHostManager: FragmentHostManager + + /** + * Provides an updated animation controller if we're animating a view in the status bar. + * + * This is needed because we have to make sure that the status bar window matches the full + * screen during the animation and that we are expanding the view below the other status bar + * text. + * + * @param rootView the root view of the animation + * @param animationController the default animation controller to use + * @return If the animation is on a view in the status bar, returns an Optional containing an + * updated animation controller that handles status-bar-related animation details. Returns an + * empty optional if the animation is *not* on a view in the status bar. + */ + fun wrapAnimationControllerIfInStatusBar( + rootView: View, + animationController: ActivityTransitionAnimator.Controller, + ): Optional<ActivityTransitionAnimator.Controller> + + /** Set force status bar visible. */ + fun setForceStatusBarVisible(forceStatusBarVisible: Boolean) + + /** + * Sets whether an ongoing process requires the status bar to be forced visible. + * + * This method is separate from {@link this#setForceStatusBarVisible} because the ongoing + * process **takes priority**. For example, if {@link this#setForceStatusBarVisible} is set to + * false but this method is set to true, then the status bar **will** be visible. + * + * TODO(b/195839150): We should likely merge this method and {@link + * this#setForceStatusBarVisible} together and use some sort of ranking system instead. + */ + fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java index c30a6b7d0f17..1a0327cdd809 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java @@ -46,6 +46,8 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; +import androidx.annotation.NonNull; + import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.policy.SystemBarUtils; import com.android.systemui.animation.ActivityTransitionAnimator; @@ -67,7 +69,7 @@ import javax.inject.Inject; * Encapsulates all logic for the status bar window state management. */ @SysUISingleton -public class StatusBarWindowController { +public class StatusBarWindowControllerImpl implements StatusBarWindowController { private static final String TAG = "StatusBarWindowController"; private static final boolean DEBUG = false; @@ -89,7 +91,7 @@ public class StatusBarWindowController { private final Binder mInsetsSourceOwner = new Binder(); @Inject - public StatusBarWindowController( + public StatusBarWindowControllerImpl( Context context, @StatusBarWindowModule.InternalWindowView StatusBarWindowView statusBarWindowView, ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, @@ -117,14 +119,12 @@ public class StatusBarWindowController { /* attachedViewProvider=*/ () -> mStatusBarWindowView))); } + @Override public int getStatusBarHeight() { return mBarHeight; } - /** - * Rereads the status bar height and reapplys the current state if the height - * is different. - */ + @Override public void refreshStatusBarHeight() { Trace.beginSection("StatusBarWindowController#refreshStatusBarHeight"); try { @@ -141,9 +141,7 @@ public class StatusBarWindowController { } } - /** - * Adds the status bar view to the window manager. - */ + @Override public void attach() { // Now that the status bar window encompasses the sliding panel and its // translucent backdrop, the entire thing is made TRANSLUCENT and is @@ -161,54 +159,47 @@ public class StatusBarWindowController { apply(mCurrentState); } - /** Adds the given view to the status bar window view. */ - public void addViewToWindow(View view, ViewGroup.LayoutParams layoutParams) { + @Override + public void addViewToWindow(@NonNull View view, @NonNull ViewGroup.LayoutParams layoutParams) { mStatusBarWindowView.addView(view, layoutParams); } - /** Returns the status bar window's background view. */ + @NonNull + @Override public View getBackgroundView() { return mStatusBarWindowView.findViewById(R.id.status_bar_container); } - /** Returns a fragment host manager for the status bar window view. */ + @NonNull + @Override public FragmentHostManager getFragmentHostManager() { return mFragmentService.getFragmentHostManager(mStatusBarWindowView); } - /** - * Provides an updated animation controller if we're animating a view in the status bar. - * - * This is needed because we have to make sure that the status bar window matches the full - * screen during the animation and that we are expanding the view below the other status bar - * text. - * - * @param rootView the root view of the animation - * @param animationController the default animation controller to use - * @return If the animation is on a view in the status bar, returns an Optional containing an - * updated animation controller that handles status-bar-related animation details. Returns an - * empty optional if the animation is *not* on a view in the status bar. - */ + @NonNull + @Override public Optional<ActivityTransitionAnimator.Controller> wrapAnimationControllerIfInStatusBar( - View rootView, ActivityTransitionAnimator.Controller animationController) { + @NonNull View rootView, + @NonNull ActivityTransitionAnimator.Controller animationController) { if (rootView != mStatusBarWindowView) { return Optional.empty(); } animationController.setTransitionContainer(mLaunchAnimationContainer); - return Optional.of(new DelegateTransitionAnimatorController(animationController) { - @Override - public void onTransitionAnimationStart(boolean isExpandingFullyAbove) { - getDelegate().onTransitionAnimationStart(isExpandingFullyAbove); - setLaunchAnimationRunning(true); - } - - @Override - public void onTransitionAnimationEnd(boolean isExpandingFullyAbove) { - getDelegate().onTransitionAnimationEnd(isExpandingFullyAbove); - setLaunchAnimationRunning(false); - } - }); + return Optional.of( + new DelegateTransitionAnimatorController(animationController) { + @Override + public void onTransitionAnimationStart(boolean isExpandingFullyAbove) { + getDelegate().onTransitionAnimationStart(isExpandingFullyAbove); + setLaunchAnimationRunning(true); + } + + @Override + public void onTransitionAnimationEnd(boolean isExpandingFullyAbove) { + getDelegate().onTransitionAnimationEnd(isExpandingFullyAbove); + setLaunchAnimationRunning(false); + } + }); } private WindowManager.LayoutParams getBarLayoutParams(int rotation) { @@ -275,22 +266,13 @@ public class StatusBarWindowController { } } - /** Set force status bar visible. */ + @Override public void setForceStatusBarVisible(boolean forceStatusBarVisible) { mCurrentState.mForceStatusBarVisible = forceStatusBarVisible; apply(mCurrentState); } - /** - * Sets whether an ongoing process requires the status bar to be forced visible. - * - * This method is separate from {@link this#setForceStatusBarVisible} because the ongoing - * process **takes priority**. For example, if {@link this#setForceStatusBarVisible} is set to - * false but this method is set to true, then the status bar **will** be visible. - * - * TODO(b/195839150): We should likely merge this method and - * {@link this#setForceStatusBarVisible} together and use some sort of ranking system instead. - */ + @Override public void setOngoingProcessRequiresStatusBarVisible(boolean visible) { mCurrentState.mOngoingProcessRequiresStatusBarVisible = visible; apply(mCurrentState); diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt index 1a41987a8e5b..80ea925eabc7 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt @@ -30,12 +30,12 @@ class TouchpadGesturesInteractor( private val logger: InputDeviceTutorialLogger, ) { fun disableGestures() { - logger.log("Disabling touchpad gestures across the system") + logger.d("Disabling touchpad gestures across the system") setGesturesState(disabled = true) } fun enableGestures() { - logger.log("Enabling touchpad gestures across the system") + logger.d("Enabling touchpad gestures across the system") setGesturesState(disabled = false) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index 5a77c04924dd..6acc891e93d5 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -78,21 +78,21 @@ private fun TutorialSelectionButtons( modifier = modifier ) { TutorialButton( - text = stringResource(R.string.touchpad_tutorial_back_gesture_button), - onClick = onBackTutorialClicked, + text = stringResource(R.string.touchpad_tutorial_home_gesture_button), + onClick = onHomeTutorialClicked, color = MaterialTheme.colorScheme.primary, modifier = Modifier.weight(1f) ) TutorialButton( - text = stringResource(R.string.touchpad_tutorial_home_gesture_button), - onClick = onHomeTutorialClicked, - color = MaterialTheme.colorScheme.secondary, + text = stringResource(R.string.touchpad_tutorial_back_gesture_button), + onClick = onBackTutorialClicked, + color = MaterialTheme.colorScheme.tertiary, modifier = Modifier.weight(1f) ) TutorialButton( text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button), onClick = onRecentAppsTutorialClicked, - color = MaterialTheme.colorScheme.tertiary, + color = MaterialTheme.colorScheme.secondary, modifier = Modifier.weight(1f) ) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index 46ea352ff85a..d03b2e717398 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt @@ -29,12 +29,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.PlatformTheme import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext -import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.RecentAppsGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.TutorialSelectionScreen -import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.ACTION_KEY import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.BACK_GESTURE import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.HOME_GESTURE import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.RECENT_APPS_GESTURE @@ -59,7 +57,7 @@ constructor( } // required to handle 3+ fingers on touchpad window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) - window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS) + logger.logOpenTutorial(TutorialContext.TOUCHPAD_TUTORIAL) } private fun finishTutorial() { @@ -104,10 +102,5 @@ fun TouchpadTutorialScreen(vm: TouchpadTutorialViewModel, closeTutorial: () -> U onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) - ACTION_KEY -> // TODO(b/358105049) move action key tutorial to OOBE flow - ActionKeyTutorialScreen( - onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, - onBack = { vm.goTo(TUTORIAL_SELECTION) }, - ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt index 599e1b1eff75..c56dcf3bf062 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt @@ -65,5 +65,4 @@ enum class Screen { BACK_GESTURE, HOME_GESTURE, RECENT_APPS_GESTURE, - ACTION_KEY, } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index db4f9ef13bd6..7166428d863f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -35,7 +35,6 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL; import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder; import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix; -import static com.android.systemui.Flags.hapticVolumeSlider; import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED; import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; @@ -928,10 +927,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, } private void addSliderHapticsToRow(VolumeRow row) { - if (hapticVolumeSlider()) { - row.createPlugin(mVibratorHelper, mSystemClock); - HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin); - } + row.createPlugin(mVibratorHelper, mSystemClock); + HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin); } @VisibleForTesting void addSliderHapticsToRows() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index 9aaf2958031a..a940bc9b3e20 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -8,7 +8,8 @@ import android.content.pm.ApplicationInfo import android.graphics.Point import android.graphics.Rect import android.os.Looper -import android.platform.test.flag.junit.SetFlagsRule +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import android.view.IRemoteAnimationFinishedCallback import android.view.RemoteAnimationAdapter @@ -63,7 +64,6 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { private lateinit var activityTransitionAnimator: ActivityTransitionAnimator @get:Rule val rule = MockitoJUnit.rule() - @get:Rule val setFlagsRule = SetFlagsRule() @Before fun setup() { @@ -90,7 +90,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { animator: ActivityTransitionAnimator = this.activityTransitionAnimator, controller: ActivityTransitionAnimator.Controller? = this.controller, animate: Boolean = true, - intentStarter: (RemoteAnimationAdapter?) -> Int + intentStarter: (RemoteAnimationAdapter?) -> Int, ) { // We start in a new thread so that we can ensure that the callbacks are called in the main // thread. @@ -98,7 +98,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { animator.startIntentWithAnimation( controller = controller, animate = animate, - intentStarter = intentStarter + intentStarter = intentStarter, ) } .join() @@ -175,9 +175,9 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { assertFalse(willAnimateCaptor.value) } + @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) @Test fun registersReturnIffCookieIsPresent() { - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) `when`(callback.isOnKeyguard()).thenReturn(false) startIntentWithAnimation(activityTransitionAnimator, controller) { _ -> @@ -203,10 +203,12 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) } + @EnableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) @Test fun registersLongLivedTransition() { - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) - activityTransitionAnimator.register( object : DelegateTransitionAnimatorController(controller) { override val transitionCookie = @@ -226,10 +228,12 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { assertEquals(4, testShellTransitions.remotes.size) } + @EnableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) @Test fun registersLongLivedTransitionOverridingPreviousRegistration() { - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) - val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie") activityTransitionAnimator.register( object : DelegateTransitionAnimatorController(controller) { @@ -251,9 +255,9 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } } + @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() { - setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) val controller = object : DelegateTransitionAnimatorController(controller) { @@ -266,9 +270,9 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } } + @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() { - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) // No TransitionCookie val controllerWithoutCookie = @@ -310,9 +314,12 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } } + @EnableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) @Test fun unregistersLongLivedTransition() { - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3) @@ -411,7 +418,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { SurfaceControl(), Rect(), taskInfo, - false + false, ) } } @@ -430,7 +437,7 @@ private class FakeShellTransitions : ShellTransitions { override fun registerRemoteForTakeover( filter: TransitionFilter, - remoteTransition: RemoteTransition + remoteTransition: RemoteTransition, ) { remotesForTakeover[filter] = remoteTransition } @@ -460,7 +467,7 @@ private class TestTransitionAnimatorController(override var transitionContainer: left = 300, right = 400, topCornerRadius = 10f, - bottomCornerRadius = 20f + bottomCornerRadius = 20f, ) private fun assertOnMainThread() { @@ -480,7 +487,7 @@ private class TestTransitionAnimatorController(override var transitionContainer: override fun onTransitionAnimationProgress( state: TransitionAnimator.State, progress: Float, - linearProgress: Float + linearProgress: Float, ) { assertOnMainThread() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt index 4b61a0d02f1e..088bb02512b5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt @@ -25,6 +25,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.ui.viewmodel.patternBouncerViewModelFactory +import com.android.systemui.haptics.msdl.bouncerHapticPlayer import com.android.systemui.lifecycle.activateIn import com.android.systemui.motion.createSysUiComposeMotionTestRule import com.android.systemui.testKosmos @@ -55,6 +56,7 @@ class PatternBouncerTest : SysuiTestCase() { kosmos.patternBouncerViewModelFactory.create( isInputEnabled = MutableStateFlow(true).asStateFlow(), onIntentionalUserInput = {}, + bouncerHapticPlayer = kosmos.bouncerHapticPlayer, ) @Before @@ -75,11 +77,11 @@ class PatternBouncerTest : SysuiTestCase() { content = { play -> if (play) PatternBouncerUnderTest() }, ComposeRecordingSpec.until( recordBefore = false, - checkDone = { motionTestValueOfNode(MotionTestKeys.entryCompleted) } + checkDone = { motionTestValueOfNode(MotionTestKeys.entryCompleted) }, ) { feature(MotionTestKeys.dotAppearFadeIn, floatArray) feature(MotionTestKeys.dotAppearMoveUp, floatArray) - } + }, ) assertThat(motion).timeSeriesMatchesGolden() @@ -100,7 +102,7 @@ class PatternBouncerTest : SysuiTestCase() { viewModel.onDragEnd() // Failure animation starts when animateFailure flips to true... viewModel.animateFailure.takeWhile { !it }.collect {} - } + }, ) { // ... and ends when the composable flips it back to false. viewModel.animateFailure.takeWhile { it }.collect {} @@ -111,7 +113,7 @@ class PatternBouncerTest : SysuiTestCase() { content = { PatternBouncerUnderTest() }, ComposeRecordingSpec(failureAnimationMotionControl) { feature(MotionTestKeys.dotScaling, floatArray) - } + }, ) assertThat(motion).timeSeriesMatchesGolden() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java index c65a1176a55b..d72b72c3d21e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java @@ -32,6 +32,7 @@ import android.content.ClipData; import android.content.ClipDescription; import android.content.ClipboardManager; import android.os.PersistableBundle; +import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.provider.Settings; @@ -101,8 +102,18 @@ public class ClipboardListenerTest extends SysuiTestCase { when(mClipboardManager.getPrimaryClip()).thenReturn(mSampleClipData); when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource); - mClipboardListener = new ClipboardListener(getContext(), mOverlayControllerProvider, - mClipboardToast, mClipboardManager, mKeyguardManager, mUiEventLogger); + mClipboardListener = new ClipboardListener( + getContext(), + mOverlayControllerProvider, + mClipboardToast, + user -> { + if (UserHandle.CURRENT.equals(user)) { + return mClipboardManager; + } + return null; + }, + mKeyguardManager, + mUiEventLogger); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 411ff91ebc2f..8731853e4939 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -77,7 +77,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { private static final int TEST_CURRENT_VOLUME = 10; // Mock - private MediaOutputController mMediaOutputController = mock(MediaOutputController.class); + private MediaSwitchingController mMediaSwitchingController = + mock(MediaSwitchingController.class); private MediaOutputDialog mMediaOutputDialog = mock(MediaOutputDialog.class); private MediaDevice mMediaDevice1 = mock(MediaDevice.class); private MediaDevice mMediaDevice2 = mock(MediaDevice.class); @@ -95,13 +96,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Before public void setUp() { - when(mMediaOutputController.getMediaItemList()).thenReturn(mMediaItems); - when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false); - when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false); - when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat); - when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat); - when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1); - when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true); + when(mMediaSwitchingController.getMediaItemList()).thenReturn(mMediaItems); + when(mMediaSwitchingController.hasAdjustVolumeUserRestriction()).thenReturn(false); + when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false); + when(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat); + when(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat); + when(mMediaSwitchingController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1); + when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(true); when(mIconCompat.toIcon(mContext)).thenReturn(mIcon); when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1); when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1); @@ -116,7 +117,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1)); mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2)); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -142,7 +143,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindPairNew_verifyView() { - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -161,11 +162,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindGroup_withSessionName_verifyView() { - when(mMediaOutputController.getSelectedMediaDevice()).thenReturn( - mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect( - Collectors.toList())); - when(mMediaOutputController.getSessionName()).thenReturn(TEST_SESSION_NAME); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + when(mMediaSwitchingController.getSelectedMediaDevice()) + .thenReturn( + mMediaItems.stream() + .map((item) -> item.getMediaDevice().get()) + .collect(Collectors.toList())); + when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -181,11 +184,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindGroup_noSessionName_verifyView() { - when(mMediaOutputController.getSelectedMediaDevice()).thenReturn( - mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect( - Collectors.toList())); - when(mMediaOutputController.getSessionName()).thenReturn(null); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + when(mMediaSwitchingController.getSelectedMediaDevice()) + .thenReturn( + mMediaItems.stream() + .map((item) -> item.getMediaDevice().get()) + .collect(Collectors.toList())); + when(mMediaSwitchingController.getSessionName()).thenReturn(null); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -214,7 +219,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() { - when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); + when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -230,9 +235,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindConnectedRemoteDevice_verifyView() { - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( - ImmutableList.of(mMediaDevice2)); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()) + .thenReturn(ImmutableList.of(mMediaDevice2)); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -249,9 +254,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindConnectedRemoteDevice_verifyContentDescriptionNotNull() { - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( - ImmutableList.of(mMediaDevice2)); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()) + .thenReturn(ImmutableList.of(mMediaDevice2)); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -263,9 +268,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() { - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( - ImmutableList.of()); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -283,9 +287,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindConnectedRemoteDeviceWithOnGoingSession_verifyView() { when(mMediaDevice1.hasOngoingSession()).thenReturn(true); - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( - ImmutableList.of()); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -305,9 +308,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void onBindViewHolder_bindConnectedRemoteDeviceWithHostOnGoingSession_verifyView() { when(mMediaDevice1.hasOngoingSession()).thenReturn(true); when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true); - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( - ImmutableList.of()); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -326,8 +328,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() { - when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false); + when(mMediaSwitchingController.hasMutingExpectedDevice()).thenReturn(true); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); @@ -340,8 +342,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_isMutingExpectedDevice_verifyView() { when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false); - when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false); + when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -378,14 +380,14 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mOnSeekBarChangeListenerCaptor.getValue().onStopTrackingTouch(mViewHolder.mSeekBar); assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); - verify(mMediaOutputController).logInteractionAdjustVolume(mMediaDevice1); + verify(mMediaSwitchingController).logInteractionAdjustVolume(mMediaDevice1); } @Test public void onBindViewHolder_bindSelectableDevice_verifyView() { List<MediaDevice> selectableDevices = new ArrayList<>(); selectableDevices.add(mMediaDevice2); - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); @@ -440,7 +442,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void subStatusSupported_onBindViewHolder_bindHostDeviceWithOngoingSession_verifyView() { - when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true); + when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true); when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true); when(mMediaDevice1.hasSubtext()).thenReturn(true); when(mMediaDevice1.getSubtext()).thenReturn(SUBTEXT_CUSTOM); @@ -540,7 +542,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() { - when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true); + when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(true); when(mMediaDevice1.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_CONNECTING); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -556,7 +558,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindGroupingDevice_verifyView() { - when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false); + when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false); when(mMediaDevice1.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_GROUPING); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -572,7 +574,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() { - when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true); + when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(true); when(mMediaDevice2.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_CONNECTING); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -586,7 +588,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() { - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -595,16 +597,16 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2); mViewHolder.mContainerLayout.performClick(); - verify(mMediaOutputController).launchBluetoothPairing(mViewHolder.mContainerLayout); + verify(mMediaSwitchingController).launchBluetoothPairing(mViewHolder.mContainerLayout); } @Test public void onItemClick_clickDevice_verifyConnectDevice() { - when(mMediaOutputController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); + when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -613,16 +615,16 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); mViewHolder.mContainerLayout.performClick(); - verify(mMediaOutputController).connectDevice(mMediaDevice2); + verify(mMediaSwitchingController).connectDevice(mMediaDevice2); } @Test public void onItemClick_clickDeviceWithSessionOngoing_verifyShowsDialog() { - when(mMediaOutputController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(true); + when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(true); assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -633,66 +635,68 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 1); spyMediaDeviceViewHolder.mContainerLayout.performClick(); - verify(mMediaOutputController, never()).connectDevice(mMediaDevice2); + verify(mMediaSwitchingController, never()).connectDevice(mMediaDevice2); verify(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2); } @Test public void onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() { - when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false); - when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false); + when(mMediaSwitchingController.isAnyDeviceTransferring()).thenReturn(false); + when(mMediaSwitchingController.hasMutingExpectedDevice()).thenReturn(true); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false); when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(false); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); mViewHolder.mContainerLayout.performClick(); - verify(mMediaOutputController).cancelMuteAwaitConnection(); + verify(mMediaSwitchingController).cancelMuteAwaitConnection(); } @Test public void onGroupActionTriggered_clicksEndAreaOfSelectableDevice_triggerGrouping() { List<MediaDevice> selectableDevices = new ArrayList<>(); selectableDevices.add(mMediaDevice2); - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); mViewHolder.mEndTouchArea.performClick(); - verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2); + verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2); } @Test public void onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() { - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( - ImmutableList.of(mMediaDevice2)); - when(mMediaOutputController.getDeselectableMediaDevice()).thenReturn( - ImmutableList.of(mMediaDevice1)); - when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()) + .thenReturn(ImmutableList.of(mMediaDevice2)); + when(mMediaSwitchingController.getDeselectableMediaDevice()) + .thenReturn(ImmutableList.of(mMediaDevice1)); + when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); mViewHolder.mEndTouchArea.performClick(); - verify(mMediaOutputController).removeDeviceFromPlayMedia(mMediaDevice1); + verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1); } @Test public void onItemClick_onGroupActionTriggered_verifySeekbarDisabled() { - when(mMediaOutputController.getSelectedMediaDevice()).thenReturn( - mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect( - Collectors.toList())); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + when(mMediaSwitchingController.getSelectedMediaDevice()) + .thenReturn( + mMediaItems.stream() + .map((item) -> item.getMediaDevice().get()) + .collect(Collectors.toList())); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); List<MediaDevice> selectableDevices = new ArrayList<>(); selectableDevices.add(mMediaDevice1); - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices); - when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(true); + when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices); + when(mMediaSwitchingController.hasAdjustVolumeUserRestriction()).thenReturn(true); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); mViewHolder.mContainerLayout.performClick(); @@ -702,11 +706,11 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_volumeControlChangeToEnabled_enableSeekbarAgain() { - when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(false); + when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(false); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); assertThat(mViewHolder.mSeekBar.isEnabled()).isFalse(); - when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true); + when(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); assertThat(mViewHolder.mSeekBar.isEnabled()).isTrue(); @@ -719,7 +723,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaOutputAdapter.updateColorScheme(wallpaperColors, true); - verify(mMediaOutputController).setCurrentColorScheme(wallpaperColors, true); + verify(mMediaSwitchingController).setCurrentColorScheme(wallpaperColors, true); } @Test @@ -727,7 +731,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaOutputAdapter.updateItems(); List<MediaItem> updatedList = new ArrayList<>(); updatedList.add(MediaItem.createPairNewDeviceMediaItem()); - when(mMediaOutputController.getMediaItemList()).thenReturn(updatedList); + when(mMediaSwitchingController.getMediaItemList()).thenReturn(updatedList); assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaItems.size()); mMediaOutputAdapter.updateItems(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index c8cc6b5fdf93..47371dfd8895 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -104,7 +104,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { private List<MediaController> mMediaControllers = new ArrayList<>(); private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl; - private MediaOutputController mMediaOutputController; + private MediaSwitchingController mMediaSwitchingController; private int mHeaderIconRes; private IconCompat mIconCompat; private CharSequence mHeaderTitle; @@ -132,8 +132,8 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor( mKosmos); - mMediaOutputController = - new MediaOutputController( + mMediaSwitchingController = + new MediaSwitchingController( mContext, TEST_PACKAGE, mContext.getUser(), @@ -153,12 +153,13 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { // Using a fake package will cause routing operations to fail, so we intercept // scanning-related operations. - mMediaOutputController.mLocalMediaManager = mock(LocalMediaManager.class); - doNothing().when(mMediaOutputController.mLocalMediaManager).startScan(); - doNothing().when(mMediaOutputController.mLocalMediaManager).stopScan(); + mMediaSwitchingController.mLocalMediaManager = mock(LocalMediaManager.class); + doNothing().when(mMediaSwitchingController.mLocalMediaManager).startScan(); + doNothing().when(mMediaSwitchingController.mLocalMediaManager).stopScan(); - mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender, - mMediaOutputController); + mMediaOutputBaseDialogImpl = + new MediaOutputBaseDialogImpl( + mContext, mBroadcastSender, mMediaSwitchingController); mMediaOutputBaseDialogImpl.onCreate(new Bundle()); } @@ -176,7 +177,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { public void refresh_withIconCompat_iconIsVisible() { mIconCompat = IconCompat.createWithBitmap( Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)); - when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaOutputController); + when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaSwitchingController); mMediaOutputBaseDialogImpl.refresh(); final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById( @@ -263,7 +264,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { when(mMediaOutputBaseAdapter.isDragging()).thenReturn(true); mMediaOutputBaseDialogImpl.refresh(); - assertThat(mMediaOutputController.isRefreshing()).isFalse(); + assertThat(mMediaSwitchingController.isRefreshing()).isFalse(); } @Test @@ -335,12 +336,14 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog { - MediaOutputBaseDialogImpl(Context context, BroadcastSender broadcastSender, - MediaOutputController mediaOutputController) { + MediaOutputBaseDialogImpl( + Context context, + BroadcastSender broadcastSender, + MediaSwitchingController mediaSwitchingController) { super( context, broadcastSender, - mediaOutputController, /* includePlaybackAndAppMetadata */ + mediaSwitchingController, /* includePlaybackAndAppMetadata */ true); mAdapter = mMediaOutputBaseAdapter; diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java index 189a56145d27..f0902e35b837 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java @@ -119,7 +119,7 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { private UserTracker mUserTracker = mock(UserTracker.class); private MediaOutputBroadcastDialog mMediaOutputBroadcastDialog; - private MediaOutputController mMediaOutputController; + private MediaSwitchingController mMediaSwitchingController; @Before public void setUp() { @@ -133,8 +133,8 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor( mKosmos); - mMediaOutputController = - new MediaOutputController( + mMediaSwitchingController = + new MediaSwitchingController( mContext, TEST_PACKAGE, mContext.getUser(), @@ -151,9 +151,10 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { mFlags, volumePanelGlobalStateInteractor, mUserTracker); - mMediaOutputController.mLocalMediaManager = mLocalMediaManager; - mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false, - mBroadcastSender, mMediaOutputController); + mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager; + mMediaOutputBroadcastDialog = + new MediaOutputBroadcastDialog( + mContext, false, mBroadcastSender, mMediaSwitchingController); mMediaOutputBroadcastDialog.show(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index 90c2930f8e49..d3ecb3d8c944 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -119,7 +119,7 @@ public class MediaOutputDialogTest extends SysuiTestCase { private List<MediaController> mMediaControllers = new ArrayList<>(); private MediaOutputDialog mMediaOutputDialog; - private MediaOutputController mMediaOutputController; + private MediaSwitchingController mMediaSwitchingController; private final List<String> mFeatures = new ArrayList<>(); @Override @@ -146,8 +146,8 @@ public class MediaOutputDialogTest extends SysuiTestCase { VolumePanelGlobalStateInteractorKosmosKt.getVolumePanelGlobalStateInteractor( mKosmos); - mMediaOutputController = - new MediaOutputController( + mMediaSwitchingController = + new MediaSwitchingController( mContext, TEST_PACKAGE, mContext.getUser(), @@ -164,8 +164,8 @@ public class MediaOutputDialogTest extends SysuiTestCase { mFlags, volumePanelGlobalStateInteractor, mUserTracker); - mMediaOutputController.mLocalMediaManager = mLocalMediaManager; - mMediaOutputDialog = makeTestDialog(mMediaOutputController); + mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager; + mMediaOutputDialog = makeTestDialog(mMediaSwitchingController); mMediaOutputDialog.show(); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice); @@ -388,12 +388,15 @@ public class MediaOutputDialogTest extends SysuiTestCase { public void getStopButtonText_notSupportsBroadcast_returnsDefaultText() { String stopText = mContext.getText( R.string.media_output_dialog_button_stop_casting).toString(); - MediaOutputController mockMediaOutputController = mock(MediaOutputController.class); - when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false); - - withTestDialog(mockMediaOutputController, testDialog -> { - assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText); - }); + MediaSwitchingController mockMediaSwitchingController = + mock(MediaSwitchingController.class); + when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(false); + + withTestDialog( + mockMediaSwitchingController, + testDialog -> { + assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText); + }); } @Test @@ -401,28 +404,35 @@ public class MediaOutputDialogTest extends SysuiTestCase { public void getStopButtonText_supportsBroadcast_returnsBroadcastText() { String stopText = mContext.getText(R.string.media_output_broadcast).toString(); MediaDevice mMediaDevice = mock(MediaDevice.class); - MediaOutputController mockMediaOutputController = mock(MediaOutputController.class); - when(mockMediaOutputController.isBroadcastSupported()).thenReturn(true); - when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice); - when(mockMediaOutputController.isBluetoothLeDevice(any())).thenReturn(true); - when(mockMediaOutputController.isPlaying()).thenReturn(true); - when(mockMediaOutputController.isBluetoothLeBroadcastEnabled()).thenReturn(false); - withTestDialog(mockMediaOutputController, testDialog -> { - assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText); - }); + MediaSwitchingController mockMediaSwitchingController = + mock(MediaSwitchingController.class); + when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(true); + when(mockMediaSwitchingController.getCurrentConnectedMediaDevice()) + .thenReturn(mMediaDevice); + when(mockMediaSwitchingController.isBluetoothLeDevice(any())).thenReturn(true); + when(mockMediaSwitchingController.isPlaying()).thenReturn(true); + when(mockMediaSwitchingController.isBluetoothLeBroadcastEnabled()).thenReturn(false); + withTestDialog( + mockMediaSwitchingController, + testDialog -> { + assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText); + }); } @Test public void onStopButtonClick_notPlaying_releaseSession() { - MediaOutputController mockMediaOutputController = mock(MediaOutputController.class); - when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false); - when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(null); - when(mockMediaOutputController.isPlaying()).thenReturn(false); - withTestDialog(mockMediaOutputController, testDialog -> { - testDialog.onStopButtonClick(); - }); - - verify(mockMediaOutputController).releaseSession(); + MediaSwitchingController mockMediaSwitchingController = + mock(MediaSwitchingController.class); + when(mockMediaSwitchingController.isBroadcastSupported()).thenReturn(false); + when(mockMediaSwitchingController.getCurrentConnectedMediaDevice()).thenReturn(null); + when(mockMediaSwitchingController.isPlaying()).thenReturn(false); + withTestDialog( + mockMediaSwitchingController, + testDialog -> { + testDialog.onStopButtonClick(); + }); + + verify(mockMediaSwitchingController).releaseSession(); verify(mDialogTransitionAnimator).disableAllCurrentDialogsExitAnimations(); } @@ -430,14 +440,14 @@ public class MediaOutputDialogTest extends SysuiTestCase { // Check the visibility metric logging by creating a new MediaOutput dialog, // and verify if the calling times increases. public void onCreate_ShouldLogVisibility() { - withTestDialog(mMediaOutputController, testDialog -> {}); + withTestDialog(mMediaSwitchingController, testDialog -> {}); verify(mUiEventLogger, times(2)) .log(MediaOutputDialog.MediaOutputEvent.MEDIA_OUTPUT_DIALOG_SHOW); } @NonNull - private MediaOutputDialog makeTestDialog(MediaOutputController controller) { + private MediaOutputDialog makeTestDialog(MediaSwitchingController controller) { return new MediaOutputDialog( mContext, false, @@ -448,7 +458,8 @@ public class MediaOutputDialogTest extends SysuiTestCase { true); } - private void withTestDialog(MediaOutputController controller, Consumer<MediaOutputDialog> c) { + private void withTestDialog( + MediaSwitchingController controller, Consumer<MediaOutputDialog> c) { MediaOutputDialog testDialog = makeTestDialog(controller); testDialog.show(); c.accept(testDialog); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index 714fad9d7478..d3e20c6e39b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -43,6 +43,7 @@ import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.MediaDescription; import android.media.MediaMetadata; @@ -58,6 +59,7 @@ import android.os.Bundle; import android.os.PowerExemptionManager; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.service.notification.StatusBarNotification; import android.testing.TestableLooper; import android.text.TextUtils; @@ -67,8 +69,10 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.media.flags.Flags; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.media.InputMediaDevice; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.systemui.SysuiTestCase; @@ -101,7 +105,7 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -public class MediaOutputControllerTest extends SysuiTestCase { +public class MediaSwitchingControllerTest extends SysuiTestCase { private static final String TEST_DEVICE_1_ID = "test_device_1_id"; private static final String TEST_DEVICE_2_ID = "test_device_2_id"; private static final String TEST_DEVICE_3_ID = "test_device_3_id"; @@ -126,8 +130,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager; @Mock private LocalBluetoothManager mLocalBluetoothManager; - @Mock - private MediaOutputController.Callback mCb; + @Mock private MediaSwitchingController.Callback mCb; @Mock private MediaDevice mMediaDevice1; @Mock @@ -166,7 +169,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { private FeatureFlags mFlags = mock(FeatureFlags.class); private View mDialogLaunchView = mock(View.class); - private MediaOutputController.Callback mCallback = mock(MediaOutputController.Callback.class); + private MediaSwitchingController.Callback mCallback = + mock(MediaSwitchingController.Callback.class); final Notification mNotification = mock(Notification.class); private final VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor = @@ -175,7 +179,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { private Context mSpyContext; private String mPackageName = null; - private MediaOutputController mMediaOutputController; + private MediaSwitchingController mMediaSwitchingController; private LocalMediaManager mLocalMediaManager; private List<MediaController> mMediaControllers = new ArrayList<>(); private List<MediaDevice> mMediaDevices = new ArrayList<>(); @@ -203,9 +207,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn( mCachedBluetoothDeviceManager); - - mMediaOutputController = - new MediaOutputController( + mMediaSwitchingController = + new MediaSwitchingController( mSpyContext, mPackageName, mContext.getUser(), @@ -222,9 +225,9 @@ public class MediaOutputControllerTest extends SysuiTestCase { mFlags, mVolumePanelGlobalStateInteractor, mUserTracker); - mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); + mLocalMediaManager = spy(mMediaSwitchingController.mLocalMediaManager); when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false); - mMediaOutputController.mLocalMediaManager = mLocalMediaManager; + mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager; MediaDescription.Builder builder = new MediaDescription.Builder(); builder.setTitle(TEST_SONG); builder.setSubtitle(TEST_ARTIST); @@ -264,26 +267,26 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void start_verifyLocalMediaManagerInit() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); - verify(mLocalMediaManager).registerCallback(mMediaOutputController); + verify(mLocalMediaManager).registerCallback(mMediaSwitchingController); verify(mLocalMediaManager).startScan(); } @Test public void stop_verifyLocalMediaManagerDeinit() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mLocalMediaManager); - mMediaOutputController.stop(); + mMediaSwitchingController.stop(); - verify(mLocalMediaManager).unregisterCallback(mMediaOutputController); + verify(mLocalMediaManager).unregisterCallback(mMediaSwitchingController); verify(mLocalMediaManager).stopScan(); } @Test public void start_notificationNotFound_mediaControllerInitFromSession() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); verify(mSessionMediaController).registerCallback(any()); } @@ -291,7 +294,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void start_MediaNotificationFound_mediaControllerNotInitFromSession() { when(mNotification.isMediaNotification()).thenReturn(true); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); verify(mSessionMediaController, never()).registerCallback(any()); verifyZeroInteractions(mMediaSessionManager); @@ -299,8 +302,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void start_withoutPackageName_verifyMediaControllerInit() { - mMediaOutputController = - new MediaOutputController( + mMediaSwitchingController = + new MediaSwitchingController( mSpyContext, null, mContext.getUser(), @@ -318,32 +321,32 @@ public class MediaOutputControllerTest extends SysuiTestCase { mVolumePanelGlobalStateInteractor, mUserTracker); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); verify(mSessionMediaController, never()).registerCallback(any()); } @Test public void start_nearbyMediaDevicesManagerNotNull_registersNearbyDevicesCallback() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); verify(mNearbyMediaDevicesManager).registerNearbyDevicesCallback(any()); } @Test public void stop_withPackageName_verifyMediaControllerDeinit() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mSessionMediaController); - mMediaOutputController.stop(); + mMediaSwitchingController.stop(); verify(mSessionMediaController).unregisterCallback(any()); } @Test public void stop_withoutPackageName_verifyMediaControllerDeinit() { - mMediaOutputController = - new MediaOutputController( + mMediaSwitchingController = + new MediaSwitchingController( mSpyContext, null, mSpyContext.getUser(), @@ -361,26 +364,26 @@ public class MediaOutputControllerTest extends SysuiTestCase { mVolumePanelGlobalStateInteractor, mUserTracker); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); - mMediaOutputController.stop(); + mMediaSwitchingController.stop(); verify(mSessionMediaController, never()).unregisterCallback(any()); } @Test public void stop_nearbyMediaDevicesManagerNotNull_unregistersNearbyDevicesCallback() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mSessionMediaController); - mMediaOutputController.stop(); + mMediaSwitchingController.stop(); verify(mNearbyMediaDevicesManager).unregisterNearbyDevicesCallback(any()); } @Test public void tryToLaunchMediaApplication_nullIntent_skip() { - mMediaOutputController.tryToLaunchMediaApplication(mDialogLaunchView); + mMediaSwitchingController.tryToLaunchMediaApplication(mDialogLaunchView); verify(mCb, never()).dismissDialog(); } @@ -391,9 +394,9 @@ public class MediaOutputControllerTest extends SysuiTestCase { .thenReturn(mController); Intent intent = new Intent(mPackageName); doReturn(intent).when(mPackageManager).getLaunchIntentForPackage(mPackageName); - mMediaOutputController.start(mCallback); + mMediaSwitchingController.start(mCallback); - mMediaOutputController.tryToLaunchMediaApplication(mDialogLaunchView); + mMediaSwitchingController.tryToLaunchMediaApplication(mDialogLaunchView); verify(mStarter).startActivity(any(Intent.class), anyBoolean(), Mockito.eq(mController)); @@ -403,11 +406,12 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void tryToLaunchInAppRoutingIntent_componentNameNotNull_startActivity() { when(mDialogTransitionAnimator.createActivityTransitionController(any(View.class))) .thenReturn(mController); - mMediaOutputController.start(mCallback); + mMediaSwitchingController.start(mCallback); when(mLocalMediaManager.getLinkedItemComponentName()).thenReturn( new ComponentName(mPackageName, "")); - mMediaOutputController.tryToLaunchInAppRoutingIntent(TEST_DEVICE_1_ID, mDialogLaunchView); + mMediaSwitchingController.tryToLaunchInAppRoutingIntent( + TEST_DEVICE_1_ID, mDialogLaunchView); verify(mStarter).startActivity(any(Intent.class), anyBoolean(), Mockito.eq(mController)); @@ -415,9 +419,9 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onDevicesUpdated_unregistersNearbyDevicesCallback() throws RemoteException { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); - mMediaOutputController.onDevicesUpdated(ImmutableList.of()); + mMediaSwitchingController.onDevicesUpdated(ImmutableList.of()); verify(mNearbyMediaDevicesManager).unregisterNearbyDevicesCallback(any()); } @@ -425,11 +429,11 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onDeviceListUpdate_withNearbyDevices_updatesRangeInformation() throws RemoteException { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.onDevicesUpdated(mNearbyDevices); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDevicesUpdated(mNearbyDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_FAR); verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_CLOSE); @@ -438,11 +442,11 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onDeviceListUpdate_withNearbyDevices_rankByRangeInformation() throws RemoteException { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.onDevicesUpdated(mNearbyDevices); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDevicesUpdated(mNearbyDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); assertThat(mMediaDevices.get(0).getId()).isEqualTo(TEST_DEVICE_1_ID); } @@ -451,11 +455,11 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation() throws RemoteException { when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.onDevicesUpdated(mNearbyDevices); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDevicesUpdated(mNearbyDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); verify(mMediaDevice1, never()).setRangeZone(anyInt()); verify(mMediaDevice2, never()).setRangeZone(anyInt()); @@ -463,7 +467,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onDeviceListUpdate_verifyDeviceListCallback() { - // This test relies on mMediaOutputController.start being called while the selected device + // This test relies on mMediaSwitchingController.start being called while the selected + // device // list has exactly one item, and that item's id is: // - Different from both ids in mMediaDevices. // - Different from the id of the route published by the device under test (usually the @@ -475,12 +480,12 @@ public class MediaOutputControllerTest extends SysuiTestCase { .when(mLocalMediaManager) .getSelectedMediaDevice(); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); final List<MediaDevice> devices = new ArrayList<>(); - for (MediaItem item : mMediaOutputController.getMediaItemList()) { + for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { if (item.getMediaDevice().isPresent()) { devices.add(item.getMediaDevice().get()); } @@ -488,14 +493,15 @@ public class MediaOutputControllerTest extends SysuiTestCase { assertThat(devices.containsAll(mMediaDevices)).isTrue(); assertThat(devices.size()).isEqualTo(mMediaDevices.size()); - assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo( - mMediaDevices.size() + 2); + assertThat(mMediaSwitchingController.getMediaItemList().size()) + .isEqualTo(mMediaDevices.size() + 2); verify(mCb).onDeviceListChanged(); } @Test public void advanced_onDeviceListUpdateWithConnectedDeviceRemote_verifyItemSize() { - // This test relies on mMediaOutputController.start being called while the selected device + // This test relies on mMediaSwitchingController.start being called while the selected + // device // list has exactly one item, and that item's id is: // - Different from both ids in mMediaDevices. // - Different from the id of the route published by the device under test (usually the @@ -510,12 +516,12 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mMediaDevice1.getFeatures()).thenReturn( ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); final List<MediaDevice> devices = new ArrayList<>(); - for (MediaItem item : mMediaOutputController.getMediaItemList()) { + for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { if (item.getMediaDevice().isPresent()) { devices.add(item.getMediaDevice().get()); } @@ -523,23 +529,72 @@ public class MediaOutputControllerTest extends SysuiTestCase { assertThat(devices.containsAll(mMediaDevices)).isTrue(); assertThat(devices.size()).isEqualTo(mMediaDevices.size()); - assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo( - mMediaDevices.size() + 1); + assertThat(mMediaSwitchingController.getMediaItemList().size()) + .isEqualTo(mMediaDevices.size() + 1); verify(mCb).onDeviceListChanged(); } + @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) + @Test + public void onInputDeviceListUpdate_verifyDeviceListCallback() { + AudioDeviceInfo[] audioDeviceInfos = {}; + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) + .thenReturn(audioDeviceInfos); + mMediaSwitchingController.start(mCb); + + // Output devices have changed. + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + final int MAX_VOLUME = 1; + final int CURRENT_VOLUME = 0; + final boolean IS_VOLUME_FIXED = true; + final MediaDevice mediaDevice3 = + InputMediaDevice.create( + mContext, + TEST_DEVICE_3_ID, + AudioDeviceInfo.TYPE_BUILTIN_MIC, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED); + final MediaDevice mediaDevice4 = + InputMediaDevice.create( + mContext, + TEST_DEVICE_4_ID, + AudioDeviceInfo.TYPE_WIRED_HEADSET, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED); + final List<MediaDevice> inputDevices = new ArrayList<>(); + inputDevices.add(mediaDevice3); + inputDevices.add(mediaDevice4); + + // Input devices have changed. + mMediaSwitchingController.mInputDeviceCallback.onInputDeviceListUpdated(inputDevices); + + final List<MediaDevice> devices = new ArrayList<>(); + for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { + if (item.getMediaDevice().isPresent()) { + devices.add(item.getMediaDevice().get()); + } + } + + assertThat(devices).containsAtLeastElementsIn(mMediaDevices); + assertThat(devices).hasSize(mMediaDevices.size() + inputDevices.size()); + verify(mCb, atLeastOnce()).onDeviceListChanged(); + } + @Test public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() { when(mMediaDevice1.isSuggestedDevice()).thenReturn(true); when(mMediaDevice2.isSuggestedDevice()).thenReturn(false); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.getMediaItemList().clear(); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); final List<MediaDevice> devices = new ArrayList<>(); int dividerSize = 0; - for (MediaItem item : mMediaOutputController.getMediaItemList()) { + for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { if (item.getMediaDevice().isPresent()) { devices.add(item.getMediaDevice().get()); } @@ -556,33 +611,33 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.mIsRefreshing = true; + mMediaSwitchingController.mIsRefreshing = true; - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - assertThat(mMediaOutputController.mNeedRefresh).isTrue(); + assertThat(mMediaSwitchingController.mNeedRefresh).isTrue(); } @Test public void advanced_onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.mIsRefreshing = true; + mMediaSwitchingController.mIsRefreshing = true; - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - assertThat(mMediaOutputController.mNeedRefresh).isTrue(); + assertThat(mMediaSwitchingController.mNeedRefresh).isTrue(); } @Test public void cancelMuteAwaitConnection_cancelsWithMediaManager() { when(mAudioManager.getMutingExpectedDevice()).thenReturn(mock(AudioDeviceAttributes.class)); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.cancelMuteAwaitConnection(); + mMediaSwitchingController.cancelMuteAwaitConnection(); verify(mAudioManager).cancelMuteAwaitConnection(any()); } @@ -590,17 +645,17 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void cancelMuteAwaitConnection_audioManagerIsNull_noAction() { when(mAudioManager.getMutingExpectedDevice()).thenReturn(null); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.cancelMuteAwaitConnection(); + mMediaSwitchingController.cancelMuteAwaitConnection(); verify(mAudioManager, never()).cancelMuteAwaitConnection(any()); } @Test public void getAppSourceName_packageNameIsNull_returnsNull() { - MediaOutputController testMediaOutputController = - new MediaOutputController( + MediaSwitchingController testMediaSwitchingController = + new MediaSwitchingController( mSpyContext, "", mSpyContext.getUser(), @@ -617,25 +672,25 @@ public class MediaOutputControllerTest extends SysuiTestCase { mFlags, mVolumePanelGlobalStateInteractor, mUserTracker); - testMediaOutputController.start(mCb); + testMediaSwitchingController.start(mCb); reset(mCb); - testMediaOutputController.getAppSourceName(); + testMediaSwitchingController.getAppSourceName(); - assertThat(testMediaOutputController.getAppSourceName()).isNull(); + assertThat(testMediaSwitchingController.getAppSourceName()).isNull(); } @Test public void isActiveItem_deviceNotConnected_returnsFalse() { when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2); - assertThat(mMediaOutputController.isActiveItem(mMediaDevice1)).isFalse(); + assertThat(mMediaSwitchingController.isActiveItem(mMediaDevice1)).isFalse(); } @Test public void getNotificationSmallIcon_packageNameIsNull_returnsNull() { - MediaOutputController testMediaOutputController = - new MediaOutputController( + MediaSwitchingController testMediaSwitchingController = + new MediaSwitchingController( mSpyContext, "", mSpyContext.getUser(), @@ -652,23 +707,23 @@ public class MediaOutputControllerTest extends SysuiTestCase { mFlags, mVolumePanelGlobalStateInteractor, mUserTracker); - testMediaOutputController.start(mCb); + testMediaSwitchingController.start(mCb); reset(mCb); - testMediaOutputController.getAppSourceName(); + testMediaSwitchingController.getAppSourceName(); - assertThat(testMediaOutputController.getNotificationSmallIcon()).isNull(); + assertThat(testMediaSwitchingController.getNotificationSmallIcon()).isNull(); } @Test public void refreshDataSetIfNeeded_needRefreshIsTrue_setsToFalse() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.mNeedRefresh = true; + mMediaSwitchingController.mNeedRefresh = true; - mMediaOutputController.refreshDataSetIfNeeded(); + mMediaSwitchingController.refreshDataSetIfNeeded(); - assertThat(mMediaOutputController.mNeedRefresh).isFalse(); + assertThat(mMediaSwitchingController.mNeedRefresh).isFalse(); } @Test @@ -677,13 +732,13 @@ public class MediaOutputControllerTest extends SysuiTestCase { ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); - assertThat(mMediaOutputController.isCurrentConnectedDeviceRemote()).isTrue(); + assertThat(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).isTrue(); } @Test public void addDeviceToPlayMedia_callsLocalMediaManager() { - MediaOutputController testMediaOutputController = - new MediaOutputController( + MediaSwitchingController testMediaSwitchingController = + new MediaSwitchingController( mSpyContext, null, mSpyContext.getUser(), @@ -702,16 +757,16 @@ public class MediaOutputControllerTest extends SysuiTestCase { mUserTracker); LocalMediaManager mockLocalMediaManager = mock(LocalMediaManager.class); - testMediaOutputController.mLocalMediaManager = mockLocalMediaManager; + testMediaSwitchingController.mLocalMediaManager = mockLocalMediaManager; - testMediaOutputController.addDeviceToPlayMedia(mMediaDevice2); + testMediaSwitchingController.addDeviceToPlayMedia(mMediaDevice2); verify(mockLocalMediaManager).addDeviceToPlayMedia(mMediaDevice2); } @Test public void removeDeviceFromPlayMedia_callsLocalMediaManager() { - MediaOutputController testMediaOutputController = - new MediaOutputController( + MediaSwitchingController testMediaSwitchingController = + new MediaSwitchingController( mSpyContext, null, mSpyContext.getUser(), @@ -730,15 +785,15 @@ public class MediaOutputControllerTest extends SysuiTestCase { mUserTracker); LocalMediaManager mockLocalMediaManager = mock(LocalMediaManager.class); - testMediaOutputController.mLocalMediaManager = mockLocalMediaManager; + testMediaSwitchingController.mLocalMediaManager = mockLocalMediaManager; - testMediaOutputController.removeDeviceFromPlayMedia(mMediaDevice2); + testMediaSwitchingController.removeDeviceFromPlayMedia(mMediaDevice2); verify(mockLocalMediaManager).removeDeviceFromPlayMedia(mMediaDevice2); } @Test public void getDeselectableMediaDevice_triggersFromLocalMediaManager() { - mMediaOutputController.getDeselectableMediaDevice(); + mMediaSwitchingController.getDeselectableMediaDevice(); verify(mLocalMediaManager).getDeselectableMediaDevice(); } @@ -746,108 +801,108 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void adjustSessionVolume_adjustWithoutId_triggersFromLocalMediaManager() { int testVolume = 10; - mMediaOutputController.adjustSessionVolume(testVolume); + mMediaSwitchingController.adjustSessionVolume(testVolume); verify(mLocalMediaManager).adjustSessionVolume(testVolume); } @Test public void logInteractionAdjustVolume_triggersFromMetricLogger() { - MediaOutputMetricLogger spyMediaOutputMetricLogger = spy( - mMediaOutputController.mMetricLogger); - mMediaOutputController.mMetricLogger = spyMediaOutputMetricLogger; + MediaOutputMetricLogger spyMediaOutputMetricLogger = + spy(mMediaSwitchingController.mMetricLogger); + mMediaSwitchingController.mMetricLogger = spyMediaOutputMetricLogger; - mMediaOutputController.logInteractionAdjustVolume(mMediaDevice1); + mMediaSwitchingController.logInteractionAdjustVolume(mMediaDevice1); verify(spyMediaOutputMetricLogger).logInteractionAdjustVolume(mMediaDevice1); } @Test public void getSessionVolumeMax_triggersFromLocalMediaManager() { - mMediaOutputController.getSessionVolumeMax(); + mMediaSwitchingController.getSessionVolumeMax(); verify(mLocalMediaManager).getSessionVolumeMax(); } @Test public void getSessionVolume_triggersFromLocalMediaManager() { - mMediaOutputController.getSessionVolume(); + mMediaSwitchingController.getSessionVolume(); verify(mLocalMediaManager).getSessionVolume(); } @Test public void getSessionName_triggersFromLocalMediaManager() { - mMediaOutputController.getSessionName(); + mMediaSwitchingController.getSessionName(); verify(mLocalMediaManager).getSessionName(); } @Test public void releaseSession_triggersFromLocalMediaManager() { - mMediaOutputController.releaseSession(); + mMediaSwitchingController.releaseSession(); verify(mLocalMediaManager).releaseSession(); } @Test public void isAnyDeviceTransferring_noDevicesStateIsConnecting_returnsFalse() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - assertThat(mMediaOutputController.isAnyDeviceTransferring()).isFalse(); + assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isFalse(); } @Test public void isAnyDeviceTransferring_deviceStateIsConnecting_returnsTrue() { when(mMediaDevice1.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_CONNECTING); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - assertThat(mMediaOutputController.isAnyDeviceTransferring()).isTrue(); + assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isTrue(); } @Test public void isAnyDeviceTransferring_advancedLayoutSupport() { when(mMediaDevice1.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_CONNECTING); - mMediaOutputController.start(mCb); - mMediaOutputController.onDeviceListUpdate(mMediaDevices); + mMediaSwitchingController.start(mCb); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - assertThat(mMediaOutputController.isAnyDeviceTransferring()).isTrue(); + assertThat(mMediaSwitchingController.isAnyDeviceTransferring()).isTrue(); } @Test public void isPlaying_stateIsNull() { when(mSessionMediaController.getPlaybackState()).thenReturn(null); - assertThat(mMediaOutputController.isPlaying()).isFalse(); + assertThat(mMediaSwitchingController.isPlaying()).isFalse(); } @Test public void onSelectedDeviceStateChanged_verifyCallback() { when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.connectDevice(mMediaDevice1); + mMediaSwitchingController.connectDevice(mMediaDevice1); - mMediaOutputController.onSelectedDeviceStateChanged(mMediaDevice1, - LocalMediaManager.MediaDeviceState.STATE_CONNECTED); + mMediaSwitchingController.onSelectedDeviceStateChanged( + mMediaDevice1, LocalMediaManager.MediaDeviceState.STATE_CONNECTED); verify(mCb).onRouteChanged(); } @Test public void onDeviceAttributesChanged_verifyCallback() { - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.onDeviceAttributesChanged(); + mMediaSwitchingController.onDeviceAttributesChanged(); verify(mCb).onRouteChanged(); } @@ -855,11 +910,11 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onRequestFailed_verifyCallback() { when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); reset(mCb); - mMediaOutputController.connectDevice(mMediaDevice2); + mMediaSwitchingController.connectDevice(mMediaDevice2); - mMediaOutputController.onRequestFailed(0 /* reason */); + mMediaSwitchingController.onRequestFailed(0 /* reason */); verify(mCb, atLeastOnce()).onRouteChanged(); } @@ -868,37 +923,40 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void getHeaderTitle_withoutMetadata_returnDefaultString() { when(mSessionMediaController.getMetadata()).thenReturn(null); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); - assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo( - mContext.getText(R.string.controls_media_title)); + assertThat( + mMediaSwitchingController + .getHeaderTitle() + .equals(mContext.getText(R.string.controls_media_title))) + .isTrue(); } @Test public void getHeaderTitle_withMetadata_returnSongName() { when(mSessionMediaController.getMetadata()).thenReturn(mMediaMetadata); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); - assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(TEST_SONG); + assertThat(mMediaSwitchingController.getHeaderTitle().equals(TEST_SONG)).isTrue(); } @Test public void getHeaderSubTitle_withoutMetadata_returnNull() { when(mSessionMediaController.getMetadata()).thenReturn(null); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); - assertThat(mMediaOutputController.getHeaderSubTitle()).isNull(); + assertThat(mMediaSwitchingController.getHeaderSubTitle()).isNull(); } @Test public void getHeaderSubTitle_withMetadata_returnArtistName() { when(mSessionMediaController.getMetadata()).thenReturn(mMediaMetadata); - mMediaOutputController.start(mCb); + mMediaSwitchingController.start(mCb); - assertThat(mMediaOutputController.getHeaderSubTitle()).isEqualTo(TEST_ARTIST); + assertThat(mMediaSwitchingController.getHeaderSubTitle().equals(TEST_ARTIST)).isTrue(); } @Test @@ -911,8 +969,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { mRoutingSessionInfos.add(mRemoteSessionInfo); when(mLocalMediaManager.getRemoteRoutingSessions()).thenReturn(mRoutingSessionInfos); - assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).containsExactly( - mRemoteSessionInfo); + assertThat(mMediaSwitchingController.getActiveRemoteMediaDevices()) + .containsExactly(mRemoteSessionInfo); } @Test @@ -933,7 +991,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { selectableMediaDevices.add(selectableMediaDevice2); doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice(); doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice(); - final List<MediaDevice> groupMediaDevices = mMediaOutputController.getGroupMediaDevices(); + final List<MediaDevice> groupMediaDevices = + mMediaSwitchingController.getGroupMediaDevices(); // Reset order selectedMediaDevices.clear(); selectedMediaDevices.add(selectedMediaDevice2); @@ -941,7 +1000,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { selectableMediaDevices.clear(); selectableMediaDevices.add(selectableMediaDevice2); selectableMediaDevices.add(selectableMediaDevice1); - final List<MediaDevice> newDevices = mMediaOutputController.getGroupMediaDevices(); + final List<MediaDevice> newDevices = mMediaSwitchingController.getGroupMediaDevices(); assertThat(newDevices.size()).isEqualTo(groupMediaDevices.size()); for (int i = 0; i < groupMediaDevices.size(); i++) { @@ -970,7 +1029,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { selectableMediaDevices.add(selectableMediaDevice2); doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice(); doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice(); - final List<MediaDevice> groupMediaDevices = mMediaOutputController.getGroupMediaDevices(); + final List<MediaDevice> groupMediaDevices = + mMediaSwitchingController.getGroupMediaDevices(); // Reset order selectedMediaDevices.clear(); selectedMediaDevices.add(selectedMediaDevice2); @@ -979,7 +1039,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { selectableMediaDevices.add(selectableMediaDevice3); selectableMediaDevices.add(selectableMediaDevice2); selectableMediaDevices.add(selectableMediaDevice1); - final List<MediaDevice> newDevices = mMediaOutputController.getGroupMediaDevices(); + final List<MediaDevice> newDevices = mMediaSwitchingController.getGroupMediaDevices(); assertThat(newDevices.size()).isEqualTo(5); for (int i = 0; i < groupMediaDevices.size(); i++) { @@ -991,8 +1051,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void getNotificationLargeIcon_withoutPackageName_returnsNull() { - mMediaOutputController = - new MediaOutputController( + mMediaSwitchingController = + new MediaSwitchingController( mSpyContext, null, mSpyContext.getUser(), @@ -1010,7 +1070,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mVolumePanelGlobalStateInteractor, mUserTracker); - assertThat(mMediaOutputController.getNotificationIcon()).isNull(); + assertThat(mMediaSwitchingController.getNotificationIcon()).isNull(); } @Test @@ -1028,7 +1088,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(notification.isMediaNotification()).thenReturn(true); when(notification.getLargeIcon()).thenReturn(null); - assertThat(mMediaOutputController.getNotificationIcon()).isNull(); + assertThat(mMediaSwitchingController.getNotificationIcon()).isNull(); } @Test @@ -1047,7 +1107,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(notification.isMediaNotification()).thenReturn(true); when(notification.getLargeIcon()).thenReturn(icon); - assertThat(mMediaOutputController.getNotificationIcon()).isInstanceOf(IconCompat.class); + assertThat(mMediaSwitchingController.getNotificationIcon()).isInstanceOf(IconCompat.class); } @Test @@ -1066,7 +1126,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(notification.isMediaNotification()).thenReturn(false); when(notification.getLargeIcon()).thenReturn(icon); - assertThat(mMediaOutputController.getNotificationIcon()).isNull(); + assertThat(mMediaSwitchingController.getNotificationIcon()).isNull(); } @Test @@ -1084,7 +1144,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(notification.isMediaNotification()).thenReturn(true); when(notification.getSmallIcon()).thenReturn(null); - assertThat(mMediaOutputController.getNotificationSmallIcon()).isNull(); + assertThat(mMediaSwitchingController.getNotificationSmallIcon()).isNull(); } @Test @@ -1103,8 +1163,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(notification.isMediaNotification()).thenReturn(true); when(notification.getSmallIcon()).thenReturn(icon); - assertThat(mMediaOutputController.getNotificationSmallIcon()).isInstanceOf( - IconCompat.class); + assertThat(mMediaSwitchingController.getNotificationSmallIcon()) + .isInstanceOf(IconCompat.class); } @Test @@ -1112,8 +1172,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2); when(mMediaDevice1.getIcon()).thenReturn(mDrawable); - assertThat(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).isInstanceOf( - IconCompat.class); + assertThat(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1)) + .isInstanceOf(IconCompat.class); } @Test @@ -1121,13 +1181,13 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2); when(mMediaDevice1.getIcon()).thenReturn(null); - assertThat(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).isInstanceOf( - IconCompat.class); + assertThat(mMediaSwitchingController.getDeviceIconCompat(mMediaDevice1)) + .isInstanceOf(IconCompat.class); } @Test public void setColorFilter_setColorFilterToDrawable() { - mMediaOutputController.setColorFilter(mDrawable, true); + mMediaSwitchingController.setColorFilter(mDrawable, true); verify(mDrawable).setColorFilter(any(PorterDuffColorFilter.class)); } @@ -1150,11 +1210,11 @@ public class MediaOutputControllerTest extends SysuiTestCase { selectableMediaDevices.add(selectableMediaDevice2); doReturn(selectedMediaDevices).when(mLocalMediaManager).getSelectedMediaDevice(); doReturn(selectableMediaDevices).when(mLocalMediaManager).getSelectableMediaDevice(); - assertThat(mMediaOutputController.getGroupMediaDevices().isEmpty()).isFalse(); + assertThat(mMediaSwitchingController.getGroupMediaDevices().isEmpty()).isFalse(); - mMediaOutputController.resetGroupMediaDevices(); + mMediaSwitchingController.resetGroupMediaDevices(); - assertThat(mMediaOutputController.mGroupMediaDevices.isEmpty()).isTrue(); + assertThat(mMediaSwitchingController.mGroupMediaDevices.isEmpty()).isTrue(); } @Test @@ -1164,7 +1224,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mMediaDevice1.isVolumeFixed()).thenReturn(true); - assertThat(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).isFalse(); + assertThat(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).isFalse(); } @Test @@ -1174,7 +1234,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mMediaDevice1.isVolumeFixed()).thenReturn(false); - assertThat(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).isTrue(); + assertThat(mMediaSwitchingController.isVolumeControlEnabled(mMediaDevice1)).isTrue(); } @Test @@ -1187,7 +1247,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mMediaDevice2.getDeviceType()).thenReturn( MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE); - mMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2); + mMediaSwitchingController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2); verify(mPowerExemptionManager).addToTemporaryAllowList(anyString(), anyInt(), anyString(), anyLong()); @@ -1195,8 +1255,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void setTemporaryAllowListExceptionIfNeeded_packageNameIsNull_NoAction() { - MediaOutputController testMediaOutputController = - new MediaOutputController( + MediaSwitchingController testMediaSwitchingController = + new MediaSwitchingController( mSpyContext, null, mSpyContext.getUser(), @@ -1214,7 +1274,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mVolumePanelGlobalStateInteractor, mUserTracker); - testMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2); + testMediaSwitchingController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2); verify(mPowerExemptionManager, never()).addToTemporaryAllowList(anyString(), anyInt(), anyString(), @@ -1223,22 +1283,22 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onMetadataChanged_triggersOnMetadataChanged() { - mMediaOutputController.mCallback = this.mCallback; + mMediaSwitchingController.mCallback = this.mCallback; - mMediaOutputController.mCb.onMetadataChanged(mMediaMetadata); + mMediaSwitchingController.mCb.onMetadataChanged(mMediaMetadata); - verify(mMediaOutputController.mCallback).onMediaChanged(); + verify(mMediaSwitchingController.mCallback).onMediaChanged(); } @Test public void onPlaybackStateChanged_updateWithNullState_onMediaStoppedOrPaused() { when(mPlaybackState.getState()).thenReturn(PlaybackState.STATE_PLAYING); - mMediaOutputController.mCallback = this.mCallback; - mMediaOutputController.start(mCb); + mMediaSwitchingController.mCallback = this.mCallback; + mMediaSwitchingController.start(mCb); - mMediaOutputController.mCb.onPlaybackStateChanged(null); + mMediaSwitchingController.mCb.onPlaybackStateChanged(null); - verify(mMediaOutputController.mCallback).onMediaStoppedOrPaused(); + verify(mMediaSwitchingController.mCallback).onMediaStoppedOrPaused(); } @Test @@ -1246,9 +1306,9 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mDialogTransitionAnimator.createActivityTransitionController(mDialogLaunchView)) .thenReturn(mActivityTransitionAnimatorController); when(mKeyguardManager.isKeyguardLocked()).thenReturn(true); - mMediaOutputController.mCallback = this.mCallback; + mMediaSwitchingController.mCallback = this.mCallback; - mMediaOutputController.launchBluetoothPairing(mDialogLaunchView); + mMediaSwitchingController.launchBluetoothPairing(mDialogLaunchView); verify(mCallback).dismissDialog(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt index 70af5e75105d..755adc68192a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.test.assert import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onChildAt import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag @@ -40,6 +39,7 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.SizedTileImpl +import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.TileCategory @@ -57,7 +57,7 @@ class DragAndDropTest : SysuiTestCase() { @Composable private fun EditTileGridUnderTest( listState: EditTileListState, - onSetTiles: (List<TileSpec>) -> Unit + onSetTiles: (List<TileSpec>) -> Unit, ) { DefaultEditTileGrid( currentListState = listState, @@ -182,7 +182,7 @@ class DragAndDropTest : SysuiTestCase() { private fun ComposeContentTestRule.assertTileGridContainsExactly(specs: List<String>) { onNodeWithTag(CURRENT_TILES_GRID_TEST_TAG).onChildren().apply { fetchSemanticsNodes().forEachIndexed { index, _ -> - get(index).onChildAt(0).assert(hasContentDescription(specs[index])) + get(index).assert(hasContentDescription(specs[index])) } } } @@ -198,7 +198,7 @@ class DragAndDropTest : SysuiTestCase() { icon = Icon.Resource( android.R.drawable.star_on, - ContentDescription.Loaded(tileSpec) + ContentDescription.Loaded(tileSpec), ), label = AnnotatedString(tileSpec), appName = null, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index af043093b6f7..c710c56fd516 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -168,7 +168,7 @@ import com.android.systemui.statusbar.OperatorNameViewController; import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.StatusBarStateControllerImpl; -import com.android.systemui.statusbar.core.StatusBarInitializer; +import com.android.systemui.statusbar.core.StatusBarInitializerImpl; import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.NotificationActivityStarter; @@ -504,7 +504,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mock(FragmentService.class), mLightBarController, mAutoHideController, - new StatusBarInitializer( + new StatusBarInitializerImpl( mStatusBarWindowController, mCollapsedStatusBarFragmentProvider, emptySet()), diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 1e2648b228f3..ecc7909a857a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -20,7 +20,6 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; -import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER; import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN; import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN; import static com.android.systemui.volume.VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST; @@ -51,7 +50,6 @@ import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.media.AudioSystem; import android.os.SystemClock; -import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.TestableLooper; @@ -285,23 +283,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { } @Test - @DisableFlags(FLAG_HAPTIC_VOLUME_SLIDER) - public void addSliderHaptics_withHapticsDisabled_doesNotDeliverOnProgressChangedHaptics() { - // GIVEN that the slider haptics flag is disabled and we try to add haptics to volume rows - mDialog.addSliderHapticsToRows(); - - // WHEN haptics try to be delivered to a volume stream - boolean canDeliverHaptics = - mDialog.canDeliverProgressHapticsToStream(AudioSystem.STREAM_MUSIC, true, 50); - - // THEN the result is that haptics are not successfully delivered - assertFalse(canDeliverHaptics); - } - - @Test - @EnableFlags(FLAG_HAPTIC_VOLUME_SLIDER) - public void addSliderHaptics_withHapticsEnabled_canDeliverOnProgressChangedHaptics() { - // GIVEN that the slider haptics flag is enabled and we try to add haptics to volume rows + public void addSliderHaptics_canDeliverOnProgressChangedHaptics() { + // GIVEN that the slider haptics are added to rows mDialog.addSliderHapticsToRows(); // WHEN haptics try to be delivered to a volume stream diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index 649e4e8a6f7e..1b1d8c5d0f63 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -25,6 +25,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer +import com.android.systemui.haptics.msdl.bouncerHapticPlayer import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -34,9 +36,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.StateFlow val Kosmos.bouncerUserActionsViewModel by Fixture { - BouncerUserActionsViewModel( - bouncerInteractor = bouncerInteractor, - ) + BouncerUserActionsViewModel(bouncerInteractor = bouncerInteractor) } val Kosmos.bouncerUserActionsViewModelFactory by Fixture { @@ -59,6 +59,7 @@ val Kosmos.bouncerSceneContentViewModel by Fixture { pinViewModelFactory = pinBouncerViewModelFactory, patternViewModelFactory = patternBouncerViewModelFactory, passwordViewModelFactory = passwordBouncerViewModelFactory, + bouncerHapticPlayer = bouncerHapticPlayer, ) } @@ -76,6 +77,7 @@ val Kosmos.pinBouncerViewModelFactory by Fixture { isInputEnabled: StateFlow<Boolean>, onIntentionalUserInput: () -> Unit, authenticationMethod: AuthenticationMethodModel, + bouncerHapticPlayer: BouncerHapticPlayer, ): PinBouncerViewModel { return PinBouncerViewModel( applicationContext = applicationContext, @@ -84,6 +86,7 @@ val Kosmos.pinBouncerViewModelFactory by Fixture { isInputEnabled = isInputEnabled, onIntentionalUserInput = onIntentionalUserInput, authenticationMethod = authenticationMethod, + bouncerHapticPlayer = bouncerHapticPlayer, ) } } @@ -92,6 +95,7 @@ val Kosmos.pinBouncerViewModelFactory by Fixture { val Kosmos.patternBouncerViewModelFactory by Fixture { object : PatternBouncerViewModel.Factory { override fun create( + bouncerHapticPlayer: BouncerHapticPlayer, isInputEnabled: StateFlow<Boolean>, onIntentionalUserInput: () -> Unit, ): PatternBouncerViewModel { @@ -100,6 +104,7 @@ val Kosmos.patternBouncerViewModelFactory by Fixture { interactor = bouncerInteractor, isInputEnabled = isInputEnabled, onIntentionalUserInput = onIntentionalUserInput, + bouncerHapticPlayer = bouncerHapticPlayer, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt index 1ed10fbe94c3..8922b2f5c5ef 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.authentication.domain.interactor.authenticationInter import com.android.systemui.deviceentry.data.repository.deviceEntryRepository import com.android.systemui.flags.fakeSystemPropertiesHelper import com.android.systemui.flags.systemPropertiesHelper +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.trustInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -37,5 +38,6 @@ val Kosmos.deviceUnlockedInteractor by Fixture { powerInteractor = powerInteractor, biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor, systemPropertiesHelper = fakeSystemPropertiesHelper, + keyguardTransitionInteractor = keyguardTransitionInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt index 1d2bce2f9b99..1df3ef48d5a7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt @@ -19,7 +19,7 @@ package com.android.systemui.education.data.repository import com.android.systemui.kosmos.Kosmos import java.time.Instant -var Kosmos.contextualEducationRepository: ContextualEducationRepository by +var Kosmos.contextualEducationRepository: FakeContextualEducationRepository by Kosmos.Fixture { FakeContextualEducationRepository() } var Kosmos.fakeEduClock: FakeEduClock by Kosmos.Fixture { FakeEduClock(Instant.MIN) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt index fb4e2fb5fd14..4667bf5292fa 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt @@ -17,39 +17,77 @@ package com.android.systemui.education.data.repository import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.contextualeducation.GestureType.ALL_APPS +import com.android.systemui.contextualeducation.GestureType.BACK +import com.android.systemui.contextualeducation.GestureType.HOME +import com.android.systemui.contextualeducation.GestureType.OVERVIEW import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull class FakeContextualEducationRepository : ContextualEducationRepository { - private val userGestureMap = mutableMapOf<Int, GestureEduModel>() - private val _gestureEduModels = MutableStateFlow(GestureEduModel(userId = 0)) - private val gestureEduModelsFlow = _gestureEduModels.asStateFlow() + private val userGestureMap = mutableMapOf<Int, MutableMap<GestureType, GestureEduModel>>() + + private val _backGestureEduModels = MutableStateFlow(GestureEduModel(BACK, userId = 0)) + private val backGestureEduModelsFlow = _backGestureEduModels.asStateFlow() + + private val _homeGestureEduModels = MutableStateFlow(GestureEduModel(HOME, userId = 0)) + private val homeEduModelsFlow = _homeGestureEduModels.asStateFlow() + + private val _allAppsGestureEduModels = MutableStateFlow(GestureEduModel(ALL_APPS, userId = 0)) + private val allAppsGestureEduModels = _allAppsGestureEduModels.asStateFlow() + + private val _overviewsGestureEduModels = MutableStateFlow(GestureEduModel(OVERVIEW, userId = 0)) + private val overviewsGestureEduModels = _overviewsGestureEduModels.asStateFlow() private val userEduDeviceConnectionTimeMap = mutableMapOf<Int, EduDeviceConnectionTime>() private val _eduDeviceConnectionTime = MutableStateFlow(EduDeviceConnectionTime()) private val eduDeviceConnectionTime = _eduDeviceConnectionTime.asStateFlow() + private val _keyboardShortcutTriggered = MutableStateFlow<GestureType?>(null) + private var currentUser: Int = 0 override fun setUser(userId: Int) { if (!userGestureMap.contains(userId)) { - userGestureMap[userId] = GestureEduModel(userId = userId) + userGestureMap[userId] = createGestureEduModelMap(userId = userId) userEduDeviceConnectionTimeMap[userId] = EduDeviceConnectionTime() } // save data of current user to the map - userGestureMap[currentUser] = _gestureEduModels.value - userEduDeviceConnectionTimeMap[currentUser] = _eduDeviceConnectionTime.value + val currentUserMap = userGestureMap[currentUser]!! + currentUserMap[BACK] = _backGestureEduModels.value + currentUserMap[HOME] = _homeGestureEduModels.value + currentUserMap[ALL_APPS] = _allAppsGestureEduModels.value + currentUserMap[OVERVIEW] = _overviewsGestureEduModels.value + // switch to data of new user - _gestureEduModels.value = userGestureMap[userId]!! + val newUserGestureMap = userGestureMap[userId]!! + newUserGestureMap[BACK]?.let { _backGestureEduModels.value = it } + newUserGestureMap[HOME]?.let { _homeGestureEduModels.value = it } + newUserGestureMap[ALL_APPS]?.let { _allAppsGestureEduModels.value = it } + newUserGestureMap[OVERVIEW]?.let { _overviewsGestureEduModels.value = it } + + userEduDeviceConnectionTimeMap[currentUser] = _eduDeviceConnectionTime.value _eduDeviceConnectionTime.value = userEduDeviceConnectionTimeMap[userId]!! } + private fun createGestureEduModelMap(userId: Int): MutableMap<GestureType, GestureEduModel> { + val gestureModelMap = mutableMapOf<GestureType, GestureEduModel>() + GestureType.values().forEach { gestureModelMap[it] = GestureEduModel(it, userId = userId) } + return gestureModelMap + } + override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> { - return gestureEduModelsFlow + return when (gestureType) { + BACK -> backGestureEduModelsFlow + HOME -> homeEduModelsFlow + ALL_APPS -> allAppsGestureEduModels + OVERVIEW -> overviewsGestureEduModels + } } override fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> { @@ -60,8 +98,16 @@ class FakeContextualEducationRepository : ContextualEducationRepository { gestureType: GestureType, transform: (GestureEduModel) -> GestureEduModel ) { - val currentModel = _gestureEduModels.value - _gestureEduModels.value = transform(currentModel) + val gestureModels = + when (gestureType) { + BACK -> _backGestureEduModels + HOME -> _homeGestureEduModels + ALL_APPS -> _allAppsGestureEduModels + OVERVIEW -> _overviewsGestureEduModels + } + + val currentModel = gestureModels.value + gestureModels.value = transform(currentModel) } override suspend fun updateEduDeviceConnectionTime( @@ -70,4 +116,11 @@ class FakeContextualEducationRepository : ContextualEducationRepository { val currentModel = _eduDeviceConnectionTime.value _eduDeviceConnectionTime.value = transform(currentModel) } + + override val keyboardShortcutTriggered: Flow<GestureType> + get() = _keyboardShortcutTriggered.filterNotNull() + + fun setKeyboardShortcutTriggered(gestureType: GestureType) { + _keyboardShortcutTriggered.value = gestureType + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt index 80f6fc24ef2c..2d275f9e9691 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -40,8 +40,7 @@ var Kosmos.keyboardTouchpadEduInteractor by touchpadRepository, userRepository ), - clock = fakeEduClock, - inputManager = mockEduInputManager + clock = fakeEduClock ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt index c252924f4d2d..c0152b26d7a3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt @@ -17,7 +17,6 @@ package com.android.systemui.flags import android.platform.test.annotations.EnableFlags -import com.android.systemui.Flags.FLAG_COMPOSE_LOCKSCREEN import com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR @@ -31,7 +30,6 @@ import com.android.systemui.Flags.FLAG_SCENE_CONTAINER * that feature. It is also picked up by [SceneContainerRule] to set non-aconfig prerequisites. */ @EnableFlags( - FLAG_COMPOSE_LOCKSCREEN, FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, FLAG_KEYGUARD_WM_STATE_REFACTOR, FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt index 5ad973a54252..2b81da33b9bc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt @@ -20,8 +20,10 @@ import com.google.android.msdl.data.model.FeedbackLevel import com.google.android.msdl.data.model.MSDLToken import com.google.android.msdl.domain.InteractionProperties import com.google.android.msdl.domain.MSDLPlayer +import com.google.android.msdl.logging.MSDLEvent class FakeMSDLPlayer : MSDLPlayer { + private val history = arrayListOf<MSDLEvent>() var currentFeedbackLevel = FeedbackLevel.DEFAULT var latestTokenPlayed: MSDLToken? = null private set @@ -34,5 +36,8 @@ class FakeMSDLPlayer : MSDLPlayer { override fun playToken(token: MSDLToken, properties: InteractionProperties?) { latestTokenPlayed = token latestPropertiesPlayed = properties + history.add(MSDLEvent(token, properties)) } + + override fun getHistory(): List<MSDLEvent> = history } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt index ca748b661d04..80db1e9b5e29 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/qs/QSLongPressEffectKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.haptics.qs +import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.Kosmos import com.android.systemui.log.core.FakeLogBuffer @@ -26,6 +27,7 @@ val Kosmos.qsLongPressEffect by QSLongPressEffect( vibratorHelper, keyguardStateController, + fakeFalsingManager, FakeLogBuffer.Factory.create(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt deleted file mode 100644 index edbc4c1cd65e..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorKosmos.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.interactor - -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.log.core.FakeLogBuffer -import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor - -val Kosmos.gridConsistencyInteractor by - Kosmos.Fixture { - GridConsistencyInteractor( - gridLayoutTypeInteractor, - currentTilesInteractor, - gridConsistencyInteractorsMap, - noopGridConsistencyInteractor, - FakeLogBuffer.Factory.create(), - applicationCoroutineScope, - ) - } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt index 34e99d3a9a3c..c9516429553b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorKosmos.kt @@ -27,6 +27,3 @@ val Kosmos.gridLayoutTypeInteractor by val Kosmos.gridLayoutMap: Map<GridLayoutType, GridLayout> by Kosmos.Fixture { mapOf(Pair(InfiniteGridLayoutType, infiniteGridLayout)) } - -var Kosmos.gridConsistencyInteractorsMap: Map<GridLayoutType, GridTypeConsistencyInteractor> by - Kosmos.Fixture { mapOf(Pair(InfiniteGridLayoutType, infiniteGridConsistencyInteractor)) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt deleted file mode 100644 index 320c2ec1bb99..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.interactor - -import com.android.systemui.kosmos.Kosmos - -val Kosmos.infiniteGridConsistencyInteractor by - Kosmos.Fixture { - InfiniteGridConsistencyInteractor(iconTilesInteractor, fixedColumnsSizeInteractor) - } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt index be00152cb511..3f62b4d9f9cb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt @@ -17,7 +17,7 @@ package com.android.systemui.qs.panels.domain.interactor import com.android.systemui.kosmos.Kosmos -import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout +import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt index c416ea1c1b39..91602c23ac17 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt @@ -16,8 +16,13 @@ package com.android.systemui.statusbar.data.repository +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf class FakeRemoteInputRepository : RemoteInputRepository { override val isRemoteInputActive = MutableStateFlow(false) + override val remoteInputRowBottomBound: Flow<Float?> = flowOf(null) + + override fun setRemoteInputRowBottomBound(bottom: Float?) {} } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt index 6370a5d9c80b..7244d465ed7e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor val Kosmos.notificationScrollViewModel by Fixture { @@ -29,6 +30,7 @@ val Kosmos.notificationScrollViewModel by Fixture { dumpManager = dumpManager, stackAppearanceInteractor = notificationStackAppearanceInteractor, shadeInteractor = shadeInteractor, + remoteInputInteractor = remoteInputInteractor, sceneInteractor = sceneInteractor, keyguardInteractor = { keyguardInteractor }, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index 8bfc390ecfa3..e5cf0a90ebbd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor @@ -31,6 +32,7 @@ val Kosmos.notificationsPlaceholderViewModel by Fixture { sceneInteractor = sceneInteractor, shadeInteractor = shadeInteractor, headsUpNotificationInteractor = headsUpNotificationInteractor, + remoteInputInteractor = remoteInputInteractor, featureFlags = featureFlagsClassic, dumpManager = dumpManager, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt index 61b53c9a2067..99cd8309631e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt @@ -22,6 +22,8 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository +import com.android.systemui.statusbar.policy.data.repository.deviceProvisioningRepository +import com.android.systemui.statusbar.policy.data.repository.userSetupRepository import com.android.systemui.statusbar.policy.data.repository.zenModeRepository val Kosmos.zenModeInteractor by Fixture { @@ -31,5 +33,7 @@ val Kosmos.zenModeInteractor by Fixture { notificationSettingsRepository = notificationSettingsRepository, bgDispatcher = testDispatcher, iconLoader = zenIconLoader, + deviceProvisioningRepository = deviceProvisioningRepository, + userSetupRepository = userSetupRepository, ) } diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index d1a3bf9b529f..10e4f3820cd7 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -343,6 +343,8 @@ android_ravenwood_libgroup { data: [ ":framework-res", ":ravenwood-empty-res", + ":framework-platform-compat-config", + ":services-platform-compat-config", ], libs: [ "100-framework-minus-apex.ravenwood", diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java index c3b7087a44c3..1f98334bb8ce 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java @@ -16,7 +16,15 @@ package com.android.server.appfunctions; +import android.annotation.NonNull; +import android.os.UserHandle; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; + import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -33,5 +41,50 @@ public final class AppFunctionExecutors { /* unit= */ TimeUnit.SECONDS, /* workQueue= */ new LinkedBlockingQueue<>()); + /** A map of per-user executors for queued work. */ + @GuardedBy("sLock") + private static final SparseArray<ExecutorService> mPerUserExecutorsLocked = new SparseArray<>(); + + private static final Object sLock = new Object(); + + /** + * Returns a per-user executor for queued metadata sync request. + * + * <p>The work submitted to these executor (Sync request) needs to be synchronous per user hence + * the use of a single thread. + * + * <p>Note: Use a different executor if not calling {@code submitSyncRequest} on a {@code + * MetadataSyncAdapter}. + */ + // TODO(b/357551503): Restrict the scope of this executor to the MetadataSyncAdapter itself. + public static ExecutorService getPerUserSyncExecutor(@NonNull UserHandle user) { + synchronized (sLock) { + ExecutorService executor = mPerUserExecutorsLocked.get(user.getIdentifier(), null); + if (executor == null) { + executor = Executors.newSingleThreadExecutor(); + mPerUserExecutorsLocked.put(user.getIdentifier(), executor); + } + return executor; + } + } + + /** + * Shuts down and removes the per-user executor for queued work. + * + * <p>This should be called when the user is removed. + */ + public static void shutDownAndRemoveUserExecutor(@NonNull UserHandle user) + throws InterruptedException { + ExecutorService executor; + synchronized (sLock) { + executor = mPerUserExecutorsLocked.get(user.getIdentifier()); + mPerUserExecutorsLocked.remove(user.getIdentifier()); + } + if (executor != null) { + executor.shutdown(); + var unused = executor.awaitTermination(30, TimeUnit.SECONDS); + } + } + private AppFunctionExecutors() {} } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java index 02800cbf4f3d..c293087defb6 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java @@ -16,6 +16,7 @@ package com.android.server.appfunctions; +import android.annotation.NonNull; import android.app.appfunctions.AppFunctionManagerConfiguration; import android.content.Context; @@ -36,4 +37,14 @@ public class AppFunctionManagerService extends SystemService { publishBinderService(Context.APP_FUNCTION_SERVICE, mServiceImpl); } } + + @Override + public void onUserUnlocked(@NonNull TargetUser user) { + mServiceImpl.onUserUnlocked(user); + } + + @Override + public void onUserStopping(@NonNull TargetUser user) { + mServiceImpl.onUserStopping(user); + } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 2362b91c826e..cf039df3bc96 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -19,29 +19,35 @@ package com.android.server.appfunctions; import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_EXECUTOR; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.appfunctions.AppFunctionStaticMetadataHelper; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; import android.app.appfunctions.ExecuteAppFunctionResponse; import android.app.appfunctions.IAppFunctionManager; import android.app.appfunctions.IAppFunctionService; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback; +import android.app.appsearch.AppSearchManager; +import android.app.appsearch.AppSearchResult; +import android.app.appsearch.observer.DocumentChangeInfo; +import android.app.appsearch.observer.ObserverCallback; +import android.app.appsearch.observer.ObserverSpec; +import android.app.appsearch.observer.SchemaChangeInfo; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.UserHandle; import android.text.TextUtils; import android.util.Slog; -import android.app.appsearch.AppSearchResult; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.SystemService.TargetUser; import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback; import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener; +import java.io.IOException; import java.util.Objects; import java.util.concurrent.CompletionException; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; /** Implementation of the AppFunctionManagerService. */ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { @@ -51,9 +57,11 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { private final CallerValidator mCallerValidator; private final ServiceHelper mInternalServiceHelper; private final ServiceConfig mServiceConfig; + private final Context mContext; public AppFunctionManagerServiceImpl(@NonNull Context context) { this( + context, new RemoteServiceCallerImpl<>( context, IAppFunctionService.Stub::asInterface, THREAD_POOL_EXECUTOR), new CallerValidatorImpl(context), @@ -63,10 +71,12 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { @VisibleForTesting AppFunctionManagerServiceImpl( + Context context, RemoteServiceCaller<IAppFunctionService> remoteServiceCaller, CallerValidator callerValidator, ServiceHelper appFunctionInternalServiceHelper, ServiceConfig serviceConfig) { + mContext = Objects.requireNonNull(context); mRemoteServiceCaller = Objects.requireNonNull(remoteServiceCaller); mCallerValidator = Objects.requireNonNull(callerValidator); mInternalServiceHelper = Objects.requireNonNull(appFunctionInternalServiceHelper); @@ -90,6 +100,26 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } } + /** Called when the user is unlocked. */ + public void onUserUnlocked(TargetUser user) { + Objects.requireNonNull(user); + + registerAppSearchObserver(user); + trySyncRuntimeMetadata(user); + } + + /** Called when the user is stopping. */ + public void onUserStopping(@NonNull TargetUser user) { + Objects.requireNonNull(user); + + try { + AppFunctionExecutors.shutDownAndRemoveUserExecutor(user.getUserHandle()); + MetadataSyncPerUser.removeUserSyncAdapter(user.getUserHandle()); + } catch (InterruptedException e) { + Slog.e(TAG, "Unable to remove data for: " + user.getUserHandle(), e); + } + } + private void executeAppFunctionInternal( ExecuteAppFunctionAidlRequest requestInternal, SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) { @@ -132,53 +162,55 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { return; } - var unused = mCallerValidator - .verifyCallerCanExecuteAppFunction( - validatedCallingPackage, - targetPackageName, - requestInternal.getClientRequest().getFunctionIdentifier()) - .thenAccept( - canExecute -> { - if (!canExecute) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_DENIED, - "Caller does not have permission to execute the" - + " appfunction", - /* extras= */ null)); - return; - } - Intent serviceIntent = - mInternalServiceHelper.resolveAppFunctionService( - targetPackageName, targetUser); - if (serviceIntent == null) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, - "Cannot find the target service.", - /* extras= */ null)); - return; - } - final long token = Binder.clearCallingIdentity(); - try { - bindAppFunctionServiceUnchecked( - requestInternal, - serviceIntent, - targetUser, - safeExecuteAppFunctionCallback, - /* bindFlags= */ Context.BIND_AUTO_CREATE, - /* timeoutInMillis= */ mServiceConfig - .getExecuteAppFunctionTimeoutMillis()); - } finally { - Binder.restoreCallingIdentity(token); - } - }) - .exceptionally( - ex -> { - safeExecuteAppFunctionCallback.onResult( - mapExceptionToExecuteAppFunctionResponse(ex)); - return null; - }); + var unused = + mCallerValidator + .verifyCallerCanExecuteAppFunction( + validatedCallingPackage, + targetPackageName, + requestInternal.getClientRequest().getFunctionIdentifier()) + .thenAccept( + canExecute -> { + if (!canExecute) { + safeExecuteAppFunctionCallback.onResult( + ExecuteAppFunctionResponse.newFailure( + ExecuteAppFunctionResponse.RESULT_DENIED, + "Caller does not have permission to execute" + + " the appfunction", + /* extras= */ null)); + return; + } + Intent serviceIntent = + mInternalServiceHelper.resolveAppFunctionService( + targetPackageName, targetUser); + if (serviceIntent == null) { + safeExecuteAppFunctionCallback.onResult( + ExecuteAppFunctionResponse.newFailure( + ExecuteAppFunctionResponse + .RESULT_INTERNAL_ERROR, + "Cannot find the target service.", + /* extras= */ null)); + return; + } + final long token = Binder.clearCallingIdentity(); + try { + bindAppFunctionServiceUnchecked( + requestInternal, + serviceIntent, + targetUser, + safeExecuteAppFunctionCallback, + /* bindFlags= */ Context.BIND_AUTO_CREATE, + /* timeoutInMillis= */ mServiceConfig + .getExecuteAppFunctionTimeoutMillis()); + } finally { + Binder.restoreCallingIdentity(token); + } + }) + .exceptionally( + ex -> { + safeExecuteAppFunctionCallback.onResult( + mapExceptionToExecuteAppFunctionResponse(ex)); + return null; + }); } private void bindAppFunctionServiceUnchecked( @@ -256,7 +288,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) { - if(e instanceof CompletionException) { + if (e instanceof CompletionException) { e = e.getCause(); } @@ -291,4 +323,103 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } return ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR; } + + private void registerAppSearchObserver(@NonNull TargetUser user) { + AppSearchManager perUserAppSearchManager = + mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0) + .getSystemService(AppSearchManager.class); + if (perUserAppSearchManager == null) { + Slog.d(TAG, "AppSearch Manager not found for user: " + user.getUserIdentifier()); + return; + } + try (FutureGlobalSearchSession futureGlobalSearchSession = + new FutureGlobalSearchSession( + perUserAppSearchManager, AppFunctionExecutors.THREAD_POOL_EXECUTOR)) { + AppFunctionMetadataObserver appFunctionMetadataObserver = + new AppFunctionMetadataObserver( + user.getUserHandle(), + mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0)); + var unused = + futureGlobalSearchSession + .registerObserverCallbackAsync( + "android", + new ObserverSpec.Builder().build(), + THREAD_POOL_EXECUTOR, + appFunctionMetadataObserver) + .whenComplete( + (voidResult, ex) -> { + if (ex != null) { + Slog.e(TAG, "Failed to register observer: ", ex); + } + }); + + } catch (IOException ex) { + Slog.e(TAG, "Failed to close observer session: ", ex); + } + } + + private void trySyncRuntimeMetadata(@NonNull TargetUser user) { + MetadataSyncAdapter metadataSyncAdapter = + MetadataSyncPerUser.getPerUserMetadataSyncAdapter( + user.getUserHandle(), + mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0)); + if (metadataSyncAdapter != null) { + var unused = + metadataSyncAdapter + .submitSyncRequest() + .whenComplete( + (isSuccess, ex) -> { + if (ex != null || !isSuccess) { + Slog.e(TAG, "Sync was not successful"); + } + }); + } + } + + private static class AppFunctionMetadataObserver implements ObserverCallback { + @Nullable private final MetadataSyncAdapter mPerUserMetadataSyncAdapter; + + AppFunctionMetadataObserver(@NonNull UserHandle userHandle, @NonNull Context userContext) { + mPerUserMetadataSyncAdapter = + MetadataSyncPerUser.getPerUserMetadataSyncAdapter(userHandle, userContext); + } + + @Override + public void onDocumentChanged(@NonNull DocumentChangeInfo documentChangeInfo) { + if (mPerUserMetadataSyncAdapter == null) { + return; + } + if (documentChangeInfo + .getDatabaseName() + .equals(AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB) + && documentChangeInfo + .getNamespace() + .equals( + AppFunctionStaticMetadataHelper + .APP_FUNCTION_STATIC_NAMESPACE)) { + var unused = mPerUserMetadataSyncAdapter.submitSyncRequest(); + } + } + + @Override + public void onSchemaChanged(@NonNull SchemaChangeInfo schemaChangeInfo) { + if (mPerUserMetadataSyncAdapter == null) { + return; + } + if (schemaChangeInfo + .getDatabaseName() + .equals(AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)) { + boolean shouldInitiateSync = false; + for (String schemaName : schemaChangeInfo.getChangedSchemaNames()) { + if (schemaName.startsWith(AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE)) { + shouldInitiateSync = true; + break; + } + } + if (shouldInitiateSync) { + var unused = mPerUserMetadataSyncAdapter.submitSyncRequest(); + } + } + } + } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java index e2573590bf5d..8c6f50e5c1bd 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java @@ -24,6 +24,8 @@ import android.annotation.WorkerThread; import android.app.appfunctions.AppFunctionRuntimeMetadata; import android.app.appfunctions.AppFunctionStaticMetadataHelper; import android.app.appsearch.AppSearchBatchResult; +import android.app.appsearch.AppSearchManager; +import android.app.appsearch.AppSearchManager.SearchContext; import android.app.appsearch.AppSearchResult; import android.app.appsearch.AppSearchSchema; import android.app.appsearch.PackageIdentifier; @@ -61,9 +63,8 @@ import java.util.concurrent.Executor; */ public class MetadataSyncAdapter { private static final String TAG = MetadataSyncAdapter.class.getSimpleName(); - private final FutureAppSearchSession mRuntimeMetadataSearchSession; - private final FutureAppSearchSession mStaticMetadataSearchSession; private final Executor mSyncExecutor; + private final AppSearchManager mAppSearchManager; private final PackageManager mPackageManager; // Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility @@ -73,13 +74,11 @@ public class MetadataSyncAdapter { public MetadataSyncAdapter( @NonNull Executor syncExecutor, - @NonNull FutureAppSearchSession runtimeMetadataSearchSession, - @NonNull FutureAppSearchSession staticMetadataSearchSession, - @NonNull PackageManager packageManager) { + @NonNull PackageManager packageManager, + @NonNull AppSearchManager appSearchManager) { mSyncExecutor = Objects.requireNonNull(syncExecutor); - mRuntimeMetadataSearchSession = Objects.requireNonNull(runtimeMetadataSearchSession); - mStaticMetadataSearchSession = Objects.requireNonNull(staticMetadataSearchSession); mPackageManager = Objects.requireNonNull(packageManager); + mAppSearchManager = Objects.requireNonNull(appSearchManager); } /** @@ -89,31 +88,54 @@ public class MetadataSyncAdapter { * synchronization was successful. */ public AndroidFuture<Boolean> submitSyncRequest() { + SearchContext staticMetadataSearchContext = + new SearchContext.Builder( + AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB) + .build(); + SearchContext runtimeMetadataSearchContext = + new SearchContext.Builder( + AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB) + .build(); AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>(); mSyncExecutor.execute( () -> { - try { - trySyncAppFunctionMetadataBlocking(); + try (FutureAppSearchSession staticMetadataSearchSession = + new FutureAppSearchSessionImpl( + mAppSearchManager, + AppFunctionExecutors.THREAD_POOL_EXECUTOR, + staticMetadataSearchContext); + FutureAppSearchSession runtimeMetadataSearchSession = + new FutureAppSearchSessionImpl( + mAppSearchManager, + AppFunctionExecutors.THREAD_POOL_EXECUTOR, + runtimeMetadataSearchContext)) { + + trySyncAppFunctionMetadataBlocking( + staticMetadataSearchSession, runtimeMetadataSearchSession); settableSyncStatus.complete(true); - } catch (Exception e) { - settableSyncStatus.completeExceptionally(e); + + } catch (Exception ex) { + settableSyncStatus.completeExceptionally(ex); } }); return settableSyncStatus; } @WorkerThread - private void trySyncAppFunctionMetadataBlocking() + @VisibleForTesting + void trySyncAppFunctionMetadataBlocking( + @NonNull FutureAppSearchSession staticMetadataSearchSession, + @NonNull FutureAppSearchSession runtimeMetadataSearchSession) throws ExecutionException, InterruptedException { ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap = getPackageToFunctionIdMap( - mStaticMetadataSearchSession, + staticMetadataSearchSession, AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE, AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID, AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME); ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap = getPackageToFunctionIdMap( - mRuntimeMetadataSearchSession, + runtimeMetadataSearchSession, RUNTIME_SCHEMA_TYPE, AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID, AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME); @@ -134,7 +156,7 @@ public class MetadataSyncAdapter { RemoveByDocumentIdRequest removeByDocumentIdRequest = buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap); AppSearchBatchResult<String, Void> removeDocumentBatchResult = - mRuntimeMetadataSearchSession.remove(removeByDocumentIdRequest).get(); + runtimeMetadataSearchSession.remove(removeByDocumentIdRequest).get(); if (!removeDocumentBatchResult.isSuccess()) { throw convertFailedAppSearchResultToException( removeDocumentBatchResult.getFailures().values()); @@ -144,13 +166,14 @@ public class MetadataSyncAdapter { if (!addedFunctionsDiffMap.isEmpty()) { // TODO(b/357551503): only set schema on package diff SetSchemaRequest addSetSchemaRequest = - buildSetSchemaRequestForRuntimeMetadataSchemas(appRuntimeMetadataSchemas); + buildSetSchemaRequestForRuntimeMetadataSchemas( + mPackageManager, appRuntimeMetadataSchemas); Objects.requireNonNull( - mRuntimeMetadataSearchSession.setSchema(addSetSchemaRequest).get()); + runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get()); PutDocumentsRequest putDocumentsRequest = buildPutRuntimeMetadataRequest(addedFunctionsDiffMap); AppSearchBatchResult<String, Void> putDocumentBatchResult = - mRuntimeMetadataSearchSession.put(putDocumentsRequest).get(); + runtimeMetadataSearchSession.put(putDocumentsRequest).get(); if (!putDocumentBatchResult.isSuccess()) { throw convertFailedAppSearchResultToException( putDocumentBatchResult.getFailures().values()); @@ -211,6 +234,7 @@ public class MetadataSyncAdapter { @NonNull private SetSchemaRequest buildSetSchemaRequestForRuntimeMetadataSchemas( + @NonNull PackageManager packageManager, @NonNull Set<AppSearchSchema> metadataSchemaSet) { Objects.requireNonNull(metadataSchemaSet); SetSchemaRequest.Builder setSchemaRequestBuilder = @@ -220,7 +244,7 @@ public class MetadataSyncAdapter { String packageName = AppFunctionRuntimeMetadata.getPackageNameFromSchema( runtimeMetadataSchema.getSchemaType()); - byte[] packageCert = getCertificate(packageName); + byte[] packageCert = getCertificate(packageManager, packageName); if (packageCert == null) { continue; } @@ -399,13 +423,15 @@ public class MetadataSyncAdapter { /** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found. */ @Nullable - private byte[] getCertificate(@NonNull String packageName) { + private byte[] getCertificate( + @NonNull PackageManager packageManager, @NonNull String packageName) { + Objects.requireNonNull(packageManager); Objects.requireNonNull(packageName); PackageInfo packageInfo; try { packageInfo = Objects.requireNonNull( - mPackageManager.getPackageInfo( + packageManager.getPackageInfo( packageName, PackageManager.GET_META_DATA | PackageManager.GET_SIGNING_CERTIFICATES)); diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java new file mode 100644 index 000000000000..f421527e72d0 --- /dev/null +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appfunctions; + +import android.annotation.Nullable; +import android.app.appsearch.AppSearchManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; + +/** A Singleton class that manages per-user metadata sync adapters. */ +public final class MetadataSyncPerUser { + private static final String TAG = MetadataSyncPerUser.class.getSimpleName(); + + /** A map of per-user adapter for synchronizing appFunction metadata. */ + @GuardedBy("sLock") + private static final SparseArray<MetadataSyncAdapter> sPerUserMetadataSyncAdapter = + new SparseArray<>(); + + private static final Object sLock = new Object(); + + /** + * Returns the per-user metadata sync adapter for the given user. + * + * @param user The user for which to get the metadata sync adapter. + * @param userContext The user context for the given user. + * @return The metadata sync adapter for the given user. + */ + @Nullable + public static MetadataSyncAdapter getPerUserMetadataSyncAdapter( + UserHandle user, Context userContext) { + synchronized (sLock) { + MetadataSyncAdapter metadataSyncAdapter = + sPerUserMetadataSyncAdapter.get(user.getIdentifier(), null); + if (metadataSyncAdapter == null) { + AppSearchManager perUserAppSearchManager = + userContext.getSystemService(AppSearchManager.class); + PackageManager perUserPackageManager = userContext.getPackageManager(); + if (perUserAppSearchManager != null) { + metadataSyncAdapter = + new MetadataSyncAdapter( + AppFunctionExecutors.getPerUserSyncExecutor(user), + perUserPackageManager, + perUserAppSearchManager); + sPerUserMetadataSyncAdapter.put(user.getIdentifier(), metadataSyncAdapter); + return metadataSyncAdapter; + } + } + return metadataSyncAdapter; + } + } + + /** + * Removes the per-user metadata sync adapter for the given user. + * + * @param user The user for which to remove the metadata sync adapter. + */ + public static void removeUserSyncAdapter(UserHandle user) { + synchronized (sLock) { + sPerUserMetadataSyncAdapter.remove(user.getIdentifier()); + } + } +} diff --git a/services/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS index 4fe0592f9075..4b732ac8e5a9 100644 --- a/services/companion/java/com/android/server/companion/virtual/OWNERS +++ b/services/companion/java/com/android/server/companion/virtual/OWNERS @@ -2,7 +2,9 @@ set noparent -marvinramin@google.com vladokom@google.com +marvinramin@google.com +caen@google.com +biswarupp@google.com ogunwale@google.com michaelwr@google.com
\ No newline at end of file diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index f0cc09f7c232..22ec7904f972 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -73,6 +73,7 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; import static android.media.audio.Flags.roForegroundAudioControl; import static android.os.Process.THREAD_GROUP_BACKGROUND; import static android.os.Process.THREAD_GROUP_DEFAULT; +import static android.os.Process.THREAD_GROUP_FOREGROUND_WINDOW; import static android.os.Process.THREAD_GROUP_RESTRICTED; import static android.os.Process.THREAD_GROUP_TOP_APP; import static android.os.Process.THREAD_PRIORITY_DISPLAY; @@ -116,6 +117,7 @@ import static com.android.server.am.ProcessList.PERSISTENT_SERVICE_ADJ; import static com.android.server.am.ProcessList.PREVIOUS_APP_ADJ; import static com.android.server.am.ProcessList.SCHED_GROUP_BACKGROUND; import static com.android.server.am.ProcessList.SCHED_GROUP_DEFAULT; +import static com.android.server.am.ProcessList.SCHED_GROUP_FOREGROUND_WINDOW; import static com.android.server.am.ProcessList.SCHED_GROUP_RESTRICTED; import static com.android.server.am.ProcessList.SCHED_GROUP_TOP_APP; import static com.android.server.am.ProcessList.SCHED_GROUP_TOP_APP_BOUND; @@ -1731,6 +1733,11 @@ public class OomAdjuster { // The recently used non-top visible freeform app. schedGroup = SCHED_GROUP_TOP_APP; mAdjType = "perceptible-freeform-activity"; + } else if ((flags + & WindowProcessController.ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE) != 0) { + // Currently the only case is from freeform apps which are not close to top. + schedGroup = SCHED_GROUP_FOREGROUND_WINDOW; + mAdjType = "vis-multi-window-activity"; } foregroundActivities = true; mHasVisibleActivities = true; @@ -3438,6 +3445,9 @@ public class OomAdjuster { case SCHED_GROUP_RESTRICTED: processGroup = THREAD_GROUP_RESTRICTED; break; + case SCHED_GROUP_FOREGROUND_WINDOW: + processGroup = THREAD_GROUP_FOREGROUND_WINDOW; + break; default: processGroup = THREAD_GROUP_DEFAULT; break; diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java index 21842db590b0..fb1c2e9a1f9d 100644 --- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java +++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java @@ -331,7 +331,7 @@ public class OomAdjusterModernImpl extends OomAdjuster { void forEachNewNode(int slot, @NonNull Consumer<OomAdjusterArgs> callback) { ProcessRecordNode node = mLastNode[slot].mNext; final ProcessRecordNode tail = mProcessRecordNodes[slot].TAIL; - while (node != tail) { + while (node != null && node != tail) { mTmpOomAdjusterArgs.mApp = node.mApp; if (node.mApp == null) { // TODO(b/336178916) - Temporary logging for root causing b/336178916. @@ -365,7 +365,9 @@ public class OomAdjusterModernImpl extends OomAdjuster { } // Save the next before calling callback, since that may change the node.mNext. final ProcessRecordNode next = node.mNext; - callback.accept(mTmpOomAdjusterArgs); + if (mTmpOomAdjusterArgs.mApp != null) { + callback.accept(mTmpOomAdjusterArgs); + } // There are couple of cases: // a) The current node is moved to another slot // - for this case, we'd need to keep using the "next" node. diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index cb918a045ec6..00250b4ef463 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -303,6 +303,9 @@ public final class ProcessList { // Activity manager's version of Process.THREAD_GROUP_TOP_APP // Disambiguate between actual top app and processes bound to the top app static final int SCHED_GROUP_TOP_APP_BOUND = 4; + // Activity manager's version of Process.THREAD_GROUP_FOREGROUND_WINDOW + // The priority is like between default and top-app. + static final int SCHED_GROUP_FOREGROUND_WINDOW = 5; // The minimum number of cached apps we want to be able to keep around, // without empty apps being able to push them out of memory. diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index e145c90fb1c1..55d9c6eac87a 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -533,7 +533,8 @@ public class AudioDeviceBroker { AudioDeviceInfo.TYPE_BLE_SPEAKER, AudioDeviceInfo.TYPE_LINE_ANALOG, AudioDeviceInfo.TYPE_HDMI, - AudioDeviceInfo.TYPE_AUX_LINE + AudioDeviceInfo.TYPE_AUX_LINE, + AudioDeviceInfo.TYPE_BUS }; /*package */ static boolean isValidCommunicationDevice(@NonNull AudioDeviceInfo device) { diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java index 38eb416ffdd8..ddea285d3564 100644 --- a/services/core/java/com/android/server/display/DisplayControl.java +++ b/services/core/java/com/android/server/display/DisplayControl.java @@ -109,7 +109,7 @@ public class DisplayControl { /** * Sets the HDR conversion mode for the device. * - * Returns the system preferred Hdr output type nn case when HDR conversion mode is + * Returns the system preferred HDR output type in case when HDR conversion mode is * {@link android.hardware.display.HdrConversionMode#HDR_CONVERSION_SYSTEM}. * Returns Hdr::INVALID in other cases. * @hide diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java index 93bd92614403..acf4db30ba93 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java +++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java @@ -318,13 +318,16 @@ final class DisplayDeviceInfo { */ public Display.HdrCapabilities hdrCapabilities; + /** When true, all HDR capabilities are hidden from public APIs */ + public boolean isForceSdr; + /** * Indicates whether this display supports Auto Low Latency Mode. */ public boolean allmSupported; /** - * Indicates whether this display suppors Game content type. + * Indicates whether this display supports Game content type. */ public boolean gameContentTypeSupported; @@ -516,6 +519,7 @@ final class DisplayDeviceInfo { || !Arrays.equals(supportedModes, other.supportedModes) || !Arrays.equals(supportedColorModes, other.supportedColorModes) || !Objects.equals(hdrCapabilities, other.hdrCapabilities) + || isForceSdr != other.isForceSdr || allmSupported != other.allmSupported || gameContentTypeSupported != other.gameContentTypeSupported || densityDpi != other.densityDpi @@ -560,6 +564,7 @@ final class DisplayDeviceInfo { colorMode = other.colorMode; supportedColorModes = other.supportedColorModes; hdrCapabilities = other.hdrCapabilities; + isForceSdr = other.isForceSdr; allmSupported = other.allmSupported; gameContentTypeSupported = other.gameContentTypeSupported; densityDpi = other.densityDpi; @@ -603,6 +608,7 @@ final class DisplayDeviceInfo { sb.append(", colorMode ").append(colorMode); sb.append(", supportedColorModes ").append(Arrays.toString(supportedColorModes)); sb.append(", hdrCapabilities ").append(hdrCapabilities); + sb.append(", isForceSdr ").append(isForceSdr); sb.append(", allmSupported ").append(allmSupported); sb.append(", gameContentTypeSupported ").append(gameContentTypeSupported); sb.append(", density ").append(densityDpi); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 3c2167e7a4ef..e7fd8f7db182 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -48,6 +48,7 @@ import static android.os.Process.ROOT_UID; import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL; import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH; import static android.provider.Settings.Secure.RESOLUTION_MODE_UNKNOWN; +import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID; import static com.android.server.display.layout.Layout.Display.POSITION_REAR; @@ -284,7 +285,7 @@ public final class DisplayManagerService extends SystemService { @GuardedBy("mSyncRoot") private int[] mUserDisabledHdrTypes = {}; @Display.HdrCapabilities.HdrType - private int[] mSupportedHdrOutputType; + private int[] mSupportedHdrOutputTypes; @GuardedBy("mSyncRoot") private boolean mAreUserDisabledHdrTypesAllowed = true; @@ -299,10 +300,10 @@ public final class DisplayManagerService extends SystemService { // HDR conversion mode chosen by user @GuardedBy("mSyncRoot") private HdrConversionMode mHdrConversionMode = null; - // Actual HDR conversion mode, which takes app overrides into account. - private HdrConversionMode mOverrideHdrConversionMode = null; + // Whether app has disabled HDR conversion + private boolean mShouldDisableHdrConversion = false; @GuardedBy("mSyncRoot") - private int mSystemPreferredHdrOutputType = Display.HdrCapabilities.HDR_TYPE_INVALID; + private int mSystemPreferredHdrOutputType = HDR_TYPE_INVALID; // The synchronization root for the display manager. @@ -1419,7 +1420,8 @@ public final class DisplayManagerService extends SystemService { } } - private void setUserDisabledHdrTypesInternal(int[] userDisabledHdrTypes) { + @VisibleForTesting + void setUserDisabledHdrTypesInternal(int[] userDisabledHdrTypes) { synchronized (mSyncRoot) { if (userDisabledHdrTypes == null) { Slog.e(TAG, "Null is not an expected argument to " @@ -1437,6 +1439,7 @@ public final class DisplayManagerService extends SystemService { if (Arrays.equals(mUserDisabledHdrTypes, userDisabledHdrTypes)) { return; } + String userDisabledFormatsString = ""; if (userDisabledHdrTypes.length != 0) { userDisabledFormatsString = TextUtils.join(",", @@ -1452,6 +1455,15 @@ public final class DisplayManagerService extends SystemService { handleLogicalDisplayChangedLocked(display); }); } + /* Note: it may be expected to reset the Conversion Mode when an HDR type is enabled + and the Conversion Mode is set to System Preferred. This is handled in the Settings + code because in the special case where HDR is indirectly disabled by Force SDR + Conversion, manually enabling HDR is not recognized as an action that reduces the + disabled HDR count. Thus, this case needs to be checked in the Settings code when we + know we're enabling an HDR mode. If we split checking for SystemConversion and + isForceSdr in two places, we may have duplicate calls to resetting to System Conversion + and get two black screens. + */ } } @@ -1464,19 +1476,20 @@ public final class DisplayManagerService extends SystemService { return true; } - private void setAreUserDisabledHdrTypesAllowedInternal( + @VisibleForTesting + void setAreUserDisabledHdrTypesAllowedInternal( boolean areUserDisabledHdrTypesAllowed) { synchronized (mSyncRoot) { if (mAreUserDisabledHdrTypesAllowed == areUserDisabledHdrTypesAllowed) { return; } mAreUserDisabledHdrTypesAllowed = areUserDisabledHdrTypesAllowed; - if (mUserDisabledHdrTypes.length == 0) { - return; - } Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.ARE_USER_DISABLED_HDR_FORMATS_ALLOWED, areUserDisabledHdrTypesAllowed ? 1 : 0); + if (mUserDisabledHdrTypes.length == 0) { + return; + } int userDisabledHdrTypes[] = {}; if (!mAreUserDisabledHdrTypesAllowed) { userDisabledHdrTypes = mUserDisabledHdrTypes; @@ -1487,6 +1500,14 @@ public final class DisplayManagerService extends SystemService { display.setUserDisabledHdrTypes(finalUserDisabledHdrTypes); handleLogicalDisplayChangedLocked(display); }); + // When HDR conversion mode is set to SYSTEM, modification to + // areUserDisabledHdrTypesAllowed requires refreshing the HDR conversion mode to tell + // the system which HDR types it is not allowed to use. + if (getHdrConversionModeInternal().getConversionMode() + == HdrConversionMode.HDR_CONVERSION_SYSTEM) { + setHdrConversionModeInternal( + new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM)); + } } } @@ -2357,7 +2378,7 @@ public final class DisplayManagerService extends SystemService { final int preferredHdrOutputType = hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_FORCE ? hdrConversionMode.getPreferredHdrOutputType() - : Display.HdrCapabilities.HDR_TYPE_INVALID; + : HDR_TYPE_INVALID; Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.HDR_FORCE_CONVERSION_TYPE, preferredHdrOutputType); } @@ -2370,7 +2391,7 @@ public final class DisplayManagerService extends SystemService { ? Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.HDR_FORCE_CONVERSION_TYPE, Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION) - : Display.HdrCapabilities.HDR_TYPE_INVALID; + : HDR_TYPE_INVALID; mHdrConversionMode = new HdrConversionMode(conversionMode, preferredHdrOutputType); setHdrConversionModeInternal(mHdrConversionMode); } @@ -2507,22 +2528,38 @@ public final class DisplayManagerService extends SystemService { }); } + /** + * Returns the HDR output types that are supported by the device's HDR conversion capabilities, + * stripping out any user-disabled HDR types if mAreUserDisabledHdrTypesAllowed is false. + */ @GuardedBy("mSyncRoot") - private int[] getEnabledAutoHdrTypesLocked() { - IntArray autoHdrOutputTypesArray = new IntArray(); + @VisibleForTesting + int[] getEnabledHdrOutputTypesLocked() { + if (mAreUserDisabledHdrTypesAllowed) { + return getSupportedHdrOutputTypesInternal(); + } + // Strip out all HDR formats that are currently user-disabled + IntArray enabledHdrOutputTypesArray = new IntArray(); for (int type : getSupportedHdrOutputTypesInternal()) { - boolean isDisabled = false; + boolean isEnabled = true; for (int disabledType : mUserDisabledHdrTypes) { if (type == disabledType) { - isDisabled = true; + isEnabled = false; break; } } - if (!isDisabled) { - autoHdrOutputTypesArray.add(type); + if (isEnabled) { + enabledHdrOutputTypesArray.add(type); } } - return autoHdrOutputTypesArray.toArray(); + return enabledHdrOutputTypesArray.toArray(); + } + + @VisibleForTesting + int[] getEnabledHdrOutputTypes() { + synchronized (mSyncRoot) { + return getEnabledHdrOutputTypesLocked(); + } } @GuardedBy("mSyncRoot") @@ -2531,7 +2568,7 @@ public final class DisplayManagerService extends SystemService { final int preferredHdrOutputType = mode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM ? mSystemPreferredHdrOutputType : mode.getPreferredHdrOutputType(); - if (preferredHdrOutputType != Display.HdrCapabilities.HDR_TYPE_INVALID) { + if (preferredHdrOutputType != HDR_TYPE_INVALID) { int[] hdrTypesWithLatency = mInjector.getHdrOutputTypesWithLatency(); return ArrayUtils.contains(hdrTypesWithLatency, preferredHdrOutputType); } @@ -2565,41 +2602,57 @@ public final class DisplayManagerService extends SystemService { if (!mInjector.getHdrOutputConversionSupport()) { return; } - int[] autoHdrOutputTypes = null; + synchronized (mSyncRoot) { if (hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM && hdrConversionMode.getPreferredHdrOutputType() - != Display.HdrCapabilities.HDR_TYPE_INVALID) { + != HDR_TYPE_INVALID) { throw new IllegalArgumentException("preferredHdrOutputType must not be set if" + " the conversion mode is HDR_CONVERSION_SYSTEM"); } mHdrConversionMode = hdrConversionMode; storeHdrConversionModeLocked(mHdrConversionMode); - // For auto mode, all supported HDR types are allowed except the ones specifically - // disabled by the user. + // If the HDR conversion is HDR_CONVERSION_SYSTEM, all supported HDR types are allowed + // except the ones specifically disabled by the user. + int[] enabledHdrOutputTypes = null; if (hdrConversionMode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM) { - autoHdrOutputTypes = getEnabledAutoHdrTypesLocked(); + enabledHdrOutputTypes = getEnabledHdrOutputTypesLocked(); } int conversionMode = hdrConversionMode.getConversionMode(); int preferredHdrType = hdrConversionMode.getPreferredHdrOutputType(); + // If the HDR conversion is disabled by an app through WindowManager.LayoutParams, then // set HDR conversion mode to HDR_CONVERSION_PASSTHROUGH. - if (mOverrideHdrConversionMode == null) { + if (mShouldDisableHdrConversion) { + conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH; + preferredHdrType = -1; + enabledHdrOutputTypes = null; + } else { // HDR_CONVERSION_FORCE with HDR_TYPE_INVALID is used to represent forcing SDR type. - // But, internally SDR is selected by using passthrough mode. + // But, internally SDR is forced by using passthrough mode and not reporting any + // HDR capabilities to apps. if (conversionMode == HdrConversionMode.HDR_CONVERSION_FORCE - && preferredHdrType == Display.HdrCapabilities.HDR_TYPE_INVALID) { + && preferredHdrType == HDR_TYPE_INVALID) { conversionMode = HdrConversionMode.HDR_CONVERSION_PASSTHROUGH; + mLogicalDisplayMapper.forEachLocked( + logicalDisplay -> { + if (logicalDisplay.setIsForceSdr(true)) { + handleLogicalDisplayChangedLocked(logicalDisplay); + } + }); + } else { + mLogicalDisplayMapper.forEachLocked( + logicalDisplay -> { + if (logicalDisplay.setIsForceSdr(false)) { + handleLogicalDisplayChangedLocked(logicalDisplay); + } + }); } - } else { - conversionMode = mOverrideHdrConversionMode.getConversionMode(); - preferredHdrType = mOverrideHdrConversionMode.getPreferredHdrOutputType(); - autoHdrOutputTypes = null; } mSystemPreferredHdrOutputType = mInjector.setHdrConversionMode( - conversionMode, preferredHdrType, autoHdrOutputTypes); + conversionMode, preferredHdrType, enabledHdrOutputTypes); } } @@ -2621,8 +2674,8 @@ public final class DisplayManagerService extends SystemService { } HdrConversionMode mode; synchronized (mSyncRoot) { - mode = mOverrideHdrConversionMode != null - ? mOverrideHdrConversionMode + mode = mShouldDisableHdrConversion + ? new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH) : mHdrConversionMode; // Handle default: PASSTHROUGH. Don't include the system-preferred type. if (mode == null @@ -2630,8 +2683,6 @@ public final class DisplayManagerService extends SystemService { return new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH); } // Handle default or current mode: SYSTEM. Include the system preferred type. - // mOverrideHdrConversionMode and mHdrConversionMode do not include the system - // preferred type, it is kept separately in mSystemPreferredHdrOutputType. if (mode == null || mode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM) { return new HdrConversionMode( @@ -2642,10 +2693,10 @@ public final class DisplayManagerService extends SystemService { } private @Display.HdrCapabilities.HdrType int[] getSupportedHdrOutputTypesInternal() { - if (mSupportedHdrOutputType == null) { - mSupportedHdrOutputType = mInjector.getSupportedHdrOutputTypes(); + if (mSupportedHdrOutputTypes == null) { + mSupportedHdrOutputTypes = mInjector.getSupportedHdrOutputTypes(); } - return mSupportedHdrOutputType; + return mSupportedHdrOutputTypes; } void setShouldAlwaysRespectAppRequestedModeInternal(boolean enabled) { @@ -2831,15 +2882,9 @@ public final class DisplayManagerService extends SystemService { // HDR conversion is disabled in two cases: // - HDR conversion introduces latency and minimal post-processing is requested // - app requests to disable HDR conversion - if (mOverrideHdrConversionMode == null && (disableHdrConversion - || disableHdrConversionForLatency)) { - mOverrideHdrConversionMode = - new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_PASSTHROUGH); - setHdrConversionModeInternal(mHdrConversionMode); - handleLogicalDisplayChangedLocked(display); - } else if (mOverrideHdrConversionMode != null && !disableHdrConversion - && !disableHdrConversionForLatency) { - mOverrideHdrConversionMode = null; + boolean previousShouldDisableHdrConversion = mShouldDisableHdrConversion; + mShouldDisableHdrConversion = disableHdrConversion || disableHdrConversionForLatency; + if (previousShouldDisableHdrConversion != mShouldDisableHdrConversion) { setHdrConversionModeInternal(mHdrConversionMode); handleLogicalDisplayChangedLocked(display); } @@ -3530,9 +3575,9 @@ public final class DisplayManagerService extends SystemService { } int setHdrConversionMode(int conversionMode, int preferredHdrOutputType, - int[] autoHdrTypes) { + int[] allowedHdrOutputTypes) { return DisplayControl.setHdrConversionMode(conversionMode, preferredHdrOutputType, - autoHdrTypes); + allowedHdrOutputTypes); } @Display.HdrCapabilities.HdrType diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index e8be8a449652..007e3a8fde2f 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -518,6 +518,7 @@ final class LogicalDisplay { deviceInfo.supportedColorModes, deviceInfo.supportedColorModes.length); mBaseDisplayInfo.hdrCapabilities = deviceInfo.hdrCapabilities; + mBaseDisplayInfo.isForceSdr = deviceInfo.isForceSdr; mBaseDisplayInfo.userDisabledHdrTypes = mUserDisabledHdrTypes; mBaseDisplayInfo.minimalPostProcessingSupported = deviceInfo.allmSupported || deviceInfo.gameContentTypeSupported; @@ -899,6 +900,29 @@ final class LogicalDisplay { } /** + * Checks whether display is of the type where HDR settings are relevant, and then sets + * whether Force SDR conversion mode is active. isForceSdr is checked by the Display when + * returning HDR capabilities. + * + * @param isForceSdr Whether Force SDR conversion mode is active + * @return Whether Display Manager should call handleLogicalDisplayChangedLocked() + */ + public boolean setIsForceSdr(boolean isForceSdr) { + int displayType = getDisplayInfoLocked().type; + boolean isTargetDisplayType = displayType == Display.TYPE_INTERNAL + || displayType == Display.TYPE_EXTERNAL + || displayType == Display.TYPE_OVERLAY; + + boolean handleLogicalDisplayChangedLocked = false; + if (isTargetDisplayType && mBaseDisplayInfo.isForceSdr != isForceSdr) { + mBaseDisplayInfo.isForceSdr = isForceSdr; + mInfo.set(null); + handleLogicalDisplayChangedLocked = true; + } + return handleLogicalDisplayChangedLocked; + } + + /** * Swap the underlying {@link DisplayDevice} with the specified LogicalDisplay. * * @param targetDisplay The display with which to swap display-devices. diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java index ea240c75452d..9d04682f2374 100644 --- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java +++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java @@ -196,12 +196,7 @@ final class UpdatableFontDir { File signatureFile = new File(dir, FONT_SIGNATURE_FILE); if (!signatureFile.exists()) { Slog.i(TAG, "The signature file is missing."); - if (com.android.text.flags.Flags.fixFontUpdateFailure()) { - return; - } else { - FileUtils.deleteContentsAndDir(dir); - continue; - } + return; } byte[] signature; try { @@ -226,39 +221,33 @@ final class UpdatableFontDir { FontFileInfo fontFileInfo = validateFontFile(fontFile, signature); if (fontConfig == null) { - if (com.android.text.flags.Flags.fixFontUpdateFailure()) { - // Use preinstalled font config for checking revision number. - fontConfig = mConfigSupplier.apply(Collections.emptyMap()); - } else { - fontConfig = getSystemFontConfig(); - } + // Use preinstalled font config for checking revision number. + fontConfig = mConfigSupplier.apply(Collections.emptyMap()); } addFileToMapIfSameOrNewer(fontFileInfo, fontConfig, true /* deleteOldFile */); } - if (com.android.text.flags.Flags.fixFontUpdateFailure()) { - // Treat as error if post script name of font family was not installed. - for (int i = 0; i < config.fontFamilies.size(); ++i) { - FontUpdateRequest.Family family = config.fontFamilies.get(i); - for (int j = 0; j < family.getFonts().size(); ++j) { - FontUpdateRequest.Font font = family.getFonts().get(j); - if (mFontFileInfoMap.containsKey(font.getPostScriptName())) { - continue; - } - - if (fontConfig == null) { - fontConfig = mConfigSupplier.apply(Collections.emptyMap()); - } - - if (getFontByPostScriptName(font.getPostScriptName(), fontConfig) != null) { - continue; - } - - Slog.e(TAG, "Unknown font that has PostScript name " - + font.getPostScriptName() + " is requested in FontFamily " - + family.getName()); - return; + // Treat as error if post script name of font family was not installed. + for (int i = 0; i < config.fontFamilies.size(); ++i) { + FontUpdateRequest.Family family = config.fontFamilies.get(i); + for (int j = 0; j < family.getFonts().size(); ++j) { + FontUpdateRequest.Font font = family.getFonts().get(j); + if (mFontFileInfoMap.containsKey(font.getPostScriptName())) { + continue; } + + if (fontConfig == null) { + fontConfig = mConfigSupplier.apply(Collections.emptyMap()); + } + + if (getFontByPostScriptName(font.getPostScriptName(), fontConfig) != null) { + continue; + } + + Slog.e(TAG, "Unknown font that has PostScript name " + + font.getPostScriptName() + " is requested in FontFamily " + + family.getName()); + return; } } @@ -273,9 +262,7 @@ final class UpdatableFontDir { mFontFileInfoMap.clear(); mLastModifiedMillis = 0; FileUtils.deleteContents(mFilesDir); - if (com.android.text.flags.Flags.fixFontUpdateFailure()) { - mConfigFile.delete(); - } + mConfigFile.delete(); } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 164b230a0d3b..ac75ef7b4656 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1380,7 +1380,8 @@ public class HdmiControlService extends SystemService { @ServiceThreadOnly private List<Integer> getCecLocalDeviceTypes() { ArrayList<Integer> allLocalDeviceTypes = new ArrayList<>(mCecLocalDevices); - if (isDsmEnabled() && !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) + if (!isTvDevice() && isDsmEnabled() + && !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) && isArcSupported() && mSoundbarModeFeatureFlagEnabled) { allLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); } diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java index 3f11e7836609..5ff8568f81b2 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java @@ -29,7 +29,9 @@ import android.util.Slog; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; +import android.view.SurfaceControl; import android.view.ViewConfiguration; +import android.view.ViewRootImpl; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; @@ -49,6 +51,7 @@ public class TouchpadDebugView extends LinearLayout { private static final float DEFAULT_RES_X = 47f; private static final float DEFAULT_RES_Y = 45f; private static final int TEXT_PADDING_DP = 12; + private static final int ROUNDED_CORNER_RADIUS_DP = 24; /** * Input device ID for the touchpad that this debug view is displaying. @@ -152,6 +155,30 @@ public class TouchpadDebugView extends LinearLayout { } @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + postDelayed(() -> { + final ViewRootImpl viewRootImpl = getRootView().getViewRootImpl(); + if (viewRootImpl == null) { + Slog.d("TouchpadDebugView", "ViewRootImpl is null."); + return; + } + + SurfaceControl surfaceControl = viewRootImpl.getSurfaceControl(); + if (surfaceControl != null && surfaceControl.isValid()) { + try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { + transaction.setCornerRadius(surfaceControl, + TypedValue.applyDimension(COMPLEX_UNIT_DIP, + ROUNDED_CORNER_RADIUS_DP, + getResources().getDisplayMetrics())).apply(); + } + } else { + Slog.d("TouchpadDebugView", "SurfaceControl is invalid or has been released."); + } + }, 100); + } + + @Override public boolean onTouchEvent(MotionEvent event) { float deltaX; float deltaY; diff --git a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java index 67c3621b7c8c..2eed9ba95413 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java +++ b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java @@ -27,20 +27,28 @@ import com.android.server.input.TouchpadFingerState; import com.android.server.input.TouchpadHardwareProperties; import com.android.server.input.TouchpadHardwareState; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.Map; + public class TouchpadVisualizationView extends View { private static final String TAG = "TouchpadVizMain"; private static final boolean DEBUG = true; private static final float DEFAULT_RES_X = 47f; private static final float DEFAULT_RES_Y = 45f; + private static final float MAX_TRACE_HISTORY_DURATION_SECONDS = 1f; private final TouchpadHardwareProperties mTouchpadHardwareProperties; private float mScaleFactor; - TouchpadHardwareState mLatestHardwareState = new TouchpadHardwareState(0, 0, 0, 0, - new TouchpadFingerState[]{}); + private final ArrayDeque<TouchpadHardwareState> mHardwareStateHistory = + new ArrayDeque<TouchpadHardwareState>(); + private final Map<Integer, TouchpadFingerState> mTempFingerStatesByTrackingId = new HashMap<>(); private final Paint mOvalStrokePaint; private final Paint mOvalFillPaint; + private final Paint mTracePaint; + private final Paint mCenterPointPaint; private final RectF mTempOvalRect = new RectF(); public TouchpadVisualizationView(Context context, @@ -55,6 +63,29 @@ public class TouchpadVisualizationView extends View { mOvalFillPaint = new Paint(); mOvalFillPaint.setAntiAlias(true); mOvalFillPaint.setARGB(255, 0, 0, 0); + mTracePaint = new Paint(); + mTracePaint.setAntiAlias(false); + mTracePaint.setARGB(255, 0, 0, 255); + mTracePaint.setStyle(Paint.Style.STROKE); + mTracePaint.setStrokeWidth(2); + mCenterPointPaint = new Paint(); + mCenterPointPaint.setAntiAlias(true); + mCenterPointPaint.setARGB(255, 255, 0, 0); + mCenterPointPaint.setStrokeWidth(2); + } + + private void removeOldPoints() { + float latestTimestamp = mHardwareStateHistory.getLast().getTimestamp(); + + while (!mHardwareStateHistory.isEmpty()) { + TouchpadHardwareState oldestPoint = mHardwareStateHistory.getFirst(); + float onScreenTime = latestTimestamp - oldestPoint.getTimestamp(); + if (onScreenTime >= MAX_TRACE_HISTORY_DURATION_SECONDS) { + mHardwareStateHistory.removeFirst(); + } else { + break; + } + } } private void drawOval(Canvas canvas, float x, float y, float major, float minor, float angle) { @@ -71,19 +102,22 @@ public class TouchpadVisualizationView extends View { @Override protected void onDraw(Canvas canvas) { + if (mHardwareStateHistory.isEmpty()) { + return; + } + + TouchpadHardwareState latestHardwareState = mHardwareStateHistory.getLast(); + float maximumPressure = 0; - for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) { + for (TouchpadFingerState touchpadFingerState : latestHardwareState.getFingerStates()) { maximumPressure = Math.max(maximumPressure, touchpadFingerState.getPressure()); } - for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) { - float newX = translateRange(mTouchpadHardwareProperties.getLeft(), - mTouchpadHardwareProperties.getRight(), 0, getWidth(), - touchpadFingerState.getPositionX()); + // Visualizing fingers as ovals + for (TouchpadFingerState touchpadFingerState : latestHardwareState.getFingerStates()) { + float newX = translateX(touchpadFingerState.getPositionX()); - float newY = translateRange(mTouchpadHardwareProperties.getTop(), - mTouchpadHardwareProperties.getBottom(), 0, getHeight(), - touchpadFingerState.getPositionY()); + float newY = translateY(touchpadFingerState.getPositionY()); float newAngle = translateRange(0, mTouchpadHardwareProperties.getOrientationMaximum(), 0, 90, touchpadFingerState.getOrientation()); @@ -102,6 +136,28 @@ public class TouchpadVisualizationView extends View { drawOval(canvas, newX, newY, newTouchMajor, newTouchMinor, newAngle); } + + mTempFingerStatesByTrackingId.clear(); + + // Drawing the trace + for (TouchpadHardwareState currentHardwareState : mHardwareStateHistory) { + for (TouchpadFingerState currentFingerState : currentHardwareState.getFingerStates()) { + TouchpadFingerState prevFingerState = mTempFingerStatesByTrackingId.put( + currentFingerState.getTrackingId(), currentFingerState); + + if (prevFingerState == null) { + continue; + } + + float currentX = translateX(currentFingerState.getPositionX()); + float currentY = translateY(currentFingerState.getPositionY()); + float prevX = translateX(prevFingerState.getPositionX()); + float prevY = translateY(prevFingerState.getPositionY()); + + canvas.drawLine(prevX, prevY, currentX, currentY, mTracePaint); + canvas.drawPoint(currentX, currentY, mCenterPointPaint); + } + } } /** @@ -114,7 +170,18 @@ public class TouchpadVisualizationView extends View { logHardwareState(schs); } - mLatestHardwareState = schs; + if (!mHardwareStateHistory.isEmpty() + && mHardwareStateHistory.getLast().getFingerCount() == 0 + && schs.getFingerCount() > 0) { + mHardwareStateHistory.clear(); + } + + mHardwareStateHistory.addLast(schs); + removeOldPoints(); + + if (DEBUG) { + logFingerTrace(); + } invalidate(); } @@ -128,6 +195,16 @@ public class TouchpadVisualizationView extends View { mScaleFactor = scaleFactor; } + private float translateX(float x) { + return translateRange(mTouchpadHardwareProperties.getLeft(), + mTouchpadHardwareProperties.getRight(), 0, getWidth(), x); + } + + private float translateY(float y) { + return translateRange(mTouchpadHardwareProperties.getTop(), + mTouchpadHardwareProperties.getBottom(), 0, getHeight(), y); + } + private float translateRange(float rangeBeforeMin, float rangeBeforeMax, float rangeAfterMin, float rangeAfterMax, float value) { return rangeAfterMin + (value - rangeBeforeMin) / (rangeBeforeMax - rangeBeforeMin) * ( @@ -154,4 +231,10 @@ public class TouchpadVisualizationView extends View { } } -} + private void logFingerTrace() { + Slog.d(TAG, "Trace size= " + mHardwareStateHistory.size()); + for (TouchpadFingerState tfs : mHardwareStateHistory.getLast().getFingerStates()) { + Slog.d(TAG, "ID= " + tfs.getTrackingId()); + } + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/notification/VibratorHelper.java b/services/core/java/com/android/server/notification/VibratorHelper.java index fbe77720b9fc..f54c1f7654dc 100644 --- a/services/core/java/com/android/server/notification/VibratorHelper.java +++ b/services/core/java/com/android/server/notification/VibratorHelper.java @@ -218,10 +218,16 @@ public final class VibratorHelper { * @param uri {@code Uri} an uri including query parameter "vibraiton_uri" */ public @Nullable VibrationEffect createVibrationEffectFromSoundUri(Uri uri) { - if (uri == null) { + if (uri == null || uri.isOpaque()) { return null; } - return Utils.parseVibrationEffect(mVibrator, Utils.getVibrationUri(uri)); + + try { + return Utils.parseVibrationEffect(mVibrator, Utils.getVibrationUri(uri)); + } catch (Exception e) { + Slog.e(TAG, "Failed to get vibration effect: ", e); + } + return null; } /** Returns if a given vibration can be played by the vibrator that does notification buzz. */ diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index d495ef5ce108..50bfbc3530a9 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -157,7 +157,7 @@ public class ZenModeConditions implements ConditionProviders.Callback { } // empty rule? disable and bail early if (rule.component == null && rule.enabler == null) { - if (!android.app.Flags.modesUi() || (android.app.Flags.modesUi() && !isManual)) { + if (!isManual) { Log.w(TAG, "No component found for automatic rule: " + rule.conditionId); rule.enabled = false; } diff --git a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java index 41351613331d..5aea356a4173 100644 --- a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java +++ b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.media.AudioAttributes.USAGE_ALARM; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.Notification; @@ -42,6 +43,8 @@ import android.util.Log; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import java.util.List; + public class BackgroundUserSoundNotifier { private static final boolean DEBUG = false; @@ -49,11 +52,21 @@ public class BackgroundUserSoundNotifier { private static final String BUSN_CHANNEL_ID = "bg_user_sound_channel"; private static final String BUSN_CHANNEL_NAME = "BackgroundUserSound"; public static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER"; - private static final String EXTRA_NOTIFICATION_ID = "com.android.server.EXTRA_CLIENT_UID"; - private static final String EXTRA_CURRENT_USER_ID = "com.android.server.EXTRA_CURRENT_USER_ID"; private static final String ACTION_SWITCH_USER = "com.android.server.ACTION_SWITCH_TO_USER"; - /** ID of user with notification displayed, -1 if notification is not showing*/ - private int mUserWithNotification = -1; + private static final String ACTION_DISMISS_NOTIFICATION = + "com.android.server.ACTION_DISMISS_NOTIFICATION"; + /** + * The clientUid from the AudioFocusInfo of the background user, + * for which an active notification is currently displayed. + * Set to -1 if no notification is being shown. + * TODO: b/367615180 - add support for multiple simultaneous alarms + */ + @VisibleForTesting + int mNotificationClientUid = -1; + @VisibleForTesting + AudioPolicy mFocusControlAudioPolicy; + @VisibleForTesting + BackgroundUserListener mBgUserListener; private final Context mSystemUserContext; @VisibleForTesting final NotificationManager mNotificationManager; @@ -67,11 +80,18 @@ public class BackgroundUserSoundNotifier { mSystemUserContext = context; mNotificationManager = mSystemUserContext.getSystemService(NotificationManager.class); mUserManager = mSystemUserContext.getSystemService(UserManager.class); + createNotificationChannel(); + setupFocusControlAudioPolicy(); + } + + /** + * Creates a dedicated channel for background user related notifications. + */ + private void createNotificationChannel() { NotificationChannel channel = new NotificationChannel(BUSN_CHANNEL_ID, BUSN_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH); channel.setSound(null, null); mNotificationManager.createNotificationChannel(channel); - setupFocusControlAudioPolicy(); } private void setupFocusControlAudioPolicy() { @@ -81,15 +101,16 @@ public class BackgroundUserSoundNotifier { ActivityManager am = mSystemUserContext.getSystemService(ActivityManager.class); registerReceiver(am); - BackgroundUserListener bgUserListener = new BackgroundUserListener(mSystemUserContext); + mBgUserListener = new BackgroundUserListener(mSystemUserContext); AudioPolicy.Builder focusControlPolicyBuilder = new AudioPolicy.Builder(mSystemUserContext); focusControlPolicyBuilder.setLooper(Looper.getMainLooper()); - focusControlPolicyBuilder.setAudioPolicyFocusListener(bgUserListener); + focusControlPolicyBuilder.setAudioPolicyFocusListener(mBgUserListener); - AudioPolicy mFocusControlAudioPolicy = focusControlPolicyBuilder.build(); + mFocusControlAudioPolicy = focusControlPolicyBuilder.build(); int status = mSystemUserContext.getSystemService(AudioManager.class) .registerAudioPolicy(mFocusControlAudioPolicy); + if (status != AudioManager.SUCCESS) { Log.w(LOG_TAG , "Could not register the service's focus" + " control audio policy, error: " + status); @@ -117,123 +138,170 @@ public class BackgroundUserSoundNotifier { @SuppressLint("MissingPermission") public void onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) { - BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(afi); + BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(); } } + @VisibleForTesting + BackgroundUserListener getAudioPolicyFocusListener() { + return mBgUserListener; + } + /** * Registers a BroadcastReceiver for actions related to background user sound notifications. * When ACTION_MUTE_SOUND is received, it mutes a background user's alarm sound. * When ACTION_SWITCH_USER is received, a switch to the background user with alarm is started. */ - private void registerReceiver(ActivityManager service) { + private void registerReceiver(ActivityManager activityManager) { BroadcastReceiver backgroundUserNotificationBroadcastReceiver = new BroadcastReceiver() { @SuppressLint("MissingPermission") @Override public void onReceive(Context context, Intent intent) { - if (!(intent.hasExtra(EXTRA_NOTIFICATION_ID) - && intent.hasExtra(EXTRA_CURRENT_USER_ID) - && intent.hasExtra(Intent.EXTRA_USER_ID))) { + if (mNotificationClientUid == -1) { return; } - final int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); + dismissNotification(); if (DEBUG) { - Log.d(LOG_TAG, - "User with alarm id " + intent.getIntExtra(Intent.EXTRA_USER_ID, - -1) + " current user id " + intent.getIntExtra( - EXTRA_CURRENT_USER_ID, -1)); + final int actionIndex = intent.getAction().lastIndexOf(".") + 1; + final String action = intent.getAction().substring(actionIndex); + Log.d(LOG_TAG, "Action requested: " + action + ", by userId " + + ActivityManager.getCurrentUser() + " for alarm on user " + + UserHandle.getUserHandleForUid(mNotificationClientUid)); } - mUserWithNotification = -1; - mNotificationManager.cancelAsUser(LOG_TAG, notificationId, - UserHandle.of(intent.getIntExtra(EXTRA_CURRENT_USER_ID, -1))); + if (ACTION_MUTE_SOUND.equals(intent.getAction())) { - final AudioManager audioManager = - mSystemUserContext.getSystemService(AudioManager.class); - if (audioManager != null) { - for (AudioPlaybackConfiguration apc : - audioManager.getActivePlaybackConfigurations()) { - if (apc.getAudioAttributes().getUsage() == USAGE_ALARM) { - if (apc.getPlayerProxy() != null) { - apc.getPlayerProxy().stop(); - } - } - } - } + muteAlarmSounds(mSystemUserContext); } else if (ACTION_SWITCH_USER.equals(intent.getAction())) { - service.switchUser(intent.getIntExtra(Intent.EXTRA_USER_ID, -1)); + activityManager.switchUser(UserHandle.getUserId(mNotificationClientUid)); } + + mNotificationClientUid = -1; } }; IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_MUTE_SOUND); filter.addAction(ACTION_SWITCH_USER); + filter.addAction(ACTION_DISMISS_NOTIFICATION); mSystemUserContext.registerReceiver(backgroundUserNotificationBroadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED); } /** + * Stop player proxy for the ongoing alarm and drop focus for its AudioFocusInfo. + */ + @VisibleForTesting + void muteAlarmSounds(Context context) { + AudioManager audioManager = context.getSystemService(AudioManager.class); + if (audioManager != null) { + for (AudioPlaybackConfiguration apc : audioManager.getActivePlaybackConfigurations()) { + if (apc.getClientUid() == mNotificationClientUid && apc.getPlayerProxy() != null) { + apc.getPlayerProxy().stop(); + } + } + } + } + + /** * Check if sound is coming from background user and show notification is required. */ @VisibleForTesting - void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context - foregroundContext) throws RemoteException { + void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context foregroundContext) + throws RemoteException { final int userId = UserHandle.getUserId(afi.getClientUid()); final int usage = afi.getAttributes().getUsage(); UserInfo userInfo = mUserManager.getUserInfo(userId); - if (userInfo != null && userId != foregroundContext.getUserId()) { + // Only show notification if the sound is coming from background user and the notification + // is not already shown. + if (userInfo != null && userId != foregroundContext.getUserId() + && mNotificationClientUid == -1) { //TODO: b/349138482 - Add handling of cases when usage == USAGE_NOTIFICATION_RINGTONE if (usage == USAGE_ALARM) { - Intent muteIntent = createIntent(ACTION_MUTE_SOUND, afi, foregroundContext, userId); - PendingIntent mutePI = PendingIntent.getBroadcast(mSystemUserContext, 0, - muteIntent, PendingIntent.FLAG_UPDATE_CURRENT - | PendingIntent.FLAG_IMMUTABLE); - Intent switchIntent = createIntent(ACTION_SWITCH_USER, afi, foregroundContext, - userId); - PendingIntent switchPI = PendingIntent.getBroadcast(mSystemUserContext, 0, - switchIntent, PendingIntent.FLAG_UPDATE_CURRENT - | PendingIntent.FLAG_IMMUTABLE); - - mUserWithNotification = foregroundContext.getUserId(); - mNotificationManager.notifyAsUser(LOG_TAG, afi.getClientUid(), - createNotification(userInfo.name, mutePI, switchPI, foregroundContext), + if (DEBUG) { + Log.d(LOG_TAG, "Alarm ringing on background user " + userId + + ", displaying notification for current user " + + foregroundContext.getUserId()); + } + + mNotificationClientUid = afi.getClientUid(); + + mNotificationManager.notifyAsUser(LOG_TAG, mNotificationClientUid, + createNotification(userInfo.name, foregroundContext), foregroundContext.getUser()); } } } /** - * If notification is present, dismisses it. To be called when the relevant sound loses focus. + * Dismisses notification if the associated focus has been removed from the focus stack. + * Notification remains if the focus is temporarily lost due to another client taking over the + * focus ownership. */ - private void dismissNotificationIfNecessary(AudioFocusInfo afi) { - if (mUserWithNotification >= 0) { - mNotificationManager.cancelAsUser(LOG_TAG, afi.getClientUid(), - UserHandle.of(mUserWithNotification)); + @VisibleForTesting + void dismissNotificationIfNecessary() { + if (getAudioFocusInfoForNotification() == null && mNotificationClientUid >= 0) { + if (DEBUG) { + Log.d(LOG_TAG, "Alarm ringing on background user " + + UserHandle.getUserHandleForUid(mNotificationClientUid).getIdentifier() + + " left focus stack, dismissing notification"); + } + dismissNotification(); + mNotificationClientUid = -1; } - mUserWithNotification = -1; } - private Intent createIntent(String intentAction, AudioFocusInfo afi, Context fgUserContext, - int userId) { + /** + * Dismisses notification for all users in case user switch occurred after notification was + * shown. + */ + @SuppressLint("MissingPermission") + private void dismissNotification() { + mNotificationManager.cancelAsUser(LOG_TAG, mNotificationClientUid, UserHandle.ALL); + } + + /** + * Returns AudioFocusInfo associated with the current notification. + */ + @SuppressLint("MissingPermission") + @VisibleForTesting + @Nullable + AudioFocusInfo getAudioFocusInfoForNotification() { + if (mNotificationClientUid >= 0) { + List<AudioFocusInfo> stack = mFocusControlAudioPolicy.getFocusStack(); + for (int i = stack.size() - 1; i >= 0; i--) { + if (stack.get(i).getClientUid() == mNotificationClientUid) { + return stack.get(i); + } + } + } + return null; + } + + private PendingIntent createPendingIntent(String intentAction) { final Intent intent = new Intent(intentAction); - intent.putExtra(EXTRA_CURRENT_USER_ID, fgUserContext.getUserId()); - intent.putExtra(EXTRA_NOTIFICATION_ID, afi.getClientUid()); - intent.putExtra(Intent.EXTRA_USER_ID, userId); - return intent; + PendingIntent resultPI = PendingIntent.getBroadcast(mSystemUserContext, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + return resultPI; } - private Notification createNotification(String userName, PendingIntent muteIntent, - PendingIntent switchIntent, Context fgContext) { + @VisibleForTesting + Notification createNotification(String userName, Context fgContext) { final String title = fgContext.getString(R.string.bg_user_sound_notification_title_alarm, userName); final int icon = R.drawable.ic_audio_alarm; + + PendingIntent mutePI = createPendingIntent(ACTION_MUTE_SOUND); + PendingIntent switchPI = createPendingIntent(ACTION_SWITCH_USER); + PendingIntent dismissNotificationPI = createPendingIntent(ACTION_DISMISS_NOTIFICATION); + final Notification.Action mute = new Notification.Action.Builder(null, fgContext.getString(R.string.bg_user_sound_notification_button_mute), - muteIntent).build(); + mutePI).build(); final Notification.Action switchUser = new Notification.Action.Builder(null, fgContext.getString(R.string.bg_user_sound_notification_button_switch_user), - switchIntent).build(); + switchPI).build(); + Notification.Builder notificationBuilder = new Notification.Builder(mSystemUserContext, BUSN_CHANNEL_ID) .setSmallIcon(icon) @@ -243,16 +311,18 @@ public class BackgroundUserSoundNotifier { .setOngoing(true) .setColor(fgContext.getColor(R.color.system_notification_accent_color)) .setContentTitle(title) - .setContentIntent(muteIntent) + .setContentIntent(mutePI) .setAutoCancel(true) + .setDeleteIntent(dismissNotificationPI) .setVisibility(Notification.VISIBILITY_PUBLIC); + if (mUserManager.isUserSwitcherEnabled() && (mUserManager.getUserSwitchability( - UserHandle.of(fgContext.getUserId())) == UserManager.SWITCHABILITY_STATUS_OK)) { + fgContext.getUser()) == UserManager.SWITCHABILITY_STATUS_OK)) { notificationBuilder.setActions(mute, switchUser); } else { notificationBuilder.setActions(mute); } + return notificationBuilder.build(); } } - diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index ee15bec0d62b..efd58ed6edcc 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -92,7 +92,6 @@ import android.graphics.Rect; import android.multiuser.Flags; import android.net.Uri; import android.os.Binder; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IInterface; @@ -215,7 +214,7 @@ public class LauncherAppsService extends SystemService { @VisibleForTesting static class LauncherAppsImpl extends ILauncherApps.Stub { - private static final boolean DEBUG = Build.IS_DEBUGGABLE; + private static final boolean DEBUG = false; private static final String TAG = "LauncherAppsService"; private static final String NAMESPACE_MULTIUSER = "multiuser"; private static final String FLAG_NON_SYSTEM_ACCESS_TO_HIDDEN_PROFILES = @@ -496,28 +495,8 @@ public class LauncherAppsService extends SystemService { private boolean canAccessProfile(int callingUid, int callingUserId, int callingPid, int targetUserId, String message) { - if (DEBUG) { - final AndroidPackage callingPackage = - mPackageManagerInternal.getPackage(callingUid); - final String callingPackageName = callingPackage == null - ? null : callingPackage.getPackageName(); - Slog.v(TAG, "canAccessProfile called by " + callingPackageName - + " for user " + callingUserId - + " requesting to access user " - + targetUserId + " when invoking " + message); - } - if (targetUserId == callingUserId) { - if (DEBUG) { - Slog.v(TAG, message + " passed canAccessProfile for targetuser" - + targetUserId + " because it is the same as the calling user"); - } - return true; - } + if (targetUserId == callingUserId) return true; if (injectHasInteractAcrossUsersFullPermission(callingPid, callingUid)) { - if (DEBUG) { - Slog.v(TAG, message + " passed because calling process" - + "has permission to interact across users"); - } return true; } @@ -535,25 +514,11 @@ public class LauncherAppsService extends SystemService { if (isHiddenProfile(UserHandle.of(targetUserId)) && !canAccessHiddenProfile(callingUid, callingPid)) { - Slog.w(TAG, message + " for hidden profile user " + targetUserId - + " from " + callingUserId + " not allowed"); - return false; } - final boolean ret = mUserManagerInternal.isProfileAccessible( - callingUserId, targetUserId, message, true); - if (DEBUG) { - final AndroidPackage callingPackage = - mPackageManagerInternal.getPackage(callingUid); - final String callingPackageName = callingPackage == null - ? null : callingPackage.getPackageName(); - Slog.v(TAG, "canAccessProfile returned " + ret + " for " + callingPackageName - + " for user " + callingUserId - + " requesting to access user " - + targetUserId + " when invoking " + message); - } - return ret; + return mUserManagerInternal.isProfileAccessible(callingUserId, targetUserId, + message, true); } private boolean isHiddenProfile(UserHandle targetUser) { @@ -1376,10 +1341,6 @@ public class LauncherAppsService extends SystemService { @Override public void pinShortcuts(String callingPackage, String packageName, List<String> ids, UserHandle targetUser) { - if (DEBUG) { - Slog.v(TAG, "pinShortcuts: " + callingPackage + " is pinning shortcuts from " - + packageName + " for user " + targetUser); - } if (!mShortcutServiceInternal .areShortcutsSupportedOnHomeScreen(targetUser.getIdentifier())) { // Requires strict ACCESS_SHORTCUTS permission for user-profiles with items @@ -1390,11 +1351,6 @@ public class LauncherAppsService extends SystemService { } ensureShortcutPermission(callingPackage); if (!canAccessProfile(targetUser.getIdentifier(), "Cannot pin shortcuts")) { - if (DEBUG) { - Slog.v(TAG, "pinShortcuts: " + callingPackage - + " is pinning shortcuts from " + packageName - + " for user " + targetUser + " but cannot access profile"); - } return; } @@ -2451,7 +2407,7 @@ public class LauncherAppsService extends SystemService { final int callbackUserId = callbackUser.getIdentifier(); final int shortcutUserId = shortcutUser.getIdentifier(); - if (shortcutUser == callbackUser) return true; + if ((shortcutUser.equals(callbackUser))) return true; return mUserManagerInternal.isProfileAccessible(callbackUserId, shortcutUserId, null, false); } @@ -2485,16 +2441,28 @@ public class LauncherAppsService extends SystemService { final BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i); if (!isEnabledProfileOf(cookie, user, "onPackageRemoved")) { + // b/350144057 + Slog.d(TAG, "onPackageRemoved: Skipping - profile not enabled" + + " or not accessible for user=" + user + + ", packageName=" + packageName); continue; } if (!isCallingAppIdAllowed(appIdAllowList, UserHandle.getAppId( cookie.callingUid))) { + // b/350144057 + Slog.d(TAG, "onPackageRemoved: Skipping - appId not allowed" + + " for user=" + user + + ", packageName=" + packageName); continue; } try { + // b/350144057 + Slog.d(TAG, "onPackageRemoved: triggering onPackageRemoved" + + " for user=" + user + + ", packageName=" + packageName); listener.onPackageRemoved(user, packageName); } catch (RemoteException re) { - Slog.d(TAG, "Callback failed ", re); + Slog.d(TAG, "onPackageRemoved: Callback failed ", re); } } } finally { @@ -2524,15 +2492,27 @@ public class LauncherAppsService extends SystemService { IOnAppsChangedListener listener = mListeners.getBroadcastItem(i); BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i); if (!isEnabledProfileOf(cookie, user, "onPackageAdded")) { + // b/350144057 + Slog.d(TAG, "onPackageAdded: Skipping - profile not enabled" + + " or not accessible for user=" + user + + ", packageName=" + packageName); continue; } if (!isPackageVisibleToListener(packageName, cookie, user)) { + // b/350144057 + Slog.d(TAG, "onPackageAdded: Skipping - package filtered" + + " for user=" + user + + ", packageName=" + packageName); continue; } try { + // b/350144057 + Slog.d(TAG, "onPackageAdded: triggering onPackageAdded" + + " for user=" + user + + ", packageName=" + packageName); listener.onPackageAdded(user, packageName); } catch (RemoteException re) { - Slog.d(TAG, "Callback failed ", re); + Slog.d(TAG, "onPackageAdded: Callback failed ", re); } } } finally { @@ -2566,7 +2546,7 @@ public class LauncherAppsService extends SystemService { try { listener.onPackageChanged(user, packageName); } catch (RemoteException re) { - Slog.d(TAG, "Callback failed ", re); + Slog.d(TAG, "onPackageChanged: Callback failed ", re); } } } finally { diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java index d65e30be9edb..045d4db0a1f1 100644 --- a/services/core/java/com/android/server/pm/ShortcutLauncher.java +++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java @@ -42,7 +42,6 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; /** * Launcher information used by {@link ShortcutService}. @@ -129,15 +128,9 @@ class ShortcutLauncher extends ShortcutPackageItem { */ public void pinShortcuts(@UserIdInt int packageUserId, @NonNull String packageName, @NonNull List<String> ids, boolean forPinRequest) { - if (ShortcutService.DEBUG) { - Slog.v(TAG, "ShortcutLauncher#pinShortcuts: pin shortcuts from " + packageName - + " with userId=" + packageUserId + " shortcutIds=" - + ids.stream().collect(Collectors.joining(", ", "[", "]"))); - } final ShortcutPackage packageShortcuts = mShortcutUser.getPackageShortcutsIfExists(packageName); if (packageShortcuts == null) { - Slog.w(TAG, "ShortcutLauncher#pinShortcuts packageShortcuts is null"); return; // No need to instantiate. } @@ -162,10 +155,6 @@ class ShortcutLauncher extends ShortcutPackageItem { final String id = ids.get(i); final ShortcutInfo si = packageShortcuts.findShortcutById(id); if (si == null) { - if (ShortcutService.DEBUG) { - Slog.w(TAG, "ShortcutLauncher#pinShortcuts: cannot pin " - + id + " because it does not exist"); - } continue; } if (si.isDynamic() || si.isLongLived() @@ -185,13 +174,6 @@ class ShortcutLauncher extends ShortcutPackageItem { } } } - if (ShortcutService.DEBUG) { - Slog.v(TAG, "ShortcutLauncher#pinShortcuts: " - + " newSet: " + newSet.stream().collect( - Collectors.joining(", ", "[", "]")) - + " floatingSet: " + floatingSet.stream().collect( - Collectors.joining(", ", "[", "]"))); - } mPinnedShortcuts.put(up, newSet); } } diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index c9ad4988f8ca..60056eb471d1 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -729,11 +729,6 @@ class ShortcutPackage extends ShortcutPackageItem { } pinnedShortcuts.addAll(pinned); }); - if (ShortcutService.DEBUG) { - Slog.v(TAG, "ShortcutPackage#refreshPinnedFlags: " - + " pinnedShortcuts: " + pinnedShortcuts.stream().collect( - Collectors.joining(", ", "[", "]"))); - } // Secondly, update the pinned state if necessary. final List<ShortcutInfo> pinned = findAll(pinnedShortcuts); if (pinned != null) { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index ea495c9bee9c..a3ff1952205f 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -169,7 +169,7 @@ import java.util.stream.Collectors; public class ShortcutService extends IShortcutService.Stub { static final String TAG = "ShortcutService"; - static final boolean DEBUG = Build.IS_DEBUGGABLE; // STOPSHIP if true + static final boolean DEBUG = false; // STOPSHIP if true static final boolean DEBUG_LOAD = false; // STOPSHIP if true static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true static final boolean DEBUG_REBOOT = Build.IS_DEBUGGABLE; @@ -3206,11 +3206,6 @@ public class ShortcutService extends IShortcutService.Stub { public void pinShortcuts(int launcherUserId, @NonNull String callingPackage, @NonNull String packageName, @NonNull List<String> shortcutIds, int userId) { - if (DEBUG) { - Slog.v(TAG, "pinShortcuts: " + callingPackage + ", with userId=" + launcherUserId - + ", is trying to pin shortcuts from " + packageName - + " with userId=" + userId); - } // Calling permission must be checked by LauncherAppsImpl. Preconditions.checkStringNotEmpty(packageName, "packageName"); Objects.requireNonNull(shortcutIds, "shortcutIds"); @@ -3235,11 +3230,6 @@ public class ShortcutService extends IShortcutService.Stub { && !si.isDeclaredInManifest(), ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO, callingPackage, launcherUserId, false); - } else { - if (DEBUG) { - Slog.w(TAG, "specified package " + packageName + ", with userId=" + userId - + ", doesn't exist."); - } } // Get list of shortcuts that will get unpinned. ArraySet<String> oldPinnedIds = launcher.getPinnedShortcutIds(packageName, userId); @@ -5458,17 +5448,6 @@ public class ShortcutService extends IShortcutService.Stub { */ private List<ShortcutInfo> prepareChangedShortcuts(ArraySet<String> changedIds, ArraySet<String> newIds, List<ShortcutInfo> deletedList, final ShortcutPackage ps) { - if (DEBUG) { - Slog.v(TAG, "prepareChangedShortcuts: " - + " changedIds=" + (changedIds == null - ? "n/a" : changedIds.stream().collect(Collectors.joining(", ", "[", "]"))) - + " newIds=" + (newIds == null - ? "n/a" : newIds.stream().collect(Collectors.joining(", ", "[", "]"))) - + " deletedList=" + (deletedList == null - ? "n/a" : deletedList.stream().map(ShortcutInfo::getId).collect( - Collectors.joining(", ", "[", "]"))) - + " ps=" + (ps == null ? "n/a" : ps.getPackageName())); - } if (ps == null) { // This can happen when package restore is not finished yet. return null; diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a683a8c54849..89417f3765ff 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1090,6 +1090,21 @@ public class UserManagerService extends IUserManager.Stub { mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null; mPrivateSpaceAutoLockSettingsObserver = new SettingsObserver(mHandler); emulateSystemUserModeIfNeeded(); + initPropertyInvalidatedCaches(); + } + + /** + * This method is used to invalidate the caches at server statup, + * so that caches can start working. + */ + private static final void initPropertyInvalidatedCaches() { + if (android.multiuser.Flags.cachesNotInvalidatedAtStartReadOnly()) { + UserManager.invalidateIsUserUnlockedCache(); + UserManager.invalidateQuietModeEnabledCache(); + UserManager.invalidateStaticUserProperties(); + UserManager.invalidateUserPropertiesCache(); + UserManager.invalidateUserSerialNumberCache(); + } } private boolean doesDeviceHardwareSupportPrivateSpace() { diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index 953aae9588dd..457196b74d2e 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -89,6 +89,7 @@ import com.android.internal.widget.LockSettingsInternal; import com.android.internal.widget.LockSettingsStateListener; import com.android.server.LocalServices; import com.android.server.SystemService; +import com.android.server.pm.UserManagerInternal; import com.android.server.servicewatcher.CurrentUserServiceSupplier; import com.android.server.servicewatcher.ServiceWatcher; import com.android.server.utils.Slogf; @@ -170,6 +171,7 @@ public class TrustManagerService extends SystemService { private final ActivityManager mActivityManager; private FingerprintManager mFingerprintManager; private FaceManager mFaceManager; + private UserManagerInternal mUserManagerInternal; private enum TrustState { // UNTRUSTED means that TrustManagerService is currently *not* giving permission for the @@ -1064,6 +1066,8 @@ public class TrustManagerService extends SystemService { Log.w(TAG, "Unable to check keyguard lock state", e); } currentUserIsUnlocked = unlockedUser == id; + } else if (isVisibleBackgroundUser(id)) { + showingKeyguard = !mUserManager.isUserUnlocked(id); } final boolean deviceLocked = secure && showingKeyguard && !trusted && !biometricAuthenticated; @@ -1095,6 +1099,16 @@ public class TrustManagerService extends SystemService { } } + private boolean isVisibleBackgroundUser(int userId) { + if (!mUserManager.isVisibleBackgroundUsersSupported()) { + return false; + } + if (mUserManagerInternal == null) { + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); + } + return mUserManagerInternal.isVisibleBackgroundFullUser(userId); + } + private void notifyTrustAgentsOfDeviceLockState(int userId, boolean isLocked) { for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo agent = mActiveAgents.valueAt(i); diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java index 3be266e2951b..f069dcdbc86b 100644 --- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -145,11 +145,13 @@ class AppCompatSizeCompatModePolicy { } } - void updateSizeCompatScale(@NonNull Rect resolvedAppBounds, @NonNull Rect containerAppBounds) { + void updateSizeCompatScale(@NonNull Rect resolvedAppBounds, @NonNull Rect containerAppBounds, + @NonNull Configuration newParentConfig) { mSizeCompatScale = mActivityRecord.mAppCompatController.getTransparentPolicy() .findOpaqueNotFinishingActivityBelow() .map(activityRecord -> mSizeCompatScale) - .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds)); + .orElseGet(() -> calculateSizeCompatScale( + resolvedAppBounds, containerAppBounds, newParentConfig)); } void clearSizeCompatModeAttributes() { @@ -290,7 +292,7 @@ class AppCompatSizeCompatModePolicy { // Calculates the scale the size compatibility bounds into the region which is available // to application. final float lastSizeCompatScale = mSizeCompatScale; - updateSizeCompatScale(resolvedAppBounds, containerAppBounds); + updateSizeCompatScale(resolvedAppBounds, containerAppBounds, newParentConfiguration); final int containerTopInset = containerAppBounds.top - containerBounds.top; final boolean topNotAligned = @@ -423,7 +425,7 @@ class AppCompatSizeCompatModePolicy { } private float calculateSizeCompatScale(@NonNull Rect resolvedAppBounds, - @NonNull Rect containerAppBounds) { + @NonNull Rect containerAppBounds, @NonNull Configuration newParentConfig) { final int contentW = resolvedAppBounds.width(); final int contentH = resolvedAppBounds.height(); final int viewportW = containerAppBounds.width(); @@ -432,7 +434,8 @@ class AppCompatSizeCompatModePolicy { // original container or if it's a freeform window in desktop mode. boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH) || (canEnterDesktopMode(mActivityRecord.mAtmService.mContext) - && mActivityRecord.getWindowingMode() == WINDOWING_MODE_FREEFORM); + && newParentConfig.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_FREEFORM); return shouldAllowUpscaling ? Math.min( (float) viewportW / contentW, (float) viewportH / contentH) : 1f; } diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index 19941741ed19..3a2cffbe1d85 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -422,6 +422,7 @@ class DeferredDisplayUpdater { || first.brightnessMaximum != second.brightnessMaximum || first.brightnessDefault != second.brightnessDefault || first.installOrientation != second.installOrientation + || first.isForceSdr != second.isForceSdr || !Objects.equals(first.layoutLimitedRefreshRate, second.layoutLimitedRefreshRate) || !BrightnessSynchronizer.floatEquals(first.hdrSdrRatio, second.hdrSdrRatio) || !first.thermalRefreshRateThrottling.contentEquals( diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 5514294ed477..e007b1d07b34 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -181,22 +181,30 @@ class EmbeddedWindowController { return true; } - boolean transferToHost(@NonNull InputTransferToken embeddedWindowToken, + boolean transferToHost(int callingUid, @NonNull InputTransferToken embeddedWindowToken, @NonNull WindowState transferToHostWindowState) { EmbeddedWindow ew = getByInputTransferToken(embeddedWindowToken); if (!isValidTouchGestureParams(transferToHostWindowState, ew)) { return false; } + if (callingUid != ew.mOwnerUid) { + throw new SecurityException( + "Transfer request must originate from owner of transferFromToken"); + } return mInputManagerService.transferTouchGesture(ew.getInputChannelToken(), transferToHostWindowState.mInputChannelToken); } - boolean transferToEmbedded(WindowState hostWindowState, + boolean transferToEmbedded(int callingUid, WindowState hostWindowState, @NonNull InputTransferToken transferToToken) { final EmbeddedWindowController.EmbeddedWindow ew = getByInputTransferToken(transferToToken); if (!isValidTouchGestureParams(hostWindowState, ew)) { return false; } + if (callingUid != hostWindowState.mOwnerUid) { + throw new SecurityException( + "Transfer request must originate from owner of transferFromToken"); + } return mInputManagerService.transferTouchGesture(hostWindowState.mInputChannelToken, ew.getInputChannelToken()); } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 638e92f112c7..42ea5a88a09b 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -28,6 +28,7 @@ import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.Display.INVALID_DISPLAY; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; import static com.android.server.wm.ActivityRecord.State.RESUMED; @@ -57,6 +58,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ArrayUtils; import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.util.function.pooled.PooledPredicate; +import com.android.server.pm.UserManagerInternal; import com.android.server.wm.LaunchParamsController.LaunchParams; import java.io.PrintWriter; @@ -1761,10 +1763,10 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { * @return last reparented root task, or {@code null} if the root tasks had to be destroyed. */ Task remove() { + final TaskDisplayArea toDisplayArea = getReparentToTaskDisplayArea(getFocusedRootTask()); mPreferredTopFocusableRootTask = null; // TODO(b/153090332): Allow setting content removal mode per task display area final boolean destroyContentOnRemoval = mDisplayContent.shouldDestroyContentOnRemove(); - final TaskDisplayArea toDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea(); Task lastReparentedRootTask = null; // Root tasks could be reparented from the removed display area to other display area. After @@ -1830,6 +1832,41 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { return lastReparentedRootTask; } + /** + * Returns the {@link TaskDisplayArea} to which root tasks should be reparented. + * + * <p>In the automotive multi-user multi-display environment where background users have + * UI access on their assigned displays (a.k.a. visible background users), it's not allowed to + * launch an activity on an unassigned display. If an activity is attempted to launch on an + * unassigned display, it throws an exception. + * <p>This method determines the appropriate {@link TaskDisplayArea} for reparenting root tasks + * when a display is removed, in order to avoid the exception. If the root task is null, + * the visible background user is not supported or the user associated with the root task is + * not a visible background user, it returns the default {@link TaskDisplayArea} of the default + * display. Otherwise, it returns the default {@link TaskDisplayArea} of the main display + * assigned to the user. + * + * @param rootTask The root task whose {@link TaskDisplayArea} needs to be determined. + * @return The {@link TaskDisplayArea} where the root tasks should be reparented to. + */ + private TaskDisplayArea getReparentToTaskDisplayArea(Task rootTask) { + final TaskDisplayArea defaultTaskDisplayArea = + mRootWindowContainer.getDefaultTaskDisplayArea(); + if (rootTask == null) { + return defaultTaskDisplayArea; + } + UserManagerInternal userManagerInternal = mAtmService.mWindowManager.mUmInternal; + if (!userManagerInternal.isVisibleBackgroundFullUser(rootTask.mUserId)) { + return defaultTaskDisplayArea; + } + int toDisplayId = userManagerInternal.getMainDisplayAssignedToUser(rootTask.mUserId); + if (toDisplayId == INVALID_DISPLAY) { + return defaultTaskDisplayArea; + } + DisplayContent dc = mRootWindowContainer.getDisplayContent(toDisplayId); + return dc != null ? dc.getDefaultTaskDisplayArea() : defaultTaskDisplayArea; + } + /** Whether this task display area can request orientation. */ boolean canSpecifyOrientation(@ScreenOrientation int orientation) { // Only allow to specify orientation if this TDA is the last focused one on this logical diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 33f2dd103c2e..b8f47cce6005 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -9212,6 +9212,8 @@ public class WindowManagerService extends IWindowManager.Stub final InputApplicationHandle applicationHandle; final String name; Objects.requireNonNull(outInputChannel); + Objects.requireNonNull(inputTransferToken); + synchronized (mGlobalLock) { WindowState hostWindowState = hostInputTransferToken != null ? mInputToWindowMap.get(hostInputTransferToken.getToken()) : null; @@ -9236,6 +9238,7 @@ public class WindowManagerService extends IWindowManager.Stub Objects.requireNonNull(transferFromToken); Objects.requireNonNull(transferToToken); + final int callingUid = Binder.getCallingUid(); final long identity = Binder.clearCallingIdentity(); boolean didTransfer; try { @@ -9245,12 +9248,14 @@ public class WindowManagerService extends IWindowManager.Stub // represents an embedded window so transfer from host to embedded. WindowState windowStateTo = mInputToWindowMap.get(transferToToken.getToken()); if (windowStateTo != null) { - didTransfer = mEmbeddedWindowController.transferToHost(transferFromToken, + didTransfer = mEmbeddedWindowController.transferToHost(callingUid, + transferFromToken, windowStateTo); } else { WindowState windowStateFrom = mInputToWindowMap.get( transferFromToken.getToken()); - didTransfer = mEmbeddedWindowController.transferToEmbedded(windowStateFrom, + didTransfer = mEmbeddedWindowController.transferToEmbedded(callingUid, + windowStateFrom, transferToToken); } } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 976be4aa3bd4..30d6f0a46bae 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -326,6 +326,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio public static final int ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK = 1 << 22; public static final int ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN = 1 << 23; public static final int ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM = 1 << 24; + public static final int ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE = 1 << 25; public static final int ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER = 0x0000ffff; /** @@ -1293,8 +1294,12 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio if (hasResumedFreeform && com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode() // Exclude task layer 1 because it is already the top most. - && minTaskLayer > 1 && minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) { - stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM; + && minTaskLayer > 1) { + if (minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) { + stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM; + } else { + stateFlags |= ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE; + } } stateFlags |= minTaskLayer & ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER; if (visible) { diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt index 63cf7bf7cb5e..c05c3819ca28 100644 --- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt @@ -139,16 +139,15 @@ class MetadataSyncAdapterTest { runtimeSearchSession.put(putDocumentsRequest).get() staticSearchSession.put(putDocumentsRequest).get() val metadataSyncAdapter = - MetadataSyncAdapter( - testExecutor, - runtimeSearchSession, + MetadataSyncAdapter(testExecutor, packageManager, appSearchManager) + + val submitSyncRequest = + metadataSyncAdapter.trySyncAppFunctionMetadataBlocking( staticSearchSession, - packageManager, + runtimeSearchSession, ) - val submitSyncRequest = metadataSyncAdapter.submitSyncRequest() - - assertThat(submitSyncRequest.get()).isTrue() + assertThat(submitSyncRequest).isInstanceOf(Unit::class.java) } @Test @@ -182,16 +181,15 @@ class MetadataSyncAdapterTest { PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build() staticSearchSession.put(putDocumentsRequest).get() val metadataSyncAdapter = - MetadataSyncAdapter( - testExecutor, - runtimeSearchSession, + MetadataSyncAdapter(testExecutor, packageManager, appSearchManager) + + val submitSyncRequest = + metadataSyncAdapter.trySyncAppFunctionMetadataBlocking( staticSearchSession, - packageManager, + runtimeSearchSession, ) - val submitSyncRequest = metadataSyncAdapter.submitSyncRequest() - - assertThat(submitSyncRequest.get()).isTrue() + assertThat(submitSyncRequest).isInstanceOf(Unit::class.java) } @Test @@ -239,16 +237,15 @@ class MetadataSyncAdapterTest { PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build() runtimeSearchSession.put(putDocumentsRequest).get() val metadataSyncAdapter = - MetadataSyncAdapter( - testExecutor, - runtimeSearchSession, + MetadataSyncAdapter(testExecutor, packageManager, appSearchManager) + + val submitSyncRequest = + metadataSyncAdapter.trySyncAppFunctionMetadataBlocking( staticSearchSession, - packageManager, + runtimeSearchSession, ) - val submitSyncRequest = metadataSyncAdapter.submitSyncRequest() - - assertThat(submitSyncRequest.get()).isTrue() + assertThat(submitSyncRequest).isInstanceOf(Unit::class.java) } @Test diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 8b80f85aec00..255dcb083518 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -27,6 +27,7 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_D import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; +import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.server.display.ExternalDisplayPolicy.ENABLE_ON_CONNECT; @@ -195,8 +196,8 @@ public class DisplayManagerServiceTest { private static final String VIRTUAL_DISPLAY_NAME = "Test Virtual Display"; private static final String PACKAGE_NAME = "com.android.frameworks.displayservicetests"; private static final long STANDARD_DISPLAY_EVENTS = DisplayManager.EVENT_FLAG_DISPLAY_ADDED - | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED - | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED; + | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED + | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED; private static final long STANDARD_AND_CONNECTION_DISPLAY_EVENTS = STANDARD_DISPLAY_EVENTS | DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED; @@ -238,6 +239,8 @@ public class DisplayManagerServiceTest { private UserManager mUserManager; + private int[] mAllowedHdrOutputTypes; + private final DisplayManagerService.Injector mShortMockedInjector = new DisplayManagerService.Injector() { @Override @@ -256,11 +259,12 @@ public class DisplayManagerServiceTest { displayAdapterListener, flags, mMockedDisplayNotificationManager, new LocalDisplayAdapter.Injector() { - @Override - public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() { - return mSurfaceControlProxy; - } - }); + @Override + public LocalDisplayAdapter.SurfaceControlProxy + getSurfaceControlProxy() { + return mSurfaceControlProxy; + } + }); } @Override @@ -320,7 +324,7 @@ public class DisplayManagerServiceTest { @Override int setHdrConversionMode(int conversionMode, int preferredHdrOutputType, - int[] autoHdrTypes) { + int[] allowedHdrOutputTypes) { mHdrConversionMode = conversionMode; mPreferredHdrOutputType = preferredHdrOutputType; return Display.HdrCapabilities.HDR_TYPE_INVALID; @@ -1295,11 +1299,11 @@ public class DisplayManagerServiceTest { .setUniqueId("uniqueId --- mirror display"); assertThrows(SecurityException.class, () -> { localService.createVirtualDisplay( - builder.build(), - mMockAppToken /* callback */, - null /* virtualDeviceToken */, - mock(DisplayWindowPolicyController.class), - PACKAGE_NAME); + builder.build(), + mMockAppToken /* callback */, + null /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); }); } @@ -1433,7 +1437,7 @@ public class DisplayManagerServiceTest { // The virtual display should not have FLAG_ALWAYS_UNLOCKED set. assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags - & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED)); + & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED)); } /** @@ -1466,7 +1470,7 @@ public class DisplayManagerServiceTest { // The virtual display should not have FLAG_PRESENTATION set. assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags - & DisplayDeviceInfo.FLAG_PRESENTATION)); + & DisplayDeviceInfo.FLAG_PRESENTATION)); } @Test @@ -2358,6 +2362,7 @@ public class DisplayManagerServiceTest { HdrConversionMode.HDR_CONVERSION_FORCE, Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION); displayManager.setHdrConversionModeInternal(mode); + assertEquals(mode, displayManager.getHdrConversionModeSettingInternal()); assertEquals(mode.getConversionMode(), mHdrConversionMode); assertEquals(mode.getPreferredHdrOutputType(), mPreferredHdrOutputType); @@ -2402,6 +2407,86 @@ public class DisplayManagerServiceTest { } @Test + public void testSetAreUserDisabledHdrTypesAllowed_withFalse_whenHdrDisabled_stripsHdrType() { + DisplayManagerService displayManager = new DisplayManagerService( + mContext, new BasicInjector() { + @Override + int setHdrConversionMode(int conversionMode, int preferredHdrOutputType, + int[] allowedTypes) { + mAllowedHdrOutputTypes = allowedTypes; + return Display.HdrCapabilities.HDR_TYPE_INVALID; + } + + // Overriding this method to capture the allowed HDR type + @Override + int[] getSupportedHdrOutputTypes() { + return new int[]{Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION}; + } + }); + + // Setup: no HDR types disabled, userDisabledTypes allowed, system conversion + displayManager.setUserDisabledHdrTypesInternal(new int [0]); + displayManager.setAreUserDisabledHdrTypesAllowedInternal(true); + displayManager.setHdrConversionModeInternal( + new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM)); + + assertEquals(1, mAllowedHdrOutputTypes.length); + assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == mAllowedHdrOutputTypes[0]); + + // Action: disable Dolby Vision, set userDisabledTypes not allowed + displayManager.setUserDisabledHdrTypesInternal( + new int [] {Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION}); + displayManager.setAreUserDisabledHdrTypesAllowedInternal(false); + + assertEquals(0, mAllowedHdrOutputTypes.length); + } + + @Test + public void testGetEnabledHdrTypesLocked_whenTypesDisabled_stripsDisabledTypes() { + DisplayManagerService displayManager = new DisplayManagerService( + mContext, new BasicInjector() { + @Override + int[] getSupportedHdrOutputTypes() { + return new int[]{Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION}; + } + }); + + displayManager.setUserDisabledHdrTypesInternal(new int [0]); + displayManager.setAreUserDisabledHdrTypesAllowedInternal(true); + int [] enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes(); + assertEquals(1, enabledHdrOutputTypes.length); + assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == enabledHdrOutputTypes[0]); + + displayManager.setAreUserDisabledHdrTypesAllowedInternal(false); + enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes(); + assertEquals(1, enabledHdrOutputTypes.length); + assertTrue(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION == enabledHdrOutputTypes[0]); + + displayManager.setUserDisabledHdrTypesInternal( + new int [] {Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION}); + enabledHdrOutputTypes = displayManager.getEnabledHdrOutputTypes(); + assertEquals(0, enabledHdrOutputTypes.length); + } + + @Test + public void testSetHdrConversionModeInternal_isForceSdrIsUpdated() { + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); + FakeDisplayDevice displayDevice = + createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL); + LogicalDisplay logicalDisplay = + logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true); + + displayManager.setHdrConversionModeInternal( + new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_FORCE, HDR_TYPE_INVALID)); + assertTrue(logicalDisplay.getDisplayInfoLocked().isForceSdr); + + displayManager.setHdrConversionModeInternal( + new HdrConversionMode(HdrConversionMode.HDR_CONVERSION_SYSTEM)); + assertFalse(logicalDisplay.getDisplayInfoLocked().isForceSdr); + } + + @Test public void testReturnsRefreshRateForDisplayAndSensor_proximitySensorSet() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); @@ -3505,7 +3590,7 @@ public class DisplayManagerServiceTest { } private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager, - Display.Mode[] modes) { + Display.Mode[] modes) { FakeDisplayDevice displayDevice = new FakeDisplayDevice(); DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo(); displayDeviceInfo.supportedModes = modes; @@ -3761,9 +3846,9 @@ public class DisplayManagerServiceTest { public void setUserPreferredDisplayModeLocked(Display.Mode preferredMode) { for (Display.Mode mode : mDisplayDeviceInfo.supportedModes) { if (mode.matchesIfValid( - preferredMode.getPhysicalWidth(), - preferredMode.getPhysicalHeight(), - preferredMode.getRefreshRate())) { + preferredMode.getPhysicalWidth(), + preferredMode.getPhysicalHeight(), + preferredMode.getRefreshRate())) { mPreferredMode = mode; break; } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 5ec53023dc67..f6ad07d03673 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -62,6 +62,7 @@ import static com.android.server.am.ProcessList.PERSISTENT_SERVICE_ADJ; import static com.android.server.am.ProcessList.PREVIOUS_APP_ADJ; import static com.android.server.am.ProcessList.SCHED_GROUP_BACKGROUND; import static com.android.server.am.ProcessList.SCHED_GROUP_DEFAULT; +import static com.android.server.am.ProcessList.SCHED_GROUP_FOREGROUND_WINDOW; import static com.android.server.am.ProcessList.SCHED_GROUP_RESTRICTED; import static com.android.server.am.ProcessList.SCHED_GROUP_TOP_APP; import static com.android.server.am.ProcessList.SCHED_GROUP_TOP_APP_BOUND; @@ -534,6 +535,14 @@ public class MockingOomAdjusterTests { updateOomAdj(app); assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP); assertEquals("perceptible-freeform-activity", app.mState.getAdjType()); + + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE + | WindowProcessController.ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE) + .when(wpc).getActivityStateFlags(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, + SCHED_GROUP_FOREGROUND_WINDOW); + assertEquals("vis-multi-window-activity", app.mState.getAdjType()); } @SuppressWarnings("GuardedBy") diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java index a82658b52a77..3062d5120e6f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java @@ -16,13 +16,19 @@ package com.android.server.pm; +import static android.media.AudioAttributes.USAGE_ALARM; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.testng.AssertJUnit.assertEquals; import android.app.Notification; import android.app.NotificationManager; @@ -31,6 +37,9 @@ import android.content.pm.UserInfo; import android.media.AudioAttributes; import android.media.AudioFocusInfo; import android.media.AudioManager; +import android.media.AudioPlaybackConfiguration; +import android.media.PlayerProxy; +import android.media.audiopolicy.AudioPolicy; import android.os.Build; import android.os.RemoteException; import android.os.UserHandle; @@ -45,6 +54,10 @@ import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + @RunWith(JUnit4.class) public class BackgroundUserSoundNotifierTest { @@ -63,7 +76,10 @@ public class BackgroundUserSoundNotifierTest { MockitoAnnotations.initMocks(this); mSpiedContext = spy(mRealContext); mUsersToRemove = new ArraySet<>(); - mUserManager = UserManager.get(mRealContext); + + mUserManager = spy(mSpiedContext.getSystemService(UserManager.class)); + doReturn(mUserManager) + .when(mSpiedContext).getSystemService(UserManager.class); doReturn(mNotificationManager) .when(mSpiedContext).getSystemService(NotificationManager.class); mBackgroundUserSoundNotifier = new BackgroundUserSoundNotifier(mSpiedContext); @@ -74,12 +90,9 @@ public class BackgroundUserSoundNotifierTest { mUsersToRemove.stream().toList().forEach(this::removeUser); } @Test - public void testAlarmOnBackgroundUser_ForegroundUserNotified() throws RemoteException { - AudioAttributes aa = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ALARM).build(); - UserInfo user = createUser("User", - UserManager.USER_TYPE_FULL_SECONDARY, - 0); + public void testAlarmOnBackgroundUser_foregroundUserNotified() throws RemoteException { + AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); + UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); final int fgUserId = mSpiedContext.getUserId(); final int bgUserUid = user.id * 100000; doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser(); @@ -95,10 +108,9 @@ public class BackgroundUserSoundNotifierTest { } @Test - public void testMediaOnBackgroundUser_ForegroundUserNotNotified() throws RemoteException { + public void testMediaOnBackgroundUser_foregroundUserNotNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA).build(); - UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); final int bgUserUid = mSpiedContext.getUserId() * 100000; AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", /* packageName= */ "com.android.car.audio", AudioManager.AUDIOFOCUS_GAIN, @@ -109,9 +121,9 @@ public class BackgroundUserSoundNotifierTest { } @Test - public void testAlarmOnForegroundUser_ForegroundUserNotNotified() throws RemoteException { + public void testAlarmOnForegroundUser_foregroundUserNotNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ALARM).build(); + .setUsage(USAGE_ALARM).build(); final int fgUserId = mSpiedContext.getUserId(); final int fgUserUid = fgUserId * 100000; doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser(); @@ -123,6 +135,109 @@ public class BackgroundUserSoundNotifierTest { verifyZeroInteractions(mNotificationManager); } + @Test + public void testMuteAlarmSounds() { + final int fgUserId = mSpiedContext.getUserId(); + int bgUserId = fgUserId + 1; + int bgUserUid = bgUserId * 100000; + mBackgroundUserSoundNotifier.mNotificationClientUid = bgUserUid; + + AudioManager mockAudioManager = mock(AudioManager.class); + when(mSpiedContext.getSystemService(AudioManager.class)).thenReturn(mockAudioManager); + + AudioPlaybackConfiguration apc1 = mock(AudioPlaybackConfiguration.class); + when(apc1.getClientUid()).thenReturn(bgUserUid); + when(apc1.getPlayerProxy()).thenReturn(mock(PlayerProxy.class)); + + AudioPlaybackConfiguration apc2 = mock(AudioPlaybackConfiguration.class); + when(apc2.getClientUid()).thenReturn(bgUserUid + 1); + when(apc2.getPlayerProxy()).thenReturn(mock(PlayerProxy.class)); + + List<AudioPlaybackConfiguration> configs = new ArrayList<>(); + configs.add(apc1); + configs.add(apc2); + when(mockAudioManager.getActivePlaybackConfigurations()).thenReturn(configs); + + AudioPolicy mockAudioPolicy = mock(AudioPolicy.class); + + AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); + AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", /* packageName= */ "", + AudioManager.AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_NONE, /* flags= */ 0, + Build.VERSION.SDK_INT); + Stack<AudioFocusInfo> focusStack = new Stack<>(); + focusStack.add(afi); + doReturn(focusStack).when(mockAudioPolicy).getFocusStack(); + mBackgroundUserSoundNotifier.mFocusControlAudioPolicy = mockAudioPolicy; + + mBackgroundUserSoundNotifier.muteAlarmSounds(mSpiedContext); + + verify(apc1.getPlayerProxy()).stop(); + verify(apc2.getPlayerProxy(), never()).stop(); + } + + @Test + public void testOnAudioFocusGrant_alarmOnBackgroundUser_notifiesForegroundUser() { + final int fgUserId = mSpiedContext.getUserId(); + UserInfo bgUser = createUser("Background User", UserManager.USER_TYPE_FULL_SECONDARY, 0); + int bgUserUid = bgUser.id * 100000; + + AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); + AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", "", + AudioManager.AUDIOFOCUS_GAIN, 0, 0, Build.VERSION.SDK_INT); + + mBackgroundUserSoundNotifier.getAudioPolicyFocusListener() + .onAudioFocusGrant(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + + verify(mNotificationManager) + .notifyAsUser(eq(BackgroundUserSoundNotifier.class.getSimpleName()), + eq(afi.getClientUid()), any(Notification.class), + eq(UserHandle.of(fgUserId))); + } + + + @Test + public void testCreateNotification_UserSwitcherEnabled_bothActionsAvailable() { + String userName = "BgUser"; + + doReturn(true).when(mUserManager).isUserSwitcherEnabled(); + doReturn(UserManager.SWITCHABILITY_STATUS_OK) + .when(mUserManager).getUserSwitchability(any()); + + Notification notification = mBackgroundUserSoundNotifier.createNotification(userName, + mSpiedContext); + + assertEquals("Alarm for BgUser", notification.extras.getString( + Notification.EXTRA_TITLE)); + assertEquals(Notification.CATEGORY_REMINDER, notification.category); + assertEquals(Notification.VISIBILITY_PUBLIC, notification.visibility); + assertEquals(com.android.internal.R.drawable.ic_audio_alarm, + notification.getSmallIcon().getResId()); + + assertEquals(2, notification.actions.length); + assertEquals(mSpiedContext.getString( + com.android.internal.R.string.bg_user_sound_notification_button_mute), + notification.actions[0].title); + assertEquals(mSpiedContext.getString( + com.android.internal.R.string.bg_user_sound_notification_button_switch_user), + notification.actions[1].title); + } + + @Test + public void testCreateNotification_UserSwitcherDisabled_onlyMuteActionAvailable() { + String userName = "BgUser"; + + doReturn(false).when(mUserManager).isUserSwitcherEnabled(); + doReturn(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED) + .when(mUserManager).getUserSwitchability(any()); + + Notification notification = mBackgroundUserSoundNotifier.createNotification(userName, + mSpiedContext); + + assertEquals(1, notification.actions.length); + assertEquals(mSpiedContext.getString( + com.android.internal.R.string.bg_user_sound_notification_button_mute), + notification.actions[0].title); + } private UserInfo createUser(String name, String userType, int flags) { UserInfo user = mUserManager.createUser(name, userType, flags); diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java index 1a398c5f1ec3..e0c393cada49 100644 --- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java @@ -100,6 +100,7 @@ import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.SystemServiceManager; +import com.android.server.pm.UserManagerInternal; import org.junit.After; import org.junit.Before; @@ -145,6 +146,7 @@ public class TrustManagerServiceTest { private static final String URI_SCHEME_PACKAGE = "package"; private static final int TEST_USER_ID = 50; + private static final int TEST_VISIBLE_BACKGROUND_USER_ID = 51; private static final UserInfo TEST_USER = new UserInfo(TEST_USER_ID, "user", UserInfo.FLAG_FULL); private static final int PARENT_USER_ID = 60; @@ -170,6 +172,7 @@ public class TrustManagerServiceTest { private @Mock KeyStoreAuthorization mKeyStoreAuthorization; private @Mock LockPatternUtils mLockPatternUtils; private @Mock LockSettingsInternal mLockSettingsInternal; + private @Mock UserManagerInternal mUserManagerInternal; private @Mock PackageManager mPackageManager; private @Mock UserManager mUserManager; private @Mock IWindowManager mWindowManager; @@ -224,6 +227,7 @@ public class TrustManagerServiceTest { when(mUserManager.getAliveUsers()).thenReturn(List.of(TEST_USER)); when(mUserManager.getEnabledProfileIds(TEST_USER_ID)).thenReturn(new int[0]); when(mUserManager.getUserInfo(TEST_USER_ID)).thenReturn(TEST_USER); + when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(false); when(mWindowManager.isKeyguardLocked()).thenReturn(true); @@ -593,6 +597,54 @@ public class TrustManagerServiceTest { verify(mTrustListener, never()).onTrustManagedChanged(anyBoolean(), anyInt()); } + @Test + public void testDeviceLocked_visibleBackgroundUser_userLocked() throws RemoteException { + setupVisibleBackgroundUser(/* visible= */ true, /* unlocked= */ false); + mService.waitForIdle(); + mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID); + mService.waitForIdle(); + assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isTrue(); + } + + @Test + public void testDeviceLocked_visibleBackgroundUser_userUnlocked() throws RemoteException { + setupVisibleBackgroundUser(/* visible= */ true, /* unlocked= */ true); + mService.waitForIdle(); + mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID); + mService.waitForIdle(); + assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isFalse(); + } + + @Test + public void testDeviceLocked_invisibleBackgroundUser_userUnlocked() throws RemoteException { + setupVisibleBackgroundUser(/* visible= */ false, /* unlocked= */ true); + mService.waitForIdle(); + mTrustManager.reportEnabledTrustAgentsChanged(TEST_VISIBLE_BACKGROUND_USER_ID); + mService.waitForIdle(); + assertThat(mService.isDeviceLockedInner(TEST_VISIBLE_BACKGROUND_USER_ID)).isTrue(); + } + + private void setupVisibleBackgroundUser(boolean visible, boolean unlocked) { + UserInfo info = new UserInfo(TEST_VISIBLE_BACKGROUND_USER_ID, "visible bg user", + UserInfo.FLAG_FULL); + + when(mActivityManager.isUserRunning(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(true); + + when(mLockPatternUtils.isSecure(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(true); + + when(mUserManager.getAliveUsers()).thenReturn(List.of(TEST_USER, info)); + when(mUserManager.getEnabledProfileIds(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn( + new int[0]); + when(mUserManager.getUserInfo(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(info); + when(mUserManager.isUserUnlocked(TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(unlocked); + when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true); + + LocalServices.removeServiceForTest(UserManagerInternal.class); + LocalServices.addService(UserManagerInternal.class, mUserManagerInternal); + when(mUserManagerInternal.isVisibleBackgroundFullUser( + TEST_VISIBLE_BACKGROUND_USER_ID)).thenReturn(visible); + } + private void setUpRenewableTrust(ITrustAgentService trustAgent) throws RemoteException { ITrustAgentServiceCallback callback = getCallback(trustAgent); callback.setManagingTrust(true); diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java index 44aa868716eb..a55aa2364aa5 100644 --- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java +++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java @@ -30,7 +30,6 @@ import android.graphics.fonts.SystemFonts; import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.platform.test.annotations.Presubmit; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.system.Os; @@ -41,8 +40,6 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.text.flags.Flags; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -1106,7 +1103,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1126,7 +1122,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1); @@ -1146,7 +1141,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf and bar.ttf installTestFontFile(2 /* numFonts */, 1 /* version */); @@ -1166,7 +1160,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf and bar.ttf installTestFontFile(2 /* numFonts */, 1 /* version */); @@ -1186,7 +1179,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1206,7 +1198,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureAllMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1226,7 +1217,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureAllMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1246,7 +1236,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureAllMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1266,7 +1255,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1286,7 +1274,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1); @@ -1306,7 +1293,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf and bar.ttf installTestFontFile(2 /* numFonts */, 1 /* version */); @@ -1326,7 +1312,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf and bar.ttf installTestFontFile(2 /* numFonts */, 1 /* version */); @@ -1346,7 +1331,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1366,7 +1350,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontAllMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1386,7 +1369,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontAllMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1406,7 +1388,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontAllMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1426,7 +1407,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontDirAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1446,7 +1426,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontDirAllMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1466,7 +1445,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontDirAllMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1486,7 +1464,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontDirAllMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1506,7 +1483,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void dirContentAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1527,7 +1503,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void dirContentAllMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1548,7 +1523,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void dirContentAllMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1569,7 +1543,6 @@ public final class UpdatableFontDirTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void dirContentAllMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java index 4d2396c78d16..65b4ac116b34 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java @@ -104,6 +104,12 @@ public class VibratorHelperTest extends UiServiceTestCase { } @Test + public void createVibrationEffectFromSoundUri_opaqueUri() { + Uri uri = Uri.parse("a:b#c"); + assertNull(mVibratorHelper.createVibrationEffectFromSoundUri(uri)); + } + + @Test public void createVibrationEffectFromSoundUri_uriWithoutRequiredQueryParameter() { Uri uri = Settings.System.DEFAULT_NOTIFICATION_URI; assertNull(mVibratorHelper.createVibrationEffectFromSoundUri(uri)); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index 8ee7e0304d2b..84c4f620f394 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -32,6 +32,7 @@ import static android.service.notification.Condition.STATE_FALSE; import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON; import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_API; +import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_UI; import static android.service.notification.ZenModeConfig.ZEN_TAG; import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE; import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE; @@ -1169,6 +1170,23 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertThat(suppressedEffectsOf(result)).isEqualTo(suppressedEffectsOf(policy)); } + @Test + public void readXml_fixesWronglyDisabledManualRule() throws Exception { + ZenModeConfig config = getCustomConfig(); + if (!Flags.modesUi()) { + config.manualRule = new ZenModeConfig.ZenRule(); + config.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + } + config.manualRule.enabled = false; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeConfigXml(config, XML_VERSION_MODES_UI, /* forBackup= */ false, baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ZenModeConfig fromXml = readConfigXml(bais); + + assertThat(fromXml.manualRule.enabled).isTrue(); + } + private static String suppressedEffectsOf(Policy policy) { return suppressedEffectsToString(policy.suppressedVisualEffects) + "(" + policy.suppressedVisualEffects + ")"; diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index f74340113a04..7bce8285972c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -1640,7 +1640,7 @@ public class SizeCompatTests extends WindowTestsBase { .build(); setUpApp(display); prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT); - mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM); + mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM); assertFalse(mActivity.inSizeCompatMode()); // Resize app to make original app bounds larger than parent bounds. @@ -1667,7 +1667,7 @@ public class SizeCompatTests extends WindowTestsBase { .build(); setUpApp(display); prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT); - mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM); + mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM); assertFalse(mActivity.inSizeCompatMode()); // Resize app to make original app bounds smaller than parent bounds. @@ -1692,7 +1692,7 @@ public class SizeCompatTests extends WindowTestsBase { .build(); setUpApp(display); prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT); - mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM); + mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM); assertFalse(mActivity.inSizeCompatMode()); final Rect originalAppBounds = mActivity.getBounds(); @@ -1705,6 +1705,38 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(originalAppBounds, mActivity.getBounds()); } + /** + * Test that when desktop mode is enabled, a freeform unresizeable activity is not up-scaled + * when exiting freeform despite its larger parent bounds. + */ + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void testCompatScaling_freeformUnresizeableApp_exitFreeform_notScaled() { + doReturn(true).when(() -> + DesktopModeHelper.canEnterDesktopMode(any())); + final int dw = 600; + final int dh = 800; + final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .build(); + setUpApp(display); + prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT); + mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FREEFORM); + final Rect originalAppBounds = mActivity.getBounds(); + + assertFalse(mActivity.inSizeCompatMode()); + + // Resize app to make original app bounds smaller than parent bounds. + mTask.getWindowConfiguration().setAppBounds( + new Rect(0, 0, dw + 300, dh + 400)); + // Change windowing mode from freeform to fullscreen + mTask.getWindowConfiguration().setWindowingMode(WINDOWING_MODE_FULLSCREEN); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + // App should enter size compat mode but remain its original size. + assertTrue(mActivity.inSizeCompatMode()); + assertEquals(originalAppBounds, mActivity.getBounds()); + } + @Test public void testGetLetterboxInnerBounds_noScalingApplied() { // Set up a display in portrait and ignoring orientation request. diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt index 92b6b934874f..82e53c81daaa 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt @@ -54,7 +54,7 @@ class ShowImeOnUnlockScreenTest(flicker: LegacyFlickerTest) : BaseTest(flicker) } transitions { device.sleep() - wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify() + wmHelper.StateSyncBuilder().withKeyguardShowing().waitForAndVerify() UnlockScreenRule.unlockScreen(device) wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify() } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt new file mode 100644 index 000000000000..69fde0168b14 --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.helpers + +import android.app.Instrumentation +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.helpers.SYSTEMUI_PACKAGE +import android.tools.traces.component.ComponentNameMatcher +import android.tools.traces.parsers.WindowManagerStateHelper +import android.tools.traces.parsers.toFlickerComponent +import android.util.Log +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.UiObjectNotFoundException +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.testapp.ActivityOptions +import java.util.regex.Pattern + +class StartMediaProjectionAppHelper +@JvmOverloads +constructor( + instr: Instrumentation, + launcherName: String = ActivityOptions.StartMediaProjectionActivity.LABEL, + component: ComponentNameMatcher = + ActivityOptions.StartMediaProjectionActivity.COMPONENT.toFlickerComponent() +) : StandardAppHelper(instr, launcherName, component) { + private val packageManager = instr.context.packageManager + + fun startEntireScreenMediaProjection(wmHelper: WindowManagerStateHelper) { + clickStartMediaProjectionButton() + chooseEntireScreenOption() + startScreenSharing() + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + } + + fun startSingleAppMediaProjection( + wmHelper: WindowManagerStateHelper, + targetApp: StandardAppHelper + ) { + clickStartMediaProjectionButton() + chooseSingleAppOption() + startScreenSharing() + selectTargetApp(targetApp.appName) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(targetApp) + .waitForAndVerify() + } + + private fun clickStartMediaProjectionButton() { + findObject(By.res(packageName, START_MEDIA_PROJECTION_BUTTON_ID)).also { it.click() } + } + + private fun chooseEntireScreenOption() { + findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() } + + val entireScreenString = getSysUiResourceString(ENTIRE_SCREEN_STRING_RES_NAME) + findObject(By.text(entireScreenString)).also { it.click() } + } + + private fun selectTargetApp(targetAppName: String) { + // Scroll to to find target app to launch then click app icon it to start capture + val scrollable = UiScrollable(UiSelector().scrollable(true)) + try { + scrollable.scrollForward() + if (!scrollable.scrollIntoView(UiSelector().text(targetAppName))) { + Log.e(TAG, "Didn't find target app when scrolling") + return + } + } catch (e: UiObjectNotFoundException) { + Log.d(TAG, "There was no scrolling (UI may not be scrollable") + } + + findObject(By.text(targetAppName)).also { it.click() } + } + + private fun chooseSingleAppOption() { + findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() } + + val singleAppString = getSysUiResourceString(SINGLE_APP_STRING_RES_NAME) + findObject(By.text(singleAppString)).also { it.click() } + } + + private fun startScreenSharing() { + findObject(By.res(ACCEPT_RESOURCE_ID)).also { it.click() } + } + + private fun findObject(selector: BySelector): UiObject2 = + uiDevice.wait(Until.findObject(selector), TIMEOUT) ?: error("Can't find object $selector") + + private fun getSysUiResourceString(resName: String): String = + with(packageManager.getResourcesForApplication(SYSTEMUI_PACKAGE)) { + getString(getIdentifier(resName, "string", SYSTEMUI_PACKAGE)) + } + + companion object { + const val TAG: String = "StartMediaProjectionAppHelper" + const val TIMEOUT: Long = 5000L + const val ACCEPT_RESOURCE_ID: String = "android:id/button1" + const val START_MEDIA_PROJECTION_BUTTON_ID: String = "button_start_mp" + val SCREEN_SHARE_OPTIONS_PATTERN: Pattern = + Pattern.compile("$SYSTEMUI_PACKAGE:id/screen_share_mode_(options|spinner)") + const val ENTIRE_SCREEN_STRING_RES_NAME: String = + "screen_share_permission_dialog_option_entire_screen" + const val SINGLE_APP_STRING_RES_NAME: String = + "screen_share_permission_dialog_option_single_app" + } +} diff --git a/tests/FlickerTests/test-apps/flickerapp/Android.bp b/tests/FlickerTests/test-apps/flickerapp/Android.bp index a186679790b6..c55df8604362 100644 --- a/tests/FlickerTests/test-apps/flickerapp/Android.bp +++ b/tests/FlickerTests/test-apps/flickerapp/Android.bp @@ -47,6 +47,7 @@ android_test { "wm-flicker-common-app-helpers", "wm-flicker-common-assertions", "wm-flicker-window-extensions", + "wm-shell-flicker-utils", ], } diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index 45260bddd355..f891606f0066 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -21,6 +21,15 @@ android:targetSdkVersion="35"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.INSTANT_APP_FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <application android:allowBackup="false" android:supportsRtl="true"> @@ -106,6 +115,17 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity android:name=".StartMediaProjectionActivity" + android:theme="@style/CutoutNever" + android:resizeableActivity="false" + android:taskAffinity="com.android.server.wm.flicker.testapp.StartMediaProjectionActivity" + android:label="StartMediaProjectionActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> <activity android:name=".PortraitImmersiveActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.PortraitImmersiveActivity" android:immersive="true" @@ -404,6 +424,11 @@ android:name="android.voice_interaction" android:resource="@xml/interaction_service"/> </service> + <service android:name="com.android.wm.shell.flicker.utils.MediaProjectionService" + android:foregroundServiceType="mediaProjection" + android:label="WMShellTestsMediaProjectionService" + android:enabled="true"> + </service> </application> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/> </manifest> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml new file mode 100644 index 000000000000..46f01e6c9752 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:background="@android:color/holo_orange_light"> + + <Button + android:id="@+id/button_start_mp" + android:layout_width="500dp" + android:layout_height="500dp" + android:gravity="center_vertical|center_horizontal" + android:text="Start Media Projection" + android:textAppearance="?android:attr/textAppearanceLarge"/> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java index 80c1dd072df7..e4de2c574553 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java @@ -85,6 +85,12 @@ public class ActivityOptions { FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity"); } + public static class StartMediaProjectionActivity { + public static final String LABEL = "StartMediaProjectionActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".StartMediaProjectionActivity"); + } + public static class PortraitImmersiveActivity { public static final String LABEL = "PortraitImmersiveActivity"; public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java new file mode 100644 index 000000000000..a24a48269d7c --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.EXTRA_MESSENGER; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.MSG_SERVICE_DESTROYED; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.MSG_START_FOREGROUND_DONE; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.hardware.display.VirtualDisplay; +import android.media.ImageReader; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Messenger; +import android.util.DisplayMetrics; +import android.util.Log; +import android.widget.Button; + +import com.android.wm.shell.flicker.utils.MediaProjectionService; + +public class StartMediaProjectionActivity extends Activity { + + private static final String TAG = "StartMediaProjectionActivity"; + private MediaProjectionManager mService; + private ImageReader mImageReader; + private VirtualDisplay mVirtualDisplay; + private MediaProjection mMediaProjection; + private MediaProjection.Callback mMediaProjectionCallback = new MediaProjection.Callback() { + @Override + public void onStop() { + super.onStop(); + } + + @Override + public void onCapturedContentResize(int width, int height) { + super.onCapturedContentResize(width, height); + } + + @Override + public void onCapturedContentVisibilityChanged(boolean isVisible) { + super.onCapturedContentVisibilityChanged(isVisible); + } + }; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + mService = getSystemService(MediaProjectionManager.class); + setContentView(R.layout.activity_start_media_projection); + + Button startMediaProjectionButton = findViewById(R.id.button_start_mp); + startMediaProjectionButton.setOnClickListener(v -> + startActivityForResult(mService.createScreenCaptureIntent(), REQUEST_CODE)); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_CODE) { + throw new IllegalStateException("Unknown request code: " + requestCode); + } + if (resultCode != RESULT_OK) { + throw new IllegalStateException("User denied screen sharing permission"); + } + Log.d(TAG, "onActivityResult"); + startMediaProjectionService(resultCode, data); + } + + private void startMediaProjectionService(int resultCode, Intent resultData) { + final Messenger messenger = new Messenger(new Handler(Looper.getMainLooper(), + msg -> { + switch (msg.what) { + case MSG_START_FOREGROUND_DONE: + setupMediaProjection(resultCode, resultData); + return true; + case MSG_SERVICE_DESTROYED: + return true; + } + Log.e(TAG, "Unknown message from the FlickerMPService: " + msg.what); + return false; + } + )); + + final Intent intent = new Intent() + .setComponent(new ComponentName(this, MediaProjectionService.class)) + .putExtra(EXTRA_MESSENGER, messenger); + startForegroundService(intent); + } + + private void setupMediaProjection(int resultCode, Intent resultData) { + mMediaProjection = mService.getMediaProjection(resultCode, resultData); + if (mMediaProjection == null) { + throw new IllegalStateException("cannot create new MediaProjection"); + } + + mMediaProjection.registerCallback( + mMediaProjectionCallback, new Handler(Looper.getMainLooper())); + + Rect displayBounds = getWindowManager().getMaximumWindowMetrics().getBounds(); + mImageReader = ImageReader.newInstance( + displayBounds.width(), displayBounds.height(), PixelFormat.RGBA_8888, 1); + + mVirtualDisplay = mMediaProjection.createVirtualDisplay( + "DanielDisplay", + displayBounds.width(), + displayBounds.height(), + DisplayMetrics.DENSITY_HIGH, + /* flags= */ 0, + mImageReader.getSurface(), + new VirtualDisplay.Callback() { + @Override + public void onStopped() { + if (mMediaProjection != null) { + if (mMediaProjectionCallback != null) { + mMediaProjection.unregisterCallback(mMediaProjectionCallback); + mMediaProjectionCallback = null; + } + mMediaProjection.stop(); + mMediaProjection = null; + } + if (mImageReader != null) { + mImageReader = null; + } + if (mVirtualDisplay != null) { + mVirtualDisplay.getSurface().release(); + mVirtualDisplay.release(); + mVirtualDisplay = null; + } + } + }, + new Handler(Looper.getMainLooper()) + ); + } + +} diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt index aa73c397a663..c61a25021949 100644 --- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt +++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt @@ -36,7 +36,6 @@ import com.android.cts.input.inputeventmatchers.withMotionAction import com.android.cts.input.inputeventmatchers.withPressure import com.android.cts.input.inputeventmatchers.withRawCoords import com.android.cts.input.inputeventmatchers.withSource -import java.io.InputStream import junit.framework.Assert.fail import org.hamcrest.Matchers.allOf import org.junit.Before @@ -130,17 +129,18 @@ class UinputRecordingIntegrationTests { scenario.virtualDisplay.display.uniqueId!!, ) - injectUinputEvents() - - if (DEBUG_RECEIVED_EVENTS) { - printReceivedEventsToLogcat(scenario.activity) - fail("Test cannot pass in debug mode!") + injectUinputEvents().use { + if (DEBUG_RECEIVED_EVENTS) { + printReceivedEventsToLogcat(scenario.activity) + fail("Test cannot pass in debug mode!") + } + + val verifier = EventVerifier( + BatchedEventSplitter { scenario.activity.getInputEvent() } + ) + verifyEvents(verifier) + scenario.activity.assertNoEvents() } - - val verifier = - EventVerifier(BatchedEventSplitter { scenario.activity.getInputEvent() }) - verifyEvents(verifier) - scenario.activity.assertNoEvents() } finally { inputManager.removeUniqueIdAssociationByPort(inputPort) } @@ -162,14 +162,32 @@ class UinputRecordingIntegrationTests { } } - private fun injectUinputEvents() { + /** + * Plays back the evemu recording associated with the current test case by injecting it via + * the `uinput` shell command in interactive mode. The recording playback will begin + * immediately, and the shell command (and the associated input device) will remain alive + * until the returned [AutoCloseable] is closed. + */ + private fun injectUinputEvents(): AutoCloseable { val fds = instrumentation.uiAutomation!!.executeShellCommandRw("uinput -") + // We do not need to use stdout in this test. + fds[0].close() - ParcelFileDescriptor.AutoCloseOutputStream(fds[1]).use { stdIn -> - val inputStream: InputStream = instrumentation.context.resources.openRawResource( + return ParcelFileDescriptor.AutoCloseOutputStream(fds[1]).also { stdin -> + instrumentation.context.resources.openRawResource( testData.uinputRecordingResource, - ) - stdIn.write(inputStream.readBytes()) + ).use { inputStream -> + stdin.write(inputStream.readBytes()) + + // TODO(b/367419268): Remove extra event injection when uinput parsing is fixed. + // Inject an extra sync event with an arbitrarily large timestamp, because the + // uinput command will not process the last event until either the next event is + // parsed, or fd is closed. Injecting this sync allows us complete injection of + // the evemu recording and extend the lifetime of the input device by keeping this + // fd open. + stdin.write("\nE: 9999.99 0 0 0\n".toByteArray()) + stdin.flush() + } } } diff --git a/tests/Tracing/Android.bp b/tests/Tracing/Android.bp new file mode 100644 index 000000000000..5a7f12f56655 --- /dev/null +++ b/tests/Tracing/Android.bp @@ -0,0 +1,33 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_team: "trendy_team_windowing_tools", + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "TracingTests", + proto: { + type: "nano", + }, + // Include some source files directly to be able to access package members + srcs: ["src/**/*.java"], + libs: ["android.test.runner"], + static_libs: [ + "junit", + "androidx.test.rules", + "mockito-target-minus-junit4", + "truth", + "platform-test-annotations", + "flickerlib-parsers", + "perfetto_trace_java_protos", + "flickerlib-trace_processor_shell", + ], + java_resource_dirs: ["res"], + certificate: "platform", + platform_apis: true, + test_suites: ["device-tests"], +} diff --git a/tests/Tracing/AndroidManifest.xml b/tests/Tracing/AndroidManifest.xml new file mode 100644 index 000000000000..7254f81307ad --- /dev/null +++ b/tests/Tracing/AndroidManifest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 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 + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tracing.tests"> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.BIND_WALLPAPER"/> + <!-- Allow the test to connect to perfetto trace processor --> + <uses-permission android:name="android.permission.INTERNET"/> + <application + android:requestLegacyExternalStorage="true" + android:networkSecurityConfig="@xml/network_security_config"> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.tracing.tests" + android:label="Tracing Tests"/> +</manifest> diff --git a/tests/Tracing/AndroidTest.xml b/tests/Tracing/AndroidTest.xml new file mode 100644 index 000000000000..9a404203ee18 --- /dev/null +++ b/tests/Tracing/AndroidTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 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 + --> +<configuration description="Runs tests for tracing classes/utilities."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="TracingTests.apk" /> + </target_preparer> + + <!-- Needed for pushing the trace config file --> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="TracingTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.tracing.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + </test> + + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="perfetto_file_path"/> + <option name="directory-keys" + value="/data/user/0/com.android.tracing.tests/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> +</configuration>
\ No newline at end of file diff --git a/tests/Internal/src/com/android/internal/protolog/OWNERS b/tests/Tracing/OWNERS index 18cf2be9f7df..4a5033800b8e 100644 --- a/tests/Internal/src/com/android/internal/protolog/OWNERS +++ b/tests/Tracing/OWNERS @@ -1,3 +1,3 @@ -# ProtoLog owners +# Tracing owners # Bug component: 1157642 include platform/development:/tools/winscope/OWNERS diff --git a/tests/Tracing/TEST_MAPPING b/tests/Tracing/TEST_MAPPING new file mode 100644 index 000000000000..7f58fceee24d --- /dev/null +++ b/tests/Tracing/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "TracingTests" + } + ] +}
\ No newline at end of file diff --git a/tests/Tracing/res/xml/network_security_config.xml b/tests/Tracing/res/xml/network_security_config.xml new file mode 100644 index 000000000000..fdf1dbbe7672 --- /dev/null +++ b/tests/Tracing/res/xml/network_security_config.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config> diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java index 9657225588b7..8913e8c1996e 100644 --- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java @@ -180,7 +180,6 @@ public class LegacyProtoLogImplTest { verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( LogLevel.INFO), eq("test 5")); - verify(mReader, never()).getViewerString(anyLong()); } @Test diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java index 253965337824..253965337824 100644 --- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java index e841d9ea0880..e841d9ea0880 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java index aba6722c0813..be0c7daebb57 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.endsWith; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.times; +import android.os.Binder; import android.platform.test.annotations.Presubmit; import org.junit.Test; @@ -44,6 +45,8 @@ public class ProtoLogCommandHandlerTest { ProtoLogConfigurationService mProtoLogConfigurationService; @Mock PrintWriter mPrintWriter; + @Mock + Binder mMockBinder; @Test public void printsHelpForAllAvailableCommands() { @@ -70,7 +73,7 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "groups", "list" }); Mockito.verify(mPrintWriter, times(1)) @@ -84,7 +87,7 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "groups" }); Mockito.verify(mPrintWriter, times(1)) @@ -99,7 +102,7 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "groups", "status", "MY_GROUP" }); Mockito.verify(mPrintWriter, times(1)) @@ -114,7 +117,7 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "groups", "status", "MY_GROUP" }); Mockito.verify(mPrintWriter, times(1)) @@ -128,7 +131,7 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "groups", "status" }); Mockito.verify(mPrintWriter, times(1)) @@ -140,7 +143,7 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat" }); Mockito.verify(mPrintWriter, times(1)) @@ -152,11 +155,11 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP" }); Mockito.verify(mProtoLogConfigurationService).enableProtoLogToLogcat("MY_GROUP"); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP", "MY_OTHER_GROUP" }); Mockito.verify(mProtoLogConfigurationService) @@ -168,11 +171,11 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP" }); Mockito.verify(mProtoLogConfigurationService).disableProtoLogToLogcat("MY_GROUP"); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP", "MY_OTHER_GROUP" }); Mockito.verify(mProtoLogConfigurationService) @@ -184,7 +187,7 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "enable" }); Mockito.verify(mPrintWriter).println(contains("Incomplete command")); } @@ -194,7 +197,7 @@ public class ProtoLogCommandHandlerTest { final ProtoLogCommandHandler cmdHandler = new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "disable" }); Mockito.verify(mPrintWriter).println(contains("Incomplete command")); } diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java index e1bdd777dc5f..e1bdd777dc5f 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogImplTest.java index 0496240f01e4..0496240f01e4 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogImplTest.java diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java index 9d56a92fad52..9d56a92fad52 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java index be0e8bc0fc07..28d7b42764c4 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java @@ -27,7 +27,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import perfetto.protos.Protolog; import perfetto.protos.ProtologCommon; @Presubmit @@ -48,7 +47,7 @@ public class ProtoLogViewerConfigReaderTest { .setTag(TEST_GROUP_TAG) ).addGroups( perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder() - .setId(1) + .setId(2) .setName(OTHER_TEST_GROUP_NAME) .setTag(OTHER_TEST_GROUP_TAG) ).addMessages( diff --git a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java index 9a062e3b2f80..ce519b7a1576 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java @@ -67,7 +67,8 @@ public class ProtologDataSourceTest { @Test public void allEnabledTraceMode() { - final ProtoLogDataSource ds = new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {}); + final ProtoLogDataSource ds = + new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {}); final ProtoLogDataSource.TlsState tlsState = createTlsState( DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig( diff --git a/tests/Internal/src/com/android/internal/protolog/common/LogDataTypeTest.java b/tests/Tracing/src/com/android/internal/protolog/common/LogDataTypeTest.java index 9c2f74eabe02..9c2f74eabe02 100644 --- a/tests/Internal/src/com/android/internal/protolog/common/LogDataTypeTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/common/LogDataTypeTest.java |