diff options
110 files changed, 2040 insertions, 397 deletions
diff --git a/MEMORY_OWNERS b/MEMORY_OWNERS new file mode 100644 index 000000000000..89ce5140d8ea --- /dev/null +++ b/MEMORY_OWNERS @@ -0,0 +1,6 @@ +surenb@google.com +tjmercier@google.com +kaleshsingh@google.com +jyescas@google.com +carlosgalo@google.com +jji@google.com diff --git a/core/api/current.txt b/core/api/current.txt index d7a99d3e1eb9..a3775b05cf2e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9514,8 +9514,8 @@ package android.appwidget { method public static android.appwidget.AppWidgetManager getInstance(android.content.Context); method @FlaggedApi("android.appwidget.flags.generated_previews") @Nullable public android.widget.RemoteViews getWidgetPreview(@NonNull android.content.ComponentName, @Nullable android.os.UserHandle, int); method public boolean isRequestPinAppWidgetSupported(); - method @Deprecated public void notifyAppWidgetViewDataChanged(int[], int); - method @Deprecated public void notifyAppWidgetViewDataChanged(int, int); + method public void notifyAppWidgetViewDataChanged(int[], int); + method public void notifyAppWidgetViewDataChanged(int, int); method public void partiallyUpdateAppWidget(int[], android.widget.RemoteViews); method public void partiallyUpdateAppWidget(int, android.widget.RemoteViews); method @FlaggedApi("android.appwidget.flags.generated_previews") public void removeWidgetPreview(@NonNull android.content.ComponentName, int); @@ -45225,13 +45225,13 @@ package android.telephony { method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) public android.telephony.SubscriptionManager createForAllUserProfiles(); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.os.ParcelUuid createSubscriptionGroup(@NonNull java.util.List<java.lang.Integer>); method @Deprecated public static android.telephony.SubscriptionManager from(android.content.Context); - method public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList(); + method @Nullable public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList(); method public static int getActiveDataSubscriptionId(); method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telephony.SubscriptionInfo getActiveSubscriptionInfo(int); method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public int getActiveSubscriptionInfoCount(); method public int getActiveSubscriptionInfoCountMax(); method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telephony.SubscriptionInfo getActiveSubscriptionInfoForSimSlotIndex(int); - method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getActiveSubscriptionInfoList(); + method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getActiveSubscriptionInfoList(); method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public java.util.List<android.telephony.SubscriptionInfo> getAllSubscriptionInfoList(); method @NonNull public java.util.List<android.telephony.SubscriptionInfo> getCompleteActiveSubscriptionInfoList(); method public static int getDefaultDataSubscriptionId(); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 37be5c7e3069..783bebd23df3 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -384,6 +384,10 @@ package android.os { field public static final int DEVICE_INITIAL_SDK_INT; } + public class Environment { + method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") @NonNull public static java.io.File getDataSystemDeDirectory(); + } + public class IpcDataCache<Query, Result> { ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>); method public void disableForCurrentProcess(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index d0fdf694cad7..fe97349adb10 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -14590,7 +14590,7 @@ package android.telephony { method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getActiveSubscriptionIdList(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.SubscriptionInfo getActiveSubscriptionInfoForIcc(@NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public byte[] getAllSimSpecificSettingsForBackup(); - method public java.util.List<android.telephony.SubscriptionInfo> getAvailableSubscriptionInfoList(); + method @Nullable public java.util.List<android.telephony.SubscriptionInfo> getAvailableSubscriptionInfoList(); method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int); method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 77add41f6805..850f1495f689 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1745,6 +1745,7 @@ package android.hardware.input { method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice); method @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping(); + method public int getMousePointerSpeed(); method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int); method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String); method public void removeUniqueIdAssociation(@NonNull String); @@ -1754,6 +1755,7 @@ package android.hardware.input { public class InputSettings { method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static void setMaximumObscuringOpacityForTouch(@NonNull android.content.Context, @FloatRange(from=0, to=1) float); + field public static final int DEFAULT_POINTER_SPEED = 0; // 0x0 } } @@ -2882,6 +2884,10 @@ package android.provider { field public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service"; } + public static final class Settings.System extends android.provider.Settings.NameValueTable { + field public static final String POINTER_SPEED = "pointer_speed"; + } + public static final class Telephony.Sms.Intents { field public static final String SMS_CARRIER_PROVISION_ACTION = "android.provider.Telephony.SMS_CARRIER_PROVISION"; } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 88839071bf6c..0a34d36f204e 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -4649,13 +4649,24 @@ public class Notification implements Parcelable * to turn it off and use a normal notification, as this can be extremely * disruptive. * - * <p> - * The system UI may choose to display a heads-up notification, instead of - * launching this intent, while the user is using the device. - * </p> * <p>Apps targeting {@link Build.VERSION_CODES#Q} and above will have to request * a permission ({@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}) in order to - * use full screen intents.</p> + * use full screen intents. </p> + * <p> + * Prior to {@link Build.VERSION_CODES#TIRAMISU}, the system may display a + * heads up notification (which may display on screen longer than other heads up + * notifications), instead of launching the intent, while the user is using the device. + * From {@link Build.VERSION_CODES#TIRAMISU}, + * the system UI will display a heads up notification, instead of launching this intent, + * while the user is using the device. This notification will display with emphasized + * action buttons. If the posting app holds + * {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}, then the heads + * up notification will appear persistently until the user dismisses or snoozes it, or + * the app cancels it. If the posting app does not hold + * {@link android.Manifest.permission#USE_FULL_SCREEN_INTENT}, then the notification will + * appear as heads up notification even when the screen is locked or turned off, and this + * notification will only be persistent for 60 seconds. + * </p> * <p> * To be launched as a full screen intent, the notification must also be posted to a * channel with importance level set to IMPORTANCE_HIGH or higher. diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 6204edc5b2e8..eb82e1fbb16c 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -822,18 +822,7 @@ public class AppWidgetManager { * * @param appWidgetIds The AppWidget instances to notify of view data changes. * @param viewId The collection view id. - * @deprecated The corresponding API - * {@link RemoteViews#setRemoteAdapter(int, Intent)} associated with this method has been - * deprecated. Moving forward please use - * {@link RemoteViews#setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)} - * instead to set {@link android.widget.RemoteViews.RemoteCollectionItems} for the remote - * adapter and update the widget views by calling {@link #updateAppWidget(int[], RemoteViews)}, - * {@link #updateAppWidget(int, RemoteViews)}, - * {@link #updateAppWidget(ComponentName, RemoteViews)}, - * {@link #partiallyUpdateAppWidget(int[], RemoteViews)}, - * or {@link #partiallyUpdateAppWidget(int, RemoteViews)}, whichever applicable. - */ - @Deprecated + */ public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) { if (mService == null) { return; @@ -884,18 +873,7 @@ public class AppWidgetManager { * * @param appWidgetId The AppWidget instance to notify of view data changes. * @param viewId The collection view id. - * @deprecated The corresponding API - * {@link RemoteViews#setRemoteAdapter(int, Intent)} associated with this method has been - * deprecated. Moving forward please use - * {@link RemoteViews#setRemoteAdapter(int, android.widget.RemoteViews.RemoteCollectionItems)} - * instead to set {@link android.widget.RemoteViews.RemoteCollectionItems} for the remote - * adapter and update the widget views by calling {@link #updateAppWidget(int[], RemoteViews)}, - * {@link #updateAppWidget(int, RemoteViews)}, - * {@link #updateAppWidget(ComponentName, RemoteViews)}, - * {@link #partiallyUpdateAppWidget(int[], RemoteViews)}, - * or {@link #partiallyUpdateAppWidget(int, RemoteViews)}, whichever applicable. - */ - @Deprecated + */ public void notifyAppWidgetViewDataChanged(int appWidgetId, int viewId) { if (mService == null) { return; diff --git a/core/java/android/content/pm/OWNERS b/core/java/android/content/pm/OWNERS index 53dd3bffe06c..fb9560889507 100644 --- a/core/java/android/content/pm/OWNERS +++ b/core/java/android/content/pm/OWNERS @@ -10,3 +10,4 @@ per-file *Launcher* = file:/core/java/android/content/pm/LAUNCHER_OWNERS per-file UserInfo* = file:/MULTIUSER_OWNERS per-file *UserProperties* = file:/MULTIUSER_OWNERS per-file *multiuser* = file:/MULTIUSER_OWNERS +per-file IBackgroundInstallControlService.aidl = file:/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 19bce0bb3abd..f31521da354f 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -199,3 +199,10 @@ flag { bug: "282783453" is_fixed_read_only: true } + +flag { + name: "set_pre_verified_domains" + namespace: "package_manager_service" + description: "Feature flag to enable pre-verified domains" + bug: "307327678" +} diff --git a/core/java/android/hardware/camera2/extension/AdvancedExtender.java b/core/java/android/hardware/camera2/extension/AdvancedExtender.java index a06f017d11d6..665357726ebf 100644 --- a/core/java/android/hardware/camera2/extension/AdvancedExtender.java +++ b/core/java/android/hardware/camera2/extension/AdvancedExtender.java @@ -57,6 +57,7 @@ public abstract class AdvancedExtender { private HashMap<String, Long> mMetadataVendorIdMap = new HashMap<>(); private final CameraManager mCameraManager; + private CameraUsageTracker mCameraUsageTracker; private static final String TAG = "AdvancedExtender"; @FlaggedApi(Flags.FLAG_CONCERT_MODE) @@ -82,6 +83,10 @@ public abstract class AdvancedExtender { } } + void setCameraUsageTracker(CameraUsageTracker tracker) { + mCameraUsageTracker = tracker; + } + @FlaggedApi(Flags.FLAG_CONCERT_MODE) public long getMetadataVendorId(@NonNull String cameraId) { long vendorId = mMetadataVendorIdMap.containsKey(cameraId) ? @@ -282,7 +287,9 @@ public abstract class AdvancedExtender { @Override public ISessionProcessorImpl getSessionProcessor() { - return AdvancedExtender.this.getSessionProcessor().getSessionProcessorBinder(); + SessionProcessor processor =AdvancedExtender.this.getSessionProcessor(); + processor.setCameraUsageTracker(mCameraUsageTracker); + return processor.getSessionProcessorBinder(); } @Override diff --git a/core/java/android/hardware/camera2/extension/CameraExtensionService.java b/core/java/android/hardware/camera2/extension/CameraExtensionService.java index 1426d7bceb7f..fa0d14a3f05a 100644 --- a/core/java/android/hardware/camera2/extension/CameraExtensionService.java +++ b/core/java/android/hardware/camera2/extension/CameraExtensionService.java @@ -20,6 +20,7 @@ import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.app.AppOpsManager; import android.app.Service; import android.content.Intent; import android.os.IBinder; @@ -29,6 +30,11 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.camera.flags.Flags; +interface CameraUsageTracker { + void startCameraOperation(); + void finishCameraOperation(); +} + /** * Base service class that extension service implementations must extend. * @@ -38,8 +44,33 @@ import com.android.internal.camera.flags.Flags; @FlaggedApi(Flags.FLAG_CONCERT_MODE) public abstract class CameraExtensionService extends Service { private static final String TAG = "CameraExtensionService"; + private CameraUsageTracker mCameraUsageTracker; private static Object mLock = new Object(); + private final class CameraTracker implements CameraUsageTracker { + + private final AppOpsManager mAppOpsService = getApplicationContext().getSystemService( + AppOpsManager.class); + private final String mPackageName = getPackageName(); + private final String mAttributionTag = getAttributionTag(); + private int mUid = getApplicationInfo().uid; + + @Override + public void startCameraOperation() { + if (mAppOpsService != null) { + mAppOpsService.startOp(AppOpsManager.OPSTR_CAMERA, mUid, mPackageName, + mAttributionTag, "Camera extensions"); + } + } + + @Override + public void finishCameraOperation() { + if (mAppOpsService != null) { + mAppOpsService.finishOp(AppOpsManager.OPSTR_CAMERA, mUid, mPackageName, + mAttributionTag); + } + } + } @GuardedBy("mLock") private static IInitializeSessionCallback mInitializeCb = null; @@ -49,16 +80,22 @@ public abstract class CameraExtensionService extends Service { synchronized (mLock) { mInitializeCb = null; } + if (mCameraUsageTracker != null) { + mCameraUsageTracker.finishCameraOperation(); + } } }; @FlaggedApi(Flags.FLAG_CONCERT_MODE) - protected CameraExtensionService() {} + protected CameraExtensionService() { } @FlaggedApi(Flags.FLAG_CONCERT_MODE) @Override @NonNull public IBinder onBind(@Nullable Intent intent) { + if (mCameraUsageTracker == null) { + mCameraUsageTracker = new CameraTracker(); + } return new CameraExtensionServiceImpl(); } @@ -132,8 +169,10 @@ public abstract class CameraExtensionService extends Service { @Override public IAdvancedExtenderImpl initializeAdvancedExtension(int extensionType) throws RemoteException { - return CameraExtensionService.this.onInitializeAdvancedExtension( - extensionType).getAdvancedExtenderBinder(); + AdvancedExtender extender = CameraExtensionService.this.onInitializeAdvancedExtension( + extensionType); + extender.setCameraUsageTracker(mCameraUsageTracker); + return extender.getAdvancedExtenderBinder(); } } diff --git a/core/java/android/hardware/camera2/extension/SessionProcessor.java b/core/java/android/hardware/camera2/extension/SessionProcessor.java index 6ed0c1404212..9c5136bcf903 100644 --- a/core/java/android/hardware/camera2/extension/SessionProcessor.java +++ b/core/java/android/hardware/camera2/extension/SessionProcessor.java @@ -76,10 +76,15 @@ import java.util.concurrent.Executor; @FlaggedApi(Flags.FLAG_CONCERT_MODE) public abstract class SessionProcessor { private static final String TAG = "SessionProcessor"; + private CameraUsageTracker mCameraUsageTracker; @FlaggedApi(Flags.FLAG_CONCERT_MODE) protected SessionProcessor() {} + void setCameraUsageTracker(CameraUsageTracker tracker) { + mCameraUsageTracker = tracker; + } + /** * Callback for notifying the status of {@link * #startCapture} and {@link #startRepeating}. @@ -379,12 +384,18 @@ public abstract class SessionProcessor { @Override public void onCaptureSessionStart(IRequestProcessorImpl requestProcessor, String statsKey) throws RemoteException { + if (mCameraUsageTracker != null) { + mCameraUsageTracker.startCameraOperation(); + } SessionProcessor.this.onCaptureSessionStart( new RequestProcessor(requestProcessor, mVendorId), statsKey); } @Override public void onCaptureSessionEnd() throws RemoteException { + if (mCameraUsageTracker != null) { + mCameraUsageTracker.finishCameraOperation(); + } SessionProcessor.this.onCaptureSessionEnd(); } diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 7bea9aeeb86b..1f5495999416 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -67,6 +67,9 @@ interface IInputManager { KeyCharacterMap getKeyCharacterMap(String layoutDescriptor); + // Returns the mouse pointer speed. + int getMousePointerSpeed(); + // Temporarily changes the pointer speed. void tryPointerSpeed(int speed); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index db992cdd20db..744dfae97108 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -28,6 +28,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UserIdInt; @@ -857,6 +858,28 @@ public final class InputManager { } /** + * Returns the mouse pointer speed. + * + * <p>The pointer speed is a value between {@link InputSettings#MIN_POINTER_SPEED} and + * {@link InputSettings#MAX_POINTER_SPEED}, the default value being + * {@link InputSettings#DEFAULT_POINTER_SPEED}. + * + * <p> Note that while setting the mouse pointer speed, it's possible that the input reader has + * only received this value and has not yet completed reconfiguring itself with this value. + * + * @hide + */ + @SuppressLint("UnflaggedApi") // TestApi without associated feature. + @TestApi + public int getMousePointerSpeed() { + try { + return mIm.getMousePointerSpeed(); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** * Changes the mouse pointer speed temporarily, but does not save the setting. * <p> * Requires {@link android.Manifest.permission#SET_POINTER_SPEED}. diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index 89fa5fb47a07..54e34ecf7006 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -54,8 +54,8 @@ public class InputSettings { /** * Pointer Speed: The default pointer speed (0). - * @hide */ + @SuppressLint("UnflaggedApi") // TestApi without associated feature. public static final int DEFAULT_POINTER_SPEED = 0; /** diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 536ef31f334a..a459aaa42930 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -17,6 +17,7 @@ package android.os; import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -412,7 +413,9 @@ public class Environment { * Returns the base directory for per-user system directory, device encrypted. * {@hide} */ - public static File getDataSystemDeDirectory() { + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(android.crashrecovery.flags.Flags.FLAG_ENABLE_CRASHRECOVERY) + public static @NonNull File getDataSystemDeDirectory() { return buildPath(getDataDirectory(), "system_de"); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 524b7336718f..76fda0636282 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6015,8 +6015,10 @@ public final class Settings { * +7 = fastest * @hide */ + @SuppressLint({"NoSettingsProvider", "UnflaggedApi"}) // TestApi without associated feature. @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @Readable + @TestApi public static final String POINTER_SPEED = "pointer_speed"; /** diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java index 163dfa21c3ef..021bbf7b9c9f 100644 --- a/core/java/android/view/TextureView.java +++ b/core/java/android/view/TextureView.java @@ -16,7 +16,6 @@ package android.view; -import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; @@ -197,7 +196,6 @@ public class TextureView extends View { private Canvas mCanvas; private int mSaveCount; - @FloatRange(from = 0.0) float mFrameRate; @Surface.FrameRateCompatibility int mFrameRateCompatibility; private final Object[] mNativeWindowLock = new Object[0]; @@ -473,13 +471,13 @@ public class TextureView extends View { mLayer.setSurfaceTexture(mSurface); mSurface.setDefaultBufferSize(getWidth(), getHeight()); mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler); - if (Flags.toolkitSetFrameRate()) { + if (Flags.toolkitSetFrameRateReadOnly()) { mSurface.setOnSetFrameRateListener( (surfaceTexture, frameRate, compatibility, strategy) -> { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.instant(Trace.TRACE_TAG_VIEW, "setFrameRate: " + frameRate); } - mFrameRate = frameRate; + setRequestedFrameRate(frameRate); mFrameRateCompatibility = compatibility; }, mAttachInfo.mHandler); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c27b2b174587..653435403b74 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -26,6 +26,7 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.InputDevice.SOURCE_CLASS_NONE; import static android.view.InsetsSource.ID_IME; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; +import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; @@ -1013,8 +1014,10 @@ public final class ViewRootImpl implements ViewParent, // Used to check if there were any view invalidations in // the previous time frame (FRAME_RATE_IDLENESS_REEVALUATE_TIME). private boolean mHasInvalidation = false; - // Used to check if it is in the touch boosting period. + // Used to check if it is in the frame rate boosting period. private boolean mIsFrameRateBoosting = false; + // Used to check if it is in touch boosting period. + private boolean mIsTouchBoosting = false; // Used to check if there is a message in the message queue // for idleness handling. private boolean mHasIdledMessage = false; @@ -1024,6 +1027,9 @@ public final class ViewRootImpl implements ViewParent, private static final int FRAME_RATE_IDLENESS_CHECK_TIME_MILLIS = 500; // time for revaluating the idle status before lowering the frame rate. private static final int FRAME_RATE_IDLENESS_REEVALUATE_TIME = 500; + // time for evaluating the interval between current time and + // the time when frame rate was set previously. + private static final int FRAME_RATE_SETTING_REEVALUATE_TIME = 100; /* * the variables below are used to determine whther a dVRR feature should be enabled @@ -4080,7 +4086,6 @@ public final class ViewRootImpl implements ViewParent, setPreferredFrameRate(mPreferredFrameRate); setPreferredFrameRateCategory(mPreferredFrameRateCategory); mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; - mPreferredFrameRate = 0; } private void createSyncIfNeeded() { @@ -6134,6 +6139,7 @@ public final class ViewRootImpl implements ViewParent, private static final int MSG_TOUCH_BOOST_TIMEOUT = 39; private static final int MSG_CHECK_INVALIDATION_IDLE = 40; private static final int MSG_REFRESH_POINTER_ICON = 41; + private static final int MSG_FRAME_RATE_SETTING = 42; final class ViewRootHandler extends Handler { @Override @@ -6445,11 +6451,12 @@ public final class ViewRootImpl implements ViewParent, * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME). */ mIsFrameRateBoosting = false; + mIsTouchBoosting = false; setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory, mLastPreferredFrameRateCategory)); break; case MSG_CHECK_INVALIDATION_IDLE: - if (!mHasInvalidation && !mIsFrameRateBoosting) { + if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) { mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; setPreferredFrameRateCategory(mPreferredFrameRateCategory); mHasIdledMessage = false; @@ -6472,6 +6479,10 @@ public final class ViewRootImpl implements ViewParent, } updatePointerIcon(mPointerIconEvent); break; + case MSG_FRAME_RATE_SETTING: + mPreferredFrameRate = 0; + setPreferredFrameRate(mPreferredFrameRate); + break; } } } @@ -7482,7 +7493,7 @@ public final class ViewRootImpl implements ViewParent, // For the variable refresh rate project if (handled && shouldTouchBoost(action, mWindowAttributes.type)) { // set the frame rate to the maximum value. - mIsFrameRateBoosting = true; + mIsTouchBoosting = true; setPreferredFrameRateCategory(mPreferredFrameRateCategory); } /** @@ -7490,7 +7501,7 @@ public final class ViewRootImpl implements ViewParent, * MotionEvent.ACTION_CANCEL is detected. * Not using ACTION_MOVE to avoid checking and sending messages too frequently. */ - if (mIsFrameRateBoosting && (action == MotionEvent.ACTION_UP + if (mIsTouchBoosting && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) { mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT); mHandler.sendEmptyMessageDelayed(MSG_TOUCH_BOOST_TIMEOUT, @@ -12234,17 +12245,32 @@ public final class ViewRootImpl implements ViewParent, return; } - int frameRateCategory = mIsFrameRateBoosting || mInsetsAnimationRunning - ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory; + int frameRateCategory = mIsTouchBoosting + ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory; + + // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT + // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction. + // FRAME_RATE_CATEGORY_HIGH is for boosting without user interaction + // (e.g., Window Initialization). + if (mIsFrameRateBoosting || mInsetsAnimationRunning) { + frameRateCategory = FRAME_RATE_CATEGORY_HIGH; + } try { if (mLastPreferredFrameRateCategory != frameRateCategory) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + Trace.traceBegin( + Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRateCategory " + + frameRateCategory); + } mFrameRateTransaction.setFrameRateCategory(mSurfaceControl, frameRateCategory, false).applyAsyncUnsafe(); mLastPreferredFrameRateCategory = frameRateCategory; } } catch (Exception e) { Log.e(mTag, "Unable to set frame rate category", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); } if (mPreferredFrameRateCategory != FRAME_RATE_CATEGORY_NO_PREFERENCE && !mHasIdledMessage) { @@ -12263,12 +12289,19 @@ public final class ViewRootImpl implements ViewParent, try { if (mLastPreferredFrameRate != preferredFrameRate) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + Trace.traceBegin( + Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRate " + + preferredFrameRate); + } mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT).applyAsyncUnsafe(); mLastPreferredFrameRate = preferredFrameRate; } } catch (Exception e) { Log.e(mTag, "Unable to set frame rate", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } @@ -12285,7 +12318,7 @@ public final class ViewRootImpl implements ViewParent, private boolean shouldSetFrameRate() { // use toolkitSetFrameRate flag to gate the change - return mPreferredFrameRate > 0 && sToolkitSetFrameRateReadOnlyFlagValue; + return sToolkitSetFrameRateReadOnlyFlagValue; } private boolean shouldTouchBoost(int motionEventAction, int windowType) { @@ -12336,6 +12369,9 @@ public final class ViewRootImpl implements ViewParent, } mHasInvalidation = true; + mHandler.removeMessages(MSG_FRAME_RATE_SETTING); + mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING, + FRAME_RATE_SETTING_REEVALUATE_TIME); } /** diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 1de77f6d29e7..82067defd336 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -66,3 +66,14 @@ flag { bug: "314952133" is_fixed_read_only: true } + +flag { + name: "app_compat_refactoring" + namespace: "large_screen_experiences_app_compat" + description: "Whether the changes about app compat refactoring are enabled./n" + "The goal is to simplify code readability unblocking the implementation of /n" + "app compat feature like reachability, animations and others related to/n" + "freeform windowing mode." + bug: "309593314" + is_fixed_read_only: true +} diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java index 627e8779f9d0..e59132780c56 100644 --- a/core/java/com/android/internal/widget/LockSettingsInternal.java +++ b/core/java/com/android/internal/widget/LockSettingsInternal.java @@ -171,11 +171,11 @@ public abstract class LockSettingsInternal { * Register a LockSettingsStateListener * @param listener The listener to be registered */ - public abstract void registerLockSettingsStateListener(ILockSettingsStateListener listener); + public abstract void registerLockSettingsStateListener(LockSettingsStateListener listener); /** * Unregister a LockSettingsStateListener * @param listener The listener to be unregistered */ - public abstract void unregisterLockSettingsStateListener(ILockSettingsStateListener listener); + public abstract void unregisterLockSettingsStateListener(LockSettingsStateListener listener); } diff --git a/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl b/core/java/com/android/internal/widget/LockSettingsStateListener.java index 25e30034fe8f..869e676f4a42 100644 --- a/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl +++ b/core/java/com/android/internal/widget/LockSettingsStateListener.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -21,7 +21,7 @@ package com.android.internal.widget; * state of primary authentication (i.e. PIN/pattern/password). * @hide */ -oneway interface ILockSettingsStateListener { +public interface LockSettingsStateListener { /** * Defines behavior in response to a successful authentication * @param userId The user Id for the requested authentication @@ -33,4 +33,4 @@ oneway interface ILockSettingsStateListener { * @param userId The user Id for the requested authentication */ void onAuthenticationFailed(int userId); -}
\ No newline at end of file +} diff --git a/core/tests/coretests/src/android/os/HandlerThreadTest.java b/core/tests/coretests/src/android/os/HandlerThreadTest.java index 1ad71da7c2d1..32378127d897 100644 --- a/core/tests/coretests/src/android/os/HandlerThreadTest.java +++ b/core/tests/coretests/src/android/os/HandlerThreadTest.java @@ -126,7 +126,7 @@ public class HandlerThreadTest { public void testUncaughtExceptionFails() throws Exception { // For the moment we can only test Ravenwood; on a physical device uncaught exceptions // are detected, but reported as test failures at a higher level where we can't inspect - Assume.assumeTrue(RavenwoodRule.isOnRavenwood()); + Assume.assumeTrue(false); // TODO: re-enable mThrown.expect(IllegalStateException.class); final HandlerThread thread = new HandlerThread("HandlerThreadTest"); diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 60769c7acd45..52e996cab3ed 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -20,6 +20,7 @@ import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; +import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; @@ -669,8 +670,13 @@ public class ViewRootImplTest { assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_LOW); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL); + viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_HIGH_HINT); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW); @@ -796,6 +802,33 @@ public class ViewRootImplTest { } /** + * Test votePreferredFrameRate_voteFrameRateTimeOut + * If no frame rate is voted in 100 milliseconds, the value of + * mPreferredFrameRate should be set to 0. + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) + public void votePreferredFrameRate_voteFrameRateTimeOut() throws InterruptedException { + final long delay = 200L; + + View view = new View(sContext); + attachViewToWindow(view); + sInstrumentation.waitForIdleSync(); + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + + sInstrumentation.runOnMainSync(() -> { + assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1); + viewRootImpl.votePreferredFrameRate(24); + assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRate(), 24, 0.1); + }); + + Thread.sleep(delay); + assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1); + } + + /** * Test the logic of infrequent layer: * - NORMAL for infrequent update: FT2-FT1 > 100 && FT3-FT2 > 100. * - HIGH/NORMAL based on size for frequent update: (FT3-FT2) + (FT2 - FT1) < 100. diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java index dd82fed03087..50b167ef9f46 100644 --- a/graphics/java/android/graphics/SurfaceTexture.java +++ b/graphics/java/android/graphics/SurfaceTexture.java @@ -489,7 +489,7 @@ public class SurfaceTexture { @Surface.ChangeFrameRateStrategy int changeFrameRateStrategy) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "postOnSetFrameRateEventFromNative"); try { - if (Flags.toolkitSetFrameRate()) { + if (Flags.toolkitSetFrameRateReadOnly()) { SurfaceTexture st = weakSelf.get(); if (st != null) { Handler handler = st.mOnSetFrameRateHandler; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index f82212d97bd3..7c8fcbb16711 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -248,6 +248,12 @@ class DesktopModeTaskRepository { // Check if count changed if (prevCount != newCount) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: visibleTaskCount has changed from %d to %d", + prevCount, + newCount + ) notifyVisibleTaskListeners(displayId, newCount) } } @@ -262,6 +268,11 @@ class DesktopModeTaskRepository { * Get number of tasks that are marked as visible on given [displayId] */ fun getVisibleTaskCount(displayId: Int): Int { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: visibleTaskCount= %d", + displayData[displayId]?.visibleTasks?.size ?: 0 + ) return displayData[displayId]?.visibleTasks?.size ?: 0 } @@ -290,6 +301,10 @@ class DesktopModeTaskRepository { taskId ) freeformTasksInZOrder.remove(taskId) + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString() + ) } /** diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 72ddeccd26b2..3d7e559bebe0 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -1,6 +1,13 @@ package: "com.android.graphics.hwui.flags" flag { + name: "clip_shader" + namespace: "core_graphics" + description: "API for canvas shader clipping operations" + bug: "280116960" +} + +flag { name: "matrix_44" namespace: "core_graphics" description: "API for 4x4 matrix and related canvas functions" diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 9742d46b8ae9..9f94ef924eae 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -98,12 +98,12 @@ package android.nfc { field public static final String EXTRA_SECURE_ELEMENT_NAME = "android.nfc.extra.SECURE_ELEMENT_NAME"; field public static final String EXTRA_TAG = "android.nfc.extra.TAG"; field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_DISABLE = 0; // 0x0 - field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_KEEP = -1; // 0xffffffff + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_KEEP = -2147483648; // 0x80000000 field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_A = 1; // 0x1 field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_B = 2; // 0x2 field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_LISTEN_NFC_PASSIVE_F = 4; // 0x4 field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_READER_DISABLE = 0; // 0x0 - field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_READER_KEEP = -1; // 0xffffffff + field @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public static final int FLAG_READER_KEEP = -2147483648; // 0x80000000 field public static final int FLAG_READER_NFC_A = 1; // 0x1 field public static final int FLAG_READER_NFC_B = 2; // 0x2 field public static final int FLAG_READER_NFC_BARCODE = 16; // 0x10 diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 55506a1a7579..5b917a1a6dcf 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -416,18 +416,18 @@ public final class NfcAdapter { /** * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. * <p> - * Setting this flag makes listening to use current flags. + * Setting this flag makes listening to keep the current technology configuration. */ @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) - public static final int FLAG_LISTEN_KEEP = -1; + public static final int FLAG_LISTEN_KEEP = 0x80000000; /** * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. * <p> - * Setting this flag makes polling to use current flags. + * Setting this flag makes polling to keep the current technology configuration. */ @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) - public static final int FLAG_READER_KEEP = -1; + public static final int FLAG_READER_KEEP = 0x80000000; /** @hide */ public static final int FLAG_USE_ALL_TECH = 0xff; @@ -1785,6 +1785,8 @@ public final class NfcAdapter { * * Use {@link #FLAG_READER_KEEP} to keep current polling technology. * Use {@link #FLAG_LISTEN_KEEP} to keep current listenig technology. + * (if the _KEEP flag is specified the other technology flags shouldn't be set + * and are quietly ignored otherwise). * Use {@link #FLAG_READER_DISABLE} to disable polling. * Use {@link #FLAG_LISTEN_DISABLE} to disable listening. * Also refer to {@link #resetDiscoveryTechnology(Activity)} to restore these changes. @@ -1816,6 +1818,15 @@ public final class NfcAdapter { @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) public void setDiscoveryTechnology(@NonNull Activity activity, @PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) { + + // A special treatment of the _KEEP flags + if ((listenTechnology & FLAG_LISTEN_KEEP) != 0) { + listenTechnology = -1; + } + if ((pollTechnology & FLAG_READER_KEEP) != 0) { + pollTechnology = -1; + } + if (listenTechnology == FLAG_LISTEN_DISABLE) { synchronized (sLock) { if (!sHasNfcFeature) { @@ -1842,10 +1853,10 @@ public final class NfcAdapter { } /** - * Restore the poll/listen technologies of NFC controller, + * Restore the poll/listen technologies of NFC controller to its default state, * which were changed by {@link #setDiscoveryTechnology(Activity , int , int)} * - * @param activity The Activity that requests to changed technologies. + * @param activity The Activity that requested to change technologies. */ @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig index 563626634068..572a66922ea3 100644 --- a/packages/CrashRecovery/aconfig/flags.aconfig +++ b/packages/CrashRecovery/aconfig/flags.aconfig @@ -6,4 +6,11 @@ flag { description: "Feature flag for recoverability detection" bug: "310236690" is_fixed_read_only: true -}
\ No newline at end of file +} + +flag { + name: "enable_crashrecovery" + namespace: "crashrecovery" + description: "Enables various dependencies of crashrecovery module" + bug: "289203818" +} diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index fd2f9bd345d2..bab678178ebf 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -7,3 +7,13 @@ flag { bug: "314812750" } +flag { + name: "enable_cached_bluetooth_device_dedup" + namespace: "bluetooth" + description: "Enable dedup in CachedBluetoothDevice" + bug: "319197962" + metadata { + purpose: PURPOSE_BUGFIX + } +} + diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index c97445f04eea..647fcb9f67fa 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -16,6 +16,8 @@ package com.android.settingslib.bluetooth; +import static com.android.settingslib.flags.Flags.enableCachedBluetoothDeviceDedup; + import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothCsipSetCoordinator; @@ -377,6 +379,10 @@ public class BluetoothEventManager { cachedDevice = mDeviceManager.addDevice(device); } + if (enableCachedBluetoothDeviceDedup() && bondState == BluetoothDevice.BOND_BONDED) { + mDeviceManager.removeDuplicateInstanceForIdentityAddress(device); + } + for (BluetoothCallback callback : mCallbacks) { callback.onDeviceBondStateChanged(cachedDevice, bondState); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 89fe268b3258..32eec7e709af 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -340,6 +340,20 @@ public class CachedBluetoothDeviceManager { } } + synchronized void removeDuplicateInstanceForIdentityAddress(BluetoothDevice device) { + String identityAddress = device.getIdentityAddress(); + if (identityAddress == null || identityAddress.equals(device.getAddress())) { + return; + } + mCachedDevices.removeIf(d -> { + boolean shouldRemove = d.getDevice().getAddress().equals(identityAddress); + if (shouldRemove) { + Log.d(TAG, "Remove instance for identity address " + d); + } + return shouldRemove; + }); + } + public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state, int profileId) { if (profileId == BluetoothProfile.HEARING_AID) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 1d2f7902b781..6ee403d50751 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -21,6 +21,7 @@ import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import android.annotation.CallbackExecutor; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeAudioContentMetadata; import android.bluetooth.BluetoothLeBroadcast; @@ -62,6 +63,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; /** * LocalBluetoothLeBroadcast provides an interface between the Settings app and the functionality of @@ -88,6 +90,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY), }; + private final Context mContext; + private final CachedBluetoothDeviceManager mDeviceManager; private BluetoothLeBroadcast mServiceBroadcast; private BluetoothLeBroadcastAssistant mServiceBroadcastAssistant; private BluetoothLeAudioContentMetadata mBluetoothLeAudioContentMetadata; @@ -256,8 +260,19 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = new BluetoothLeBroadcastAssistant.Callback() { @Override - public void onSourceAdded( - @NonNull BluetoothDevice sink, int sourceId, int reason) {} + public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) { + if (DEBUG) { + Log.d( + TAG, + "onSourceAdded(), sink = " + + sink + + ", reason = " + + reason + + ", sourceId = " + + sourceId); + } + updateFallbackActiveDeviceIfNeeded(); + } @Override public void onSearchStarted(int reason) {} @@ -301,6 +316,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { + ", sourceId = " + sourceId); } + updateFallbackActiveDeviceIfNeeded(); } @Override @@ -348,7 +364,9 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } } - LocalBluetoothLeBroadcast(Context context) { + LocalBluetoothLeBroadcast(Context context, CachedBluetoothDeviceManager deviceManager) { + mContext = context; + mDeviceManager = deviceManager; mExecutor = Executors.newSingleThreadExecutor(); mBuilder = new BluetoothLeAudioContentMetadata.Builder(); mContentResolver = context.getContentResolver(); @@ -430,49 +448,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { mServiceBroadcast.startBroadcast(settings); } - /** - * Start the private Broadcast for personal audio sharing or qr code sharing. - * - * <p>The broadcast will use random string for both broadcast name and subgroup program info; - * The broadcast will use random string for broadcast code; The broadcast will only have one - * subgroup due to system limitation; The subgroup language will be null. - * - * <p>If the system started the LE Broadcast, then the system calls the corresponding callback - * {@link BluetoothLeBroadcast.Callback}. - */ - public void startPrivateBroadcast(int quality) { - mNewAppSourceName = "Sharing audio"; - if (mServiceBroadcast == null) { - Log.d(TAG, "The BluetoothLeBroadcast is null when starting the private broadcast."); - return; - } - if (mServiceBroadcast.getAllBroadcastMetadata().size() - >= mServiceBroadcast.getMaximumNumberOfBroadcasts()) { - Log.d(TAG, "Skip starting the broadcast due to number limit."); - return; - } - String programInfo = getProgramInfo(); - if (DEBUG) { - Log.d(TAG, "startBroadcast: language = null ,programInfo = " + programInfo); - } - // Current broadcast framework only support one subgroup - BluetoothLeBroadcastSubgroupSettings subgroupSettings = - buildBroadcastSubgroupSettings( - /* language= */ null, - programInfo, - /* improveCompatibility= */ - BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD == quality); - BluetoothLeBroadcastSettings settings = - buildBroadcastSettings( - true, // TODO: set to false after framework fix - TextUtils.isEmpty(programInfo) ? null : programInfo, - (mBroadcastCode != null && mBroadcastCode.length > 0) - ? mBroadcastCode - : null, - ImmutableList.of(subgroupSettings)); - mServiceBroadcast.startBroadcast(settings); - } - private BluetoothLeBroadcastSettings buildBroadcastSettings( boolean isPublic, @Nullable String broadcastName, @@ -1027,4 +1002,80 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } } } + + /** Update fallback active device if needed. */ + public void updateFallbackActiveDeviceIfNeeded() { + if (!isEnabled(null)) { + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no ongoing broadcast"); + return; + } + if (mServiceBroadcastAssistant == null) { + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null"); + return; + } + List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices(); + List<BluetoothDevice> devicesInSharing = + connectedDevices.stream() + .filter( + bluetoothDevice -> { + List<BluetoothLeBroadcastReceiveState> sourceList = + mServiceBroadcastAssistant.getAllSources( + bluetoothDevice); + return !sourceList.isEmpty(); + }) + .collect(Collectors.toList()); + if (devicesInSharing.isEmpty()) { + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast"); + return; + } + List<BluetoothDevice> devices = + BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices(); + BluetoothDevice targetDevice = null; + // Find the earliest connected device in sharing session. + int targetDeviceIdx = -1; + for (BluetoothDevice device : devicesInSharing) { + if (devices.contains(device)) { + int idx = devices.indexOf(device); + if (idx > targetDeviceIdx) { + targetDeviceIdx = idx; + targetDevice = device; + } + } + } + if (targetDevice == null) { + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null"); + return; + } + Log.d( + TAG, + "updateFallbackActiveDeviceIfNeeded, set active device: " + + targetDevice.getAnonymizedAddress()); + CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice); + if (targetCachedDevice == null) { + Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device"); + return; + } + int fallbackActiveGroupId = getFallbackActiveGroupId(); + if (targetCachedDevice.getGroupId() == fallbackActiveGroupId) { + Log.d( + TAG, + "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: " + + fallbackActiveGroupId); + return; + } + targetCachedDevice.setActive(); + } + + private boolean isDecryptedSource(BluetoothLeBroadcastReceiveState state) { + return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED + && state.getBigEncryptionState() + == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING; + } + + private int getFallbackActiveGroupId() { + return Settings.Secure.getInt( + mContext.getContentResolver(), + "bluetooth_le_broadcast_fallback_active_group_id", + BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManagerExt.kt new file mode 100644 index 000000000000..5dc0237e508c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManagerExt.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.bluetooth + +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch + +/** Returns a [Flow] that emits a [Unit] whenever the headset audio mode changes. */ +val LocalBluetoothManager.headsetAudioModeChanges: Flow<Unit> + get() { + return callbackFlow { + val callback = + object : BluetoothCallback { + override fun onAudioModeChanged() { + launch { send(Unit) } + } + } + + eventManager.registerCallback(callback) + awaitClose { eventManager.unregisterCallback(callback) } + } + } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index 119aef6da12e..79e4c374667e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -257,7 +257,7 @@ public class LocalBluetoothProfileManager { if (DEBUG) { Log.d(TAG, "Adding local LE_AUDIO_BROADCAST profile"); } - mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext); + mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext, mDeviceManager); // no event handler for the LE boradcast. mProfileNameMap.put(LocalBluetoothLeBroadcast.NAME, mLeAudioBroadcast); } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt new file mode 100644 index 000000000000..1597b7712863 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.volume.data.repository + +import com.android.settingslib.media.LocalMediaManager +import com.android.settingslib.media.MediaDevice +import kotlin.coroutines.CoroutineContext +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.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn + +/** Repository providing data about connected media devices. */ +interface LocalMediaRepository { + + /** Available devices list */ + val mediaDevices: StateFlow<Collection<MediaDevice>> + + /** Currently connected media device */ + val currentConnectedDevice: StateFlow<MediaDevice?> +} + +class LocalMediaRepositoryImpl( + private val localMediaManager: LocalMediaManager, + coroutineScope: CoroutineScope, + backgroundContext: CoroutineContext, +) : LocalMediaRepository { + + private val deviceUpdates: Flow<DevicesUpdate> = callbackFlow { + val callback = + object : LocalMediaManager.DeviceCallback { + override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) { + trySend(DevicesUpdate.DeviceListUpdate(newDevices ?: emptyList())) + } + + override fun onSelectedDeviceStateChanged( + device: MediaDevice?, + state: Int, + ) { + trySend(DevicesUpdate.SelectedDeviceStateChanged) + } + + override fun onDeviceAttributesChanged() { + trySend(DevicesUpdate.DeviceAttributesChanged) + } + } + localMediaManager.registerCallback(callback) + localMediaManager.startScan() + + awaitClose { + localMediaManager.stopScan() + localMediaManager.unregisterCallback(callback) + } + } + + override val mediaDevices: StateFlow<Collection<MediaDevice>> = + deviceUpdates + .mapNotNull { + if (it is DevicesUpdate.DeviceListUpdate) { + it.newDevices ?: emptyList() + } else { + null + } + } + .flowOn(backgroundContext) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) + + override val currentConnectedDevice: StateFlow<MediaDevice?> = + deviceUpdates + .map { localMediaManager.currentConnectedDevice } + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + localMediaManager.currentConnectedDevice + ) + + private sealed interface DevicesUpdate { + + data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate + + data object SelectedDeviceStateChanged : DevicesUpdate + + data object DeviceAttributesChanged : DevicesUpdate + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt new file mode 100644 index 000000000000..93aa90d11678 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.data.repository + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.media.AudioManager +import android.media.session.MediaController +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.bluetooth.headsetAudioModeChanges +import kotlin.coroutines.CoroutineContext +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.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Provides controllers for currently active device media sessions. */ +interface MediaControllerRepository { + + /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */ + val activeMediaController: StateFlow<MediaController?> +} + +class MediaControllerRepositoryImpl( + private val context: Context, + private val mediaSessionManager: MediaSessionManager, + localBluetoothManager: LocalBluetoothManager?, + coroutineScope: CoroutineScope, + backgroundContext: CoroutineContext, +) : MediaControllerRepository { + + private val devicesChanges: Flow<Unit> = + callbackFlow { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (AudioManager.STREAM_DEVICES_CHANGED_ACTION == intent?.action) { + launch { send(Unit) } + } + } + } + context.registerReceiver( + receiver, + IntentFilter(AudioManager.STREAM_DEVICES_CHANGED_ACTION) + ) + + awaitClose { context.unregisterReceiver(receiver) } + } + .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) + + override val activeMediaController: StateFlow<MediaController?> = + combine( + localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) } + ?: emptyFlow(), + devicesChanges.onStart { emit(Unit) }, + ) { _, _ -> + getActiveLocalMediaController() + } + .flowOn(backgroundContext) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) + + private fun getActiveLocalMediaController(): MediaController? { + var localController: MediaController? = null + val remoteMediaSessionLists: MutableList<String> = ArrayList() + for (controller in mediaSessionManager.getActiveSessions(null)) { + val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue + val playbackState = controller.playbackState ?: continue + if (inactivePlaybackStates.contains(playbackState.state)) { + continue + } + when (playbackInfo.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + if (localController?.packageName.equals(controller.packageName)) { + localController = null + } + if (!remoteMediaSessionLists.contains(controller.packageName)) { + remoteMediaSessionLists.add(controller.packageName) + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if ( + localController == null && + !remoteMediaSessionLists.contains(controller.packageName) + ) { + localController = controller + } + } + } + } + return localController + } + + private companion object { + val inactivePlaybackStates = + setOf(PlaybackState.STATE_STOPPED, PlaybackState.STATE_NONE, PlaybackState.STATE_ERROR) + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt new file mode 100644 index 000000000000..d106bceb6b85 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.volume.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.media.LocalMediaManager +import com.android.settingslib.media.MediaDevice +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +@SmallTest +class LocalMediaRepositoryImplTest { + + @Mock private lateinit var localMediaManager: LocalMediaManager + @Mock private lateinit var mediaDevice1: MediaDevice + @Mock private lateinit var mediaDevice2: MediaDevice + + @Captor + private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback> + + private val testScope = TestScope() + + private lateinit var underTest: LocalMediaRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + underTest = + LocalMediaRepositoryImpl( + localMediaManager, + testScope.backgroundScope, + testScope.testScheduler, + ) + } + + @Test + fun mediaDevices_areUpdated() { + testScope.runTest { + var mediaDevices: Collection<MediaDevice>? = null + underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope) + runCurrent() + verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture()) + deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2)) + runCurrent() + + assertThat(mediaDevices).hasSize(2) + assertThat(mediaDevices).contains(mediaDevice1) + assertThat(mediaDevices).contains(mediaDevice2) + } + } + + @Test + fun deviceListUpdated_currentConnectedDeviceUpdated() { + testScope.runTest { + var currentConnectedDevice: MediaDevice? = null + underTest.currentConnectedDevice + .onEach { currentConnectedDevice = it } + .launchIn(backgroundScope) + runCurrent() + + `when`(localMediaManager.currentConnectedDevice).thenReturn(mediaDevice1) + verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture()) + deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2)) + runCurrent() + + assertThat(currentConnectedDevice).isEqualTo(mediaDevice1) + } + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt new file mode 100644 index 000000000000..f07b1bff0f31 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.data.repository + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.media.AudioManager +import android.media.session.MediaController +import android.media.session.MediaController.PlaybackInfo +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.BluetoothCallback +import com.android.settingslib.bluetooth.BluetoothEventManager +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +@SmallTest +class MediaControllerRepositoryImplTest { + + @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver> + @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback> + + @Mock private lateinit var context: Context + @Mock private lateinit var mediaSessionManager: MediaSessionManager + @Mock private lateinit var localBluetoothManager: LocalBluetoothManager + @Mock private lateinit var eventManager: BluetoothEventManager + + @Mock private lateinit var stoppedMediaController: MediaController + @Mock private lateinit var statelessMediaController: MediaController + @Mock private lateinit var errorMediaController: MediaController + @Mock private lateinit var remoteMediaController: MediaController + @Mock private lateinit var localMediaController: MediaController + + @Mock private lateinit var remotePlaybackInfo: PlaybackInfo + @Mock private lateinit var localPlaybackInfo: PlaybackInfo + + private val testScope = TestScope() + + private lateinit var underTest: MediaControllerRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + `when`(localBluetoothManager.eventManager).thenReturn(eventManager) + + `when`(stoppedMediaController.playbackState).thenReturn(stateStopped) + `when`(stoppedMediaController.packageName).thenReturn("test.pkg.stopped") + `when`(statelessMediaController.playbackState).thenReturn(stateNone) + `when`(statelessMediaController.packageName).thenReturn("test.pkg.stateless") + `when`(errorMediaController.playbackState).thenReturn(stateError) + `when`(errorMediaController.packageName).thenReturn("test.pkg.error") + `when`(remoteMediaController.playbackState).thenReturn(statePlaying) + `when`(remoteMediaController.playbackInfo).thenReturn(remotePlaybackInfo) + `when`(remoteMediaController.packageName).thenReturn("test.pkg.remote") + `when`(localMediaController.playbackState).thenReturn(statePlaying) + `when`(localMediaController.playbackInfo).thenReturn(localPlaybackInfo) + `when`(localMediaController.packageName).thenReturn("test.pkg.local") + + `when`(remotePlaybackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) + `when`(localPlaybackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL) + + underTest = + MediaControllerRepositoryImpl( + context, + mediaSessionManager, + localBluetoothManager, + testScope.backgroundScope, + testScope.testScheduler, + ) + } + + @Test + fun playingMediaDevicesAvailable_sessionIsActive() { + testScope.runTest { + `when`(mediaSessionManager.getActiveSessions(any())) + .thenReturn( + listOf( + stoppedMediaController, + statelessMediaController, + errorMediaController, + remoteMediaController, + localMediaController + ) + ) + var mediaController: MediaController? = null + underTest.activeMediaController + .onEach { mediaController = it } + .launchIn(backgroundScope) + runCurrent() + + triggerDevicesChange() + triggerOnAudioModeChanged() + runCurrent() + + assertThat(mediaController).isSameInstanceAs(localMediaController) + } + } + + @Test + fun noPlayingMediaDevicesAvailable_sessionIsInactive() { + testScope.runTest { + `when`(mediaSessionManager.getActiveSessions(any())) + .thenReturn( + listOf( + stoppedMediaController, + statelessMediaController, + errorMediaController, + ) + ) + var mediaController: MediaController? = null + underTest.activeMediaController + .onEach { mediaController = it } + .launchIn(backgroundScope) + runCurrent() + + triggerDevicesChange() + triggerOnAudioModeChanged() + runCurrent() + + assertThat(mediaController).isNull() + } + } + + private fun triggerDevicesChange() { + verify(context).registerReceiver(receiverCaptor.capture(), any()) + receiverCaptor.value.onReceive(context, Intent(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) + } + + private fun triggerOnAudioModeChanged() { + verify(eventManager).registerCallback(callbackCaptor.capture()) + callbackCaptor.value.onAudioModeChanged() + } + + private companion object { + val statePlaying = + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build() + val stateError = PlaybackState.Builder().setState(PlaybackState.STATE_ERROR, 0, 0f).build() + val stateStopped = + PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0, 0f).build() + val stateNone = PlaybackState.Builder().setState(PlaybackState.STATE_NONE, 0, 0f).build() + } +} diff --git a/packages/SettingsLib/tests/unit/Android.bp b/packages/SettingsLib/tests/unit/Android.bp index e2eda4f2691f..6d6e2ff8e59b 100644 --- a/packages/SettingsLib/tests/unit/Android.bp +++ b/packages/SettingsLib/tests/unit/Android.bp @@ -32,7 +32,5 @@ android_test { "androidx.test.ext.junit", "androidx.test.runner", "truth", - "kotlinx_coroutines_test", - "mockito-target-minus-junit4", ], } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 6f3c88fc8706..ae71cece803f 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -612,12 +612,19 @@ final class SettingsState { String packageName) { List<String> changedKeys = new ArrayList<>(); final Iterator<Map.Entry<String, Setting>> iterator = mSettings.entrySet().iterator(); + int index = prefix.lastIndexOf('/'); + String namespace = index < 0 ? "" : prefix.substring(0, index); + Map<String, String> trunkFlagMap = + mNamespaceDefaults.get(namespace); // Delete old keys with the prefix that are not part of the new set. + // trunk flags will not be configured with restricted propagation + // trunk flags will be explicitly set, so not removing them here while (iterator.hasNext()) { Map.Entry<String, Setting> entry = iterator.next(); final String key = entry.getKey(); final Setting oldState = entry.getValue(); - if (key != null && key.startsWith(prefix) && !keyValues.containsKey(key)) { + if (key != null && (trunkFlagMap == null || !trunkFlagMap.containsKey(key)) + && key.startsWith(prefix) && !keyValues.containsKey(key)) { iterator.remove(); FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, key, diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index 13ee1960da72..9a34d6f25551 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -27,6 +27,8 @@ import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel +import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene @@ -112,6 +114,12 @@ object ComposeFacade : BaseComposeFacade { dialogFactory: BouncerDialogFactory, ): View = throwComposeUnavailableError() + override fun createLockscreen( + context: Context, + viewModel: LockscreenContentViewModel, + blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>, + ): View = throwComposeUnavailableError() + private fun throwComposeUnavailableError(): Nothing { error( "Compose is not available. Make sure to check isComposeAvailable() before calling any" + diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/LockscreenSceneModule.kt index 725aef26cb86..fc3912e2aa52 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/LockscreenSceneModule.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/LockscreenSceneModule.kt @@ -16,6 +16,16 @@ package com.android.systemui.scene +import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint import dagger.Module +import dagger.Provides -@Module interface LockscreenSceneModule +@Module +interface LockscreenSceneModule { + companion object { + @Provides + fun providesLockscreenBlueprints(): Set<LockscreenSceneBlueprint> { + return emptySet() + } + } +} diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index f05c7f3f1616..4cc7332aa8a9 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -22,6 +22,8 @@ import android.view.View import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -40,6 +42,10 @@ import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.keyboard.stickykeys.ui.view.createStickyKeyIndicatorView import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel +import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint +import com.android.systemui.keyguard.ui.composable.LockscreenContent +import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.people.ui.compose.PeopleScreen import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -211,4 +217,19 @@ object ComposeFacade : BaseComposeFacade { setContent { PlatformTheme { BouncerContent(viewModel, dialogFactory) } } } } + + override fun createLockscreen( + context: Context, + viewModel: LockscreenContentViewModel, + blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>, + ): View { + val sceneBlueprints = + blueprints.mapNotNull { it as? ComposableLockscreenSceneBlueprint }.toSet() + return ComposeView(context).apply { + setContent { + LockscreenContent(viewModel = viewModel, blueprints = sceneBlueprints) + .Content(modifier = Modifier.fillMaxSize()) + } + } + } } diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt index cbf249653577..f5dc15485ee8 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt @@ -20,8 +20,10 @@ import android.view.View import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardViewConfigurator import com.android.systemui.keyguard.qualifiers.KeyguardRootView +import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint import com.android.systemui.keyguard.ui.composable.LockscreenScene import com.android.systemui.keyguard.ui.composable.LockscreenSceneBlueprintModule +import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint import com.android.systemui.scene.shared.model.Scene import dagger.Binds import dagger.Module @@ -51,5 +53,12 @@ interface LockscreenSceneModule { ): () -> View { return { configurator.get().getKeyguardRootView() } } + + @Provides + fun providesLockscreenBlueprints( + blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint> + ): Set<LockscreenSceneBlueprint> { + return blueprints + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt index 2cb00342cba5..b5499b7bce35 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.animation.scene.transitions -import com.android.systemui.keyguard.ui.composable.blueprint.LockscreenSceneBlueprint +import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import javax.inject.Inject @@ -39,10 +39,10 @@ class LockscreenContent @Inject constructor( private val viewModel: LockscreenContentViewModel, - private val blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>, + private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>, ) { - private val sceneKeyByBlueprint: Map<LockscreenSceneBlueprint, SceneKey> by lazy { + private val sceneKeyByBlueprint: Map<ComposableLockscreenSceneBlueprint, SceneKey> by lazy { blueprints.associateWith { blueprint -> SceneKey(blueprint.id) } } private val sceneKeyByBlueprintId: Map<String, SceneKey> by lazy { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt index 86124c635684..6b210af1308d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt @@ -36,7 +36,7 @@ class CommunalBlueprint @Inject constructor( private val viewModel: LockscreenContentViewModel, -) : LockscreenSceneBlueprint { +) : ComposableLockscreenSceneBlueprint { override val id: String = "communal" @@ -59,5 +59,5 @@ constructor( @Module interface CommunalBlueprintModule { - @Binds @IntoSet fun blueprint(blueprint: CommunalBlueprint): LockscreenSceneBlueprint + @Binds @IntoSet fun blueprint(blueprint: CommunalBlueprint): ComposableLockscreenSceneBlueprint } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt index 6d9cba4acb39..cb739830a24b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/LockscreenSceneBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt @@ -19,13 +19,10 @@ package com.android.systemui.keyguard.ui.composable.blueprint import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope +import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint /** Defines interface for classes that can render the content for a specific blueprint/layout. */ -interface LockscreenSceneBlueprint { - - /** The ID that uniquely identifies this blueprint across all other blueprints. */ - val id: String - +interface ComposableLockscreenSceneBlueprint : LockscreenSceneBlueprint { /** Renders the content of this blueprint. */ @Composable fun SceneScope.Content(modifier: Modifier) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index bf02d8abf73c..a07ab4a11731 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -20,10 +20,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.IntRect import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.padding @@ -38,6 +40,7 @@ import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import com.android.systemui.res.R import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet @@ -61,7 +64,7 @@ constructor( private val bottomAreaSection: BottomAreaSection, private val settingsMenuSection: SettingsMenuSection, private val clockInteractor: KeyguardClockInteractor, -) : LockscreenSceneBlueprint { +) : ComposableLockscreenSceneBlueprint { override val id: String = "default" @@ -84,6 +87,7 @@ constructor( with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } with(clockSection) { SmallClock( + burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmallClockTopChanged, modifier = Modifier.fillMaxWidth(), ) @@ -95,7 +99,13 @@ constructor( modifier = Modifier.fillMaxWidth() .padding( - top = { viewModel.getSmartSpacePaddingTop(resources) } + top = { viewModel.getSmartSpacePaddingTop(resources) }, + ) + .padding( + bottom = + dimensionResource( + R.dimen.keyguard_status_view_bottom_margin + ), ), ) } @@ -214,5 +224,5 @@ constructor( @Module interface DefaultBlueprintModule { - @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): LockscreenSceneBlueprint + @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): ComposableLockscreenSceneBlueprint } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt index d0aa4443d487..b035e4220a5b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt @@ -20,10 +20,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.IntRect import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.padding @@ -38,6 +40,7 @@ import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import com.android.systemui.res.R import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet @@ -61,7 +64,7 @@ constructor( private val bottomAreaSection: BottomAreaSection, private val settingsMenuSection: SettingsMenuSection, private val clockInteractor: KeyguardClockInteractor, -) : LockscreenSceneBlueprint { +) : ComposableLockscreenSceneBlueprint { override val id: String = "shortcuts-besides-udfps" @@ -86,6 +89,7 @@ constructor( SmallClock( onTopChanged = burnIn.onSmallClockTopChanged, modifier = Modifier.fillMaxWidth(), + burnInParams = burnIn.parameters, ) } with(smartSpaceSection) { @@ -96,6 +100,12 @@ constructor( Modifier.fillMaxWidth() .padding( top = { viewModel.getSmartSpacePaddingTop(resources) } + ) + .padding( + bottom = + dimensionResource( + R.dimen.keyguard_status_view_bottom_margin + ) ), ) } @@ -222,5 +232,5 @@ constructor( interface ShortcutsBesideUdfpsBlueprintModule { @Binds @IntoSet - fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): LockscreenSceneBlueprint + fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): ComposableLockscreenSceneBlueprint } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt index 616a7b4752a0..660fc5a98c03 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt @@ -72,7 +72,7 @@ constructor( private val settingsMenuSection: SettingsMenuSection, private val clockInteractor: KeyguardClockInteractor, private val largeScreenHeaderHelper: LargeScreenHeaderHelper, -) : LockscreenSceneBlueprint { +) : ComposableLockscreenSceneBlueprint { override val id: String = "split-shade" @@ -109,7 +109,14 @@ constructor( .padding( top = { viewModel.getSmartSpacePaddingTop(resources) - } + }, + ) + .padding( + bottom = + dimensionResource( + R.dimen + .keyguard_status_view_bottom_margin + ) ), ) } @@ -237,5 +244,7 @@ constructor( @Module interface SplitShadeBlueprintModule { - @Binds @IntoSet fun blueprint(blueprint: SplitShadeBlueprint): LockscreenSceneBlueprint + @Binds + @IntoSet + fun blueprint(blueprint: SplitShadeBlueprint): ComposableLockscreenSceneBlueprint } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt index 8f218792ee32..fa07bafda82c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt @@ -33,7 +33,10 @@ import com.android.compose.modifiers.padding import com.android.keyguard.KeyguardClockSwitch import com.android.systemui.customization.R as customizationR import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor +import com.android.systemui.keyguard.ui.composable.modifier.burnInAware import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged +import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import javax.inject.Inject @@ -42,10 +45,12 @@ class ClockSection constructor( private val viewModel: KeyguardClockViewModel, private val clockInteractor: KeyguardClockInteractor, + private val aodBurnInViewModel: AodBurnInViewModel, ) { @Composable fun SceneScope.SmallClock( + burnInParams: BurnInParameters, onTopChanged: (top: Float?) -> Unit, modifier: Modifier = Modifier, ) { @@ -89,7 +94,11 @@ constructor( dimensionResource(customizationR.dimen.clock_padding_start) ) .padding(top = { viewModel.getSmallClockTopMargin(view.context) }) - .onTopPlacementChanged(onTopChanged), + .onTopPlacementChanged(onTopChanged) + .burnInAware( + viewModel = aodBurnInViewModel, + params = burnInParams, + ), update = { val newClockView = checkNotNull(currentClock).smallClock.view it.removeAllViews() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 5e27d8299c16..6a8da10944d8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -19,6 +19,11 @@ package com.android.systemui.keyguard.ui.composable.section import android.content.Context import android.view.ViewGroup import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton @@ -40,56 +45,82 @@ import com.android.systemui.statusbar.notification.stack.ui.viewmodel.Notificati import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle @SysUISingleton class NotificationSection @Inject constructor( - @Application context: Context, + @Application private val context: Context, private val viewModel: NotificationsPlaceholderViewModel, - controller: NotificationStackScrollLayoutController, - sceneContainerFlags: SceneContainerFlags, - sharedNotificationContainer: SharedNotificationContainer, - sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - stackScrollLayout: NotificationStackScrollLayout, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main mainDispatcher: CoroutineDispatcher, + private val controller: NotificationStackScrollLayoutController, + private val sceneContainerFlags: SceneContainerFlags, + private val sharedNotificationContainer: SharedNotificationContainer, + private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, + private val stackScrollLayout: NotificationStackScrollLayout, + private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, + private val ambientState: AmbientState, + private val notificationStackSizeCalculator: NotificationStackSizeCalculator, + @Main private val mainDispatcher: CoroutineDispatcher, ) { - init { - if (!KeyguardShadeMigrationNssl.isUnexpectedlyInLegacyMode()) { - // This scene container section moves the NSSL to the SharedNotificationContainer. This - // also requires that SharedNotificationContainer gets moved to the SceneWindowRootView - // by the SceneWindowRootViewBinder. - // Prior to Scene Container, but when the KeyguardShadeMigrationNssl flag is enabled, - // NSSL is moved into this container by the NotificationStackScrollLayoutSection. - (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout) - sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) + @Composable + fun SceneScope.Notifications(modifier: Modifier = Modifier) { + if (KeyguardShadeMigrationNssl.isUnexpectedlyInLegacyMode()) { + // This scene container section moves the NSSL to the SharedNotificationContainer. + // This also requires that SharedNotificationContainer gets moved to the + // SceneWindowRootView by the SceneWindowRootViewBinder. Prior to Scene Container, + // but when the KeyguardShadeMigrationNssl flag is enabled, NSSL is moved into this + // container by the NotificationStackScrollLayoutSection. + return + } - SharedNotificationContainerBinder.bind( - sharedNotificationContainer, - sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainDispatcher, - ) + var isBound by remember { mutableStateOf(false) } - if (sceneContainerFlags.flexiNotifsEnabled()) { - NotificationStackAppearanceViewBinder.bind( - context, + DisposableEffect(Unit) { + val disposableHandles: MutableList<DisposableHandle> = mutableListOf() + + // Ensure stackScrollLayout is a child of sharedNotificationContainer. + if (stackScrollLayout.parent != sharedNotificationContainer) { + (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout) + sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) + } + + disposableHandles.add( + SharedNotificationContainerBinder.bind( sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, + sharedNotificationContainerViewModel, + sceneContainerFlags, controller, + notificationStackSizeCalculator, + mainDispatcher, + ) + ) + + if (sceneContainerFlags.flexiNotifsEnabled()) { + disposableHandles.add( + NotificationStackAppearanceViewBinder.bind( + context, + sharedNotificationContainer, + notificationStackAppearanceViewModel, + ambientState, + controller, + ) ) } + + isBound = true + + onDispose { + disposableHandles.forEach { it.dispose() } + disposableHandles.clear() + isBound = false + } + } + + if (!isBound) { + return } - } - @Composable - fun SceneScope.Notifications(modifier: Modifier = Modifier) { NotificationStack( viewModel = viewModel, modifier = modifier, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt index dcd22febbc86..a7ec93f5e81d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.ui.composable import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Slider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -33,6 +34,7 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( modifier = modifier, verticalArrangement = Arrangement.spacedBy(20.dp), ) { + Slider(0.5f, {}) for (component in components) { AnimatedVisibility(component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt new file mode 100644 index 000000000000..693de55211f8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt @@ -0,0 +1,48 @@ +/* + * 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.notification.stack.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.NotificationContainerBounds +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class NotificationsPlaceholderViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val underTest = kosmos.notificationsPlaceholderViewModel + @Test + fun onBoundsChanged_setsNotificationContainerBounds() { + underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f) + assertThat(kosmos.keyguardInteractor.notificationContainerBounds.value) + .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) + assertThat(kosmos.notificationStackAppearanceInteractor.stackBounds.value) + .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) + } + @Test + fun onContentTopChanged_setsContentTop() { + underTest.onContentTopChanged(padding = 5f) + assertThat(kosmos.notificationStackAppearanceInteractor.contentTop.value).isEqualTo(5f) + } +} diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 4e04af6d00bd..6bf490637033 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -54,7 +54,7 @@ android_library { "SystemUIUnfoldLib", "SystemUISharedLib-Keyguard", "WindowManager-Shell-shared", - "tracinglib", + "tracinglib-platform", "androidx.dynamicanimation_dynamicanimation", "androidx.concurrent_concurrent-futures", "androidx.lifecycle_lifecycle-runtime-ktx", diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt index 11c7a3196913..ecbd3f97f4e5 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt @@ -69,7 +69,7 @@ class CameraGestureHelper @Inject constructor( } val resolveInfo: ResolveInfo? = packageManager.resolveActivityAsUser( - getStartCameraIntent(), + getStartCameraIntent(selectedUserInteractor.getSelectedUserId()), PackageManager.MATCH_DEFAULT_ONLY, selectedUserInteractor.getSelectedUserId() ) @@ -85,7 +85,7 @@ class CameraGestureHelper @Inject constructor( * @param source The source of the camera launch, to be passed to the camera app via [Intent] */ fun launchCamera(source: Int) { - val intent: Intent = getStartCameraIntent() + val intent: Intent = getStartCameraIntent(selectedUserInteractor.getSelectedUserId()) intent.putExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, source) val wouldLaunchResolverActivity = activityIntentHelper.wouldLaunchResolverActivity( intent, selectedUserInteractor.getSelectedUserId() @@ -143,13 +143,13 @@ class CameraGestureHelper @Inject constructor( * Returns an [Intent] that can be used to start the camera app such that it occludes the * lock-screen, if needed. */ - private fun getStartCameraIntent(): Intent { + private fun getStartCameraIntent(userId: Int): Intent { val isLockScreenDismissible = keyguardStateController.canDismissLockScreen() val isSecure = keyguardStateController.isMethodSecure return if (isSecure && !isLockScreenDismissible) { - cameraIntents.getSecureCameraIntent() + cameraIntents.getSecureCameraIntent(userId) } else { - cameraIntents.getInsecureCameraIntent() + cameraIntents.getInsecureCameraIntent(userId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt index 1e17059d20e4..11375862142c 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt @@ -18,9 +18,11 @@ package com.android.systemui.camera import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.provider.MediaStore import android.text.TextUtils import com.android.systemui.res.R +import android.util.Log class CameraIntents { companion object { @@ -28,28 +30,33 @@ class CameraIntents { val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA private val VIDEO_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_VIDEO_CAMERA const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source" + const val TAG = "CameraIntents" @JvmStatic - fun getOverrideCameraPackage(context: Context): String? { - context.resources.getString(R.string.config_cameraGesturePackage)?.let { - if (!TextUtils.isEmpty(it)) { - return it + fun getOverrideCameraPackage(context: Context, userId: Int): String? { + val packageName = context.resources.getString(R.string.config_cameraGesturePackage)!! + try { + if (!TextUtils.isEmpty(packageName) + && context.packageManager.getApplicationInfoAsUser(packageName, 0, userId).enabled ?: false) { + return packageName } + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Missing cameraGesturePackage $packageName", e) } return null } @JvmStatic - fun getInsecureCameraIntent(context: Context): Intent { + fun getInsecureCameraIntent(context: Context, userId: Int): Intent { val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) - getOverrideCameraPackage(context)?.let { intent.setPackage(it) } + getOverrideCameraPackage(context, userId)?.let { intent.setPackage(it) } return intent } @JvmStatic - fun getSecureCameraIntent(context: Context): Intent { + fun getSecureCameraIntent(context: Context, userId: Int): Intent { val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION) - getOverrideCameraPackage(context)?.let { intent.setPackage(it) } + getOverrideCameraPackage(context, userId)?.let { intent.setPackage(it) } return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) } @@ -65,7 +72,7 @@ class CameraIntents { /** Returns an [Intent] that can be used to start the camera in video mode. */ @JvmStatic - fun getVideoCameraIntent(): Intent { + fun getVideoCameraIntent(userId: Int): Intent { return Intent(VIDEO_CAMERA_INTENT_ACTION) } } diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt index a434617f2da7..b65c0d49fd96 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt @@ -29,22 +29,22 @@ constructor( /** * Returns an [Intent] that can be used to start the camera, suitable for when the device is - * already unlocked + * locked */ - fun getSecureCameraIntent(): Intent { - return CameraIntents.getSecureCameraIntent(context) + fun getSecureCameraIntent(userId: Int): Intent { + return CameraIntents.getSecureCameraIntent(context, userId) } /** * Returns an [Intent] that can be used to start the camera, suitable for when the device is not * already unlocked */ - fun getInsecureCameraIntent(): Intent { - return CameraIntents.getInsecureCameraIntent(context) + fun getInsecureCameraIntent(userId: Int): Intent { + return CameraIntents.getInsecureCameraIntent(context, userId) } /** Returns an [Intent] that can be used to start the camera in video mode. */ - fun getVideoCameraIntent(): Intent { - return CameraIntents.getVideoCameraIntent() + fun getVideoCameraIntent(userId: Int): Intent { + return CameraIntents.getVideoCameraIntent(userId) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt index c5dac775c8a8..80db5353893f 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/CommunalSmartspaceController.kt @@ -19,14 +19,12 @@ package com.android.systemui.communal.smartspace import android.app.smartspace.SmartspaceConfig import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceSession -import android.app.smartspace.SmartspaceTarget import android.content.Context import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener -import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView import com.android.systemui.plugins.BcSmartspaceDataPlugin.UI_SURFACE_GLANCEABLE_HUB import com.android.systemui.smartspace.SmartspacePrecondition import com.android.systemui.smartspace.SmartspaceTargetFilter @@ -64,11 +62,6 @@ constructor( // A shadow copy of listeners is maintained to track whether the session should remain open. private var listeners = mutableSetOf<SmartspaceTargetListener>() - private var unfilteredListeners = mutableSetOf<SmartspaceTargetListener>() - - // Smartspace can be used on multiple displays, such as when the user casts their screen - private var smartspaceViews = mutableSetOf<SmartspaceView>() - var preconditionListener = object : SmartspacePrecondition.Listener { override fun onCriteriaChanged() { @@ -101,9 +94,7 @@ constructor( } private fun hasActiveSessionListeners(): Boolean { - return smartspaceViews.isNotEmpty() || - listeners.isNotEmpty() || - unfilteredListeners.isNotEmpty() + return listeners.isNotEmpty() } private fun connectSession() { @@ -188,8 +179,4 @@ constructor( private fun reloadSmartspace() { session?.requestSmartspaceUpdate() } - - private fun onTargetsAvailableUnfiltered(targets: List<SmartspaceTarget>) { - unfilteredListeners.forEach { it.onSmartspaceTargetsUpdated(targets) } - } } diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 8df7e8b430f0..9a4dfdd5d1f6 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -27,6 +27,8 @@ import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel +import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene @@ -115,4 +117,11 @@ interface BaseComposeFacade { /** Creates a container that hosts the communal UI and handles gesture transitions. */ fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View + + /** Creates a [View] that represents the Lockscreen. */ + fun createLockscreen( + context: Context, + viewModel: LockscreenContentViewModel, + blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>, + ): View } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 7f43fac575cb..abe49eefda99 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -20,6 +20,12 @@ package com.android.systemui.keyguard import android.content.Context import android.view.LayoutInflater import android.view.View +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.internal.jank.InteractionJankMonitor import com.android.keyguard.KeyguardStatusView import com.android.keyguard.KeyguardStatusViewController @@ -29,10 +35,13 @@ import com.android.keyguard.dagger.KeyguardStatusViewComponent import com.android.systemui.CoreStartable import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.compose.ComposeFacade import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlagsClassic +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 import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder @@ -44,6 +53,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R @@ -88,6 +98,8 @@ constructor( private val falsingManager: FalsingManager, private val aodAlphaViewModel: AodAlphaViewModel, private val keyguardClockViewModel: KeyguardClockViewModel, + private val lockscreenContentViewModel: LockscreenContentViewModel, + private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -115,11 +127,28 @@ constructor( initializeViews() if (!SceneContainerFlag.isEnabled) { - KeyguardBlueprintViewBinder.bind( - keyguardRootView, - keyguardBlueprintViewModel, - keyguardClockViewModel - ) + if (ComposeLockscreen.isEnabled) { + val composeView = + ComposeFacade.createLockscreen( + context = context, + viewModel = lockscreenContentViewModel, + 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, + ) + } } keyguardBlueprintCommandListener.start() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt index bbdd90375ece..3e6e3b79a820 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt @@ -50,14 +50,13 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, ) : KeyguardQuickAffordanceConfig { - private val intent: Intent by lazy { - cameraIntents.getVideoCameraIntent().apply { + private val intent: Intent + get() = cameraIntents.getVideoCameraIntent(userTracker.userId).apply { putExtra( CameraIntents.EXTRA_LAUNCH_SOURCE, StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE, ) } - } override val key: String get() = BuiltInKeyguardQuickAffordanceKeys.VIDEO_CAMERA diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.kt new file mode 100644 index 000000000000..2eafb83dc605 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/LockscreenSceneBlueprint.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.shared.model + +/** + * Defines interface for classes that can render the content for a specific blueprint/layout. + * + * The actual rendering is done by a compose-aware sub-interface. + */ +interface LockscreenSceneBlueprint { + /** The ID that uniquely identifies this blueprint across all other blueprints. */ + val id: String +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index a651c10d1c35..52d94a087110 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt @@ -57,7 +57,7 @@ constructor( private val mainDispatcher: CoroutineDispatcher, ) : KeyguardSection() { private val placeHolderId = R.id.nssl_placeholder - private var disposableHandle: DisposableHandle? = null + private val disposableHandles: MutableList<DisposableHandle> = mutableListOf() /** * Align the notification placeholder bottom to the top of either the lock icon or the ambient @@ -102,8 +102,9 @@ constructor( if (!KeyguardShadeMigrationNssl.isEnabled) { return } - disposableHandle?.dispose() - disposableHandle = + + disposeHandles() + disposableHandles.add( SharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, @@ -112,19 +113,28 @@ constructor( notificationStackSizeCalculator, mainDispatcher, ) + ) + if (sceneContainerFlags.flexiNotifsEnabled()) { - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, + disposableHandles.add( + NotificationStackAppearanceViewBinder.bind( + context, + sharedNotificationContainer, + notificationStackAppearanceViewModel, + ambientState, + controller, + ) ) } } override fun removeViews(constraintLayout: ConstraintLayout) { - disposableHandle?.dispose() + disposeHandles() constraintLayout.removeView(placeHolderId) } + + private fun disposeHandles() { + disposableHandles.forEach { it.dispose() } + disposableHandles.clear() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt index d4ea728bbffb..9cf3c955b35c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onStart /** Models UI state for the alpha of the AOD (always-on display). */ @SysUISingleton @@ -43,15 +42,13 @@ constructor( /** The alpha level for the entire lockscreen while in AOD. */ val alpha: Flow<Float> = combine( - keyguardTransitionInteractor.transitionValue(KeyguardState.GONE).onStart { - emit(0f) - }, + keyguardTransitionInteractor.currentKeyguardState, merge( keyguardInteractor.keyguardAlpha, occludedToLockscreenTransitionViewModel.lockscreenAlpha, ) - ) { transitionToGone, alpha -> - if (transitionToGone == 1f) { + ) { currentKeyguardState, alpha -> + if (currentKeyguardState == KeyguardState.GONE) { // Ensures content is not visible when in GONE state 0f } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt index ba04fd3741a4..f5e61355df37 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt @@ -67,6 +67,8 @@ constructor( duration = 500.milliseconds, onStart = { 0f }, onStep = { it }, + onFinish = { 1f }, + onCancel = { 1f }, ) val deviceEntryBackgroundViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt index 785a1e818465..1d3cfd2569aa 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt @@ -30,7 +30,7 @@ constructor( private val localBluetoothManager: LocalBluetoothManager? ) { /** Creates a [LocalMediaManager] for the given package. */ - fun create(packageName: String): LocalMediaManager { + fun create(packageName: String?): LocalMediaManager { return InfoMediaManager.createInstance(context, packageName, null, localBluetoothManager) .run { LocalMediaManager(context, localBluetoothManager, this, packageName) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 09e4e751c5f0..06ca3af73e09 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -2472,11 +2472,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return 0; } if (!mKeyguardBypassController.getBypassEnabled()) { - if (migrateClocksToBlueprint()) { - View nsslPlaceholder = mView.getRootView().findViewById(R.id.nssl_placeholder); - if (!mSplitShadeEnabled && nsslPlaceholder != null) { - return nsslPlaceholder.getTop(); - } + if (migrateClocksToBlueprint() && !mSplitShadeEnabled) { + return (int) mKeyguardInteractor.getNotificationContainerBounds() + .getValue().getTop(); } return mClockPositionResult.stackScrollerPadding; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt new file mode 100644 index 000000000000..c74c396741d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt @@ -0,0 +1,71 @@ +/* + * 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.notification.interruption + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.util.Log +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject + +// Class to track avalanche trigger event time. +@SysUISingleton +class AvalancheProvider +@Inject +constructor( + private val broadcastDispatcher: BroadcastDispatcher, + private val logger: VisualInterruptionDecisionLogger, +) { + val TAG = "AvalancheProvider" + val timeoutMs = 120000 + var startTime: Long = 0L + + private val avalancheTriggerIntents = mutableSetOf( + Intent.ACTION_AIRPLANE_MODE_CHANGED, + Intent.ACTION_BOOT_COMPLETED, + Intent.ACTION_MANAGED_PROFILE_AVAILABLE, + Intent.ACTION_USER_SWITCHED + ) + + private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action in avalancheTriggerIntents) { + + // Ignore when airplane mode turned on + if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED + && intent.getBooleanExtra(/* name= */ "state", /* defaultValue */ false)) { + Log.d(TAG, "broadcastReceiver: ignore airplane mode on") + return + } + Log.d(TAG, "broadcastReceiver received intent.action=" + intent.action) + startTime = System.currentTimeMillis() + } + } + } + + fun register() { + val intentFilter = IntentFilter() + for (intent in avalancheTriggerIntents) { + intentFilter.addAction(intent) + } + broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 8e824423973f..20c8add6cc60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -16,7 +16,10 @@ package com.android.systemui.statusbar.notification.interruption +import android.app.Notification import android.app.Notification.BubbleMetadata +import android.app.Notification.CATEGORY_EVENT +import android.app.Notification.CATEGORY_REMINDER import android.app.Notification.VISIBILITY_PRIVATE import android.app.NotificationManager.IMPORTANCE_DEFAULT import android.app.NotificationManager.IMPORTANCE_HIGH @@ -224,3 +227,68 @@ class AlertKeyguardVisibilitySuppressor( override fun shouldSuppress(entry: NotificationEntry) = keyguardNotificationVisibilityProvider.shouldHideNotification(entry) } + + +class AvalancheSuppressor( + private val avalancheProvider: AvalancheProvider, + private val systemClock: SystemClock, +) : VisualInterruptionFilter( + types = setOf(PEEK, PULSE), + reason = "avalanche", + ) { + val TAG = "AvalancheSuppressor" + + enum class State { + ALLOW_CONVERSATION_AFTER_AVALANCHE, + ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME, + ALLOW_CALLSTYLE, + ALLOW_CATEGORY_REMINDER, + ALLOW_CATEGORY_EVENT, + ALLOW_FSI_WITH_PERMISSION_ON, + ALLOW_COLORIZED, + SUPPRESS + } + + override fun shouldSuppress(entry: NotificationEntry): Boolean { + val timeSinceAvalanche = systemClock.currentTimeMillis() - avalancheProvider.startTime + val isActive = timeSinceAvalanche < avalancheProvider.timeoutMs + val state = allow(entry) + val suppress = isActive && state == State.SUPPRESS + reason = "avalanche suppress=$suppress isActive=$isActive state=$state" + return suppress + } + + fun allow(entry: NotificationEntry): State { + if ( + entry.ranking.isConversation && + entry.sbn.notification.`when` > avalancheProvider.startTime + ) { + return State.ALLOW_CONVERSATION_AFTER_AVALANCHE + } + + if (entry.channel?.isImportantConversation == true) { + return State.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME + } + + if (entry.sbn.notification.isStyle(Notification.CallStyle::class.java)) { + return State.ALLOW_CALLSTYLE + } + + if (entry.sbn.notification.category == CATEGORY_REMINDER) { + return State.ALLOW_CATEGORY_REMINDER + } + + if (entry.sbn.notification.category == CATEGORY_EVENT) { + return State.ALLOW_CATEGORY_EVENT + } + + if (entry.sbn.notification.fullScreenIntent != null) { + return State.ALLOW_FSI_WITH_PERMISSION_ON + } + + if (entry.sbn.notification.isColorized) { + return State.ALLOW_COLORIZED + } + return State.SUPPRESS + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index 6878a1e9062c..dabb18b5a1c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -33,6 +33,7 @@ import com.android.systemui.statusbar.notification.interruption.VisualInterrupti import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE +import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.HeadsUpManager @@ -45,22 +46,25 @@ import javax.inject.Inject class VisualInterruptionDecisionProviderImpl @Inject constructor( - private val ambientDisplayConfiguration: AmbientDisplayConfiguration, - private val batteryController: BatteryController, - deviceProvisionedController: DeviceProvisionedController, - private val eventLog: EventLog, - private val globalSettings: GlobalSettings, - private val headsUpManager: HeadsUpManager, - private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, - keyguardStateController: KeyguardStateController, - private val logger: VisualInterruptionDecisionLogger, - @Main private val mainHandler: Handler, - private val powerManager: PowerManager, - private val statusBarStateController: StatusBarStateController, - private val systemClock: SystemClock, - private val uiEventLogger: UiEventLogger, - private val userTracker: UserTracker, + private val ambientDisplayConfiguration: AmbientDisplayConfiguration, + private val batteryController: BatteryController, + deviceProvisionedController: DeviceProvisionedController, + private val eventLog: EventLog, + private val globalSettings: GlobalSettings, + private val headsUpManager: HeadsUpManager, + private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, + keyguardStateController: KeyguardStateController, + private val logger: VisualInterruptionDecisionLogger, + @Main private val mainHandler: Handler, + private val powerManager: PowerManager, + private val statusBarStateController: StatusBarStateController, + private val systemClock: SystemClock, + private val uiEventLogger: UiEventLogger, + private val userTracker: UserTracker, + private val avalancheProvider: AvalancheProvider + ) : VisualInterruptionDecisionProvider { + init { check(!VisualInterruptionRefactor.isUnexpectedlyInLegacyMode()) } @@ -166,6 +170,10 @@ constructor( addFilter(HunJustLaunchedFsiSuppressor()) addFilter(AlertKeyguardVisibilitySuppressor(keyguardNotificationVisibilityProvider)) + if (NotificationAvalancheSuppression.isEnabled) { + addFilter(AvalancheSuppressor(avalancheProvider, systemClock)) + avalancheProvider.register() + } started = true } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt index ee797274deac..2f80c5d225ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt @@ -85,7 +85,7 @@ abstract class VisualInterruptionCondition( /** A reason why visual interruptions might be suppressed based on the notification. */ abstract class VisualInterruptionFilter( override val types: Set<VisualInterruptionType>, - override val reason: String, + override var reason: String, override val uiEventId: UiEventEnum? = null, override val eventLogData: EventLogData? = null ) : VisualInterruptionSuppressor { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt index 6c2cbbecb477..50b08b8bb361 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt @@ -26,6 +26,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import kotlin.math.roundToInt +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.launch /** Binds the shared notification container to its view-model. */ @@ -38,8 +39,8 @@ object NotificationStackAppearanceViewBinder { viewModel: NotificationStackAppearanceViewModel, ambientState: AmbientState, controller: NotificationStackScrollLayoutController, - ) { - view.repeatWhenAttached { + ): DisposableHandle { + return view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { viewModel.stackBounds.collect { bounds -> 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 a436f1783a0c..8b723da65162 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 @@ -20,6 +20,8 @@ import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor @@ -40,9 +42,11 @@ constructor( shadeInteractor: ShadeInteractor, flags: SceneContainerFlags, featureFlags: FeatureFlagsClassic, + private val keyguardInteractor: KeyguardInteractor, ) { /** DEBUG: whether the placeholder "Notifications" text should be shown. */ - val isPlaceholderTextVisible: Boolean = !flags.flexiNotifsEnabled() + val isPlaceholderTextVisible: Boolean = + !flags.flexiNotifsEnabled() && SceneContainerFlag.isEnabled /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */ val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES) @@ -64,7 +68,10 @@ constructor( right: Float, bottom: Float, ) { - interactor.setStackBounds(NotificationContainerBounds(left, top, right, bottom)) + val notificationContainerBounds = + NotificationContainerBounds(top = top, bottom = bottom, left = left, right = right) + keyguardInteractor.setNotificationContainerBounds(notificationContainerBounds) + interactor.setStackBounds(notificationContainerBounds) } /** The corner radius of the placeholder, in dp. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 39ca7b219663..3669ba851005 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -352,7 +352,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba } if (!mKeyguardStateController.isShowing()) { - final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext); + final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext, mUserTracker.getUserId()); cameraIntent.putExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, source); mActivityStarter.startActivityDismissingKeyguard(cameraIntent, false /* onlyProvisioned */, true /* dismissShade */, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 64fcef51755d..209936108f6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1803,10 +1803,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } pw.println("Camera gesture intents:"); - pw.println(" Insecure camera: " + CameraIntents.getInsecureCameraIntent(mContext)); - pw.println(" Secure camera: " + CameraIntents.getSecureCameraIntent(mContext)); + pw.println(" Insecure camera: " + CameraIntents.getInsecureCameraIntent(mContext, mUserTracker.getUserId())); + pw.println(" Secure camera: " + CameraIntents.getSecureCameraIntent(mContext, mUserTracker.getUserId())); pw.println(" Override package: " - + CameraIntents.getOverrideCameraPackage(mContext)); + + CameraIntents.getOverrideCameraPackage(mContext, mUserTracker.getUserId())); } private void createAndAddWindows(@Nullable RegisterStatusBarResult result) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 1a17e7c28410..665a5714e277 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -32,7 +32,7 @@ import com.android.systemui.statusbar.notification.PropertyAnimator import com.android.systemui.statusbar.notification.stack.AnimationProperties import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.app.tracing.TraceUtils +import com.android.app.tracing.namedRunnable import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject @@ -125,7 +125,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( } // FrameCallback used to delay starting the light reveal animation until the next frame - private val startLightRevealCallback = TraceUtils.namedRunnable("startLightReveal") { + private val startLightRevealCallback = namedRunnable("startLightReveal") { lightRevealAnimationPlaying = true lightRevealAnimator.start() } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt index 92a64a618ba9..4dfd5a1bae46 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -18,8 +18,8 @@ package com.android.systemui.unfold import android.content.Context import android.util.Log -import com.android.app.tracing.TraceUtils.instantForTrack import com.android.app.tracing.TraceUtils.traceAsync +import com.android.app.tracing.instantForTrack import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -81,8 +81,8 @@ constructor( .pairwise() .filter { // Start tracking only when the foldable device is - //folding(UNFOLDED/HALF_FOLDED -> FOLDED) or - //unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or + // unfolding(FOLDED -> HALF_FOLD/UNFOLDED) foldableDeviceState -> foldableDeviceState.previousValue == DeviceState.FOLDED || foldableDeviceState.newValue == DeviceState.FOLDED @@ -172,7 +172,7 @@ constructor( fromFoldableDeviceState: Int ): DisplaySwitchLatencyEvent { log { "fromFoldableDeviceState=$fromFoldableDeviceState" } - instantForTrack(TAG, "fromFoldableDeviceState=$fromFoldableDeviceState") + instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" } return copy(fromFoldableDeviceState = fromFoldableDeviceState) } @@ -187,7 +187,7 @@ constructor( "toState=$toState, " + "latencyMs=$displaySwitchTimeMs" } - instantForTrack(TAG, "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState") + instantForTrack(TAG) { "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState" } return copy( toFoldableDeviceState = toFoldableDeviceState, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt new file mode 100644 index 000000000000..2ff9af9ac3c0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dagger + +import android.content.Context +import android.media.session.MediaSessionManager +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.volume.data.repository.MediaControllerRepository +import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import dagger.Module +import dagger.Provides +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope + +@Module +interface MediaDevicesModule { + + companion object { + + @Provides + @SysUISingleton + fun provideMediaDeviceSessionRepository( + @Application context: Context, + mediaSessionManager: MediaSessionManager, + localBluetoothManager: LocalBluetoothManager?, + @Application coroutineScope: CoroutineScope, + @Background backgroundContext: CoroutineContext, + ): MediaControllerRepository = + MediaControllerRepositoryImpl( + context, + mediaSessionManager, + localBluetoothManager, + coroutineScope, + backgroundContext, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index c842e5f295b9..5cb6fa8c8046 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -62,6 +62,7 @@ import kotlinx.coroutines.CoroutineScope; @Module( includes = { AudioModule.class, + MediaDevicesModule.class }, subcomponents = { VolumePanelComponent.class diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt new file mode 100644 index 000000000000..57ac4357b9b5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.volume.panel.component.mediaoutput.data.repository + +import com.android.settingslib.volume.data.repository.LocalMediaRepository +import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.media.controls.pipeline.LocalMediaManagerFactory +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope + +class LocalMediaRepositoryFactory +@Inject +constructor( + private val localMediaManagerFactory: LocalMediaManagerFactory, + @Application private val coroutineScope: CoroutineScope, + @Background private val backgroundCoroutineContext: CoroutineContext, +) { + + fun create(packageName: String?): LocalMediaRepository = + LocalMediaRepositoryImpl( + localMediaManagerFactory.create(packageName), + coroutineScope, + backgroundCoroutineContext, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt new file mode 100644 index 000000000000..6c456f963f03 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor + +import android.content.pm.PackageManager +import android.util.Log +import com.android.settingslib.media.MediaDevice +import com.android.settingslib.volume.data.repository.LocalMediaRepository +import com.android.settingslib.volume.data.repository.MediaControllerRepository +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalCoroutinesApi::class) +@VolumePanelScope +class MediaOutputInteractor +@Inject +constructor( + private val localMediaRepositoryFactory: LocalMediaRepositoryFactory, + private val packageManager: PackageManager, + @VolumePanelScope private val coroutineScope: CoroutineScope, + @Background private val backgroundCoroutineContext: CoroutineContext, + mediaControllerRepository: MediaControllerRepository +) { + + val mediaDeviceSession: Flow<MediaDeviceSession> = + mediaControllerRepository.activeMediaController.mapNotNull { mediaController -> + if (mediaController == null) { + MediaDeviceSession.Inactive + } else { + MediaDeviceSession.Active( + appLabel = getApplicationLabel(mediaController.packageName) + ?: return@mapNotNull null, + packageName = mediaController.packageName, + sessionToken = mediaController.sessionToken, + ) + } + } + private val localMediaRepository: Flow<LocalMediaRepository> = + mediaDeviceSession + .map { (it as? MediaDeviceSession.Active)?.packageName } + .distinctUntilChanged() + .map { localMediaRepositoryFactory.create(it) } + .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 1) + + val currentConnectedDevice: Flow<MediaDevice?> = + localMediaRepository.flatMapLatest { it.currentConnectedDevice } + + val mediaDevices: Flow<Collection<MediaDevice>> = + localMediaRepository.flatMapLatest { it.mediaDevices } + + private suspend fun getApplicationLabel(packageName: String): CharSequence? { + return try { + withContext(backgroundCoroutineContext) { + val appInfo = + packageManager.getApplicationInfo( + packageName, + PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER + ) + appInfo.loadLabel(packageManager) + } + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, "Unable to find info for package: $packageName") + null + } + } + + private companion object { + const val TAG = "MediaOutputInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt new file mode 100644 index 000000000000..f250308802b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.mediaoutput.domain.model + +import android.media.session.MediaSession + +/** Represents media playing on the connected device. */ +sealed interface MediaDeviceSession { + + /** Media is playing. */ + data class Active( + val appLabel: CharSequence, + val packageName: String, + val sessionToken: MediaSession.Token, + ) : MediaDeviceSession + + /** Media is not playing. */ + data object Inactive : MediaDeviceSession + + /** Current media state is unknown yet. */ + data object Unknown : MediaDeviceSession +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt index 6d3cc4c536f1..669795bc91a8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt @@ -81,10 +81,10 @@ class CameraGestureHelperTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever(cameraIntents.getSecureCameraIntent()).thenReturn( + whenever(cameraIntents.getSecureCameraIntent(anyInt())).thenReturn( Intent(CameraIntents.DEFAULT_SECURE_CAMERA_INTENT_ACTION) ) - whenever(cameraIntents.getInsecureCameraIntent()).thenReturn( + whenever(cameraIntents.getInsecureCameraIntent(anyInt())).thenReturn( Intent(CameraIntents.DEFAULT_INSECURE_CAMERA_INTENT_ACTION) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 25a7eb8e660d..9517f823cf10 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -46,6 +46,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.After import org.junit.Assert.assertThrows import org.junit.Assume.assumeTrue import org.junit.Before @@ -118,6 +119,11 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { initAndAttachContainerView() } + @After + fun tearDown() { + ViewUtils.detachView(parentView) + } + @Test fun isEnabled_communalEnabled_returnsTrue() { communalRepository.setIsCommunalEnabled(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt index da68d9c743ce..54108642385f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.notification.interruption +import android.app.Notification.CATEGORY_EVENT +import android.app.Notification.CATEGORY_REMINDER +import android.app.NotificationManager import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest @@ -47,6 +50,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro systemClock, uiEventLogger, userTracker, + avalancheProvider ) } @@ -70,6 +74,114 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro } } + // Avalanche tests are in VisualInterruptionDecisionProviderImplTest + // instead of VisualInterruptionDecisionProviderTestBase + // because avalanche code is based on the suppression refactor. + + @Test + fun testAvalancheFilter_duringAvalanche_allowConversationFromAfterEvent() { + avalancheProvider.startTime = whenAgo(10) + + withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) { + ensurePeekState() + assertShouldHeadsUp(buildEntry { + importance = NotificationManager.IMPORTANCE_HIGH + isConversation = true + isImportantConversation = false + whenMs = whenAgo(5) + }) + } + } + + @Test + fun testAvalancheFilter_duringAvalanche_suppressConversationFromBeforeEvent() { + avalancheProvider.startTime = whenAgo(10) + + withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) { + ensurePeekState() + assertShouldNotHeadsUp(buildEntry { + importance = NotificationManager.IMPORTANCE_DEFAULT + isConversation = true + isImportantConversation = false + whenMs = whenAgo(15) + }) + } + } + + @Test + fun testAvalancheFilter_duringAvalanche_allowHighPriorityConversation() { + avalancheProvider.startTime = whenAgo(10) + + withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) { + ensurePeekState() + assertShouldHeadsUp(buildEntry { + importance = NotificationManager.IMPORTANCE_HIGH + isImportantConversation = true + }) + } + } + + @Test + fun testAvalancheFilter_duringAvalanche_allowCall() { + avalancheProvider.startTime = whenAgo(10) + + withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) { + ensurePeekState() + assertShouldHeadsUp(buildEntry { + importance = NotificationManager.IMPORTANCE_HIGH + isCall = true + }) + } + } + + @Test + fun testAvalancheFilter_duringAvalanche_allowCategoryReminder() { + avalancheProvider.startTime = whenAgo(10) + + withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) { + ensurePeekState() + assertShouldHeadsUp(buildEntry { + importance = NotificationManager.IMPORTANCE_HIGH + category = CATEGORY_REMINDER + }) + } + } + + @Test + fun testAvalancheFilter_duringAvalanche_allowCategoryEvent() { + avalancheProvider.startTime = whenAgo(10) + + withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) { + ensurePeekState() + assertShouldHeadsUp(buildEntry { + importance = NotificationManager.IMPORTANCE_HIGH + category = CATEGORY_EVENT + }) + } + } + + @Test + fun testAvalancheFilter_duringAvalanche_allowFsi() { + avalancheProvider.startTime = whenAgo(10) + + withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) { + assertFsiNotSuppressed() + } + } + + @Test + fun testAvalancheFilter_duringAvalanche_allowColorized() { + avalancheProvider.startTime = whenAgo(10) + + withFilter(AvalancheSuppressor(avalancheProvider, systemClock)) { + ensurePeekState() + assertShouldHeadsUp(buildEntry { + importance = NotificationManager.IMPORTANCE_HIGH + isColorized = true + }) + } + } + @Test fun testPeekCondition_suppressesOnlyPeek() { withCondition(TestCondition(types = setOf(PEEK)) { true }) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt index 2ac0cb7499d2..f89b9cd7410f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt @@ -19,7 +19,10 @@ package com.android.systemui.statusbar.notification.interruption import android.app.ActivityManager import android.app.Notification import android.app.Notification.BubbleMetadata +import android.app.Notification.EXTRA_COLORIZED +import android.app.Notification.EXTRA_TEMPLATE import android.app.Notification.FLAG_BUBBLE +import android.app.Notification.FLAG_CAN_COLORIZE import android.app.Notification.FLAG_FOREGROUND_SERVICE import android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED import android.app.Notification.FLAG_USER_INITIATED_JOB @@ -50,6 +53,8 @@ import android.provider.Settings.Global.HEADS_UP_ON import com.android.internal.logging.UiEventLogger.UiEventEnum import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.broadcast.FakeBroadcastDispatcher import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogcatEchoTracker import com.android.systemui.log.core.LogLevel @@ -122,6 +127,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { protected val systemClock = FakeSystemClock() protected val uiEventLogger = UiEventLoggerFake() protected val userTracker = FakeUserTracker() + protected val avalancheProvider: AvalancheProvider = mock() protected abstract val provider: VisualInterruptionDecisionProvider @@ -1097,6 +1103,8 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { var whenMs: Long? = null var isGrouped = false var isGroupSummary = false + var isCall = false + var category: String? = null var groupAlertBehavior: Int? = null var hasBubbleMetadata = false var hasFsi = false @@ -1106,10 +1114,12 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { var isUserInitiatedJob = false var isBubble = false var isStickyAndNotDemoted = false + var isColorized = false // Set on NotificationEntryBuilder: var importance = IMPORTANCE_DEFAULT var canBubble: Boolean? = null + var isImportantConversation = false // Set on NotificationEntry: var hasJustLaunchedFsi = false @@ -1118,6 +1128,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { var packageSuspended = false var visibilityOverride: Int? = null var suppressedVisualEffects: Int? = null + var isConversation = false private fun buildBubbleMetadata(): BubbleMetadata { val builder = @@ -1158,6 +1169,13 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { nb.setGroupSummary(true) } + if (isCall) { + nb.extras.putString(EXTRA_TEMPLATE, Notification.CallStyle::class.java.name) + } + + if (category != null) { + nb.setCategory(category) + } groupAlertBehavior?.let { nb.setGroupAlertBehavior(it) } if (hasBubbleMetadata) { @@ -1185,6 +1203,10 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { if (isStickyAndNotDemoted) { n.flags = n.flags or FLAG_FSI_REQUESTED_BUT_DENIED } + if (isColorized) { + n.extras.putBoolean(EXTRA_COLORIZED, true) + n.flags = n.flags or FLAG_CAN_COLORIZE + } } .let { NotificationEntryBuilder().setNotification(it) } .also { neb -> @@ -1193,9 +1215,10 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { neb.setTag(TEST_TAG) neb.setImportance(importance) - neb.setChannel( - NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_NAME, importance) - ) + val channel = + NotificationChannel(TEST_CHANNEL_ID, TEST_CHANNEL_NAME, importance) + channel.isImportantConversation = isImportantConversation + neb.setChannel(channel) canBubble?.let { neb.setCanBubble(it) } } @@ -1216,6 +1239,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } visibilityOverride?.let { mrb.setVisibilityOverride(it) } suppressedVisualEffects?.let { mrb.setSuppressedVisualEffects(it) } + mrb.setIsConversation(isConversation) } .build() } @@ -1287,7 +1311,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } } - private fun whenAgo(whenAgeMs: Long) = systemClock.currentTimeMillis() - whenAgeMs + protected fun whenAgo(whenAgeMs: Long) = systemClock.currentTimeMillis() - whenAgeMs } private const val TEST_CONTENT_TITLE = "Test Content Title" diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt index 0356c2cdbcd0..620ad9c19bfa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt @@ -19,6 +19,7 @@ import android.hardware.display.AmbientDisplayConfiguration import android.os.Handler import android.os.PowerManager import com.android.internal.logging.UiEventLogger +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.settings.UserTracker @@ -49,7 +50,8 @@ object VisualInterruptionDecisionProviderTestUtil { statusBarStateController: StatusBarStateController, systemClock: SystemClock, uiEventLogger: UiEventLogger, - userTracker: UserTracker + userTracker: UserTracker, + avalancheProvider: AvalancheProvider ): VisualInterruptionDecisionProvider { return if (VisualInterruptionRefactor.isEnabled) { VisualInterruptionDecisionProviderImpl( @@ -67,7 +69,8 @@ object VisualInterruptionDecisionProviderTestUtil { statusBarStateController, systemClock, uiEventLogger, - userTracker + userTracker, + avalancheProvider ) } else { NotificationInterruptStateProviderWrapper( 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 849a13be58ff..b048949e0e76 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 @@ -153,6 +153,7 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.init.NotificationsController; +import com.android.systemui.statusbar.notification.interruption.AvalancheProvider; import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl; @@ -313,6 +314,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private CameraLauncher mCameraLauncher; @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private UserTracker mUserTracker; + @Mock private AvalancheProvider mAvalancheProvider; @Mock private FingerprintManager mFingerprintManager; @Mock IPowerManager mPowerManagerService; @Mock ActivityStarter mActivityStarter; @@ -367,7 +369,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mStatusBarStateController, mFakeSystemClock, mock(UiEventLogger.class), - mUserTracker); + mUserTracker, + mAvalancheProvider); mVisualInterruptionDecisionProvider.start(); mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 3bde6e36a51f..99c2dc79b491 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -315,6 +315,8 @@ public class ScrimControllerTest extends SysuiTestCase { @After public void tearDown() { + // Detaching view stops flow collection and prevents memory leak. + ViewUtils.detachView(mScrimBehind); finishAnimationsImmediately(); Arrays.stream(ScrimState.values()).forEach((scrim) -> { scrim.setAodFrontScrimAlpha(0f); diff --git a/packages/SystemUI/tests/src/com/android/systemui/tracing/TraceUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/tracing/TraceUtilsTest.kt index ba34ce6c90f0..bda339f7a022 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/tracing/TraceUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/tracing/TraceUtilsTest.kt @@ -12,7 +12,7 @@ * permissions and limitations under the License. */ -package com.android.app.tracing +package com.android.systemui.tracing import android.os.Handler import android.os.Looper @@ -20,11 +20,13 @@ import android.os.Trace.TRACE_TAG_APP import android.testing.AndroidTestingRunner import android.util.Log import androidx.test.filters.SmallTest +import com.android.app.tracing.TraceUtils.traceRunnable +import com.android.app.tracing.namedRunnable +import com.android.app.tracing.traceSection import com.android.systemui.SysuiTestCase import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -68,7 +70,6 @@ class TraceUtilsTest : SysuiTestCase() { } @Test - @Ignore("b/267482189 - Enable once androidx.tracing >= 1.2.0-beta04") fun testLongTraceSection_doesNotThrow_whenUsingAndroidX() { androidx.tracing.Trace.beginSection(SECTION_NAME_THATS_TOO_LONG) } @@ -84,17 +85,13 @@ class TraceUtilsTest : SysuiTestCase() { fun testLongTraceSection_doesNotThrow_whenUsedAsTraceNameSupplier() { Handler(Looper.getMainLooper()) .runWithScissors( - TraceUtils.namedRunnable(SECTION_NAME_THATS_TOO_LONG) { - Log.v(TAG, "TraceUtils.namedRunnable() block.") - }, + namedRunnable(SECTION_NAME_THATS_TOO_LONG) { Log.v(TAG, "namedRunnable() block.") }, TEST_FAIL_TIMEOUT ) } @Test fun testLongTraceSection_doesNotThrow_whenUsingTraceRunnable() { - TraceUtils.traceRunnable(SECTION_NAME_THATS_TOO_LONG) { - Log.v(TAG, "TraceUtils.traceRunnable() block.") - } + traceRunnable(SECTION_NAME_THATS_TOO_LONG) { Log.v(TAG, "traceRunnable() block.") }.run() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index f3e920310050..f4b366594e99 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -96,6 +96,7 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; @@ -151,6 +152,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; +import com.android.systemui.statusbar.notification.interruption.AvalancheProvider; import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger; import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger; @@ -587,7 +589,9 @@ public class BubblesTest extends SysuiTestCase { mock(StatusBarStateController.class), mock(SystemClock.class), mock(UiEventLogger.class), - mock(UserTracker.class)); + mock(UserTracker.class), + mock(AvalancheProvider.class) + ); interruptionDecisionProvider.start(); mShellTaskOrganizer = new ShellTaskOrganizer(mock(ShellInit.class), 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 d7e948eefc95..29faa58d674a 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 @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.shared.flag.sceneContainerFlags @@ -29,5 +30,6 @@ val Kosmos.notificationsPlaceholderViewModel by Fixture { shadeInteractor = shadeInteractor, flags = sceneContainerFlags, featureFlags = featureFlagsClassic, + keyguardInteractor = keyguardInteractor, ) } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 85fa65ac705c..7b5932b42b84 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -58,6 +58,11 @@ public class RavenwoodRuleImpl { private static ScheduledFuture<?> sPendingTimeout; /** + * When enabled, attempt to detect uncaught exceptions from background threads. + */ + private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION = false; + + /** * When set, an unhandled exception was discovered (typically on a background thread), and we * capture it here to ensure it's reported as a test failure. */ @@ -75,8 +80,10 @@ public class RavenwoodRuleImpl { } public static void init(RavenwoodRule rule) { - maybeThrowPendingUncaughtException(false); - Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); + if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { + maybeThrowPendingUncaughtException(false); + Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); + } RuntimeInit.redirectLogStreams(); @@ -129,7 +136,9 @@ public class RavenwoodRuleImpl { android.os.Binder.reset$ravenwood(); android.os.Process.reset$ravenwood(); - maybeThrowPendingUncaughtException(true); + if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { + maybeThrowPendingUncaughtException(true); + } } public static void logTestRunner(String label, Description description) { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 687def05b1d7..a61199ab908c 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1392,6 +1392,11 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call + public int getMousePointerSpeed() { + return mNative.getMousePointerSpeed(); + } + + @Override // Binder call public void tryPointerSpeed(int speed) { if (!checkCallingPermission(android.Manifest.permission.SET_POINTER_SPEED, "tryPointerSpeed()")) { diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index e5f3484b4825..b16df0f8a28a 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -119,6 +119,8 @@ interface NativeInputManagerService { */ boolean transferTouch(IBinder destChannelToken, int displayId); + int getMousePointerSpeed(); + void setPointerSpeed(int speed); void setMousePointerAccelerationEnabled(int displayId, boolean enabled); @@ -364,6 +366,9 @@ interface NativeInputManagerService { public native boolean transferTouch(IBinder destChannelToken, int displayId); @Override + public native int getMousePointerSpeed(); + + @Override public native void setPointerSpeed(int speed); @Override diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index fff4939a5e0f..95189ab56b9e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3732,6 +3732,37 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return InputBindResult.INVALID_DISPLAY_ID; } + // In case mShowForced flag affects the next client to keep IME visible, when + // the current client is leaving due to the next focused client, we clear + // mShowForced flag when the next client's targetSdkVersion is T or higher. + final boolean shouldClearFlag = + mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid); + final boolean showForced = mVisibilityStateComputer.mShowForced; + if (mCurFocusedWindow != windowToken && showForced && shouldClearFlag) { + mVisibilityStateComputer.mShowForced = false; + } + + // Verify if caller is a background user. + final int currentUserId = mSettings.getUserId(); + if (userId != currentUserId) { + if (ArrayUtils.contains( + mUserManagerInternal.getProfileIds(currentUserId, false), userId)) { + // cross-profile access is always allowed here to allow + // profile-switching. + scheduleSwitchUserTaskLocked(userId, cs.mClient); + return InputBindResult.USER_SWITCHING; + } + Slog.w(TAG, "A background user is requesting window. Hiding IME."); + Slog.w(TAG, "If you need to impersonate a foreground user/profile from" + + " a background user, use EditorInfo.targetInputMethodUser with" + + " INTERACT_ACROSS_USERS_FULL permission."); + hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, + 0 /* flags */, + null /* resultReceiver */, + SoftInputShowHideReason.HIDE_INVALID_USER); + return InputBindResult.INVALID_USER; + } + result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, @@ -3781,32 +3812,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + " cs=" + cs); } - final boolean shouldClearFlag = mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid); - // In case mShowForced flag affects the next client to keep IME visible, when the current - // client is leaving due to the next focused client, we clear mShowForced flag when the - // next client's targetSdkVersion is T or higher. - final boolean showForced = mVisibilityStateComputer.mShowForced; - if (mCurFocusedWindow != windowToken && showForced && shouldClearFlag) { - mVisibilityStateComputer.mShowForced = false; - } - - final int currentUserId = mSettings.getUserId(); - if (userId != currentUserId) { - if (ArrayUtils.contains( - mUserManagerInternal.getProfileIds(currentUserId, false), userId)) { - // cross-profile access is always allowed here to allow profile-switching. - scheduleSwitchUserTaskLocked(userId, cs.mClient); - return InputBindResult.USER_SWITCHING; - } - Slog.w(TAG, "A background user is requesting window. Hiding IME."); - Slog.w(TAG, "If you need to impersonate a foreground user/profile from" - + " a background user, use EditorInfo.targetInputMethodUser with" - + " INTERACT_ACROSS_USERS_FULL permission."); - hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, - null /* resultReceiver */, SoftInputShowHideReason.HIDE_INVALID_USER); - return InputBindResult.INVALID_USER; - } - final boolean sameWindowFocused = mCurFocusedWindow == windowToken; final boolean isTextEditor = (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0; final boolean startInputByWinGainedFocus = diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index a06607b68fa9..7fb3e001c4c3 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -93,7 +93,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.IProgressListener; import android.os.Process; -import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; @@ -139,11 +138,11 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; import com.android.internal.widget.ICheckCredentialProgressCallback; import com.android.internal.widget.ILockSettings; -import com.android.internal.widget.ILockSettingsStateListener; import com.android.internal.widget.IWeakEscrowTokenActivatedListener; import com.android.internal.widget.IWeakEscrowTokenRemovedListener; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockSettingsInternal; +import com.android.internal.widget.LockSettingsStateListener; import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.RebootEscrowListener; import com.android.internal.widget.VerifyCredentialResponse; @@ -185,6 +184,7 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.StringJoiner; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -332,8 +332,8 @@ public class LockSettingsService extends ILockSettings.Stub { private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>(); - private final RemoteCallbackList<ILockSettingsStateListener> mLockSettingsStateListeners = - new RemoteCallbackList<>(); + private final CopyOnWriteArrayList<LockSettingsStateListener> mLockSettingsStateListeners = + new CopyOnWriteArrayList<>(); // This class manages life cycle events for encrypted users on File Based Encryption (FBE) // devices. The most basic of these is to show/hide notifications about missing features until @@ -2379,25 +2379,12 @@ public class LockSettingsService extends ILockSettings.Stub { } private void notifyLockSettingsStateListeners(boolean success, int userId) { - int i = mLockSettingsStateListeners.beginBroadcast(); - try { - while (i > 0) { - i--; - try { - if (success) { - mLockSettingsStateListeners.getBroadcastItem(i) - .onAuthenticationSucceeded(userId); - } else { - mLockSettingsStateListeners.getBroadcastItem(i) - .onAuthenticationFailed(userId); - } - } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying LockSettingsStateListener:" - + " success = " + success + ", userId = " + userId, e); - } + for (LockSettingsStateListener listener : mLockSettingsStateListeners) { + if (success) { + listener.onAuthenticationSucceeded(userId); + } else { + listener.onAuthenticationFailed(userId); } - } finally { - mLockSettingsStateListeners.finishBroadcast(); } } @@ -3720,15 +3707,15 @@ public class LockSettingsService extends ILockSettings.Stub { } @Override - public void registerLockSettingsStateListener( - @NonNull ILockSettingsStateListener listener) { - mLockSettingsStateListeners.register(listener); + public void registerLockSettingsStateListener(@NonNull LockSettingsStateListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + mLockSettingsStateListeners.add(listener); } @Override public void unregisterLockSettingsStateListener( - @NonNull ILockSettingsStateListener listener) { - mLockSettingsStateListeners.unregister(listener); + @NonNull LockSettingsStateListener listener) { + mLockSettingsStateListeners.remove(listener); } } diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 0bbad85aebcd..dc97e5fa92af 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -819,6 +819,7 @@ public class PackageArchiver { * <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple * launcher activities, only one of the icons is returned arbitrarily. */ + @Nullable public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user, String callingPackageName) { Objects.requireNonNull(packageName); @@ -844,7 +845,7 @@ public class PackageArchiver { // In the rare case the archived app defined more than two launcher activities, we choose // the first one arbitrarily. Bitmap icon = decodeIcon(archiveState.getActivityInfos().get(0)); - if (getAppOpsManager().checkOp( + if (icon != null && getAppOpsManager().checkOp( AppOpsManager.OP_ARCHIVE_ICON_OVERLAY, callingUid, callingPackageName) == MODE_ALLOWED) { icon = includeCloudOverlay(icon); @@ -900,6 +901,7 @@ public class PackageArchiver { return bitmap; } + @Nullable Bitmap includeCloudOverlay(Bitmap bitmap) { Drawable cloudDrawable = mContext.getResources() @@ -920,7 +922,9 @@ public class PackageArchiver { final int iconSize = mContext.getSystemService( ActivityManager.class).getLauncherLargeIconSize(); Bitmap appIconWithCloudOverlay = drawableToBitmap(layerDrawable, iconSize); - bitmap.recycle(); + if (bitmap != null) { + bitmap.recycle(); + } return appIconWithCloudOverlay; } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 716aee3f8692..e7431723789d 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -5150,6 +5150,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** @return the orientation of the display when it's rotation is ROTATION_0. */ int getNaturalOrientation() { + return mBaseDisplayWidth <= mBaseDisplayHeight + ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; + } + + /** + * Returns the orientation which is used for app's Configuration (excluding decor insets) when + * the display rotation is ROTATION_0. + */ + int getNaturalConfigurationOrientation() { final Configuration config = getConfiguration(); if (config.windowConfiguration.getDisplayRotation() == ROTATION_0) { return config.orientation; diff --git a/services/core/java/com/android/server/wm/InputConsumerImpl.java b/services/core/java/com/android/server/wm/InputConsumerImpl.java index 34d765117a57..420467087224 100644 --- a/services/core/java/com/android/server/wm/InputConsumerImpl.java +++ b/services/core/java/com/android/server/wm/InputConsumerImpl.java @@ -132,7 +132,7 @@ class InputConsumerImpl implements IBinder.DeathRecipient { void show(SurfaceControl.Transaction t, WindowContainer w) { t.show(mInputSurface); t.setInputWindowInfo(mInputSurface, mWindowHandle); - t.setRelativeLayer(mInputSurface, w.getSurfaceControl(), 1); + t.setRelativeLayer(mInputSurface, w.getSurfaceControl(), 1 + w.getChildCount()); } void show(SurfaceControl.Transaction t, int layer) { diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 2d2857aba781..80894b201942 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1589,7 +1589,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) { // NOSENSOR means the display's "natural" orientation, so return that. if (mDisplayContent != null) { - return mDisplayContent.getNaturalOrientation(); + return mDisplayContent.getNaturalConfigurationOrientation(); } } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LOCKED) { // LOCKED means the activity's orientation remains unchanged, so return existing value. diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS index cc08488742b2..df7fb991e39e 100644 --- a/services/core/jni/OWNERS +++ b/services/core/jni/OWNERS @@ -31,5 +31,8 @@ per-file com_android_server_vibrator_* = file:/services/core/java/com/android/se per-file com_android_server_am_CachedAppOptimizer.cpp = timmurray@google.com, edgararriaga@google.com, dualli@google.com, carmenjackson@google.com, philipcuadra@google.com per-file com_android_server_companion_virtual_InputController.cpp = file:/services/companion/java/com/android/server/companion/virtual/OWNERS +# Memory +per-file com_android_server_am_OomConnection.cpp = file:/MEMORY_OWNERS + # Bug component : 158088 = per-file *AnrTimer* per-file *AnrTimer* = file:/PERFORMANCE_OWNERS diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 4a6b31c29471..810090adbf8e 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -285,6 +285,7 @@ public: void setInputDispatchMode(bool enabled, bool frozen); void setSystemUiLightsOut(bool lightsOut); void setPointerDisplayId(int32_t displayId); + int32_t getMousePointerSpeed(); void setPointerSpeed(int32_t speed); void setMousePointerAccelerationEnabled(int32_t displayId, bool enabled); void setTouchpadPointerSpeed(int32_t speed); @@ -1219,6 +1220,11 @@ void NativeInputManager::setPointerDisplayId(int32_t displayId) { } } +int32_t NativeInputManager::getMousePointerSpeed() { + std::scoped_lock _l(mLock); + return mLocked.pointerSpeed; +} + void NativeInputManager::setPointerSpeed(int32_t speed) { { // acquire lock std::scoped_lock _l(mLock); @@ -2211,6 +2217,12 @@ static jboolean nativeTransferTouch(JNIEnv* env, jobject nativeImplObj, jobject } } +static jint nativeGetMousePointerSpeed(JNIEnv* env, jobject nativeImplObj) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + + return static_cast<jint>(im->getMousePointerSpeed()); +} + static void nativeSetPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2865,6 +2877,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"transferTouchFocus", "(Landroid/os/IBinder;Landroid/os/IBinder;Z)Z", (void*)nativeTransferTouchFocus}, {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouch}, + {"getMousePointerSpeed", "()I", (void*)nativeGetMousePointerSpeed}, {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed}, {"setMousePointerAccelerationEnabled", "(IZ)V", (void*)nativeSetMousePointerAccelerationEnabled}, diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java index 5081198f0058..705359708bc7 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java @@ -39,7 +39,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.PropertyInvalidatedCache; -import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; @@ -49,8 +48,8 @@ import android.text.TextUtils; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.internal.widget.ILockSettingsStateListener; import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockSettingsStateListener; import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.VerifyCredentialResponse; @@ -412,7 +411,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS); final LockscreenCredential password = newPassword("password"); setCredential(PRIMARY_USER_ID, password); - final ILockSettingsStateListener listener = mockLockSettingsStateListener(); + final LockSettingsStateListener listener = mock(LockSettingsStateListener.class); mLocalService.registerLockSettingsStateListener(listener); assertEquals(VerifyCredentialResponse.RESPONSE_OK, @@ -429,7 +428,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { final LockscreenCredential password = newPassword("password"); setCredential(PRIMARY_USER_ID, password); final LockscreenCredential badPassword = newPassword("badPassword"); - final ILockSettingsStateListener listener = mockLockSettingsStateListener(); + final LockSettingsStateListener listener = mock(LockSettingsStateListener.class); mLocalService.registerLockSettingsStateListener(listener); assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, @@ -445,7 +444,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { final LockscreenCredential password = newPassword("password"); setCredential(PRIMARY_USER_ID, password); final LockscreenCredential badPassword = newPassword("badPassword"); - final ILockSettingsStateListener listener = mockLockSettingsStateListener(); + final LockSettingsStateListener listener = mock(LockSettingsStateListener.class); mLocalService.registerLockSettingsStateListener(listener); assertEquals(VerifyCredentialResponse.RESPONSE_OK, @@ -599,12 +598,4 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertNotEquals(0, mGateKeeperService.getSecureUserId(userId)); } } - - private ILockSettingsStateListener mockLockSettingsStateListener() { - ILockSettingsStateListener listener = mock(ILockSettingsStateListener.Stub.class); - IBinder binder = mock(IBinder.class); - when(binder.isBinderAlive()).thenReturn(true); - when(listener.asBinder()).thenReturn(binder); - return listener; - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 95850ac2f3b2..f63ff6ec66e1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1003,7 +1003,13 @@ public class DisplayContentTests extends WindowTestsBase { dc.computeScreenConfiguration(config, ROTATION_0); dc.onRequestedOverrideConfigurationChanged(config); assertEquals(Configuration.ORIENTATION_LANDSCAPE, config.orientation); - assertEquals(Configuration.ORIENTATION_LANDSCAPE, dc.getNaturalOrientation()); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, dc.getNaturalConfigurationOrientation()); + window.setOverrideOrientation(SCREEN_ORIENTATION_NOSENSOR); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, + window.getRequestedConfigurationOrientation()); + // Note that getNaturalOrientation is based on logical display size. So it is portrait if + // the display width equals to height. + assertEquals(Configuration.ORIENTATION_PORTRAIT, dc.getNaturalOrientation()); } @Test diff --git a/telecomm/OWNERS b/telecomm/OWNERS index b57b7c79326e..bb2ac51a24f1 100644 --- a/telecomm/OWNERS +++ b/telecomm/OWNERS @@ -6,4 +6,5 @@ xiaotonj@google.com rgreenwalt@google.com grantmenke@google.com pmadapurmath@google.com -tjstuart@google.com
\ No newline at end of file +tjstuart@google.com +huiwang@google.com diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index eb7e67dccfd5..174954542b94 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1927,34 +1927,25 @@ public class SubscriptionManager { * Then for SDK 35+, if the caller identity is personal profile, then this will return * subscription 1 only and vice versa. * - * <p> The records will be sorted by {@link SubscriptionInfo#getSimSlotIndex} then by - * {@link SubscriptionInfo#getSubscriptionId}. + * <p> Returned records will be sorted by {@link SubscriptionInfo#getSimSlotIndex} then by + * {@link SubscriptionInfo#getSubscriptionId}. Beginning with Android SDK 35, this method will + * never return null. * * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} * or that the calling app has carrier privileges (see * {@link TelephonyManager#hasCarrierPrivileges}). * - * @return Sorted list of the currently {@link SubscriptionInfo} records available on the device. - * <ul> - * <li> - * If null is returned the current state is unknown but if a {@link OnSubscriptionsChangedListener} - * has been registered {@link OnSubscriptionsChangedListener#onSubscriptionsChanged} will be - * invoked in the future. - * </li> - * <li> - * If the list is empty then there are no {@link SubscriptionInfo} records currently available. - * </li> - * <li> - * if the list is non-empty the list is sorted by {@link SubscriptionInfo#getSimSlotIndex} - * then by {@link SubscriptionInfo#getSubscriptionId}. - * </li> - * </ul> + * @return a list of the active {@link SubscriptionInfo} that is visible to the caller. If + * an empty list or null is returned, then there are no active subscriptions that + * are visible to the caller. If the number of active subscriptions available to + * any caller changes, then this change will be indicated by + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged}. * * @throws UnsupportedOperationException If the device does not have - * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - public List<SubscriptionInfo> getActiveSubscriptionInfoList() { + public @Nullable List<SubscriptionInfo> getActiveSubscriptionInfoList() { List<SubscriptionInfo> activeList = null; try { @@ -1970,6 +1961,8 @@ public class SubscriptionManager { if (activeList != null) { activeList = activeList.stream().filter(subInfo -> isSubscriptionVisible(subInfo)) .collect(Collectors.toList()); + } else { + activeList = Collections.emptyList(); } return activeList; } @@ -1998,12 +1991,7 @@ public class SubscriptionManager { * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ public @NonNull List<SubscriptionInfo> getCompleteActiveSubscriptionInfoList() { - List<SubscriptionInfo> completeList = getActiveSubscriptionInfoList( - /* userVisibleonly */false); - if (completeList == null) { - completeList = new ArrayList<>(); - } - return completeList; + return getActiveSubscriptionInfoList(/* userVisibleonly */ false); } /** @@ -2032,7 +2020,7 @@ public class SubscriptionManager { * * @hide */ - public @Nullable List<SubscriptionInfo> getActiveSubscriptionInfoList(boolean userVisibleOnly) { + public @NonNull List<SubscriptionInfo> getActiveSubscriptionInfoList(boolean userVisibleOnly) { List<SubscriptionInfo> activeList = null; try { @@ -2045,11 +2033,13 @@ public class SubscriptionManager { // ignore it } - if (!userVisibleOnly || activeList == null) { - return activeList; - } else { + if (activeList == null || activeList.isEmpty()) { + return Collections.emptyList(); + } else if (userVisibleOnly) { return activeList.stream().filter(subInfo -> isSubscriptionVisible(subInfo)) .collect(Collectors.toList()); + } else { + return activeList; } } @@ -2086,7 +2076,7 @@ public class SubscriptionManager { * @hide */ @SystemApi - public List<SubscriptionInfo> getAvailableSubscriptionInfoList() { + public @Nullable List<SubscriptionInfo> getAvailableSubscriptionInfoList() { List<SubscriptionInfo> result = null; try { @@ -2098,7 +2088,7 @@ public class SubscriptionManager { } catch (RemoteException ex) { // ignore it } - return result; + return (result == null) ? Collections.emptyList() : result; } /** @@ -2128,7 +2118,7 @@ public class SubscriptionManager { * @throws UnsupportedOperationException If the device does not have * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ - public List<SubscriptionInfo> getAccessibleSubscriptionInfoList() { + public @Nullable List<SubscriptionInfo> getAccessibleSubscriptionInfoList() { List<SubscriptionInfo> result = null; try { @@ -2139,7 +2129,7 @@ public class SubscriptionManager { } catch (RemoteException ex) { // ignore it } - return result; + return (result == null) ? Collections.emptyList() : result; } /** |