diff options
121 files changed, 2626 insertions, 885 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java index 03891bbec56a..e3ba50dc635b 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java @@ -26,6 +26,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.os.SystemClock; import android.os.UserHandle; @@ -319,8 +320,8 @@ public final class BackgroundJobsController extends StateController { final boolean isStopped = mPackageManagerInternal.isPackageStopped(packageName, uid); mPackageStoppedState.add(uid, packageName, isStopped); return isStopped; - } catch (IllegalArgumentException e) { - Slog.d(TAG, "Couldn't determine stopped state for unknown package: " + packageName); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Couldn't determine stopped state for unknown package: " + packageName); return false; } } diff --git a/core/api/current.txt b/core/api/current.txt index 83e3fabe475b..7f261d450b42 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -5285,9 +5285,10 @@ package android.app { field public static final int START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START = 21; // 0x15 field public static final int START_TIMESTAMP_RESERVED_RANGE_SYSTEM = 20; // 0x14 field public static final int START_TIMESTAMP_SURFACEFLINGER_COMPOSITION_COMPLETE = 7; // 0x7 - field public static final int START_TYPE_COLD = 0; // 0x0 - field public static final int START_TYPE_HOT = 2; // 0x2 - field public static final int START_TYPE_WARM = 1; // 0x1 + field public static final int START_TYPE_COLD = 1; // 0x1 + field public static final int START_TYPE_HOT = 3; // 0x3 + field public static final int START_TYPE_UNSET = 0; // 0x0 + field public static final int START_TYPE_WARM = 2; // 0x2 } public final class AsyncNotedAppOp implements android.os.Parcelable { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 5ecc0b6a6568..7237af3f1df5 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -16987,7 +16987,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getSatelliteAttachRestrictionReasonsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback) throws android.telephony.satellite.SatelliteManager.SatelliteException; method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateCallback); @@ -16999,7 +16999,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteProvisioned(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void requestIsSatelliteSupported(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); - method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestNtnSignalStrength(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telephony.satellite.NtnSignalStrength,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestNtnSignalStrength(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telephony.satellite.NtnSignalStrength,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestSatelliteAttachEnabledForCarrier(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestSatelliteCapabilities(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telephony.satellite.SatelliteCapabilities,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestSatelliteEnabled(boolean, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 8b39ed6fb411..6ddb36a72aa9 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -4077,7 +4077,7 @@ public final class ActivityThread extends ClientTransactionHandler final LoadedApk sdkApk = getPackageInfo( contextInfo.getSdkApplicationInfo(), r.packageInfo.getCompatibilityInfo(), - ActivityContextInfo.CONTEXT_FLAGS); + contextInfo.getContextFlags()); final ContextImpl activityContext = ContextImpl.createActivityContext( this, sdkApk, r.activityInfo, r.token, displayId, r.overrideConfig); diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index c8317c8faad5..656feb0401d6 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -104,14 +104,17 @@ public final class ApplicationStartInfo implements Parcelable { /** Process started due to Activity started for any reason not explicitly listed. */ public static final int START_REASON_START_ACTIVITY = 11; + /** Start type not yet set. */ + public static final int START_TYPE_UNSET = 0; + /** Process started from scratch. */ - public static final int START_TYPE_COLD = 0; + public static final int START_TYPE_COLD = 1; /** Process retained minimally SavedInstanceState. */ - public static final int START_TYPE_WARM = 1; + public static final int START_TYPE_WARM = 2; /** Process brought back to foreground. */ - public static final int START_TYPE_HOT = 2; + public static final int START_TYPE_HOT = 3; /** * Default. The system always creates a new instance of the activity in the target task and @@ -277,6 +280,7 @@ public final class ApplicationStartInfo implements Parcelable { @IntDef( prefix = {"START_TYPE_"}, value = { + START_TYPE_UNSET, START_TYPE_COLD, START_TYPE_WARM, START_TYPE_HOT, @@ -769,6 +773,7 @@ public final class ApplicationStartInfo implements Parcelable { private static String startTypeToString(@StartType int startType) { return switch (startType) { + case START_TYPE_UNSET -> "UNSET"; case START_TYPE_COLD -> "COLD"; case START_TYPE_WARM -> "WARM"; case START_TYPE_HOT -> "HOT"; diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index e9f419e9a8ce..6f7299aa8e31 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -64,6 +64,7 @@ interface ILauncherApps { PendingIntent getActivityLaunchIntent(String callingPackage, in ComponentName component, in UserHandle user); LauncherUserInfo getLauncherUserInfo(in UserHandle user); + List<String> getPreInstalledSystemPackages(in UserHandle user); void showAppDetailsAsUser(in IApplicationThread caller, String callingPackage, String callingFeatureId, in ComponentName component, in Rect sourceBounds, in Bundle opts, in UserHandle user); diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 0cd4358b2c91..ccc8f0956865 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -800,6 +800,29 @@ public class LauncherApps { } /** + * Returns the list of the system packages that are installed at user creation. + * + * <p>An empty list denotes that all system packages are installed for that user at creation. + * This behaviour is inherited from the underlining UserManager API. + * + * @param userHandle the user for which installed system packages are required. + * @return {@link List} of {@link String}, representing the package name of the installed + * package. Can be empty but not null. + * @hide + */ + @FlaggedApi(Flags.FLAG_ALLOW_PRIVATE_PROFILE) + public List<String> getPreInstalledSystemPackages(@NonNull UserHandle userHandle) { + if (DEBUG) { + Log.i(TAG, "getPreInstalledSystemPackages for user: " + userHandle); + } + try { + return mService.getPreInstalledSystemPackages(userHandle); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Returns the activity info for a given intent and user handle, if it resolves. Otherwise it * returns null. * diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 5dee65b62201..4f61613b9c6e 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -231,7 +231,7 @@ public class PackageInfo implements Parcelable { * or null if there were none. This is only filled in if the flag * {@link PackageManager#GET_PERMISSIONS} was set. Each value matches * the corresponding entry in {@link #requestedPermissions}, and will have - * the flags {@link #REQUESTED_PERMISSION_GRANTED} and + * the flags {@link #REQUESTED_PERMISSION_GRANTED}, {@link #REQUESTED_PERMISSION_IMPLICIT}, and * {@link #REQUESTED_PERMISSION_NEVER_FOR_LOCATION} set as appropriate. */ @Nullable diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 93fbe8aee8d4..7cf10d89004f 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -1216,7 +1216,7 @@ public abstract class CameraMetadata<TKey> { * <ul> * <li>Profile {@link android.hardware.camera2.params.DynamicRangeProfiles#HLG10 }</li> * <li>All mandatory stream combinations for this specific capability as per - * <a href="CameraDevice#10-bit-output-additional-guaranteed-configurations">documentation</a></li> + * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#10-bit-output-additional-guaranteed-configurations">documentation</a></li> * <li>In case the device is not able to capture some combination of supported * standard 8-bit and/or 10-bit dynamic range profiles within the same capture request, * then those constraints must be listed in diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 06397c9a1598..ded96a23e11e 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -1484,7 +1484,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <p>To start a CaptureSession with a target FPS range different from the * capture request template's default value, the application * is strongly recommended to call - * {@link SessionConfiguration#setSessionParameters } + * {@link android.hardware.camera2.params.SessionConfiguration#setSessionParameters } * with the target fps range before creating the capture session. The aeTargetFpsRange is * typically a session parameter. Specifying it at session creation time helps avoid * session reconfiguration delays in cases like 60fps or high speed recording.</p> @@ -2161,7 +2161,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * OFF if the recording output is not stabilized, or if there are no output * Surface types that can be stabilized.</p> * <p>The application is strongly recommended to call - * {@link SessionConfiguration#setSessionParameters } + * {@link android.hardware.camera2.params.SessionConfiguration#setSessionParameters } * with the desired video stabilization mode before creating the capture session. * Video stabilization mode is a session parameter on many devices. Specifying * it at session creation time helps avoid reconfiguration delay caused by difference diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index ab4406c37c8e..1d26d69a58b9 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -899,7 +899,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p>To start a CaptureSession with a target FPS range different from the * capture request template's default value, the application * is strongly recommended to call - * {@link SessionConfiguration#setSessionParameters } + * {@link android.hardware.camera2.params.SessionConfiguration#setSessionParameters } * with the target fps range before creating the capture session. The aeTargetFpsRange is * typically a session parameter. Specifying it at session creation time helps avoid * session reconfiguration delays in cases like 60fps or high speed recording.</p> @@ -2382,7 +2382,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * OFF if the recording output is not stabilized, or if there are no output * Surface types that can be stabilized.</p> * <p>The application is strongly recommended to call - * {@link SessionConfiguration#setSessionParameters } + * {@link android.hardware.camera2.params.SessionConfiguration#setSessionParameters } * with the desired video stabilization mode before creating the capture session. * Video stabilization mode is a session parameter on many devices. Specifying * it at session creation time helps avoid reconfiguration delay caused by difference diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 145981c92283..83d237d6e53b 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -78,6 +78,13 @@ flag { } flag { + name: "adpf_use_fmq_channel" + namespace: "game" + description: "Guards use of the FMQ channel for ADPF" + bug: "315894228" +} + +flag { name: "battery_service_support_current_adb_command" namespace: "backstage_power" description: "Whether or not BatteryService supports adb commands for Current values." diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index a5b087c05dfa..fcdc5fe71e4e 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -1980,6 +1980,7 @@ public class ZenModeConfig implements Parcelable { @Nullable public ZenDeviceEffects zenDeviceEffects; public boolean modified; // rule has been modified from initial creation public String pkg; + @AutomaticZenRule.Type public int type = AutomaticZenRule.TYPE_UNKNOWN; public String triggerDescription; public String iconResName; diff --git a/core/java/android/view/ISurfaceControlViewHostParent.aidl b/core/java/android/view/ISurfaceControlViewHostParent.aidl index f42e00148587..559c20ee4825 100644 --- a/core/java/android/view/ISurfaceControlViewHostParent.aidl +++ b/core/java/android/view/ISurfaceControlViewHostParent.aidl @@ -16,6 +16,7 @@ package android.view; +import android.view.KeyEvent; import android.view.WindowManager; /** @@ -24,4 +25,6 @@ import android.view.WindowManager; */ oneway interface ISurfaceControlViewHostParent { void updateParams(in WindowManager.LayoutParams[] childAttrs); + // To forward the back key event from embedded to host app. + void forwardBackKeyToParent(in KeyEvent keyEvent); } diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index 405653123f79..4840f003da3e 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -447,6 +447,7 @@ public class SurfaceControlViewHost { addWindowToken(attrs); view.setLayoutParams(attrs); mViewRoot.setView(view, attrs, null); + mViewRoot.setBackKeyCallbackForWindowlessWindow(mWm::forwardBackKeyToParent); } /** diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index a44a95a1677f..108de281a411 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -37,6 +37,7 @@ import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.Region; import android.graphics.RenderNode; +import android.hardware.input.InputManager; import android.os.Build; import android.os.Handler; import android.os.IBinder; @@ -159,6 +160,8 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall private static final boolean DEBUG = false; private static final boolean DEBUG_POSITION = false; + private static final long FORWARD_BACK_KEY_TOLERANCE_MS = 100; + @UnsupportedAppUsage( maxTargetSdk = Build.VERSION_CODES.TIRAMISU, publicAlternatives = "Track {@link SurfaceHolder#addCallback} instead") @@ -326,6 +329,41 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall }); } } + + @Override + public void forwardBackKeyToParent(@NonNull KeyEvent keyEvent) { + runOnUiThread(() -> { + if (!isAttachedToWindow() || keyEvent.getKeyCode() != KeyEvent.KEYCODE_BACK) { + return; + } + final ViewRootImpl vri = getViewRootImpl(); + if (vri == null) { + return; + } + final InputManager inputManager = mContext.getSystemService(InputManager.class); + if (inputManager == null) { + return; + } + // Check that the event was created recently. + final long timeDiff = SystemClock.uptimeMillis() - keyEvent.getEventTime(); + if (timeDiff > FORWARD_BACK_KEY_TOLERANCE_MS) { + Log.e(TAG, "Ignore the input event that exceed the tolerance time, " + + "exceed " + timeDiff + "ms"); + return; + } + if (inputManager.verifyInputEvent(keyEvent) == null) { + Log.e(TAG, "Received invalid input event"); + return; + } + try { + vri.processingBackKey(true); + vri.enqueueInputEvent(keyEvent, null /* receiver */, 0 /* flags */, + true /* processImmediately */); + } finally { + vri.processingBackKey(false); + } + }); + } }; private final boolean mRtDrivenClipping = Flags.clipSurfaceviews(); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 40382fdcf4b8..8339d3372565 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -91,6 +91,7 @@ import static android.view.accessibility.Flags.reduceWindowContentChangedEventTh import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER; import static android.view.flags.Flags.toolkitSetFrameRateReadOnly; +import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision; import static com.android.input.flags.Flags.enablePointerChoreographer; @@ -250,7 +251,7 @@ import java.util.OptionalInt; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; -import java.util.function.Consumer; +import java.util.function.Predicate; /** * The top of a view hierarchy, implementing the needed protocol between View @@ -370,6 +371,8 @@ public final class ViewRootImpl implements ViewParent, */ private static final int KEEP_CLEAR_AREA_REPORT_RATE_MILLIS = 100; + private static final long NANOS_PER_SEC = 1000000000; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>(); @@ -615,6 +618,13 @@ public final class ViewRootImpl implements ViewParent, boolean mUpcomingWindowFocus; @GuardedBy("this") boolean mUpcomingInTouchMode; + // While set, allow this VRI to handle back key without drop it. + private boolean mProcessingBackKey; + /** + * Compatibility {@link OnBackInvokedCallback} for windowless window, to forward the back + * key event host app. + */ + private Predicate<KeyEvent> mWindowlessBackKeyCallback; public boolean mTraversalScheduled; int mTraversalBarrier; @@ -817,6 +827,8 @@ public final class ViewRootImpl implements ViewParent, private boolean mInsetsAnimationRunning; + private long mPreviousFrameDrawnTime = -1; + /** * The resolved pointer icon type requested by this window. * A null value indicates the resolved pointer icon has not yet been calculated. @@ -1054,11 +1066,14 @@ public final class ViewRootImpl implements ViewParent, private boolean mChildBoundingInsetsChanged = false; private String mTag = TAG; + private String mFpsTraceName; private static boolean sToolkitSetFrameRateReadOnlyFlagValue; + private static boolean sToolkitMetricsForFrameRateDecisionFlagValue; static { sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly(); + sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision(); } // The latest input event from the gesture that was used to resolve the pointer icon. @@ -1302,6 +1317,7 @@ public final class ViewRootImpl implements ViewParent, attrs = mWindowAttributes; setTag(); + mFpsTraceName = "FPS of " + getTitle(); if (DEBUG_KEEP_SCREEN_ON && (mClientWindowLayoutFlags & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0 @@ -3189,7 +3205,11 @@ public final class ViewRootImpl implements ViewParent, host.dispatchAttachedToWindow(mAttachInfo, 0); mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true); dispatchApplyInsets(host); - if (!mOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled()) { + if (!mOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled() + // Don't register compat OnBackInvokedCallback for windowless window. + // The onBackInvoked event by default should forward to host app, so the + // host app can decide the behavior. + && mWindowlessBackKeyCallback == null) { // For apps requesting legacy back behavior, we add a compat callback that // dispatches {@link KeyEvent#KEYCODE_BACK} to their root views. // This way from system point of view, these apps are providing custom @@ -4721,6 +4741,31 @@ public final class ViewRootImpl implements ViewParent, } } + /** + * Called from draw() to collect metrics for frame rate decision. + */ + private void collectFrameRateDecisionMetrics() { + if (!Trace.isEnabled()) { + if (mPreviousFrameDrawnTime > 0) mPreviousFrameDrawnTime = -1; + return; + } + + if (mPreviousFrameDrawnTime < 0) { + mPreviousFrameDrawnTime = mChoreographer.getExpectedPresentationTimeNanos(); + return; + } + + long expectedDrawnTime = mChoreographer.getExpectedPresentationTimeNanos(); + long timeDiff = expectedDrawnTime - mPreviousFrameDrawnTime; + if (timeDiff <= 0) { + return; + } + + long fps = NANOS_PER_SEC / timeDiff; + Trace.setCounter(mFpsTraceName, fps); + mPreviousFrameDrawnTime = expectedDrawnTime; + } + private void reportDrawFinished(@Nullable Transaction t, int seqId) { if (DEBUG_BLAST) { Log.d(mTag, "reportDrawFinished"); @@ -5039,6 +5084,9 @@ public final class ViewRootImpl implements ViewParent, if (DEBUG_FPS) { trackFPS(); } + if (sToolkitMetricsForFrameRateDecisionFlagValue) { + collectFrameRateDecisionMetrics(); + } if (!sFirstDrawComplete) { synchronized (sFirstDrawHandlers) { @@ -6655,7 +6703,8 @@ public final class ViewRootImpl implements ViewParent, // Find a reason for dropping or canceling the event. final String reason; - if (!mAttachInfo.mHasWindowFocus + // The embedded window is focused, allow this VRI to handle back key. + if (!mAttachInfo.mHasWindowFocus && !(mProcessingBackKey && isBack(q.mEvent)) && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER) && !isAutofillUiShowing()) { // This is a non-pointer event and the window doesn't currently have input focus @@ -6878,10 +6927,20 @@ public final class ViewRootImpl implements ViewParent, // If the new back dispatch is enabled, intercept KEYCODE_BACK before it reaches the // view tree or IME, and invoke the appropriate {@link OnBackInvokedCallback}. - if (isBack(keyEvent) - && mContext != null - && mOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled()) { - return doOnBackKeyEvent(keyEvent); + if (isBack(keyEvent)) { + if (mWindowlessBackKeyCallback != null) { + if (mWindowlessBackKeyCallback.test(keyEvent)) { + return keyEvent.getAction() == KeyEvent.ACTION_UP + && !keyEvent.isCanceled() + ? FINISH_HANDLED : FINISH_NOT_HANDLED; + } else { + // Unable to forward the back key to host, forward to next stage. + return FORWARD; + } + } else if (mContext != null + && mOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled()) { + return doOnBackKeyEvent(keyEvent); + } } if (mInputQueue != null) { @@ -10524,6 +10583,11 @@ public final class ViewRootImpl implements ViewParent, mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, listener).sendToTarget(); } + // Make this VRI able to process back key without drop it. + void processingBackKey(boolean processing) { + mProcessingBackKey = processing; + } + /** * Collect and include any ScrollCaptureCallback instances registered with the window. * @@ -11748,13 +11812,18 @@ public final class ViewRootImpl implements ViewParent, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, InputDevice.SOURCE_KEYBOARD); - enqueueInputEvent(ev); + enqueueInputEvent(ev, null /* receiver */, 0 /* flags */, true /* processImmediately */); } private void registerCompatOnBackInvokedCallback() { mCompatOnBackInvokedCallback = () -> { - sendBackKeyEvent(KeyEvent.ACTION_DOWN); - sendBackKeyEvent(KeyEvent.ACTION_UP); + try { + processingBackKey(true); + sendBackKeyEvent(KeyEvent.ACTION_DOWN); + sendBackKeyEvent(KeyEvent.ACTION_UP); + } finally { + processingBackKey(false); + } }; if (mOnBackInvokedDispatcher.hasImeOnBackInvokedDispatcher()) { Log.d(TAG, "Skip registering CompatOnBackInvokedCallback on IME dispatcher"); @@ -12189,4 +12258,13 @@ public final class ViewRootImpl implements ViewParent, } return false; } + + /** + * Set the default back key callback for windowless window, to forward the back key event + * to host app. + * MUST NOT call this method for normal window. + */ + void setBackKeyCallbackForWindowlessWindow(@NonNull Predicate<KeyEvent> callback) { + mWindowlessBackKeyCallback = callback; + } } diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index d817e6f51f55..393d2565fc85 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.WindowConfiguration; import android.content.res.Configuration; @@ -703,4 +704,17 @@ public class WindowlessWindowManager implements IWindowSession { } } } + + boolean forwardBackKeyToParent(@NonNull KeyEvent keyEvent) { + if (mParentInterface == null) { + return false; + } + try { + mParentInterface.forwardBackKeyToParent(keyEvent); + } catch (RemoteException e) { + Log.e(TAG, "Failed to forward back key To Parent: ", e); + return false; + } + return true; + } } diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index a467afe5d06a..0aa516e08697 100644 --- a/core/java/android/view/flags/refresh_rate_flags.aconfig +++ b/core/java/android/view/flags/refresh_rate_flags.aconfig @@ -42,4 +42,12 @@ flag { namespace: "core_graphics" description: "Enable the `setFrameRate` callback" bug: "299946220" +} + +flag { + name: "toolkit_metrics_for_frame_rate_decision" + namespace: "toolkit" + description: "Feature flag for toolkit metrics collecting for frame rate decision" + bug: "301343249" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 28fd2b488426..bf8e6135fd01 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -897,13 +897,26 @@ public class LockPatternUtils { } /** - * Returns true if {@code userHandle} is a managed profile with separate challenge. + * Returns true if {@code userHandle} is a profile with separate challenge. + * <p> + * Returns false if {@code userHandle} is a profile with unified challenge, a profile whose + * credential is not shareable with its parent, or a non-profile user. */ public boolean isSeparateProfileChallengeEnabled(int userHandle) { return isCredentialSharableWithParent(userHandle) && hasSeparateChallenge(userHandle); } /** + * Returns true if {@code userHandle} is a profile with unified challenge. + * <p> + * Returns false if {@code userHandle} is a profile with separate challenge, a profile whose + * credential is not shareable with its parent, or a non-profile user. + */ + public boolean isProfileWithUnifiedChallenge(int userHandle) { + return isCredentialSharableWithParent(userHandle) && !hasSeparateChallenge(userHandle); + } + + /** * Returns true if {@code userHandle} is a managed profile with unified challenge. */ public boolean isManagedProfileWithUnifiedChallenge(int userHandle) { diff --git a/core/java/com/android/internal/widget/LockscreenCredential.java b/core/java/com/android/internal/widget/LockscreenCredential.java index c88763ce6c97..18d5f6db6ac9 100644 --- a/core/java/com/android/internal/widget/LockscreenCredential.java +++ b/core/java/com/android/internal/widget/LockscreenCredential.java @@ -134,12 +134,12 @@ public class LockscreenCredential implements Parcelable, AutoCloseable { } /** - * Creates a LockscreenCredential object representing a managed password for profile with - * unified challenge. This credentiall will have type {@code CREDENTIAL_TYPE_PASSWORD} for now. - * TODO: consider add a new credential type for this. This can then supersede the - * isLockTiedToParent argument in various places in LSS. + * Creates a LockscreenCredential object representing the system-generated, system-managed + * password for a profile with unified challenge. This credential has type {@code + * CREDENTIAL_TYPE_PASSWORD} for now. TODO: consider add a new credential type for this. This + * can then supersede the isLockTiedToParent argument in various places in LSS. */ - public static LockscreenCredential createManagedPassword(@NonNull byte[] password) { + public static LockscreenCredential createUnifiedProfilePassword(@NonNull byte[] password) { return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD, Arrays.copyOf(password, password.length), /* hasInvalidChars= */ false); } diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index 87ab4969040e..54c4cd50a902 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -63,6 +63,7 @@ class NativeCommandBuffer { std::optional<std::pair<char*, char*>> readLine(FailFn fail_fn) { char* result = mBuffer + mNext; while (true) { + // We have scanned up to, but not including mNext for this line's newline. if (mNext == mEnd) { if (mEnd == MAX_COMMAND_BYTES) { return {}; @@ -89,7 +90,7 @@ class NativeCommandBuffer { } else { mNext = nl - mBuffer + 1; if (--mLinesLeft < 0) { - fail_fn("ZygoteCommandBuffer.readLine attempted to read past mEnd of command"); + fail_fn("ZygoteCommandBuffer.readLine attempted to read past end of command"); } return std::make_pair(result, nl); } @@ -125,8 +126,8 @@ class NativeCommandBuffer { mEnd += lineLen + 1; } - // Clear mBuffer, start reading new command, return the number of arguments, leaving mBuffer - // positioned at the beginning of first argument. Return 0 on EOF. + // Start reading new command, return the number of arguments, leaving mBuffer positioned at the + // beginning of first argument. Return 0 on EOF. template<class FailFn> int getCount(FailFn fail_fn) { mLinesLeft = 1; @@ -451,11 +452,14 @@ jboolean com_android_internal_os_ZygoteCommandBuffer_nativeForkRepeatedly( (CREATE_ERROR("Write unexpectedly returned short: %d < 5", res)); } } - // Clear buffer and get count from next command. - n_buffer->clear(); for (;;) { + // Clear buffer and get count from next command. + n_buffer->clear(); // Poll isn't strictly necessary for now. But without it, disconnect is hard to detect. int poll_res = TEMP_FAILURE_RETRY(poll(fd_structs, 2, -1 /* infinite timeout */)); + if (poll_res < 0) { + fail_fn_z(CREATE_ERROR("Poll failed: %d: %s", errno, strerror(errno))); + } if ((fd_structs[SESSION_IDX].revents & POLLIN) != 0) { if (n_buffer->getCount(fail_fn_z) != 0) { break; diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 1f6ac80a133c..4596ca74bf8f 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5252,6 +5252,8 @@ <!-- Zen mode - name of default automatic calendar time-based rule that is triggered every night (when sleeping). [CHAR LIMIT=40] --> <string name="zen_mode_default_every_night_name">Sleeping</string> + <!-- Zen mode - Trigger description of the rule, indicating which app owns it. [CHAR_LIMIT=100] --> + <string name="zen_mode_implicit_trigger_description">Managed by <xliff:g id="app_name">%1$s</xliff:g></string> <!-- Zen mode - Condition summary when a rule is activated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] --> <string name="zen_mode_implicit_activated">On</string> <!-- Zen mode - Condition summary when a rule is deactivated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 5791ddb8addf..fd6158d02b8f 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2586,6 +2586,7 @@ <java-symbol type="string" name="zen_mode_default_weekends_name" /> <java-symbol type="string" name="zen_mode_default_events_name" /> <java-symbol type="string" name="zen_mode_default_every_night_name" /> + <java-symbol type="string" name="zen_mode_implicit_trigger_description" /> <java-symbol type="string" name="zen_mode_implicit_activated" /> <java-symbol type="string" name="zen_mode_implicit_deactivated" /> <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" /> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java index 15d14e87fcf6..b315f94b5d00 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -241,7 +241,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, for (int i = 0; i < displays.length; i++) { DisplayAddress.Physical address = (DisplayAddress.Physical) displays[i].getAddress(); - if (mRearDisplayAddress == address.getPhysicalDisplayId()) { + if (address != null && mRearDisplayAddress == address.getPhysicalDisplayId()) { rearDisplayMetrics = new DisplayMetrics(); final Display rearDisplay = displays[i]; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 0f0fbd9cc12f..f801b0d01084 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -83,6 +83,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private int mStartPos; private GestureDetector mDoubleTapDetector; private boolean mInteractive; + private boolean mHideHandle; private boolean mSetTouchRegion = true; private int mLastDraggingPosition; private int mHandleRegionWidth; @@ -211,11 +212,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } /** Sets up essential dependencies of the divider bar. */ - public void setup( - SplitLayout layout, - SplitWindowManager splitWindowManager, - SurfaceControlViewHost viewHost, - InsetsState insetsState) { + public void setup(SplitLayout layout, SplitWindowManager splitWindowManager, + SurfaceControlViewHost viewHost, InsetsState insetsState) { mSplitLayout = layout; mSplitWindowManager = splitWindowManager; mViewHost = viewHost; @@ -277,6 +275,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { R.dimen.docked_stack_divider_lift_elevation); mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); mInteractive = true; + mHideHandle = false; setOnTouchListener(this); mHandle.setAccessibilityDelegate(mHandleDelegate); setWillNotDraw(false); @@ -469,10 +468,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { void setInteractive(boolean interactive, boolean hideHandle, String from) { if (interactive == mInteractive) return; ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "Set divider bar %s from %s", interactive ? "interactive" : "non-interactive", - from); + "Set divider bar %s hide handle=%b from %s", + interactive ? "interactive" : "non-interactive", hideHandle, from); mInteractive = interactive; - if (!mInteractive && hideHandle && mMoving) { + mHideHandle = hideHandle; + if (!mInteractive && mHideHandle && mMoving) { final int position = mSplitLayout.getDividePosition(); mSplitLayout.flingDividePosition( mLastDraggingPosition, @@ -482,7 +482,15 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mMoving = false; } releaseTouching(); - mHandle.setVisibility(!mInteractive && hideHandle ? View.INVISIBLE : View.VISIBLE); + mHandle.setVisibility(!mInteractive && mHideHandle ? View.INVISIBLE : View.VISIBLE); + } + + boolean isInteractive() { + return mInteractive; + } + + boolean isHandleHidden() { + return mHideHandle; } private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index b699533374df..53caddb52f23 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -59,6 +59,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; @@ -70,6 +71,7 @@ import com.android.wm.shell.common.InteractionJankMonitorUtils; import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.io.PrintWriter; import java.util.function.Consumer; @@ -420,7 +422,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void init() { if (mInitialized) return; mInitialized = true; - mSplitWindowManager.init(this, mInsetsState); + mSplitWindowManager.init(this, mInsetsState, false /* isRestoring */); mDisplayImeController.addPositionProcessor(mImePositionProcessor); } @@ -442,14 +444,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** Releases and re-inflates {@link DividerView} on the root surface. */ - public void update(SurfaceControl.Transaction t) { + public void update(SurfaceControl.Transaction t, boolean resetImePosition) { if (!mInitialized) { init(); return; } mSplitWindowManager.release(t); - mImePositionProcessor.reset(); - mSplitWindowManager.init(this, mInsetsState); + if (resetImePosition) { + mImePositionProcessor.reset(); + } + mSplitWindowManager.init(this, mInsetsState, true /* isRestoring */); + // Update the surface positions again after recreating the divider in case nothing else + // triggers it + mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } @Override @@ -868,6 +875,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange pw.println(prefix + TAG + ":"); pw.println(innerPrefix + "mAllowLeftRightSplitInPortrait=" + mAllowLeftRightSplitInPortrait); pw.println(innerPrefix + "mIsLeftRightSplit=" + mIsLeftRightSplit); + pw.println(innerPrefix + "mFreezeDividerWindow=" + mFreezeDividerWindow); + pw.println(innerPrefix + "mDimNonImeSide=" + mDimNonImeSide); + pw.println(innerPrefix + "mDividerPosition=" + mDividerPosition); pw.println(innerPrefix + "bounds1=" + mBounds1.toShortString()); pw.println(innerPrefix + "dividerBounds=" + mDividerBounds.toShortString()); pw.println(innerPrefix + "bounds2=" + mBounds2.toShortString()); @@ -1151,14 +1161,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mTargetYOffset = needOffset ? getTargetYOffset() : 0; if (mTargetYOffset != mLastYOffset) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Split IME animation starting, fromY=%d toY=%d", + mLastYOffset, mTargetYOffset); // Freeze the configuration size with offset to prevent app get a configuration // changed or relaunch. This is required to make sure client apps will calculate // insets properly after layout shifted. if (mTargetYOffset == 0) { mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); } else { - mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset - mLastYOffset, - SplitLayout.this); + mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this); } } @@ -1183,6 +1195,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void onImeEndPositioning(int displayId, boolean cancel, SurfaceControl.Transaction t) { if (displayId != mDisplayId || !mHasImeFocus || cancel) return; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Split IME animation ending, canceled=%b", cancel); onProgress(1.0f); mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 00361d9dd9cf..8fb9bda539a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -62,6 +62,10 @@ public final class SplitWindowManager extends WindowlessWindowManager { // Used to "pass" a transaction to WWM.remove so that view removal can be synchronized. private SurfaceControl.Transaction mSyncTransaction = null; + // For saving/restoring state + private boolean mLastDividerInteractive = true; + private boolean mLastDividerHandleHidden; + public interface ParentContainerCallbacks { void attachToParentSurface(SurfaceControl.Builder b); void onLeashReady(SurfaceControl leash); @@ -107,7 +111,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { } /** Inflates {@link DividerView} on to the root surface. */ - void init(SplitLayout splitLayout, InsetsState insetsState) { + void init(SplitLayout splitLayout, InsetsState insetsState, boolean isRestoring) { if (mDividerView != null || mViewHost != null) { throw new UnsupportedOperationException( "Try to inflate divider view again without release first"); @@ -130,6 +134,10 @@ public final class SplitWindowManager extends WindowlessWindowManager { lp.accessibilityTitle = mContext.getResources().getString(R.string.accessibility_divider); mViewHost.setView(mDividerView, lp); mDividerView.setup(splitLayout, this, mViewHost, insetsState); + if (isRestoring) { + mDividerView.setInteractive(mLastDividerInteractive, mLastDividerHandleHidden, + "restore_setup"); + } } /** @@ -138,6 +146,8 @@ public final class SplitWindowManager extends WindowlessWindowManager { */ void release(@Nullable SurfaceControl.Transaction t) { if (mDividerView != null) { + mLastDividerInteractive = mDividerView.isInteractive(); + mLastDividerHandleHidden = mDividerView.isHandleHidden(); mDividerView = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 449bef5608c9..77427d999aaf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1666,7 +1666,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } void finishEnterSplitScreen(SurfaceControl.Transaction finishT) { - mSplitLayout.update(finishT); + mSplitLayout.update(finishT, true /* resetImePosition */); mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash, getMainStageBounds()); mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash, @@ -1860,9 +1860,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration) && mMainStage.isActive()) { // Clear the divider remote animating flag as the divider will be re-rendered to apply - // the new rotation config. + // the new rotation config. Don't reset the IME state since those updates are not in + // sync with task info changes. mIsDividerRemoteAnimating = false; - mSplitLayout.update(null /* t */); + mSplitLayout.update(null /* t */, false /* resetImePosition */); onLayoutSizeChanged(mSplitLayout); } } @@ -2325,7 +2326,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, */ public void updateSurfaces(SurfaceControl.Transaction transaction) { updateSurfaceBounds(mSplitLayout, transaction, /* applyResizingOffset */ false); - mSplitLayout.update(transaction); + mSplitLayout.update(transaction, true /* resetImePosition */); } private void onDisplayChange(int displayId, int fromRotation, int toRotation, @@ -2598,7 +2599,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final TransitionInfo.Change change = info.getChanges().get(iC); if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { - mSplitLayout.update(startTransaction); + // Don't reset the IME state since those updates are not in sync with the + // display change transition + mSplitLayout.update(startTransaction, false /* resetImePosition */); } if (mMixedHandler.isEnteringPip(change, transitType)) { @@ -2699,7 +2702,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, startTransaction, finishTransaction, finishCallback)) { if (mSplitTransitions.isPendingResize(transition)) { // Only need to update in resize because divider exist before transition. - mSplitLayout.update(startTransaction); + mSplitLayout.update(startTransaction, true /* resetImePosition */); startTransaction.apply(); } return true; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java index 145c8f0ab8af..636c6326d213 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java @@ -69,7 +69,7 @@ public class DividerViewTest extends ShellTestCase { SplitWindowManager splitWindowManager = new SplitWindowManager("TestSplitWindowManager", mContext, configuration, mCallbacks); - splitWindowManager.init(mSplitLayout, new InsetsState()); + splitWindowManager.init(mSplitLayout, new InsetsState(), false /* isRestoring */); mDividerView = spy((DividerView) splitWindowManager.getDividerView()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java index 2e5078d86a8b..150aa13f2d00 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java @@ -59,7 +59,7 @@ public class SplitWindowManagerTests extends ShellTestCase { @Test @UiThreadTest public void testInitRelease() { - mSplitWindowManager.init(mSplitLayout, new InsetsState()); + mSplitWindowManager.init(mSplitLayout, new InsetsState(), false /* isRestoring */); assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull(); mSplitWindowManager.release(null /* t */); assertThat(mSplitWindowManager.getSurfaceControl()).isNull(); diff --git a/media/java/android/media/AudioHalVersionInfo.java b/media/java/android/media/AudioHalVersionInfo.java index c881a03c6732..0f48abeb6882 100644 --- a/media/java/android/media/AudioHalVersionInfo.java +++ b/media/java/android/media/AudioHalVersionInfo.java @@ -80,8 +80,9 @@ public final class AudioHalVersionInfo implements Parcelable, Comparable<AudioHa * List of all valid Audio HAL versions. This list need to be in sync with sAudioHALVersions * defined in frameworks/av/media/libaudiohal/FactoryHalHidl.cpp. */ + // TODO: add AIDL_1_0 with sAudioHALVersions. public static final @NonNull List<AudioHalVersionInfo> VERSIONS = - List.of(AIDL_1_0, HIDL_7_1, HIDL_7_0, HIDL_6_0, HIDL_5_0, HIDL_4_0); + List.of(HIDL_7_1, HIDL_7_0, HIDL_6_0, HIDL_5_0, HIDL_4_0); private static final String TAG = "AudioHalVersionInfo"; private AudioHalVersion mHalVersion = new AudioHalVersion(); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java index e07e9425808e..2da8c8c69ff8 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java @@ -156,10 +156,9 @@ public class UninstallAlertDialogFragment extends DialogFragment implements if (customUserManager.isUserOfType(USER_TYPE_PROFILE_MANAGED) && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { messageBuilder.append(isArchive - ? getString(R.string.archive_application_text_current_user_work_profile, - userName) : getString( - R.string.uninstall_application_text_current_user_work_profile, - userName)); + ? getString(R.string.archive_application_text_current_user_work_profile) + : getString( + R.string.uninstall_application_text_current_user_work_profile)); } else if (customUserManager.isUserOfType(USER_TYPE_PROFILE_CLONE) && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { mIsClonedApp = true; @@ -168,11 +167,11 @@ public class UninstallAlertDialogFragment extends DialogFragment implements } else if (Flags.allowPrivateProfile() && customUserManager.isPrivateProfile() && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { - messageBuilder.append(isArchive ? getString( - R.string.archive_application_text_current_user_private_profile, - userName) : getString( - R.string.uninstall_application_text_current_user_private_profile, - userName)); + messageBuilder.append( + isArchive ? getString( + R.string.archive_application_text_current_user_private_profile) + : getString( + R.string.uninstall_application_text_current_user_private_profile)); } else if (isArchive) { messageBuilder.append( getString(R.string.archive_application_text_user, userName)); diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt index d0d2dc0083a6..e099f1124bf1 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlow.kt @@ -20,13 +20,17 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOn +private const val TAG = "BroadcastReceiverFlow" + /** * A [BroadcastReceiver] flow for the given [intentFilter]. */ @@ -39,4 +43,6 @@ fun Context.broadcastReceiverFlow(intentFilter: IntentFilter): Flow<Intent> = ca registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED) awaitClose { unregisterReceiver(broadcastReceiver) } +}.catch { e -> + Log.e(TAG, "Error while broadcastReceiverFlow", e) }.conflate().flowOn(Dispatchers.Default) diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt index dfaf3c66ff8d..eef5225aef42 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt @@ -31,8 +31,10 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.stub @RunWith(AndroidJUnit4::class) class BroadcastReceiverFlowTest { @@ -74,6 +76,18 @@ class BroadcastReceiverFlowTest { assertThat(onReceiveIsCalled).isTrue() } + @Test + fun broadcastReceiverFlow_unregisterReceiverThrowException_noCrash() = runBlocking { + context.stub { + on { unregisterReceiver(any()) } doThrow IllegalArgumentException() + } + val flow = context.broadcastReceiverFlow(INTENT_FILTER) + + flow.firstWithTimeoutOrNull() + + assertThat(registeredBroadcastReceiver).isNotNull() + } + private companion object { val INTENT_FILTER = IntentFilter() } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 69b61c74625e..2cb44ec39a23 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -31,7 +31,6 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastSubgroup; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile.ServiceListener; -import android.bluetooth.BluetoothStatusCodes; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -42,7 +41,6 @@ import android.os.Looper; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import androidx.annotation.RequiresApi; @@ -53,15 +51,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; /** - * LocalBluetoothLeBroadcast provides an interface between the Settings app - * and the functionality of the local {@link BluetoothLeBroadcast}. - * Use the {@link BluetoothLeBroadcast.Callback} to get the result callback. + * LocalBluetoothLeBroadcast provides an interface between the Settings app and the functionality of + * the local {@link BluetoothLeBroadcast}. Use the {@link BluetoothLeBroadcast.Callback} to get the + * result callback. */ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private static final String TAG = "LocalBluetoothLeBroadcast"; @@ -74,11 +74,12 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { // Order of this profile in device profiles list private static final int ORDINAL = 1; private static final int UNKNOWN_VALUE_PLACEHOLDER = -1; - private static final Uri[] SETTINGS_URIS = new Uri[]{ - Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO), - Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE), - Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME), - }; + private static final Uri[] SETTINGS_URIS = + new Uri[] { + Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO), + Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE), + Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME), + }; private BluetoothLeBroadcast mServiceBroadcast; private BluetoothLeBroadcastAssistant mServiceBroadcastAssistant; @@ -95,62 +96,82 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private Executor mExecutor; private ContentResolver mContentResolver; private ContentObserver mSettingsObserver; + // Cached broadcast callbacks being register before service is connected. + private Map<BluetoothLeBroadcast.Callback, Executor> mCachedBroadcastCallbackExecutorMap = + new ConcurrentHashMap<>(); - private final ServiceListener mServiceListener = new ServiceListener() { - @Override - public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (DEBUG) { - Log.d(TAG, "Bluetooth service connected: " + profile); - } - if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST) && !mIsBroadcastProfileReady) { - mServiceBroadcast = (BluetoothLeBroadcast) proxy; - mIsBroadcastProfileReady = true; - registerServiceCallBack(mExecutor, mBroadcastCallback); - List<BluetoothLeBroadcastMetadata> metadata = getAllBroadcastMetadata(); - if (!metadata.isEmpty()) { - updateBroadcastInfoFromBroadcastMetadata(metadata.get(0)); + private final ServiceListener mServiceListener = + new ServiceListener() { + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (DEBUG) { + Log.d(TAG, "Bluetooth service connected: " + profile); + } + if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST) + && !mIsBroadcastProfileReady) { + mServiceBroadcast = (BluetoothLeBroadcast) proxy; + mIsBroadcastProfileReady = true; + registerServiceCallBack(mExecutor, mBroadcastCallback); + List<BluetoothLeBroadcastMetadata> metadata = getAllBroadcastMetadata(); + if (!metadata.isEmpty()) { + updateBroadcastInfoFromBroadcastMetadata(metadata.get(0)); + } + registerContentObserver(); + if (DEBUG) { + Log.d( + TAG, + "onServiceConnected: register " + + "mCachedBroadcastCallbackExecutorMap = " + + mCachedBroadcastCallbackExecutorMap); + } + mCachedBroadcastCallbackExecutorMap.forEach( + (callback, executor) -> + registerServiceCallBack(executor, callback)); + } else if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) + && !mIsBroadcastAssistantProfileReady) { + mIsBroadcastAssistantProfileReady = true; + mServiceBroadcastAssistant = (BluetoothLeBroadcastAssistant) proxy; + registerBroadcastAssistantCallback(mExecutor, mBroadcastAssistantCallback); + } } - registerContentObserver(); - } else if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) - && !mIsBroadcastAssistantProfileReady) { - mIsBroadcastAssistantProfileReady = true; - mServiceBroadcastAssistant = (BluetoothLeBroadcastAssistant) proxy; - registerBroadcastAssistantCallback(mExecutor, mBroadcastAssistantCallback); - } - } - @Override - public void onServiceDisconnected(int profile) { - if (DEBUG) { - Log.d(TAG, "Bluetooth service disconnected"); - } - if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST) && mIsBroadcastProfileReady) { - mIsBroadcastProfileReady = false; - unregisterServiceCallBack(mBroadcastCallback); - } - if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) - && mIsBroadcastAssistantProfileReady) { - mIsBroadcastAssistantProfileReady = false; - unregisterBroadcastAssistantCallback(mBroadcastAssistantCallback); - } + @Override + public void onServiceDisconnected(int profile) { + if (DEBUG) { + Log.d(TAG, "Bluetooth service disconnected: " + profile); + } + if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST) + && mIsBroadcastProfileReady) { + mIsBroadcastProfileReady = false; + unregisterServiceCallBack(mBroadcastCallback); + mCachedBroadcastCallbackExecutorMap.clear(); + } + if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) + && mIsBroadcastAssistantProfileReady) { + mIsBroadcastAssistantProfileReady = false; + unregisterBroadcastAssistantCallback(mBroadcastAssistantCallback); + } - if (!mIsBroadcastAssistantProfileReady && !mIsBroadcastProfileReady) { - unregisterContentObserver(); - } - } - }; + if (!mIsBroadcastAssistantProfileReady && !mIsBroadcastProfileReady) { + unregisterContentObserver(); + } + } + }; private final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @Override public void onBroadcastStarted(int reason, int broadcastId) { if (DEBUG) { - Log.d(TAG, - "onBroadcastStarted(), reason = " + reason + ", broadcastId = " + Log.d( + TAG, + "onBroadcastStarted(), reason = " + + reason + + ", broadcastId = " + broadcastId); } setLatestBroadcastId(broadcastId); - setAppSourceName(mNewAppSourceName, /*updateContentResolver=*/ true); + setAppSourceName(mNewAppSourceName, /* updateContentResolver= */ true); } @Override @@ -161,8 +182,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } @Override - public void onBroadcastMetadataChanged(int broadcastId, - @NonNull BluetoothLeBroadcastMetadata metadata) { + public void onBroadcastMetadataChanged( + int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) { if (DEBUG) { Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId); } @@ -172,8 +193,11 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { @Override public void onBroadcastStopped(int reason, int broadcastId) { if (DEBUG) { - Log.d(TAG, - "onBroadcastStopped(), reason = " + reason + ", broadcastId = " + Log.d( + TAG, + "onBroadcastStopped(), reason = " + + reason + + ", broadcastId = " + broadcastId); } @@ -191,37 +215,42 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { @Override public void onBroadcastUpdated(int reason, int broadcastId) { if (DEBUG) { - Log.d(TAG, - "onBroadcastUpdated(), reason = " + reason + ", broadcastId = " + Log.d( + TAG, + "onBroadcastUpdated(), reason = " + + reason + + ", broadcastId = " + broadcastId); } setLatestBroadcastId(broadcastId); - setAppSourceName(mNewAppSourceName, /*updateContentResolver=*/ true); + setAppSourceName(mNewAppSourceName, /* updateContentResolver= */ true); } @Override public void onBroadcastUpdateFailed(int reason, int broadcastId) { if (DEBUG) { - Log.d(TAG, - "onBroadcastUpdateFailed(), reason = " + reason + ", broadcastId = " + Log.d( + TAG, + "onBroadcastUpdateFailed(), reason = " + + reason + + ", broadcastId = " + broadcastId); } } @Override - public void onPlaybackStarted(int reason, int broadcastId) { - } + public void onPlaybackStarted(int reason, int broadcastId) {} @Override - public void onPlaybackStopped(int reason, int broadcastId) { - } + public void onPlaybackStopped(int reason, int broadcastId) {} }; 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) {} + @Override public void onSearchStarted(int reason) {} @@ -238,38 +267,65 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} @Override - public void onSourceAddFailed(@NonNull BluetoothDevice sink, - @NonNull BluetoothLeBroadcastMetadata source, int reason) {} + public void onSourceAddFailed( + @NonNull BluetoothDevice sink, + @NonNull BluetoothLeBroadcastMetadata source, + int reason) {} @Override - public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId, - int reason) {} + public void onSourceModified( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override - public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId, - int reason) {} + public void onSourceModifyFailed( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override - public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId, - int reason) { + public void onSourceRemoved( + @NonNull BluetoothDevice sink, int sourceId, int reason) { if (DEBUG) { - Log.d(TAG, "onSourceRemoved(), sink = " + sink + ", reason = " - + reason + ", sourceId = " + sourceId); + Log.d( + TAG, + "onSourceRemoved(), sink = " + + sink + + ", reason = " + + reason + + ", sourceId = " + + sourceId); } } @Override - public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId, - int reason) { + public void onSourceRemoveFailed( + @NonNull BluetoothDevice sink, int sourceId, int reason) { if (DEBUG) { - Log.d(TAG, "onSourceRemoveFailed(), sink = " + sink + ", reason = " - + reason + ", sourceId = " + sourceId); + Log.d( + TAG, + "onSourceRemoveFailed(), sink = " + + sink + + ", reason = " + + reason + + ", sourceId = " + + sourceId); } } @Override - public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId, - @NonNull BluetoothLeBroadcastReceiveState state) {} + public void onReceiveStateChanged( + @NonNull BluetoothDevice sink, + int sourceId, + @NonNull BluetoothLeBroadcastReceiveState state) { + if (DEBUG) { + Log.d( + TAG, + "onReceiveStateChanged(), sink = " + + sink + + ", sourceId = " + + sourceId + + ", state = " + + state); + } + } }; private class BroadcastSettingsObserver extends ContentObserver { @@ -296,8 +352,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { BluetoothAdapter.getDefaultAdapter() .getProfileProxy(context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST); BluetoothAdapter.getDefaultAdapter() - .getProfileProxy(context, mServiceListener, - BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + .getProfileProxy( + context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); } /** @@ -312,11 +368,11 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } String programInfo = getProgramInfo(); if (DEBUG) { - Log.d(TAG, - "startBroadcast: language = " + language + " ,programInfo = " + programInfo); + Log.d(TAG, "startBroadcast: language = " + language + " ,programInfo = " + programInfo); } buildContentMetadata(language, programInfo); - mServiceBroadcast.startBroadcast(mBluetoothLeAudioContentMetadata, + mServiceBroadcast.startBroadcast( + mBluetoothLeAudioContentMetadata, (mBroadcastCode != null && mBroadcastCode.length > 0) ? mBroadcastCode : null); } @@ -325,7 +381,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } public void setProgramInfo(String programInfo) { - setProgramInfo(programInfo, /*updateContentResolver=*/ true); + setProgramInfo(programInfo, /* updateContentResolver= */ true); } private void setProgramInfo(String programInfo, boolean updateContentResolver) { @@ -344,8 +400,10 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "mContentResolver is null"); return; } - Settings.Secure.putString(mContentResolver, - Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, programInfo); + Settings.Secure.putString( + mContentResolver, + Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, + programInfo); } } @@ -354,7 +412,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } public void setBroadcastCode(byte[] broadcastCode) { - setBroadcastCode(broadcastCode, /*updateContentResolver=*/ true); + setBroadcastCode(broadcastCode, /* updateContentResolver= */ true); } private void setBroadcastCode(byte[] broadcastCode, boolean updateContentResolver) { @@ -372,7 +430,9 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "mContentResolver is null"); return; } - Settings.Secure.putString(mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE, + Settings.Secure.putString( + mContentResolver, + Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE, new String(broadcastCode, StandardCharsets.UTF_8)); } } @@ -401,8 +461,10 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "mContentResolver is null"); return; } - Settings.Secure.putString(mContentResolver, - Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, mAppSourceName); + Settings.Secure.putString( + mContentResolver, + Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, + mAppSourceName); } } @@ -427,10 +489,11 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { if (mBluetoothLeBroadcastMetadata == null) { final List<BluetoothLeBroadcastMetadata> metadataList = mServiceBroadcast.getAllBroadcastMetadata(); - mBluetoothLeBroadcastMetadata = metadataList.stream() - .filter(i -> i.getBroadcastId() == mBroadcastId) - .findFirst() - .orElse(null); + mBluetoothLeBroadcastMetadata = + metadataList.stream() + .filter(i -> i.getBroadcastId() == mBroadcastId) + .findFirst() + .orElse(null); } return mBluetoothLeBroadcastMetadata; } @@ -440,22 +503,27 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "updateBroadcastInfoFromContentProvider: mContentResolver is null"); return; } - String programInfo = Settings.Secure.getString(mContentResolver, - Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO); + String programInfo = + Settings.Secure.getString( + mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO); if (programInfo == null) { programInfo = getDefaultValueOfProgramInfo(); } - setProgramInfo(programInfo, /*updateContentResolver=*/ false); + setProgramInfo(programInfo, /* updateContentResolver= */ false); - String prefBroadcastCode = Settings.Secure.getString(mContentResolver, - Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE); - byte[] broadcastCode = (prefBroadcastCode == null) ? getDefaultValueOfBroadcastCode() - : prefBroadcastCode.getBytes(StandardCharsets.UTF_8); - setBroadcastCode(broadcastCode, /*updateContentResolver=*/ false); + String prefBroadcastCode = + Settings.Secure.getString( + mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE); + byte[] broadcastCode = + (prefBroadcastCode == null) + ? getDefaultValueOfBroadcastCode() + : prefBroadcastCode.getBytes(StandardCharsets.UTF_8); + setBroadcastCode(broadcastCode, /* updateContentResolver= */ false); - String appSourceName = Settings.Secure.getString(mContentResolver, - Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME); - setAppSourceName(appSourceName, /*updateContentResolver=*/ false); + String appSourceName = + Settings.Secure.getString( + mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME); + setAppSourceName(appSourceName, /* updateContentResolver= */ false); } private void updateBroadcastInfoFromBroadcastMetadata( @@ -474,12 +542,12 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } BluetoothLeAudioContentMetadata contentMetadata = subgroup.get(0).getContentMetadata(); setProgramInfo(contentMetadata.getProgramInfo()); - setAppSourceName(getAppSourceName(), /*updateContentResolver=*/ true); + setAppSourceName(getAppSourceName(), /* updateContentResolver= */ true); } /** - * Stop the latest LE Broadcast. If the system stopped the LE Broadcast, then the system - * calls the corresponding callback {@link BluetoothLeBroadcast.Callback}. + * Stop the latest LE Broadcast. If the system stopped the LE Broadcast, then the system calls + * the corresponding callback {@link BluetoothLeBroadcast.Callback}. */ public void stopLatestBroadcast() { stopBroadcast(mBroadcastId); @@ -511,7 +579,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } String programInfo = getProgramInfo(); if (DEBUG) { - Log.d(TAG, + Log.d( + TAG, "updateBroadcast: language = " + language + " ,programInfo = " + programInfo); } mNewAppSourceName = appSourceName; @@ -519,50 +588,79 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { mServiceBroadcast.updateBroadcast(mBroadcastId, mBluetoothLeAudioContentMetadata); } - public void registerServiceCallBack(@NonNull @CallbackExecutor Executor executor, + /** + * Register Broadcast Callbacks to track its state and receivers + * + * @param executor Executor object for callback + * @param callback Callback object to be registered + */ + public void registerServiceCallBack( + @NonNull @CallbackExecutor Executor executor, @NonNull BluetoothLeBroadcast.Callback callback) { if (mServiceBroadcast == null) { - Log.d(TAG, "The BluetoothLeBroadcast is null."); + Log.d(TAG, "registerServiceCallBack failed, the BluetoothLeBroadcast is null."); + mCachedBroadcastCallbackExecutorMap.putIfAbsent(callback, executor); return; } - mServiceBroadcast.registerCallback(executor, callback); + try { + mServiceBroadcast.registerCallback(executor, callback); + } catch (IllegalArgumentException e) { + Log.w(TAG, "registerServiceCallBack failed. " + e.getMessage()); + } } /** - * Register Broadcast Assistant Callbacks to track it's state and receivers + * Register Broadcast Assistant Callbacks to track its state and receivers * * @param executor Executor object for callback * @param callback Callback object to be registered */ - public void registerBroadcastAssistantCallback(@NonNull @CallbackExecutor Executor executor, + private void registerBroadcastAssistantCallback( + @NonNull @CallbackExecutor Executor executor, @NonNull BluetoothLeBroadcastAssistant.Callback callback) { if (mServiceBroadcastAssistant == null) { - Log.d(TAG, "The BluetoothLeBroadcastAssisntant is null."); + Log.d( + TAG, + "registerBroadcastAssistantCallback failed, " + + "the BluetoothLeBroadcastAssistant is null."); return; } mServiceBroadcastAssistant.registerCallback(executor, callback); } + /** + * Unregister previously registered Broadcast Callbacks + * + * @param callback Callback object to be unregistered + */ public void unregisterServiceCallBack(@NonNull BluetoothLeBroadcast.Callback callback) { + mCachedBroadcastCallbackExecutorMap.remove(callback); if (mServiceBroadcast == null) { - Log.d(TAG, "The BluetoothLeBroadcast is null."); + Log.d(TAG, "unregisterServiceCallBack failed, the BluetoothLeBroadcast is null."); return; } - mServiceBroadcast.unregisterCallback(callback); + try { + mServiceBroadcast.unregisterCallback(callback); + } catch (IllegalArgumentException e) { + Log.w(TAG, "unregisterServiceCallBack failed. " + e.getMessage()); + } } /** - * Unregister previousely registered Broadcast Assistant Callbacks + * Unregister previously registered Broadcast Assistant Callbacks * * @param callback Callback object to be unregistered */ - public void unregisterBroadcastAssistantCallback( + private void unregisterBroadcastAssistantCallback( @NonNull BluetoothLeBroadcastAssistant.Callback callback) { if (mServiceBroadcastAssistant == null) { - Log.d(TAG, "The BluetoothLeBroadcastAssisntant is null."); + Log.d( + TAG, + "unregisterBroadcastAssistantCallback, " + + "the BluetoothLeBroadcastAssistant is null."); return; } @@ -570,8 +668,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } private void buildContentMetadata(String language, String programInfo) { - mBluetoothLeAudioContentMetadata = mBuilder.setLanguage(language).setProgramInfo( - programInfo).build(); + mBluetoothLeAudioContentMetadata = + mBuilder.setLanguage(language).setProgramInfo(programInfo).build(); } public LocalBluetoothLeBroadcastMetadata getLocalBluetoothLeBroadcastMetaData() { @@ -600,9 +698,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { return true; } - /** - * Not supported since LE Audio Broadcasts do not establish a connection. - */ + /** Not supported since LE Audio Broadcasts do not establish a connection. */ public int getConnectionStatus(BluetoothDevice device) { if (mServiceBroadcast == null) { return BluetoothProfile.STATE_DISCONNECTED; @@ -611,9 +707,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { return mServiceBroadcast.getConnectionState(device); } - /** - * Not supported since LE Audio Broadcasts do not establish a connection. - */ + /** Not supported since LE Audio Broadcasts do not establish a connection. */ public List<BluetoothDevice> getConnectedDevices() { if (mServiceBroadcast == null) { return new ArrayList<BluetoothDevice>(0); @@ -622,8 +716,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { return mServiceBroadcast.getConnectedDevices(); } - public @NonNull - List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() { + /** Get all broadcast metadata. */ + public @NonNull List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() { if (mServiceBroadcast == null) { Log.d(TAG, "The BluetoothLeBroadcast is null."); return Collections.emptyList(); @@ -640,16 +734,14 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { return !mServiceBroadcast.getAllBroadcastMetadata().isEmpty(); } - /** - * Service does not provide method to get/set policy. - */ + /** Service does not provide method to get/set policy. */ public int getConnectionPolicy(BluetoothDevice device) { return CONNECTION_POLICY_FORBIDDEN; } /** - * Service does not provide "setEnabled" method. Please use {@link #startBroadcast}, - * {@link #stopBroadcast()} or {@link #updateBroadcast(String, String)} + * Service does not provide "setEnabled" method. Please use {@link #startBroadcast}, {@link + * #stopBroadcast()} or {@link #updateBroadcast(String, String)} */ public boolean setEnabled(BluetoothDevice device, boolean enabled) { return false; @@ -683,9 +775,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } if (mServiceBroadcast != null) { try { - BluetoothAdapter.getDefaultAdapter().closeProfileProxy( - BluetoothProfile.LE_AUDIO_BROADCAST, - mServiceBroadcast); + BluetoothAdapter.getDefaultAdapter() + .closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST, mServiceBroadcast); mServiceBroadcast = null; } catch (Throwable t) { Log.w(TAG, "Error cleaning up LeAudio proxy", t); @@ -694,13 +785,13 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } private String getDefaultValueOfProgramInfo() { - //set the default value; + // set the default value; int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix; } private byte[] getDefaultValueOfBroadcastCode() { - //set the default value; + // set the default value; return generateRandomPassword().getBytes(StandardCharsets.UTF_8); } @@ -708,14 +799,14 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { if (DEBUG) { Log.d(TAG, "resetCacheInfo:"); } - setAppSourceName("", /*updateContentResolver=*/ true); + setAppSourceName("", /* updateContentResolver= */ true); mBluetoothLeBroadcastMetadata = null; mBroadcastId = UNKNOWN_VALUE_PLACEHOLDER; } private String generateRandomPassword() { String randomUUID = UUID.randomUUID().toString(); - //first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + // first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx return randomUUID.substring(0, 8) + randomUUID.substring(9, 13); } @@ -752,5 +843,4 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } } } - } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java index bb103b8896fd..34008ac56042 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java @@ -39,13 +39,14 @@ import com.android.settingslib.R; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; - /** - * LocalBluetoothLeBroadcastAssistant provides an interface between the Settings app - * and the functionality of the local {@link BluetoothLeBroadcastAssistant}. - * Use the {@link BluetoothLeBroadcastAssistant.Callback} to get the result callback. + * LocalBluetoothLeBroadcastAssistant provides an interface between the Settings app and the + * functionality of the local {@link BluetoothLeBroadcastAssistant}. Use the {@link + * BluetoothLeBroadcastAssistant.Callback} to get the result callback. */ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile { private static final String TAG = "LocalBluetoothLeBroadcastAssistant"; @@ -62,58 +63,76 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata; private BluetoothLeBroadcastMetadata.Builder mBuilder; private boolean mIsProfileReady; + // Cached assistant callbacks being register before service is connected. + private final Map<BluetoothLeBroadcastAssistant.Callback, Executor> mCachedCallbackExecutorMap = + new ConcurrentHashMap<>(); + + private final ServiceListener mServiceListener = + new ServiceListener() { + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (DEBUG) { + Log.d(TAG, "Bluetooth service connected"); + } + mService = (BluetoothLeBroadcastAssistant) proxy; + // We just bound to the service, so refresh the UI for any connected LeAudio + // devices. + List<BluetoothDevice> deviceList = mService.getConnectedDevices(); + while (!deviceList.isEmpty()) { + BluetoothDevice nextDevice = deviceList.remove(0); + CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice); + // we may add a new device here, but generally this should not happen + if (device == null) { + if (DEBUG) { + Log.d( + TAG, + "LocalBluetoothLeBroadcastAssistant found new device: " + + nextDevice); + } + device = mDeviceManager.addDevice(nextDevice); + } + device.onProfileStateChanged( + LocalBluetoothLeBroadcastAssistant.this, + BluetoothProfile.STATE_CONNECTED); + device.refresh(); + } - private final ServiceListener mServiceListener = new ServiceListener() { - @Override - public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (DEBUG) { - Log.d(TAG, "Bluetooth service connected"); - } - mService = (BluetoothLeBroadcastAssistant) proxy; - // We just bound to the service, so refresh the UI for any connected LeAudio devices. - List<BluetoothDevice> deviceList = mService.getConnectedDevices(); - while (!deviceList.isEmpty()) { - BluetoothDevice nextDevice = deviceList.remove(0); - CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice); - // we may add a new device here, but generally this should not happen - if (device == null) { + mProfileManager.callServiceConnectedListeners(); + mIsProfileReady = true; if (DEBUG) { - Log.d(TAG, "LocalBluetoothLeBroadcastAssistant found new device: " - + nextDevice); + Log.d( + TAG, + "onServiceConnected, register mCachedCallbackExecutorMap = " + + mCachedCallbackExecutorMap); } - device = mDeviceManager.addDevice(nextDevice); + mCachedCallbackExecutorMap.forEach( + (callback, executor) -> registerServiceCallBack(executor, callback)); } - device.onProfileStateChanged(LocalBluetoothLeBroadcastAssistant.this, - BluetoothProfile.STATE_CONNECTED); - device.refresh(); - } - mProfileManager.callServiceConnectedListeners(); - mIsProfileReady = true; - } - - @Override - public void onServiceDisconnected(int profile) { - if (profile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { - Log.d(TAG, "The profile is not LE_AUDIO_BROADCAST_ASSISTANT"); - return; - } - if (DEBUG) { - Log.d(TAG, "Bluetooth service disconnected"); - } - mProfileManager.callServiceDisconnectedListeners(); - mIsProfileReady = false; - } - }; + @Override + public void onServiceDisconnected(int profile) { + if (profile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { + Log.d(TAG, "The profile is not LE_AUDIO_BROADCAST_ASSISTANT"); + return; + } + if (DEBUG) { + Log.d(TAG, "Bluetooth service disconnected"); + } + mProfileManager.callServiceDisconnectedListeners(); + mIsProfileReady = false; + mCachedCallbackExecutorMap.clear(); + } + }; - public LocalBluetoothLeBroadcastAssistant(Context context, + public LocalBluetoothLeBroadcastAssistant( + Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { mProfileManager = profileManager; mDeviceManager = deviceManager; - BluetoothAdapter.getDefaultAdapter(). - getProfileProxy(context, mServiceListener, - BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + BluetoothAdapter.getDefaultAdapter() + .getProfileProxy( + context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); mBuilder = new BluetoothLeBroadcastMetadata.Builder(); } @@ -123,11 +142,11 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile * @param sink Broadcast Sink to which the Broadcast Source should be added * @param metadata Broadcast Source metadata to be added to the Broadcast Sink * @param isGroupOp {@code true} if Application wants to perform this operation for all - * coordinated set members throughout this session. Otherwise, caller - * would have to add, modify, and remove individual set members. + * coordinated set members throughout this session. Otherwise, caller would have to add, + * modify, and remove individual set members. */ - public void addSource(BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, - boolean isGroupOp) { + public void addSource( + BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) { if (mService == null) { Log.d(TAG, "The BluetoothLeBroadcastAssistant is null"); return; @@ -140,36 +159,55 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile * the qr code string. * * @param sink Broadcast Sink to which the Broadcast Source should be added - * @param sourceAddressType hardware MAC Address of the device. See - * {@link BluetoothDevice.AddressType}. + * @param sourceAddressType hardware MAC Address of the device. See {@link + * BluetoothDevice.AddressType}. * @param presentationDelayMicros presentation delay of this Broadcast Source in microseconds. * @param sourceAdvertisingSid 1-byte long Advertising_SID of the Broadcast Source. * @param broadcastId 3-byte long Broadcast_ID of the Broadcast Source. - * @param paSyncInterval Periodic Advertising Sync interval of the broadcast Source, - * {@link BluetoothLeBroadcastMetadata#PA_SYNC_INTERVAL_UNKNOWN} if - * unknown. + * @param paSyncInterval Periodic Advertising Sync interval of the broadcast Source, {@link + * BluetoothLeBroadcastMetadata#PA_SYNC_INTERVAL_UNKNOWN} if unknown. * @param isEncrypted whether the Broadcast Source is encrypted. * @param broadcastCode Broadcast Code for this Broadcast Source, null if code is not required. * @param sourceDevice source advertiser address. * @param isGroupOp {@code true} if Application wants to perform this operation for all - * coordinated set members throughout this session. Otherwise, caller - * would have to add, modify, and remove individual set members. + * coordinated set members throughout this session. Otherwise, caller would have to add, + * modify, and remove individual set members. */ - public void addSource(@NonNull BluetoothDevice sink, int sourceAddressType, - int presentationDelayMicros, int sourceAdvertisingSid, int broadcastId, - int paSyncInterval, boolean isEncrypted, byte[] broadcastCode, - BluetoothDevice sourceDevice, boolean isGroupOp) { + public void addSource( + @NonNull BluetoothDevice sink, + int sourceAddressType, + int presentationDelayMicros, + int sourceAdvertisingSid, + int broadcastId, + int paSyncInterval, + boolean isEncrypted, + byte[] broadcastCode, + BluetoothDevice sourceDevice, + boolean isGroupOp) { if (DEBUG) { Log.d(TAG, "addSource()"); } - buildMetadata(sourceAddressType, presentationDelayMicros, sourceAdvertisingSid, broadcastId, - paSyncInterval, isEncrypted, broadcastCode, sourceDevice); + buildMetadata( + sourceAddressType, + presentationDelayMicros, + sourceAdvertisingSid, + broadcastId, + paSyncInterval, + isEncrypted, + broadcastCode, + sourceDevice); addSource(sink, mBluetoothLeBroadcastMetadata, isGroupOp); } - private void buildMetadata(int sourceAddressType, int presentationDelayMicros, - int sourceAdvertisingSid, int broadcastId, int paSyncInterval, boolean isEncrypted, - byte[] broadcastCode, BluetoothDevice sourceDevice) { + private void buildMetadata( + int sourceAddressType, + int presentationDelayMicros, + int sourceAdvertisingSid, + int broadcastId, + int paSyncInterval, + boolean isEncrypted, + byte[] broadcastCode, + BluetoothDevice sourceDevice) { mBluetoothLeBroadcastMetadata = mBuilder.setSourceDevice(sourceDevice, sourceAddressType) .setSourceAdvertisingSid(sourceAdvertisingSid) @@ -223,10 +261,10 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile /** * Stops an ongoing search for nearby Broadcast Sources. * - * On success, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopped(int)} will be - * called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. - * On failure, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopFailed(int)} will be - * called with reason code + * <p>On success, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopped(int)} will be + * called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. On failure, + * {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopFailed(int)} will be called with + * reason code * * @throws IllegalStateException if callback was not registered */ @@ -245,8 +283,8 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile * Get information about all Broadcast Sources that a Broadcast Sink knows about. * * @param sink Broadcast Sink from which to get all Broadcast Sources - * @return the list of Broadcast Receive State {@link BluetoothLeBroadcastReceiveState} - * stored in the Broadcast Sink + * @return the list of Broadcast Receive State {@link BluetoothLeBroadcastReceiveState} stored + * in the Broadcast Sink * @throws NullPointerException when <var>sink</var> is null */ public @NonNull List<BluetoothLeBroadcastReceiveState> getAllSources( @@ -261,24 +299,50 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile return mService.getAllSources(sink); } - public void registerServiceCallBack(@NonNull @CallbackExecutor Executor executor, + /** + * Register Broadcast Assistant Callbacks to track its state and receivers + * + * @param executor Executor object for callback + * @param callback Callback object to be registered + */ + public void registerServiceCallBack( + @NonNull @CallbackExecutor Executor executor, @NonNull BluetoothLeBroadcastAssistant.Callback callback) { if (mService == null) { - Log.d(TAG, "The BluetoothLeBroadcast is null."); + Log.d( + TAG, + "registerServiceCallBack failed, the BluetoothLeBroadcastAssistant is null."); + mCachedCallbackExecutorMap.putIfAbsent(callback, executor); return; } - mService.registerCallback(executor, callback); + try { + mService.registerCallback(executor, callback); + } catch (IllegalArgumentException e) { + Log.w(TAG, "registerServiceCallBack failed. " + e.getMessage()); + } } + /** + * Unregister previously registered Broadcast Assistant Callbacks + * + * @param callback Callback object to be unregistered + */ public void unregisterServiceCallBack( @NonNull BluetoothLeBroadcastAssistant.Callback callback) { + mCachedCallbackExecutorMap.remove(callback); if (mService == null) { - Log.d(TAG, "The BluetoothLeBroadcast is null."); + Log.d( + TAG, + "unregisterServiceCallBack failed, the BluetoothLeBroadcastAssistant is null."); return; } - mService.unregisterCallback(callback); + try { + mService.unregisterCallback(callback); + } catch (IllegalArgumentException e) { + Log.w(TAG, "unregisterServiceCallBack failed. " + e.getMessage()); + } } public boolean isProfileReady() { @@ -310,9 +374,11 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile return new ArrayList<BluetoothDevice>(0); } return mService.getDevicesMatchingConnectionStates( - new int[]{BluetoothProfile.STATE_CONNECTED, - BluetoothProfile.STATE_CONNECTING, - BluetoothProfile.STATE_DISCONNECTING}); + new int[] { + BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_CONNECTING, + BluetoothProfile.STATE_DISCONNECTING + }); } public boolean isEnabled(BluetoothDevice device) { @@ -373,9 +439,8 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile } if (mService != null) { try { - BluetoothAdapter.getDefaultAdapter().closeProfileProxy( - BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, - mService); + BluetoothAdapter.getDefaultAdapter() + .closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mService); mService = null; } catch (Throwable t) { Log.w(TAG, "Error cleaning up LeAudio proxy", t); diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt index 009f8bb38e61..e538e093e60f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt @@ -175,11 +175,7 @@ private fun <T> computeValue( canOverflow: Boolean, ): T { val state = layoutImpl.state.transitionState - if ( - state !is TransitionState.Transition || - state.fromScene == state.toScene || - !layoutImpl.isTransitionReady(state) - ) { + if (state !is TransitionState.Transition || !layoutImpl.isTransitionReady(state)) { return sharedValue.value } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index 199832bc4ab6..de69c37d4630 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -47,12 +47,6 @@ internal fun CoroutineScope.animateToScene( when (state) { is TransitionState.Idle -> animate(layoutImpl, target) is TransitionState.Transition -> { - if (state.toScene == state.fromScene) { - // Same as idle. - animate(layoutImpl, target) - return - } - // A transition is currently running: first check whether `transition.toScene` or // `transition.fromScene` is the same as our target scene, in which case the transition // can be accelerated or reversed to end up in the target state. @@ -153,13 +147,13 @@ private fun CoroutineScope.animate( } private class OneOffTransition( - override val fromScene: SceneKey, - override val toScene: SceneKey, + fromScene: SceneKey, + toScene: SceneKey, override val currentScene: SceneKey, override val isInitiatedByUserInput: Boolean, override val isUserInputOngoing: Boolean, private val animatable: Animatable<Float, AnimationVector1D>, -) : TransitionState.Transition { +) : TransitionState.Transition(fromScene, toScene) { override val progress: Float get() = animatable.value } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 31604a6817f0..431a8aef6d3d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -288,7 +288,6 @@ private fun shouldDrawElement( // Always draw the element if there is no ongoing transition or if the element is not shared. if ( state !is TransitionState.Transition || - state.fromScene == state.toScene || !layoutImpl.isTransitionReady(state) || state.fromScene !in element.sceneValues || state.toScene !in element.sceneValues @@ -374,7 +373,7 @@ private fun isElementOpaque( ): Boolean { val state = layoutImpl.state.transitionState - if (state !is TransitionState.Transition || state.fromScene == state.toScene) { + if (state !is TransitionState.Transition) { return true } @@ -611,7 +610,7 @@ private inline fun <T> computeValue( val state = layoutImpl.state.transitionState // There is no ongoing transition. - if (state !is TransitionState.Transition || state.fromScene == state.toScene) { + if (state !is TransitionState.Transition) { // Even if this element SceneTransitionLayout is not animated, the layout itself might be // animated (e.g. by another parent SceneTransitionLayout), in which case this element still // need to participate in the layout phase. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt index fa385d014ccb..7029da2edb0d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt @@ -131,10 +131,6 @@ private fun shouldComposeMovableElement( val fromScene = (transitionState as TransitionState.Transition).fromScene val toScene = transitionState.toScene - if (fromScene == toScene) { - check(fromScene == scene) - return true - } val fromReady = layoutImpl.isSceneReady(fromScene) val toReady = layoutImpl.isSceneReady(toScene) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt index 1b79dbdee510..983cff83ed57 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt @@ -73,17 +73,13 @@ fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTrans when (val state = transitionState) { is TransitionState.Idle -> ObservableTransitionState.Idle(state.currentScene) is TransitionState.Transition -> { - if (state.fromScene == state.toScene) { - ObservableTransitionState.Idle(state.currentScene) - } else { - ObservableTransitionState.Transition( - fromScene = state.fromScene, - toScene = state.toScene, - progress = snapshotFlow { state.progress }, - isInitiatedByUserInput = state.isInitiatedByUserInput, - isUserInputOngoing = snapshotFlow { state.isUserInputOngoing }, - ) - } + ObservableTransitionState.Transition( + fromScene = state.fromScene, + toScene = state.toScene, + progress = snapshotFlow { state.progress }, + isInitiatedByUserInput = state.isInitiatedByUserInput, + isUserInputOngoing = snapshotFlow { state.isUserInputOngoing }, + ) } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt index 03f37d0c9bda..91decf4d8b7e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt @@ -49,8 +49,12 @@ internal class SceneGestureHandler( layoutImpl.state.transitionState = value } - internal var swipeTransition: SwipeTransition = SwipeTransition(currentScene, currentScene, 1f) - private set + private var _swipeTransition: SwipeTransition? = null + internal var swipeTransition: SwipeTransition + get() = _swipeTransition ?: error("SwipeTransition needs to be initialized") + set(value) { + _swipeTransition = value + } private fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) { if (isDrivingTransition || force) transitionState = newTransition @@ -61,7 +65,7 @@ internal class SceneGestureHandler( get() = layoutImpl.scene(transitionState.currentScene) internal val isDrivingTransition - get() = transitionState == swipeTransition + get() = transitionState == _swipeTransition /** * The velocity threshold at which the intent of the user is to swipe up or down. It is the same @@ -82,12 +86,15 @@ internal class SceneGestureHandler( private var actionDownOrRight: UserAction? = null private var actionUpOrLeftNoEdge: UserAction? = null private var actionDownOrRightNoEdge: UserAction? = null + private var upOrLeftScene: SceneKey? = null + private var downOrRightScene: SceneKey? = null internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) { if (isDrivingTransition) { // This [transition] was already driving the animation: simply take over it. // Stop animating and start from where the current offset. swipeTransition.cancelOffsetAnimation() + updateTargetScenes(swipeTransition._fromScene) return } @@ -105,11 +112,8 @@ internal class SceneGestureHandler( val fromScene = currentScene setCurrentActions(fromScene, startedPosition, pointersDown) - if (fromScene.upOrLeft() == null && fromScene.downOrRight() == null) { - return - } - - val (targetScene, distance) = fromScene.findTargetSceneAndDistance(overSlop) + val (targetScene, distance) = + findTargetSceneAndDistance(fromScene, overSlop, updateScenes = true) ?: return updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true) } @@ -179,16 +183,21 @@ internal class SceneGestureHandler( val (fromScene, acceleratedOffset) = computeFromSceneConsideringAcceleratedSwipe(swipeTransition) - swipeTransition.dragOffset += acceleratedOffset - // Compute the target scene depending on the current offset. + val isNewFromScene = fromScene.key != swipeTransition.fromScene val (targetScene, distance) = - fromScene.findTargetSceneAndDistance(swipeTransition.dragOffset) + findTargetSceneAndDistance( + fromScene, + swipeTransition.dragOffset, + updateScenes = isNewFromScene, + ) + ?: run { + onDragStopped(delta, true) + return + } + swipeTransition.dragOffset += acceleratedOffset - // TODO(b/290184746): support long scroll A => B => C? especially for non fullscreen scenes - if ( - fromScene.key != swipeTransition.fromScene || targetScene.key != swipeTransition.toScene - ) { + if (isNewFromScene || targetScene.key != swipeTransition.toScene) { updateTransition( SwipeTransition(fromScene, targetScene, distance).apply { this.dragOffset = swipeTransition.dragOffset @@ -197,6 +206,11 @@ internal class SceneGestureHandler( } } + private fun updateTargetScenes(fromScene: Scene) { + upOrLeftScene = fromScene.upOrLeft() + downOrRightScene = fromScene.downOrRight() + } + /** * Change fromScene in the case where the user quickly swiped multiple times in the same * direction to accelerate the transition from A => B then B => C. @@ -214,37 +228,71 @@ internal class SceneGestureHandler( val absoluteDistance = swipeTransition.distance.absoluteValue // If the swipe was not committed, don't do anything. - if (fromScene == toScene || swipeTransition._currentScene != toScene) { + if (swipeTransition._currentScene != toScene) { return Pair(fromScene, 0f) } // If the offset is past the distance then let's change fromScene so that the user can swipe // to the next screen or go back to the previous one. val offset = swipeTransition.dragOffset - return if (offset <= -absoluteDistance && fromScene.upOrLeft() == toScene.key) { + return if (offset <= -absoluteDistance && upOrLeftScene == toScene.key) { Pair(toScene, absoluteDistance) - } else if (offset >= absoluteDistance && fromScene.downOrRight() == toScene.key) { + } else if (offset >= absoluteDistance && downOrRightScene == toScene.key) { Pair(toScene, -absoluteDistance) } else { Pair(fromScene, 0f) } } - // TODO(b/290184746): there are two bugs here: - // 1. if both upOrLeft and downOrRight become `null` during a transition this will crash - // 2. if one of them changes during a transition, the transition will jump cut to the new target - private inline fun Scene.findTargetSceneAndDistance( - directionOffset: Float - ): Pair<Scene, Float> { - val upOrLeft = upOrLeft() - val downOrRight = downOrRight() - val absoluteDistance = getAbsoluteDistance() + /** + * Returns the target scene and distance from [fromScene] in the direction [directionOffset]. + * + * @param fromScene the scene from which we look for the target + * @param directionOffset signed float that indicates the direction. Positive is down or right + * negative is up or left. + * @param updateScenes whether the target scenes should be updated to the current values held in + * the Scenes map. Usually we don't want to update them while doing a drag, because this could + * change the target scene (jump cutting) to a different scene, when some system state changed + * the targets the background. However, an update is needed any time we calculate the targets + * for a new fromScene. + * @return null when there are no targets in either direction. If one direction is null and you + * drag into the null direction this function will return the opposite direction, assuming + * that the users intention is to start the drag into the other direction eventually. If + * [directionOffset] is 0f and both direction are available, it will default to + * [upOrLeftScene]. + */ + private inline fun findTargetSceneAndDistance( + fromScene: Scene, + directionOffset: Float, + updateScenes: Boolean, + ): Pair<Scene, Float>? { + if (updateScenes) updateTargetScenes(fromScene) + val absoluteDistance = fromScene.getAbsoluteDistance() // Compute the target scene depending on the current offset. - return if ((directionOffset < 0f && upOrLeft != null) || downOrRight == null) { - Pair(layoutImpl.scene(upOrLeft!!), -absoluteDistance) - } else { - Pair(layoutImpl.scene(downOrRight), absoluteDistance) + return when { + upOrLeftScene == null && downOrRightScene == null -> null + (directionOffset < 0f && upOrLeftScene != null) || downOrRightScene == null -> + Pair(layoutImpl.scene(upOrLeftScene!!), -absoluteDistance) + else -> Pair(layoutImpl.scene(downOrRightScene!!), absoluteDistance) + } + } + + /** + * A strict version of [findTargetSceneAndDistance] that will return null when there is no Scene + * in [directionOffset] direction + */ + private inline fun findTargetSceneAndDistanceStrict( + fromScene: Scene, + directionOffset: Float, + ): Pair<Scene, Float>? { + val absoluteDistance = fromScene.getAbsoluteDistance() + return when { + directionOffset > 0f -> + upOrLeftScene?.let { Pair(layoutImpl.scene(it), -absoluteDistance) } + directionOffset < 0f -> + downOrRightScene?.let { Pair(layoutImpl.scene(it), absoluteDistance) } + else -> null } } @@ -311,20 +359,21 @@ internal class SceneGestureHandler( val startFromIdlePosition = swipeTransition.dragOffset == 0f if (startFromIdlePosition) { - // If there is a next scene, we start the overscroll animation. - val (targetScene, distance) = fromScene.findTargetSceneAndDistance(velocity) - val isValidTarget = distance != 0f && targetScene.key != fromScene.key - if (isValidTarget) { - updateTransition( - SwipeTransition(fromScene, targetScene, distance).apply { - _currentScene = swipeTransition._currentScene + // If there is a target scene, we start the overscroll animation. + val (targetScene, distance) = + findTargetSceneAndDistanceStrict(fromScene, velocity) + ?: run { + // We will not animate + transitionState = TransitionState.Idle(fromScene.key) + return } - ) - animateTo(targetScene = fromScene, targetOffset = 0f) - } else { - // We will not animate - transitionState = TransitionState.Idle(fromScene.key) - } + + updateTransition( + SwipeTransition(fromScene, targetScene, distance).apply { + _currentScene = swipeTransition._currentScene + } + ) + animateTo(targetScene = fromScene, targetOffset = 0f) } else { // We were between two scenes: animate to the initial scene. animateTo(targetScene = fromScene, targetOffset = 0f) @@ -410,15 +459,11 @@ internal class SceneGestureHandler( * above or to the left of [toScene]. */ val distance: Float - ) : TransitionState.Transition { + ) : TransitionState.Transition(_fromScene.key, _toScene.key) { var _currentScene by mutableStateOf(_fromScene) override val currentScene: SceneKey get() = _currentScene.key - override val fromScene: SceneKey = _fromScene.key - - override val toScene: SceneKey = _toScene.key - override val progress: Float get() { val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset @@ -494,9 +539,9 @@ private class SceneDraggableHandler( } internal class SceneNestedScrollHandler( - private val gestureHandler: SceneGestureHandler, - private val topOrLeftBehavior: NestedScrollBehavior, - private val bottomOrRightBehavior: NestedScrollBehavior, + private val gestureHandler: SceneGestureHandler, + private val topOrLeftBehavior: NestedScrollBehavior, + private val bottomOrRightBehavior: NestedScrollBehavior, ) : NestedScrollHandler { override val connection: PriorityNestedScrollConnection = nestedScrollConnection() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 02ddccbc051b..00e33e24c41e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -172,7 +172,7 @@ internal class SceneTransitionLayoutImpl( val width: Int val height: Int val state = state.transitionState - if (state !is TransitionState.Transition || state.fromScene == state.toScene) { + if (state !is TransitionState.Transition) { width = placeable.width height = placeable.height } else { @@ -232,10 +232,7 @@ internal class SceneTransitionLayoutImpl( is TransitionState.Idle -> drawContent() is TransitionState.Transition -> { // Don't draw scenes that are not ready yet. - if ( - readyScenes.containsKey(key) || - state.fromScene == state.toScene - ) { + if (readyScenes.containsKey(key)) { drawContent() } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index f48e9147eef4..623725582a9d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -39,11 +39,6 @@ class SceneTransitionLayoutState(initialScene: SceneKey) { fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean { val transition = transitionState as? TransitionState.Transition ?: return false - // TODO(b/310915136): Remove this check. - if (transition.fromScene == transition.toScene) { - return false - } - return (from == null || transition.fromScene == from) && (to == null || transition.toScene == to) } @@ -71,32 +66,30 @@ sealed interface TransitionState { /** No transition/animation is currently running. */ data class Idle(override val currentScene: SceneKey) : TransitionState - /** - * There is a transition animating between two scenes. - * - * Important note: [fromScene] and [toScene] might be the same, in which case this [Transition] - * should be treated the same as [Idle]. This is designed on purpose so that a [Transition] can - * be started without knowing in advance where it is transitioning to, making the logic of - * [swipeToScene] easier to reason about. - */ - interface Transition : TransitionState { - /** The scene this transition is starting from. */ - val fromScene: SceneKey + /** There is a transition animating between two scenes. */ + abstract class Transition( + /** The scene this transition is starting from. Can't be the same as toScene */ + val fromScene: SceneKey, - /** The scene this transition is going to. */ + /** The scene this transition is going to. Can't be the same as fromScene */ val toScene: SceneKey + ) : TransitionState { + + init { + check(fromScene != toScene) + } /** * The progress of the transition. This is usually in the `[0; 1]` range, but it can also be * less than `0` or greater than `1` when using transitions with a spring AnimationSpec or * when flinging quickly during a swipe gesture. */ - val progress: Float + abstract val progress: Float /** Whether the transition was triggered by user input rather than being programmatic. */ - val isInitiatedByUserInput: Boolean + abstract val isInitiatedByUserInput: Boolean /** Whether user input is currently driving the transition. */ - val isUserInputOngoing: Boolean + abstract val isUserInputOngoing: Boolean } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt index 34afc4c91d4c..e6224df649ca 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt @@ -58,16 +58,22 @@ class SceneGestureHandlerTest { private val layoutState: SceneTransitionLayoutState = SceneTransitionLayoutState(internalCurrentScene) + val mutableUserActionsA: MutableMap<UserAction, SceneKey> = + mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC) + + val mutableUserActionsB: MutableMap<UserAction, SceneKey> = + mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA) + private val scenesBuilder: SceneTransitionLayoutScope.() -> Unit = { scene( key = SceneA, - userActions = mapOf(Swipe.Up to SceneB, Swipe.Down to SceneC), + userActions = mutableUserActionsA, ) { Text("SceneA") } scene( key = SceneB, - userActions = mapOf(Swipe.Up to SceneC, Swipe.Down to SceneA), + userActions = mutableUserActionsB, ) { Text("SceneB") } @@ -412,6 +418,70 @@ class SceneGestureHandlerTest { } @Test + fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest { + draggable.onDragStarted() + draggable.onDelta(up(0.2f)) + + draggable.onDelta(up(0.2f)) + draggable.onDragStopped(velocity = -velocityThreshold) + assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) + + mutableUserActionsA.remove(Swipe.Up) + mutableUserActionsA.remove(Swipe.Down) + mutableUserActionsB.remove(Swipe.Up) + mutableUserActionsB.remove(Swipe.Down) + + // start accelaratedScroll and scroll over to B -> null + draggable.onDragStarted() + draggable.onDelta(up(0.5f)) + draggable.onDelta(up(0.5f)) + + // here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may + // still be called. Make sure that they don't crash or change the scene + draggable.onDelta(up(0.5f)) + draggable.onDragStopped(0f) + + advanceUntilIdle() + assertIdle(SceneB) + + // These events can still come in after the animation has settled + draggable.onDelta(up(0.5f)) + draggable.onDragStopped(0f) + assertIdle(SceneB) + } + + @Test + fun onDragTargetsChanged_targetStaysTheSame() = runGestureTest { + draggable.onDragStarted(up(0.1f)) + assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) + + mutableUserActionsA[Swipe.Up] = SceneC + draggable.onDelta(up(0.1f)) + // target stays B even though UserActions changed + assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f) + draggable.onDragStopped(down(0.1f)) + advanceUntilIdle() + + // now target changed to C for new drag + draggable.onDragStarted(up(0.1f)) + assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.1f) + } + + @Test + fun onDragTargetsChanged_targetsChangeWhenStartingNewDrag() = runGestureTest { + draggable.onDragStarted(up(0.1f)) + assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) + + mutableUserActionsA[Swipe.Up] = SceneC + draggable.onDelta(up(0.1f)) + draggable.onDragStopped(down(0.1f)) + + // now target changed to C for new drag that started before previous drag settled to Idle + draggable.onDragStarted(up(0.1f)) + assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f) + } + + @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { draggable.onDragStarted() assertTransition(currentScene = SceneA) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 94c51ca50667..eeda8d46adfa 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -39,18 +39,6 @@ class SceneTransitionLayoutStateTest { } @Test - fun isTransitioningTo_fromSceneEqualToToScene() { - val state = SceneTransitionLayoutState(TestScenes.SceneA) - state.transitionState = transition(from = TestScenes.SceneA, to = TestScenes.SceneA) - - assertThat(state.isTransitioning()).isFalse() - assertThat(state.isTransitioning(from = TestScenes.SceneA)).isFalse() - assertThat(state.isTransitioning(to = TestScenes.SceneB)).isFalse() - assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)) - .isFalse() - } - - @Test fun isTransitioningTo_transition() { val state = SceneTransitionLayoutState(TestScenes.SceneA) state.transitionState = transition(from = TestScenes.SceneA, to = TestScenes.SceneB) @@ -64,10 +52,8 @@ class SceneTransitionLayoutStateTest { } private fun transition(from: SceneKey, to: SceneKey): TransitionState.Transition { - return object : TransitionState.Transition { + return object : TransitionState.Transition(from, to) { override val currentScene: SceneKey = from - override val fromScene: SceneKey = from - override val toScene: SceneKey = to override val progress: Float = 0f override val isInitiatedByUserInput: Boolean = false override val isUserInputOngoing: Boolean = false diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt new file mode 100644 index 000000000000..e8504563b4ae --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt @@ -0,0 +1,40 @@ +package com.android.systemui.keyguard.ui.preview + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardRemotePreviewManagerTest : SysuiTestCase() { + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Test + fun onDestroy_clearsReferencesToRenderer() = + testScope.runTest { + val renderer = mock<KeyguardPreviewRenderer>() + val onDestroy: (PreviewLifecycleObserver) -> Unit = {} + + val observer = PreviewLifecycleObserver(this, testDispatcher, renderer, onDestroy) + + // Precondition check. + assertThat(observer.renderer).isNotNull() + assertThat(observer.onDestroy).isNotNull() + + observer.onDestroy() + + // The verification checks renderer/requestDestruction lambda because they-re + // non-singletons which can't leak KeyguardPreviewRenderer. + assertThat(observer.renderer).isNull() + assertThat(observer.onDestroy).isNull() + } +} diff --git a/packages/SystemUI/res/drawable/arrow_pointing_down.xml b/packages/SystemUI/res/drawable/arrow_pointing_down.xml new file mode 100644 index 000000000000..be39683cd78d --- /dev/null +++ b/packages/SystemUI/res/drawable/arrow_pointing_down.xml @@ -0,0 +1,26 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M5.41,7.59L4,9l8,8 8,-8 -1.41,-1.41L12,14.17" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/record_issue_dialog.xml b/packages/SystemUI/res/layout/record_issue_dialog.xml new file mode 100644 index 000000000000..53ad9f157a2e --- /dev/null +++ b/packages/SystemUI/res/layout/record_issue_dialog.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:orientation="vertical" > + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:text="@string/qs_record_issue_dropdown_header" /> + + <Button + android:id="@+id/issue_type_button" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/qs_record_issue_dropdown_prompt" + android:lines="1" + android:drawableRight="@drawable/arrow_pointing_down" + android:layout_marginTop="@dimen/qqs_layout_margin_top" + android:focusable="false" + android:clickable="true" /> + + <!-- Screen Record Switch --> + <LinearLayout + android:id="@+id/screenrecord_switch_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/qqs_layout_margin_top" + android:orientation="horizontal"> + + <ImageView + android:layout_width="@dimen/screenrecord_option_icon_size" + android:layout_height="@dimen/screenrecord_option_icon_size" + android:layout_weight="0" + android:src="@drawable/ic_screenrecord" + app:tint="?androidprv:attr/materialColorOnSurface" + android:layout_gravity="center" + android:layout_marginEnd="@dimen/screenrecord_option_padding" /> + + <TextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="@dimen/screenrecord_option_icon_size" + android:layout_weight="1" + android:layout_gravity="fill_vertical" + android:gravity="center" + android:text="@string/quick_settings_screen_record_label" + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:importantForAccessibility="no"/> + + <Switch + android:id="@+id/screenrecord_switch" + android:layout_width="wrap_content" + android:minHeight="@dimen/screenrecord_option_icon_size" + android:layout_height="wrap_content" + android:gravity="center" + android:layout_gravity="fill_vertical" + android:layout_weight="0" + android:contentDescription="@string/quick_settings_screen_record_label" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 13d2feae1d9b..e10925d551e2 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -831,6 +831,20 @@ <!-- QuickSettings: Text to prompt the user to stop an ongoing recording [CHAR LIMIT=20] --> <string name="qs_record_issue_stop">Stop</string> + <!-- QuickSettings: Issue Type Drop down options in Record Issue Start Dialog [CHAR LIMIT=50] --> + <string name="qs_record_issue_dropdown_header">What part of your device experience was affected?</string> + <!-- QuickSettings: Issue Type Drop down prompt in Record Issue Start Dialog [CHAR LIMIT=30] --> + <string name="qs_record_issue_dropdown_prompt">Select issue type</string> + <!-- QuickSettings: Screen record switch label in Record Issue Start Dialog [CHAR LIMIT=20] --> + <string name="qs_record_issue_dropdown_screenrecord">Screen record</string> + + <!-- QuickSettings: Issue Type Drop down choices list in Record Issue Start Dialog [CHAR LIMIT=30] --> + <string-array name="qs_record_issue_types"> + <item>Performance</item> + <item>User Interface</item> + <item>Battery</item> + </string-array> + <!-- QuickSettings: Label for the toggle that controls whether One-handed mode is enabled. [CHAR LIMIT=NONE] --> <string name="quick_settings_onehanded_label">One-handed mode</string> diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt index eceaf6c3c4fd..c090688dea73 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt @@ -187,7 +187,8 @@ constructor( faceManager?.sensorPropertiesInternal?.firstOrNull()?.supportsFaceDetection ?: false private val _isAuthRunning = MutableStateFlow(false) - override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning + override val isAuthRunning: StateFlow<Boolean> + get() = _isAuthRunning private val keyguardSessionId: InstanceId? get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD) @@ -253,13 +254,6 @@ constructor( ) .andAllFlows("canFaceAuthRun", faceAuthLog) .flowOn(backgroundDispatcher) - .onEach { - faceAuthLogger.canFaceAuthRunChanged(it) - if (!it) { - // Cancel currently running auth if any of the gating checks are false. - cancel() - } - } .stateIn(applicationScope, SharingStarted.Eagerly, false) // Face detection can run only when lockscreen bypass is enabled @@ -287,12 +281,9 @@ constructor( ) .andAllFlows("canFaceDetectRun", faceDetectLog) .flowOn(backgroundDispatcher) - .onEach { - if (!it) { - cancelDetection() - } - } .stateIn(applicationScope, SharingStarted.Eagerly, false) + observeFaceAuthGatingChecks() + observeFaceDetectGatingChecks() observeFaceAuthResettingConditions() listenForSchedulingWatchdog() processPendingAuthRequests() @@ -347,6 +338,17 @@ constructor( pendingAuthenticateRequest.value = null } + private fun observeFaceDetectGatingChecks() { + canRunDetection + .onEach { + if (!it) { + cancelDetection() + } + } + .flowOn(mainDispatcher) + .launchIn(applicationScope) + } + private fun isUdfps() = deviceEntryFingerprintAuthRepository.availableFpSensorType.map { it == BiometricType.UNDER_DISPLAY_FINGERPRINT @@ -405,6 +407,20 @@ constructor( ) } + private fun observeFaceAuthGatingChecks() { + canRunFaceAuth + .onEach { + faceAuthLogger.canFaceAuthRunChanged(it) + if (!it) { + // Cancel currently running auth if any of the gating checks are false. + faceAuthLogger.cancellingFaceAuth() + cancel() + } + } + .flowOn(mainDispatcher) + .launchIn(applicationScope) + } + private val faceAuthCallback = object : FaceManager.AuthenticationCallback() { override fun onAuthenticationFailed() { @@ -539,7 +555,7 @@ constructor( authenticate(it.uiEvent, it.fallbackToDetection) } } - .flowOn(backgroundDispatcher) + .flowOn(mainDispatcher) .launchIn(applicationScope) } @@ -635,7 +651,6 @@ constructor( override fun cancel() { if (authCancellationSignal == null) return - faceAuthLogger.cancellingFaceAuth() authCancellationSignal?.cancel() cancelNotReceivedHandlerJob?.cancel() cancelNotReceivedHandlerJob = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt index fb20000471a2..e3f47397eca3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt @@ -31,7 +31,6 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository @@ -72,7 +71,6 @@ constructor( private val context: Context, @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, - @Background private val backgroundDispatcher: CoroutineDispatcher, private val repository: DeviceEntryFaceAuthRepository, private val primaryBouncerInteractor: Lazy<PrimaryBouncerInteractor>, private val alternateBouncerInteractor: AlternateBouncerInteractor, @@ -109,7 +107,6 @@ constructor( fallbackToDetect = false ) } - .flowOn(backgroundDispatcher) .launchIn(applicationScope) alternateBouncerInteractor.isVisible @@ -121,7 +118,6 @@ constructor( fallbackToDetect = false ) } - .flowOn(backgroundDispatcher) .launchIn(applicationScope) merge( @@ -150,7 +146,6 @@ constructor( fallbackToDetect = true ) } - .flowOn(backgroundDispatcher) .launchIn(applicationScope) deviceEntryFingerprintAuthRepository.isLockedOut @@ -163,7 +158,6 @@ constructor( } } } - .flowOn(backgroundDispatcher) .launchIn(applicationScope) // User switching should stop face auth and then when it is complete we should trigger face @@ -187,7 +181,6 @@ constructor( ) } } - .flowOn(backgroundDispatcher) .launchIn(applicationScope) } @@ -302,7 +295,6 @@ constructor( trustManager.clearAllBiometricRecognized(BiometricSourceType.FACE, userInfo.id) } } - .flowOn(backgroundDispatcher) .onEach { (isAuthenticated, _) -> listeners.forEach { it.onAuthenticatedChanged(isAuthenticated) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt index acfd3b0bcf57..24240dfe7402 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt @@ -30,6 +30,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants +import com.android.systemui.util.kotlin.logD import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -58,11 +59,14 @@ constructor( observer = PreviewLifecycleObserver( - renderer, applicationScope, mainDispatcher, + renderer, ::destroyObserver, ) + + logD(TAG) { "Created observer $observer" } + // Destroy any previous renderer associated with this token. activePreviews[renderer.id]?.let { destroyObserver(it) } activePreviews[renderer.id] = observer @@ -80,6 +84,8 @@ constructor( observer, ) ) + // NOTE: The process on the other side can retain messenger indefinitely. + // (e.g. GC might not trigger and cleanup the reference) val msg = Message.obtain() msg.replyTo = messenger result.putParcelable(KEY_PREVIEW_CALLBACK, msg) @@ -99,57 +105,84 @@ constructor( } } - private class PreviewLifecycleObserver( - private val renderer: KeyguardPreviewRenderer, - private val scope: CoroutineScope, - private val mainDispatcher: CoroutineDispatcher, - private val requestDestruction: (PreviewLifecycleObserver) -> Unit, - ) : Handler.Callback, IBinder.DeathRecipient { + companion object { + internal const val TAG = "KeyguardRemotePreviewManager" + @VisibleForTesting const val KEY_PREVIEW_SURFACE_PACKAGE = "surface_package" + @VisibleForTesting const val KEY_PREVIEW_CALLBACK = "callback" + } +} - private var isDestroyedOrDestroying = false +/** + * Handles messages from the other process and handles cleanup. + * + * NOTE: The other process might hold on to reference of this class indefinitely. It's entirely + * possible that GC won't trigger and we'll leak this for all times even if [onDestroy] was called. + * This helps make sure no non-Singleton objects are retained beyond destruction to prevent leaks. + */ +@VisibleForTesting(VisibleForTesting.PRIVATE) +class PreviewLifecycleObserver( + private val scope: CoroutineScope, + private val mainDispatcher: CoroutineDispatcher, + renderer: KeyguardPreviewRenderer, + onDestroy: (PreviewLifecycleObserver) -> Unit, +) : Handler.Callback, IBinder.DeathRecipient { + + private var isDestroyedOrDestroying = false + // These two are null after destruction + @VisibleForTesting var renderer: KeyguardPreviewRenderer? + @VisibleForTesting var onDestroy: ((PreviewLifecycleObserver) -> Unit)? + + init { + this.renderer = renderer + this.onDestroy = onDestroy + } - override fun handleMessage(message: Message): Boolean { - if (isDestroyedOrDestroying) { - return true - } + override fun handleMessage(message: Message): Boolean { + if (isDestroyedOrDestroying) { + return true + } - when (message.what) { - KeyguardPreviewConstants.MESSAGE_ID_SLOT_SELECTED -> { - message.data.getString(KeyguardPreviewConstants.KEY_SLOT_ID)?.let { slotId -> - renderer.onSlotSelected(slotId = slotId) - } + when (message.what) { + KeyguardPreviewConstants.MESSAGE_ID_SLOT_SELECTED -> { + message.data.getString(KeyguardPreviewConstants.KEY_SLOT_ID)?.let { slotId -> + checkNotNull(renderer).onSlotSelected(slotId = slotId) } - KeyguardPreviewConstants.MESSAGE_ID_HIDE_SMART_SPACE -> { - renderer.hideSmartspace( + } + KeyguardPreviewConstants.MESSAGE_ID_HIDE_SMART_SPACE -> { + checkNotNull(renderer) + .hideSmartspace( message.data.getBoolean(KeyguardPreviewConstants.KEY_HIDE_SMART_SPACE) ) - } - else -> requestDestruction(this) } - - return true + else -> checkNotNull(onDestroy).invoke(this) } - override fun binderDied() { - requestDestruction(this) + return true + } + + override fun binderDied() { + onDestroy?.invoke(this) + } + + fun onDestroy(): Pair<IBinder?, Int>? { + if (isDestroyedOrDestroying) { + return null } - fun onDestroy(): Pair<IBinder?, Int>? { - if (isDestroyedOrDestroying) { - return null - } + logD(TAG) { "Destroying $this" } - isDestroyedOrDestroying = true - val hostToken = renderer.hostToken + isDestroyedOrDestroying = true + return renderer?.let { rendererToDestroy -> + this.renderer = null + this.onDestroy = null + val hostToken = rendererToDestroy.hostToken hostToken?.unlinkToDeath(this, 0) - scope.launch(mainDispatcher) { renderer.destroy() } - return renderer.id + scope.launch(mainDispatcher) { rendererToDestroy.destroy() } + rendererToDestroy.id } } companion object { private const val TAG = "KeyguardRemotePreviewManager" - @VisibleForTesting const val KEY_PREVIEW_SURFACE_PACKAGE = "surface_package" - @VisibleForTesting const val KEY_PREVIEW_CALLBACK = "callback" } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt index 14d4b6800b0a..c87fd14a943d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt @@ -119,7 +119,7 @@ internal constructor( ::AnimatingColorTransition ) - val bgColor = context.getColor(com.google.android.material.R.color.material_dynamic_secondary95) + val bgColor = context.getColor(com.google.android.material.R.color.material_dynamic_neutral20) val surfaceColor = animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor -> val colorList = ColorStateList.valueOf(surfaceColor) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt index a4088f81f062..0434b2d89e32 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles +import android.app.AlertDialog import android.content.Intent import android.os.Handler import android.os.Looper @@ -24,8 +25,11 @@ import android.text.TextUtils import android.view.View import android.widget.Switch import androidx.annotation.VisibleForTesting +import com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN import com.android.internal.logging.MetricsLogger import com.android.systemui.Flags.recordIssueQsTile +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter @@ -36,7 +40,11 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.KeyguardDismissUtil +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject class RecordIssueTile @@ -50,7 +58,11 @@ constructor( metricsLogger: MetricsLogger, statusBarStateController: StatusBarStateController, activityStarter: ActivityStarter, - qsLogger: QSLogger + qsLogger: QSLogger, + private val keyguardDismissUtil: KeyguardDismissUtil, + private val keyguardStateController: KeyguardStateController, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val sysuiDialogFactory: SystemUIDialog.Factory, ) : QSTileImpl<QSTile.BooleanState>( host, @@ -76,11 +88,41 @@ constructor( handlesLongClick = false } - override fun handleClick(view: View?) { - isRecording = !isRecording + @VisibleForTesting + public override fun handleClick(view: View?) { + if (isRecording) { + isRecording = false + } else { + mUiHandler.post { showPrompt(view) } + } refreshState() } + private fun showPrompt(view: View?) { + val dialog: AlertDialog = + RecordIssueDialogDelegate(sysuiDialogFactory) { + isRecording = true + refreshState() + } + .createDialog() + val dismissAction = + ActivityStarter.OnDismissAction { + // We animate from the touched view only if we are not on the keyguard, given + // that if we are we will dismiss it which will also collapse the shade. + if (view != null && !keyguardStateController.isShowing) { + dialogLaunchAnimator.showFromView( + dialog, + view, + DialogCuj(CUJ_SHADE_DIALOG_OPEN, TILE_SPEC) + ) + } else { + dialog.show() + } + false + } + keyguardDismissUtil.executeWhenUnlocked(dismissAction, false, true) + } + override fun getLongClickIntent(): Intent? = null @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt new file mode 100644 index 000000000000..8221c635741b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recordissue + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.Button +import android.widget.PopupMenu +import android.widget.Switch +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog + +class RecordIssueDialogDelegate( + private val factory: SystemUIDialog.Factory, + private val onStarted: Runnable +) : SystemUIDialog.Delegate { + + @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch + private lateinit var issueTypeButton: Button + + override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + dialog.apply { + setView(LayoutInflater.from(context).inflate(R.layout.record_issue_dialog, null)) + setTitle(context.getString(R.string.qs_record_issue_label)) + setIcon(R.drawable.qs_record_issue_icon_off) + setNegativeButton(R.string.cancel) { _, _ -> dismiss() } + setPositiveButton(R.string.qs_record_issue_start) { _, _ -> + onStarted.run() + dismiss() + } + } + } + + override fun createDialog(): SystemUIDialog = factory.create(this) + + override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + dialog.apply { + window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS) + window?.setGravity(Gravity.CENTER) + + screenRecordSwitch = requireViewById(R.id.screenrecord_switch) + issueTypeButton = requireViewById(R.id.issue_type_button) + issueTypeButton.setOnClickListener { onIssueTypeClicked(context) } + } + } + + private fun onIssueTypeClicked(context: Context) { + val selectedCategory = issueTypeButton.text.toString() + val popupMenu = PopupMenu(context, issueTypeButton) + + context.resources.getStringArray(R.array.qs_record_issue_types).forEachIndexed { i, cat -> + popupMenu.menu.add(0, 0, i, cat).apply { + setIcon(R.drawable.arrow_pointing_down) + if (selectedCategory != cat) { + iconTintList = ColorStateList.valueOf(Color.TRANSPARENT) + } + } + } + popupMenu.apply { + setOnMenuItemClickListener { + issueTypeButton.text = it.title + true + } + setForceShowIcon(true) + show() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java index 756c440eca89..0c5472f0ecfb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java @@ -41,6 +41,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.log.core.LogLevel; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -49,6 +50,7 @@ import dagger.Lazy; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Objects; +import java.util.function.Consumer; import javax.inject.Inject; @@ -210,23 +212,33 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum private void notifyKeyguardChanged() { Trace.beginSection("KeyguardStateController#notifyKeyguardChanged"); // Copy the list to allow removal during callback. - new ArrayList<>(mCallbacks).forEach(Callback::onKeyguardShowingChanged); + invokeForEachCallback(Callback::onKeyguardShowingChanged); Trace.endSection(); } private void notifyKeyguardFaceAuthEnabledChanged() { + invokeForEachCallback(Callback::onFaceEnrolledChanged); + } + + private void invokeForEachCallback(Consumer<Callback> consumer) { // Copy the list to allow removal during callback. - new ArrayList<>(mCallbacks).forEach(callback -> { + ArrayList<Callback> copyOfCallbacks = new ArrayList<>(mCallbacks); + for (int i = 0; i < copyOfCallbacks.size(); i++) { + Callback callback = copyOfCallbacks.get(i); + // Temporary fix for b/315731775, callback is null even though only non-null callbacks + // are added to the list by addCallback if (callback != null) { - callback.onFaceEnrolledChanged(); + consumer.accept(callback); + } else { + mLogger.log("KeyguardStateController callback is null", LogLevel.DEBUG); } - }); + } } private void notifyUnlockedChanged() { Trace.beginSection("KeyguardStateController#notifyUnlockedChanged"); // Copy the list to allow removal during callback. - new ArrayList<>(mCallbacks).forEach(Callback::onUnlockedChanged); + invokeForEachCallback(Callback::onUnlockedChanged); Trace.endSection(); } @@ -242,10 +254,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum Trace.traceCounter(Trace.TRACE_TAG_APP, "keyguardFadingAway", keyguardFadingAway ? 1 : 0); mKeyguardFadingAway = keyguardFadingAway; - ArrayList<Callback> callbacks = new ArrayList<>(mCallbacks); - for (int i = 0; i < callbacks.size(); i++) { - callbacks.get(i).onKeyguardFadingAwayChanged(); - } + invokeForEachCallback(Callback::onKeyguardFadingAwayChanged); } } @@ -359,7 +368,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum Trace.traceCounter(Trace.TRACE_TAG_APP, "keyguardGoingAway", keyguardGoingAway ? 1 : 0); mKeyguardGoingAway = keyguardGoingAway; - new ArrayList<>(mCallbacks).forEach(Callback::onKeyguardGoingAwayChanged); + invokeForEachCallback(Callback::onKeyguardGoingAwayChanged); } } @@ -368,7 +377,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum if (mPrimaryBouncerShowing != showing) { mPrimaryBouncerShowing = showing; - new ArrayList<>(mCallbacks).forEach(Callback::onPrimaryBouncerShowingChanged); + invokeForEachCallback(Callback::onPrimaryBouncerShowingChanged); } } @@ -392,13 +401,13 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum boolean dismissingFromTouch) { mDismissAmount = dismissAmount; mDismissingFromTouch = dismissingFromTouch; - new ArrayList<>(mCallbacks).forEach(Callback::onKeyguardDismissAmountChanged); + invokeForEachCallback(Callback::onKeyguardDismissAmountChanged); } @Override public void setLaunchTransitionFadingAway(boolean fadingAway) { mLaunchTransitionFadingAway = fadingAway; - new ArrayList<>(mCallbacks).forEach(Callback::onLaunchTransitionFadingAwayChanged); + invokeForEachCallback(Callback::onLaunchTransitionFadingAwayChanged); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Log.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Log.kt new file mode 100644 index 000000000000..2f6c450e924c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Log.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import android.util.Log + +/** Logs message at [Log.DEBUG] level. Won't call the lambda if [DEBUG] is not loggable. */ +inline fun logD(tag: String, messageLambda: () -> String) { + if (Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, messageLambda.invoke()) + } +} + +/** Logs message at [Log.VERBOSE] level. Won't call the lambda if [VERBOSE] is not loggable. */ +inline fun logV(tag: String, messageLambda: () -> String) { + if (Log.isLoggable(tag, Log.VERBOSE)) { + Log.v(tag, messageLambda.invoke()) + } +} + +/** Logs message at [Log.INFO] level. Won't call the lambda if [INFO] is not loggable. */ +inline fun logI(tag: String, messageLambda: () -> String) { + if (Log.isLoggable(tag, Log.INFO)) { + Log.i(tag, messageLambda.invoke()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt index 4ab8e28bc232..6eb95bddaf53 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt @@ -137,7 +137,6 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { mContext, testScope.backgroundScope, dispatcher, - dispatcher, faceAuthRepository, { PrimaryBouncerInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt index d8199c527cac..e9714dc524ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt @@ -23,6 +23,7 @@ import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -30,12 +31,19 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.KeyguardDismissUtil +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.isA import org.mockito.Mock +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations /** @@ -53,15 +61,22 @@ class RecordIssueTileTest : SysuiTestCase() { @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var qsLogger: QSLogger + @Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var dialogLauncherAnimator: DialogLaunchAnimator + @Mock private lateinit var dialogFactory: SystemUIDialog.Factory + @Mock private lateinit var dialog: SystemUIDialog + private lateinit var testableLooper: TestableLooper private lateinit var tile: RecordIssueTile @Before fun setUp() { MockitoAnnotations.initMocks(this) whenever(host.context).thenReturn(mContext) + whenever(dialogFactory.create(any())).thenReturn(dialog) - val testableLooper = TestableLooper.get(this) + testableLooper = TestableLooper.get(this) tile = RecordIssueTile( host, @@ -72,7 +87,11 @@ class RecordIssueTileTest : SysuiTestCase() { metricsLogger, statusBarStateController, activityStarter, - qsLogger + qsLogger, + keyguardDismissUtil, + keyguardStateController, + dialogLauncherAnimator, + dialogFactory ) } @@ -119,4 +138,18 @@ class RecordIssueTileTest : SysuiTestCase() { assertThat(testState.state).isEqualTo(Tile.STATE_ACTIVE) } + + @Test + fun showPrompt_shouldUseKeyguardDismissUtil_ToShowDialog() { + tile.isRecording = false + tile.handleClick(null) + testableLooper.processAllMessages() + + verify(keyguardDismissUtil) + .executeWhenUnlocked( + isA(ActivityStarter.OnDismissAction::class.java), + eq(false), + eq(true) + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt new file mode 100644 index 000000000000..bbc59d03ac2b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recordissue + +import android.app.Dialog +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.widget.Button +import android.widget.Switch +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.model.SysUiState +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class RecordIssueDialogDelegateTest : SysuiTestCase() { + + private lateinit var dialog: SystemUIDialog + private lateinit var latch: CountDownLatch + + @Before + fun setup() { + val dialogFactory = + SystemUIDialog.Factory( + context, + mock<FeatureFlags>(), + mock<SystemUIDialogManager>(), + mock<SysUiState>().apply { + whenever(setFlag(anyInt(), anyBoolean())).thenReturn(this) + }, + mock<BroadcastDispatcher>(), + mock<DialogLaunchAnimator>() + ) + + latch = CountDownLatch(1) + dialog = RecordIssueDialogDelegate(dialogFactory) { latch.countDown() }.createDialog() + dialog.show() + } + + @After + fun teardown() { + dialog.dismiss() + } + + @Test + fun dialog_hasCorrectUiElements_afterCreation() { + dialog.requireViewById<Switch>(R.id.screenrecord_switch) + dialog.requireViewById<Button>(R.id.issue_type_button) + + assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).text) + .isEqualTo(context.getString(R.string.qs_record_issue_start)) + assertThat(dialog.getButton(Dialog.BUTTON_NEGATIVE).text) + .isEqualTo(context.getString(R.string.cancel)) + } + + @Test + fun onStarted_isCalled_afterStartButtonIsClicked() { + dialog.getButton(Dialog.BUTTON_POSITIVE).callOnClick() + latch.await(1L, TimeUnit.MILLISECONDS) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java index 0f33aaf20195..2c7b60660165 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java @@ -56,6 +56,7 @@ import com.google.common.util.concurrent.Futures; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -138,6 +139,7 @@ public final class AppClipsActivityTest extends SysuiTestCase { } @Test + @Ignore("b/315848285") public void screenshotDisplayed_userConsented_screenshotExportedSuccessfully() { ResultReceiver resultReceiver = createResultReceiver((resultCode, data) -> { assertThat(resultCode).isEqualTo(RESULT_OK); diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index 09e79860b45c..136692eb90d5 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -1426,14 +1426,19 @@ public abstract class PackageManagerInternal { /** * Checks if package is quarantined for a specific user. + * + * @throws PackageManager.NameNotFoundException if the package is not found */ - public abstract boolean isPackageQuarantined(@NonNull String packageName, - @UserIdInt int userId); + public abstract boolean isPackageQuarantined(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException; /** * Checks if package is stopped for a specific user. + * + * @throws PackageManager.NameNotFoundException if the package is not found */ - public abstract boolean isPackageStopped(@NonNull String packageName, @UserIdInt int userId); + public abstract boolean isPackageStopped(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException; /** * Sends the PACKAGE_RESTARTED broadcast. diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 71916843fe0b..df8f17ac9d7c 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -5175,6 +5175,8 @@ public final class ActiveServices { return null; } + final long startTimeNs = SystemClock.elapsedRealtimeNanos(); + if (DEBUG_SERVICE) { Slog.v(TAG_SERVICE, "Bringing up " + r + " " + r.intent + " fg=" + r.fgRequired); } @@ -5333,9 +5335,14 @@ public final class ActiveServices { bringDownServiceLocked(r, enqueueOomAdj); return msg; } + mAm.mProcessList.getAppStartInfoTracker().handleProcessServiceStart(startTimeNs, app, r, + hostingRecord, true); if (isolated) { r.isolationHostProc = app; } + } else { + mAm.mProcessList.getAppStartInfoTracker().handleProcessServiceStart(startTimeNs, app, r, + hostingRecord, false); } if (r.fgRequired) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ac173f3b5b7a..21b2d32cea05 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1103,9 +1103,51 @@ public class ActivityManagerService extends IActivityManager.Stub private final ActivityMetricsLaunchObserver mActivityLaunchObserver = new ActivityMetricsLaunchObserver() { + + @Override + public void onIntentStarted(@NonNull Intent intent, long timestampNanos) { + synchronized (this) { + mProcessList.getAppStartInfoTracker().onIntentStarted(intent, timestampNanos); + } + } + @Override - public void onActivityLaunched(long id, ComponentName name, int temperature) { + public void onIntentFailed(long id) { + mProcessList.getAppStartInfoTracker().onIntentFailed(id); + } + + @Override + public void onActivityLaunched(long id, ComponentName name, int temperature, int userId) { mAppProfiler.onActivityLaunched(); + synchronized (ActivityManagerService.this) { + ProcessRecord record = null; + try { + record = getProcessRecordLocked(name.getPackageName(), mContext + .getPackageManager().getPackageUidAsUser(name.getPackageName(), 0, + userId)); + } catch (NameNotFoundException nnfe) { + // Ignore, record will be lost. + } + mProcessList.getAppStartInfoTracker().onActivityLaunched(id, name, temperature, + record); + } + } + + @Override + public void onActivityLaunchCancelled(long id) { + mProcessList.getAppStartInfoTracker().onActivityLaunchCancelled(id); + } + + @Override + public void onActivityLaunchFinished(long id, ComponentName name, long timestampNanos, + int launchMode) { + mProcessList.getAppStartInfoTracker().onActivityLaunchFinished(id, name, + timestampNanos, launchMode); + } + + @Override + public void onReportFullyDrawn(long id, long timestampNanos) { + mProcessList.getAppStartInfoTracker().onReportFullyDrawn(id, timestampNanos); } }; @@ -4488,13 +4530,13 @@ public class ActivityManagerService extends IActivityManager.Stub @GuardedBy("this") private void attachApplicationLocked(@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq) { - // Find the application record that is being attached... either via // the pid if we are running in multiple processes, or just pull the // next app record if we are emulating process with anonymous threads. ProcessRecord app; long startTime = SystemClock.uptimeMillis(); long bindApplicationTimeMillis; + long bindApplicationTimeNanos; if (pid != MY_PID && pid >= 0) { synchronized (mPidsSelfLocked) { app = mPidsSelfLocked.get(pid); @@ -4698,6 +4740,7 @@ public class ActivityManagerService extends IActivityManager.Stub checkTime(startTime, "attachApplicationLocked: immediately before bindApplication"); bindApplicationTimeMillis = SystemClock.uptimeMillis(); + bindApplicationTimeNanos = SystemClock.elapsedRealtimeNanos(); mAtmInternal.preBindApplication(app.getWindowProcessController()); final ActiveInstrumentation instr2 = app.getActiveInstrumentation(); if (mPlatformCompat != null) { @@ -4754,6 +4797,8 @@ public class ActivityManagerService extends IActivityManager.Stub } app.setBindApplicationTime(bindApplicationTimeMillis); + mProcessList.getAppStartInfoTracker() + .reportBindApplicationTimeNanos(app, bindApplicationTimeNanos); // Make app active after binding application or client may be running requests (e.g // starting activities) before it is ready. @@ -9799,12 +9844,12 @@ public class ActivityManagerService extends IActivityManager.Stub final int uid = enforceDumpPermissionForPackage(packageName, userId, callingUid, "getHistoricalProcessStartReasons"); if (uid != INVALID_UID) { - mProcessList.mAppStartInfoTracker.getStartInfo( + mProcessList.getAppStartInfoTracker().getStartInfo( packageName, userId, callingPid, maxNum, results); } } else { // If no package name is given, use the caller's uid as the filter uid. - mProcessList.mAppStartInfoTracker.getStartInfo( + mProcessList.getAppStartInfoTracker().getStartInfo( packageName, callingUid, callingPid, maxNum, results); } return new ParceledListSlice<ApplicationStartInfo>(results); @@ -9822,7 +9867,7 @@ public class ActivityManagerService extends IActivityManager.Stub } final int callingUid = Binder.getCallingUid(); - mProcessList.mAppStartInfoTracker.addStartInfoCompleteListener(listener, callingUid); + mProcessList.getAppStartInfoTracker().addStartInfoCompleteListener(listener, callingUid); } @@ -9836,7 +9881,7 @@ public class ActivityManagerService extends IActivityManager.Stub } final int callingUid = Binder.getCallingUid(); - mProcessList.mAppStartInfoTracker.clearStartInfoCompleteListener(callingUid, true); + mProcessList.getAppStartInfoTracker().clearStartInfoCompleteListener(callingUid, true); } @Override @@ -10138,7 +10183,7 @@ public class ActivityManagerService extends IActivityManager.Stub pw.println(); if (dumpAll) { pw.println("-------------------------------------------------------------------------------"); - mProcessList.mAppStartInfoTracker.dumpHistoryProcessStartInfo(pw, dumpPackage); + mProcessList.getAppStartInfoTracker().dumpHistoryProcessStartInfo(pw, dumpPackage); pw.println("-------------------------------------------------------------------------------"); mProcessList.mAppExitInfoTracker.dumpHistoryProcessExitInfo(pw, dumpPackage); } @@ -10541,7 +10586,7 @@ public class ActivityManagerService extends IActivityManager.Stub dumpPackage = args[opti]; opti++; } - mProcessList.mAppStartInfoTracker.dumpHistoryProcessStartInfo(pw, dumpPackage); + mProcessList.getAppStartInfoTracker().dumpHistoryProcessStartInfo(pw, dumpPackage); } else if ("exit-info".equals(cmd)) { if (opti < args.length) { dumpPackage = args[opti]; @@ -13831,6 +13876,7 @@ public class ActivityManagerService extends IActivityManager.Stub // activity manager to announce its creation. public boolean bindBackupAgent(String packageName, int backupMode, int targetUserId, @BackupDestination int backupDestination) { + long startTimeNs = SystemClock.elapsedRealtimeNanos(); if (DEBUG_BACKUP) { Slog.v(TAG, "bindBackupAgent: app=" + packageName + " mode=" + backupMode + " targetUserId=" + targetUserId + " callingUid = " + Binder.getCallingUid() @@ -13906,15 +13952,20 @@ public class ActivityManagerService extends IActivityManager.Stub ? new ComponentName(app.packageName, app.backupAgentName) : new ComponentName("android", "FullBackupAgent"); - // startProcessLocked() returns existing proc's record if it's already running - ProcessRecord proc = startProcessLocked(app.processName, app, - false, 0, - new HostingRecord(HostingRecord.HOSTING_TYPE_BACKUP, hostingName), - ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS, false, false); + ProcessRecord proc = getProcessRecordLocked(app.processName, app.uid); + boolean isProcessStarted = proc != null; + if (!isProcessStarted) { + proc = startProcessLocked(app.processName, app, + false, 0, + new HostingRecord(HostingRecord.HOSTING_TYPE_BACKUP, hostingName), + ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS, false, false); + } if (proc == null) { Slog.e(TAG, "Unable to start backup agent process " + r); return false; } + mProcessList.getAppStartInfoTracker().handleProcessBackupStart(startTimeNs, proc, r, + !isProcessStarted); // If the app is a regular app (uid >= 10000) and not the system server or phone // process, etc, then mark it as being in full backup so that certain calls to the @@ -18741,8 +18792,12 @@ public class ActivityManagerService extends IActivityManager.Stub // If the process is known as top app, set a hint so when the process is // started, the top priority can be applied immediately to avoid cpu being // preempted by other processes before attaching the process of top app. - startProcessLocked(processName, info, knownToBeDead, 0 /* intentFlags */, - new HostingRecord(hostingType, hostingName, isTop), + final long startTimeNs = SystemClock.elapsedRealtimeNanos(); + HostingRecord hostingRecord = + new HostingRecord(hostingType, hostingName, isTop); + ProcessRecord rec = getProcessRecordLocked(processName, info.uid); + ProcessRecord app = startProcessLocked(processName, info, knownToBeDead, + 0 /* intentFlags */, hostingRecord, ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE, false /* allowWhileBooting */, false /* isolated */); } diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index f3b2ef34ad6d..ae0cd65b2770 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -1362,7 +1362,7 @@ final class ActivityManagerShellCommand extends ShellCommand { } userId = user.id; } - mInternal.mProcessList.mAppStartInfoTracker + mInternal.mProcessList.getAppStartInfoTracker() .clearHistoryProcessStartInfo(packageName, userId); return 0; } diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index edca74fae0e4..82e554e67b7e 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -22,11 +22,12 @@ import static android.os.Process.THREAD_PRIORITY_BACKGROUND; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; -import android.app.ActivityOptions; +import android.annotation.NonNull; import android.app.ApplicationStartInfo; import android.app.Flags; import android.app.IApplicationStartInfoCompleteListener; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -138,6 +139,15 @@ public final class AppStartInfoTracker { /** The path to the historical proc start info file, persisted in the storage. */ @VisibleForTesting File mProcStartInfoFile; + + /** + * Temporary list of records that have not been completed. + * + * Key is timestamp of launch from {@link #ActivityMetricsLaunchObserver}. + */ + @GuardedBy("mLock") + private ArrayMap<Long, ApplicationStartInfo> mInProgRecords = new ArrayMap<>(); + AppStartInfoTracker() { mCallbacks = new SparseArray<>(); mData = new ProcessMap<AppStartInfoContainer>(); @@ -174,68 +184,99 @@ public final class AppStartInfoTracker { }); } - void handleProcessColdStarted(long startTimeNs, HostingRecord hostingRecord, - ProcessRecord app) { + void onIntentStarted(@NonNull Intent intent, long timestampNanos) { synchronized (mLock) { if (!mEnabled) { return; } ApplicationStartInfo start = new ApplicationStartInfo(); - addBaseFieldsFromProcessRecord(start, app); start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED); - start.addStartupTimestamp( - ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs); - start.addStartupTimestamp( - ApplicationStartInfo.START_TIMESTAMP_FORK, app.getStartElapsedTime()); - start.setStartType(ApplicationStartInfo.START_TYPE_COLD); - start.setReason(ApplicationStartInfo.START_REASON_OTHER); - addStartInfoLocked(start); + start.setIntent(intent); + start.setStartType(ApplicationStartInfo.START_TYPE_UNSET); + start.addStartupTimestamp(ApplicationStartInfo.START_TIMESTAMP_LAUNCH, timestampNanos); + if (intent != null && intent.getCategories() != null + && intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { + start.setReason(ApplicationStartInfo.START_REASON_LAUNCHER); + } else { + start.setReason(ApplicationStartInfo.START_REASON_START_ACTIVITY); + } + mInProgRecords.put(timestampNanos, start); } } - public void handleProcessActivityWarmOrHotStarted(long startTimeNs, - ActivityOptions activityOptions, Intent intent) { + void onIntentFailed(long id) { synchronized (mLock) { if (!mEnabled) { return; } - ApplicationStartInfo start = new ApplicationStartInfo(); - start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED); - start.addStartupTimestamp( - ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs); - start.setIntent(intent); - start.setReason(ApplicationStartInfo.START_REASON_LAUNCHER); - if (activityOptions != null) { - start.setProcessName(activityOptions.getPackageName()); + if (!mInProgRecords.containsKey(id)) { + return; } - start.setStartType(ApplicationStartInfo.START_TYPE_WARM); - if (intent != null && intent.getCategories() != null - && intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { - start.setReason(ApplicationStartInfo.START_REASON_LAUNCHER); + mInProgRecords.get(id).setStartupState(ApplicationStartInfo.STARTUP_STATE_ERROR); + mInProgRecords.remove(id); + } + } + + void onActivityLaunched(long id, ComponentName name, long temperature, ProcessRecord app) { + synchronized (mLock) { + if (!mEnabled) { + return; + } + if (!mInProgRecords.containsKey(id)) { + return; + } + if (app != null) { + ApplicationStartInfo info = mInProgRecords.get(id); + info.setStartType((int) temperature); + addBaseFieldsFromProcessRecord(info, app); + addStartInfoLocked(info); } else { - start.setReason(ApplicationStartInfo.START_REASON_START_ACTIVITY); + mInProgRecords.remove(id); } - addStartInfoLocked(start); } } - public void handleProcessActivityStartedFromRecents(long startTimeNs, - ActivityOptions activityOptions) { + void onActivityLaunchCancelled(long id) { synchronized (mLock) { if (!mEnabled) { return; } - ApplicationStartInfo start = new ApplicationStartInfo(); - start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED); - start.addStartupTimestamp( - ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs); - if (activityOptions != null) { - start.setIntent(activityOptions.getResultData()); - start.setProcessName(activityOptions.getPackageName()); + if (!mInProgRecords.containsKey(id)) { + return; } - start.setReason(ApplicationStartInfo.START_REASON_LAUNCHER_RECENTS); - start.setStartType(ApplicationStartInfo.START_TYPE_WARM); - addStartInfoLocked(start); + ApplicationStartInfo info = mInProgRecords.get(id); + info.setStartupState(ApplicationStartInfo.STARTUP_STATE_ERROR); + mInProgRecords.remove(id); + } + } + + void onActivityLaunchFinished(long id, ComponentName name, long timestampNanos, + int launchMode) { + synchronized (mLock) { + if (!mEnabled) { + return; + } + if (!mInProgRecords.containsKey(id)) { + return; + } + ApplicationStartInfo info = mInProgRecords.get(id); + info.setStartupState(ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN); + info.setLaunchMode(launchMode); + } + } + + void onReportFullyDrawn(long id, long timestampNanos) { + synchronized (mLock) { + if (!mEnabled) { + return; + } + if (!mInProgRecords.containsKey(id)) { + return; + } + ApplicationStartInfo info = mInProgRecords.get(id); + info.addStartupTimestamp(ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN, + timestampNanos); + mInProgRecords.remove(id); } } @@ -347,7 +388,8 @@ public final class AppStartInfoTracker { ApplicationStartInfo.START_TIMESTAMP_APPLICATION_ONCREATE); } - void reportBindApplicationTimeNanos(ProcessRecord app, long timeNs) { + /** Report a bind application timestamp to add to {@link ApplicationStartInfo}. */ + public void reportBindApplicationTimeNanos(ProcessRecord app, long timeNs) { addTimestampToStart(app, timeNs, ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION); } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 4ff34b1d7faa..3156e9da0ae9 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -498,7 +498,7 @@ public final class ProcessList { /** Manages the {@link android.app.ApplicationStartInfo} records. */ @GuardedBy("mAppStartInfoTracker") - final AppStartInfoTracker mAppStartInfoTracker = new AppStartInfoTracker(); + private final AppStartInfoTracker mAppStartInfoTracker = new AppStartInfoTracker(); /** * The currently running SDK sandbox processes for a uid. @@ -1523,6 +1523,10 @@ public final class ProcessList { return mCachedRestoreLevel; } + AppStartInfoTracker getAppStartInfoTracker() { + return mAppStartInfoTracker; + } + /** * Set the out-of-memory badness adjustment for a process. * If {@code pid <= 0}, this method will be a no-op. @@ -2572,6 +2576,7 @@ public final class ProcessList { boolean isSdkSandbox, int sdkSandboxUid, String sdkSandboxClientAppPackage, String abiOverride, String entryPoint, String[] entryPointArgs, Runnable crashHandler) { long startTime = SystemClock.uptimeMillis(); + final long startTimeNs = SystemClock.elapsedRealtimeNanos(); ProcessRecord app; if (!isolated) { app = getProcessRecordLocked(processName, info.uid); diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index 1e5e147749a2..df179a9b6d65 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -1294,8 +1294,8 @@ public class SyncManager { if (android.content.pm.Flags.stayStopped()) { try { return mPackageManagerInternal.isPackageStopped(packageName, userId); - } catch (IllegalArgumentException e) { - Log.d(TAG, "Couldn't determine stopped state for unknown package: " + packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Couldn't determine stopped state for unknown package: " + packageName); } } return false; diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 168715713f8d..5831b29437dc 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -194,10 +194,14 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() { @Override public void onComplete(int result) { - if (result != HdmiControlManager.RESULT_SUCCESS) { + if (!mService.getLocalActiveSource().isValid() + && result != HdmiControlManager.RESULT_SUCCESS) { mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource( getDeviceInfo().getLogicalAddress(), getDeviceInfo().getPhysicalAddress())); + updateActiveSource(getDeviceInfo().getLogicalAddress(), + getDeviceInfo().getPhysicalAddress(), + "RequestActiveSourceAction#finishWithCallback()"); } } })); @@ -257,6 +261,7 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { if (isAlreadyActiveSource(targetDevice, targetAddress, callback)) { return; } + removeAction(RequestActiveSourceAction.class); if (targetAddress == Constants.ADDR_INTERNAL) { handleSelectInternalSource(); // Switching to internal source is always successful even when CEC control is disabled. diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index f526dbe9c66d..4089a81dfc20 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -215,10 +215,20 @@ public abstract class InputMethodManagerInternal { /** * Switch the keyboard layout in response to a keyboard shortcut. * - * @param direction {@code 1} to switch to the next subtype, {@code -1} to switch to the - * previous subtype + * @param direction {@code 1} to switch to the next subtype, {@code -1} to switch to the + * previous subtype + * @param displayId the display to which the keyboard layout switch shortcut is + * dispatched. Note that there is no guarantee that an IME is + * associated with this display. This is more or less than a hint for + * cases when no IME is running for the given targetWindowToken. There + * is a longstanding discussion whether we should allow users to + * rotate keyboard layout even when there is no edit field, and this + * displayID would be helpful for such a situation. + * @param targetWindowToken the window token to which other keys are being sent while handling + * this shortcut. */ - public abstract void switchKeyboardLayout(int direction); + public abstract void onSwitchKeyboardLayoutShortcut(int direction, int displayId, + IBinder targetWindowToken); /** * Returns true if any InputConnection is currently active. @@ -314,7 +324,8 @@ public abstract class InputMethodManagerInternal { } @Override - public void switchKeyboardLayout(int direction) { + public void onSwitchKeyboardLayoutShortcut(int direction, int displayId, + IBinder targetWindowToken) { } @Override diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 30e9f5bd2a14..c440a6461de2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -5763,7 +5763,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @Override - public void switchKeyboardLayout(int direction) { + public void onSwitchKeyboardLayoutShortcut(int direction, int displayId, + IBinder targetWindowToken) { synchronized (ImfLock.class) { switchKeyboardLayoutLocked(direction); } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 42c2548264d4..0c2eee5aebbe 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -209,7 +209,7 @@ import javax.crypto.spec.GCMParameterSpec; * <li>Protect each user's data using their SP. For example, use the SP to encrypt/decrypt the * user's credential-encrypted (CE) key for file-based encryption (FBE).</li> * - * <li>Generate, protect, and use profile passwords for managed profiles.</li> + * <li>Generate, protect, and use unified profile passwords.</li> * * <li>Support unlocking the SP by alternative means: resume-on-reboot (reboot escrow) for easier * OTA updates, and escrow tokens when set up by the Device Policy Controller (DPC).</li> @@ -287,7 +287,7 @@ public class LockSettingsService extends ILockSettings.Stub { private final java.security.KeyStore mJavaKeyStore; private final RecoverableKeyStoreManager mRecoverableKeyStoreManager; - private ManagedProfilePasswordCache mManagedProfilePasswordCache; + private final UnifiedProfilePasswordCache mUnifiedProfilePasswordCache; private final RebootEscrowManager mRebootEscrowManager; @@ -404,7 +404,8 @@ public class LockSettingsService extends ILockSettings.Stub { for (int i = 0; i < newPasswordChars.length; i++) { newPassword[i] = (byte) newPasswordChars[i]; } - LockscreenCredential credential = LockscreenCredential.createManagedPassword(newPassword); + LockscreenCredential credential = + LockscreenCredential.createUnifiedProfilePassword(newPassword); Arrays.fill(newPasswordChars, '\u0000'); Arrays.fill(newPassword, (byte) 0); Arrays.fill(randomLockSeed, (byte) 0); @@ -424,7 +425,7 @@ public class LockSettingsService extends ILockSettings.Stub { if (!isCredentialSharableWithParent(profileUserId)) { return; } - // Do not tie profile when work challenge is enabled + // Do not tie profile when separate challenge is enabled if (getSeparateProfileChallengeEnabledInternal(profileUserId)) { return; } @@ -462,7 +463,7 @@ public class LockSettingsService extends ILockSettings.Stub { setLockCredentialInternal(unifiedProfilePassword, profileUserPassword, profileUserId, /* isLockTiedToParent= */ true); tieProfileLockToParent(profileUserId, parent.id, unifiedProfilePassword); - mManagedProfilePasswordCache.storePassword(profileUserId, unifiedProfilePassword, + mUnifiedProfilePasswordCache.storePassword(profileUserId, unifiedProfilePassword, parentSid); } } @@ -620,9 +621,9 @@ public class LockSettingsService extends ILockSettings.Stub { } } - public @NonNull ManagedProfilePasswordCache getManagedProfilePasswordCache( + public @NonNull UnifiedProfilePasswordCache getUnifiedProfilePasswordCache( java.security.KeyStore ks) { - return new ManagedProfilePasswordCache(ks); + return new UnifiedProfilePasswordCache(ks); } public boolean isHeadlessSystemUserMode() { @@ -665,7 +666,7 @@ public class LockSettingsService extends ILockSettings.Stub { mGatekeeperPasswords = new LongSparseArray<>(); mSpManager = injector.getSyntheticPasswordManager(mStorage); - mManagedProfilePasswordCache = injector.getManagedProfilePasswordCache(mJavaKeyStore); + mUnifiedProfilePasswordCache = injector.getUnifiedProfilePasswordCache(mJavaKeyStore); mBiometricDeferredQueue = new BiometricDeferredQueue(mSpManager, mHandler); mRebootEscrowManager = injector.getRebootEscrowManager(new RebootEscrowCallbacks(), @@ -689,8 +690,8 @@ public class LockSettingsService extends ILockSettings.Stub { } /** - * If the account is credential-encrypted, show notification requesting the user to unlock the - * device. + * If the user is a managed profile whose credential-encrypted storage is locked, show a + * notification requesting the user to unlock the device. */ private void maybeShowEncryptionNotificationForUser(@UserIdInt int userId, String reason) { final UserInfo user = mUserManager.getUserInfo(userId); @@ -846,7 +847,7 @@ public class LockSettingsService extends ILockSettings.Stub { mHandler.post(new Runnable() { @Override public void run() { - // Hide notification first, as tie managed profile lock takes time + // Hide notification first, as tie profile lock takes time hideEncryptionNotification(new UserHandle(userId)); if (isCredentialSharableWithParent(userId)) { @@ -1458,13 +1459,13 @@ public class LockSettingsService extends ILockSettings.Stub { cipher.init(Cipher.DECRYPT_MODE, decryptionKey, new GCMParameterSpec(128, iv)); decryptionResult = cipher.doFinal(encryptedPassword); - LockscreenCredential credential = LockscreenCredential.createManagedPassword( + LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword( decryptionResult); Arrays.fill(decryptionResult, (byte) 0); try { long parentSid = getGateKeeperService().getSecureUserId( mUserManager.getProfileParent(userId).id); - mManagedProfilePasswordCache.storePassword(userId, credential, parentSid); + mUnifiedProfilePasswordCache.storePassword(userId, credential, parentSid); } catch (RemoteException e) { Slogf.w(TAG, "Failed to talk to GateKeeper service", e); } @@ -1550,7 +1551,7 @@ public class LockSettingsService extends ILockSettings.Stub { // so it goes into the cache getDecryptedPasswordForTiedProfile(profile.id); } catch (GeneralSecurityException | IOException e) { - Slog.d(TAG, "Cache work profile password failed", e); + Slog.d(TAG, "Cache unified profile password failed", e); } } } @@ -1604,19 +1605,19 @@ public class LockSettingsService extends ILockSettings.Stub { } /** - * Synchronize all profile's work challenge of the given user if it's unified: tie or clear them + * Synchronize all profile's challenge of the given user if it's unified: tie or clear them * depending on the parent user's secure state. * - * When clearing tied work challenges, a pre-computed password table for profiles are required, - * since changing password for profiles requires existing password, and existing passwords can - * only be computed before the parent user's password is cleared. + * When clearing tied challenges, a pre-computed password table for profiles are required, since + * changing password for profiles requires existing password, and existing passwords can only be + * computed before the parent user's password is cleared. * * Strictly this is a recursive function, since setLockCredentialInternal ends up calling this * method again on profiles. However the recursion is guaranteed to terminate as this method * terminates when the user is a profile that shares lock credentials with parent. * (e.g. managed and clone profile). */ - private void synchronizeUnifiedWorkChallengeForProfiles(int userId, + private void synchronizeUnifiedChallengeForProfiles(int userId, Map<Integer, LockscreenCredential> profilePasswordMap) { if (isCredentialSharableWithParent(userId)) { return; @@ -1635,7 +1636,7 @@ public class LockSettingsService extends ILockSettings.Stub { tieProfileLockIfNecessary(profileUserId, LockscreenCredential.createNone()); } else { - // We use cached work profile password computed before clearing the parent's + // We use cached profile password computed before clearing the parent's // credential, otherwise they get lost if (profilePasswordMap != null && profilePasswordMap.containsKey(profileUserId)) { @@ -1777,7 +1778,7 @@ public class LockSettingsService extends ILockSettings.Stub { notifyPasswordChanged(credential, userId); } if (isCredentialSharableWithParent(userId)) { - // Make sure the profile doesn't get locked straight after setting work challenge. + // Make sure the profile doesn't get locked straight after setting challenge. setDeviceUnlockedForUser(userId); } notifySeparateProfileChallengeChanged(userId); @@ -2368,7 +2369,7 @@ public class LockSettingsService extends ILockSettings.Stub { } try { - // Unlock work profile, and work profile with unified lock must use password only + // Unlock profile with unified lock return doVerifyCredential(getDecryptedPasswordForTiedProfile(userId), userId, null /* progressCallback */, flags); } catch (UnrecoverableKeyException | InvalidKeyException | KeyStoreException @@ -2492,7 +2493,7 @@ public class LockSettingsService extends ILockSettings.Stub { mStrongAuth.removeUser(userId); AndroidKeyStoreMaintenance.onUserRemoved(userId); - mManagedProfilePasswordCache.removePassword(userId); + mUnifiedProfilePasswordCache.removePassword(userId); gateKeeperClearSecureUserId(userId); removeKeystoreProfileKey(userId); @@ -2982,7 +2983,7 @@ public class LockSettingsService extends ILockSettings.Stub { credential, sp, userId); final Map<Integer, LockscreenCredential> profilePasswords; if (!credential.isNone()) { - // not needed by synchronizeUnifiedWorkChallengeForProfiles() + // not needed by synchronizeUnifiedChallengeForProfiles() profilePasswords = null; if (!mSpManager.hasSidForUser(userId)) { @@ -2993,8 +2994,8 @@ public class LockSettingsService extends ILockSettings.Stub { } } } else { - // Cache all profile password if they use unified work challenge. This will later be - // used to clear the profile's password in synchronizeUnifiedWorkChallengeForProfiles() + // Cache all profile password if they use unified challenge. This will later be used to + // clear the profile's password in synchronizeUnifiedChallengeForProfiles(). profilePasswords = getDecryptedPasswordsForAllTiedProfiles(userId); mSpManager.clearSidForUser(userId); @@ -3010,10 +3011,10 @@ public class LockSettingsService extends ILockSettings.Stub { } setCurrentLskfBasedProtectorId(newProtectorId, userId); LockPatternUtils.invalidateCredentialTypeCache(); - synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords); + synchronizeUnifiedChallengeForProfiles(userId, profilePasswords); setUserPasswordMetrics(credential, userId); - mManagedProfilePasswordCache.removePassword(userId); + mUnifiedProfilePasswordCache.removePassword(userId); if (savedCredentialType != CREDENTIAL_TYPE_NONE) { mSpManager.destroyAllWeakTokenBasedProtectors(userId); } @@ -3114,7 +3115,7 @@ public class LockSettingsService extends ILockSettings.Stub { try { currentCredential = getDecryptedPasswordForTiedProfile(userId); } catch (Exception e) { - Slog.e(TAG, "Failed to get work profile credential", e); + Slog.e(TAG, "Failed to get unified profile password", e); return null; } } @@ -3284,7 +3285,7 @@ public class LockSettingsService extends ILockSettings.Stub { @Override public boolean tryUnlockWithCachedUnifiedChallenge(int userId) { checkPasswordReadPermission(); - try (LockscreenCredential cred = mManagedProfilePasswordCache.retrievePassword(userId)) { + try (LockscreenCredential cred = mUnifiedProfilePasswordCache.retrievePassword(userId)) { if (cred == null) { return false; } @@ -3296,7 +3297,7 @@ public class LockSettingsService extends ILockSettings.Stub { @Override public void removeCachedUnifiedChallenge(int userId) { checkWritePermission(); - mManagedProfilePasswordCache.removePassword(userId); + mUnifiedProfilePasswordCache.removePassword(userId); } static String timestampToString(long timestamp) { diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java index 1e8b387fc189..6d123ccebc7c 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java @@ -501,10 +501,10 @@ class LockSettingsStorage { final UserInfo parentInfo = um.getProfileParent(userId); if (parentInfo == null) { - // This user owns its lock settings files - safe to delete them + // Delete files specific to non-profile users. deleteFile(getRebootEscrowFile(userId)); } else { - // Managed profile + // Delete files specific to profile users. removeChildProfileLock(userId); } diff --git a/services/core/java/com/android/server/locksettings/ManagedProfilePasswordCache.java b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java index 1298fe8f07a4..21caf76d30d0 100644 --- a/services/core/java/com/android/server/locksettings/ManagedProfilePasswordCache.java +++ b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java @@ -43,30 +43,31 @@ import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; /** - * Caches *unified* work challenge for managed profiles. The cached credential is encrypted using - * a keystore key auth-bound to the parent user's lockscreen credential, similar to how unified - * work challenge is normally secured. - * - * <p> The cache is filled whenever the managed profile's unified challenge is created or derived - * (as part of the parent user's credential verification flow). It's removed when the profile is - * deleted or a (separate) lockscreen credential is explicitly set on the profile. There is also - * an ADB command to evict the cache "cmd lock_settings remove-cache --user X", to assist - * development and testing. - - * <p> The encrypted credential is stored in-memory only so the cache does not persist across - * reboots. + * An in-memory cache for unified profile passwords. A "unified profile password" is the random + * password that the system automatically generates and manages for each profile that uses a unified + * challenge and where the parent user has a secure lock screen. + * <p> + * Each password in this cache is encrypted by a Keystore key that is auth-bound to the parent user. + * This is very similar to how the password is protected on-disk, but the in-memory cache uses a + * much longer timeout on the keys: 7 days instead of 30 seconds. This enables use cases like + * unpausing work apps without requiring authentication as frequently. + * <p> + * Unified profile passwords are cached when they are created, or when they are decrypted as part of + * the parent user's LSKF verification flow. They are removed when the profile is deleted or when a + * separate challenge is explicitly set on the profile. There is also an ADB command to evict a + * cached password, "locksettings remove-cache --user X", to assist development and testing. */ @VisibleForTesting // public visibility is needed for Mockito -public class ManagedProfilePasswordCache { +public class UnifiedProfilePasswordCache { - private static final String TAG = "ManagedProfilePasswordCache"; + private static final String TAG = "UnifiedProfilePasswordCache"; private static final int KEY_LENGTH = 256; private static final int CACHE_TIMEOUT_SECONDS = (int) TimeUnit.DAYS.toSeconds(7); private final SparseArray<byte[]> mEncryptedPasswords = new SparseArray<>(); private final KeyStore mKeyStore; - public ManagedProfilePasswordCache(KeyStore keyStore) { + public UnifiedProfilePasswordCache(KeyStore keyStore) { mKeyStore = keyStore; } @@ -151,7 +152,8 @@ public class ManagedProfilePasswordCache { Slog.d(TAG, "Cannot decrypt", e); return null; } - LockscreenCredential result = LockscreenCredential.createManagedPassword(credential); + LockscreenCredential result = + LockscreenCredential.createUnifiedProfilePassword(credential); Arrays.fill(credential, (byte) 0); return result; } diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 7ec94c315798..3f8b5952a1bc 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -627,6 +627,7 @@ public class ZenModeHelper { try { ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0); rule.name = applicationInfo.loadLabel(mPm).toString(); + rule.iconResName = drawableResIdToResName(pkg, applicationInfo.icon); } catch (PackageManager.NameNotFoundException e) { // Should not happen, since it's the app calling us (?) Log.w(TAG, "Package not found for creating implicit zen rule"); @@ -634,6 +635,9 @@ public class ZenModeHelper { } }); + rule.type = AutomaticZenRule.TYPE_OTHER; + rule.triggerDescription = mContext.getString(R.string.zen_mode_implicit_trigger_description, + rule.name); rule.condition = null; rule.conditionId = new Uri.Builder() .scheme(Condition.SCHEME) diff --git a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java index f985b5b64e21..b9b09fb0e84c 100644 --- a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java +++ b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java @@ -754,9 +754,9 @@ public class PersistentDataBlockService extends SystemService { } }; - private PersistentDataBlockManagerInternal mInternalService = - new PersistentDataBlockManagerInternal() { + private InternalService mInternalService = new InternalService(); + private class InternalService implements PersistentDataBlockManagerInternal { @Override public void setFrpCredentialHandle(byte[] handle) { writeInternal(handle, getFrpCredentialDataOffset(), MAX_FRP_CREDENTIAL_HANDLE_SIZE); diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index bd725ed49f3c..27f4e11c53ad 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -508,12 +508,15 @@ public interface Computer extends PackageDataSnapshot { boolean getApplicationHiddenSettingAsUser(@NonNull String packageName, @UserIdInt int userId); - boolean isPackageSuspendedForUser(@NonNull String packageName, @UserIdInt int userId); + boolean isPackageSuspendedForUser(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException; - boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId); + boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException; /** Check if the package is in a stopped state for a given user. */ - boolean isPackageStoppedForUser(@NonNull String packageName, @UserIdInt int userId); + boolean isPackageStoppedForUser(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException; boolean isSuspendingAnyPackages(@NonNull String suspendingPackage, @UserIdInt int userId); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 2dc3fb7464fd..abfd5715810e 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -4980,31 +4980,34 @@ public class ComputerEngine implements Computer { } } - private PackageUserStateInternal getUserStageOrDefaultForUser(@NonNull String packageName, - int userId) { + private PackageUserStateInternal getUserStateOrDefaultForUser(@NonNull String packageName, + int userId) throws PackageManager.NameNotFoundException { final int callingUid = Binder.getCallingUid(); enforceCrossUserPermission(callingUid, userId, true /* requireFullPermission */, false /* checkShell */, "when asking about packages for user " + userId); final PackageStateInternal ps = mSettings.getPackage(packageName); if (ps == null || shouldFilterApplicationIncludingUninstalled(ps, callingUid, userId)) { - throw new IllegalArgumentException("Unknown target package: " + packageName); + throw new PackageManager.NameNotFoundException(packageName); } return ps.getUserStateOrDefault(userId); } @Override - public boolean isPackageSuspendedForUser(@NonNull String packageName, int userId) { - return getUserStageOrDefaultForUser(packageName, userId).isSuspended(); + public boolean isPackageSuspendedForUser(@NonNull String packageName, int userId) + throws PackageManager.NameNotFoundException { + return getUserStateOrDefaultForUser(packageName, userId).isSuspended(); } @Override - public boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId) { - return getUserStageOrDefaultForUser(packageName, userId).isQuarantined(); + public boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException { + return getUserStateOrDefaultForUser(packageName, userId).isQuarantined(); } @Override - public boolean isPackageStoppedForUser(@NonNull String packageName, @UserIdInt int userId) { - return getUserStageOrDefaultForUser(packageName, userId).isStopped(); + public boolean isPackageStoppedForUser(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException { + return getUserStateOrDefaultForUser(packageName, userId).isStopped(); } @Override diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java index 24a33f1949fa..e3bbd2d8d17e 100644 --- a/services/core/java/com/android/server/pm/IPackageManagerBase.java +++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java @@ -951,20 +951,32 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @Deprecated public final boolean isPackageSuspendedForUser(@NonNull String packageName, @UserIdInt int userId) { - return snapshot().isPackageSuspendedForUser(packageName, userId); + try { + return snapshot().isPackageSuspendedForUser(packageName, userId); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("Unknown target package: " + packageName); + } } @Override @Deprecated public final boolean isPackageQuarantinedForUser(@NonNull String packageName, @UserIdInt int userId) { - return snapshot().isPackageQuarantinedForUser(packageName, userId); + try { + return snapshot().isPackageQuarantinedForUser(packageName, userId); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("Unknown target package: " + packageName); + } } @Override public final boolean isPackageStoppedForUser(@NonNull String packageName, @UserIdInt int userId) { - return snapshot().isPackageStoppedForUser(packageName, userId); + try { + return snapshot().isPackageStoppedForUser(packageName, userId); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("Unknown target package: " + packageName); + } } @Override diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index f1c062763183..9b8ee741476e 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2450,9 +2450,9 @@ final class InstallPackageHelper { // Hardcode previousAppId to 0 to disable any data migration (http://b/221088088) mAppDataHelper.prepareAppDataPostCommitLIF(ps, 0, installRequest.getNewUsers()); if (installRequest.isClearCodeCache()) { - mAppDataHelper.clearAppDataLeafLIF(packageName, ps.getVolumeUuid(), - UserHandle.USER_ALL, FLAG_STORAGE_DE | FLAG_STORAGE_CE - | FLAG_STORAGE_EXTERNAL | Installer.FLAG_CLEAR_CODE_CACHE_ONLY); + mAppDataHelper.clearAppDataLIF(ps.getPkg(), UserHandle.USER_ALL, + FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL + | Installer.FLAG_CLEAR_CODE_CACHE_ONLY); } if (installRequest.isInstallReplace() && pkg != null) { mDexManager.notifyPackageUpdated(packageName, diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 58c1c0564f95..ba66377beb8a 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -1575,6 +1575,30 @@ public class LauncherAppsService extends SystemService { } @Override + public List<String> getPreInstalledSystemPackages(UserHandle user) { + // Only system launchers, which have access to recents should have access to this API. + // TODO(b/303803157): Update access control for this API to default Launcher app. + if (!mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid())) { + throw new SecurityException("Caller is not the recents app"); + } + if (!canAccessProfile(user.getIdentifier(), + "Can't access preinstalled packages for another user")) { + return null; + } + final long identity = Binder.clearCallingIdentity(); + try { + String userType = mUm.getUserInfo(user.getIdentifier()).userType; + Set<String> preInstalledPackages = mUm.getPreInstallableSystemPackages(userType); + if (preInstalledPackages == null) { + return new ArrayList<>(); + } + return List.copyOf(preInstalledPackages); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void startActivityAsUser(IApplicationThread caller, String callingPackage, String callingFeatureId, ComponentName component, Rect sourceBounds, Bundle opts, UserHandle user) throws RemoteException { diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index e3bab3f243c3..6b05edf7c25a 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -323,6 +323,7 @@ public class PackageArchiver { PackageStateInternal ps = getPackageState(packageName, snapshot, Binder.getCallingUid(), userId); verifyNotSystemApp(ps.getFlags()); + verifyInstalled(ps, userId); String responsibleInstallerPackage = getResponsibleInstallerPackage(ps); verifyInstaller(responsibleInstallerPackage, userId); ApplicationInfo installerInfo = snapshot.getApplicationInfo( @@ -476,6 +477,14 @@ public class PackageArchiver { } } + private void verifyInstalled(PackageStateInternal ps, int userId) + throws PackageManager.NameNotFoundException { + if (!ps.getUserStateOrDefault(userId).isInstalled()) { + throw new PackageManager.NameNotFoundException( + TextUtils.formatSimple("%s is not installed.", ps.getPackageName())); + } + } + /** * Returns true if the app is archivable. */ @@ -519,11 +528,11 @@ public class PackageArchiver { /** * Returns true if user has opted the app out of archiving through system settings. */ - // TODO(b/304256918) Switch this to a separate OP code for archiving. private boolean isAppOptedOutOfArchiving(String packageName, int uid) { return Binder.withCleanCallingIdentity(() -> - getAppOpsManager().checkOp(AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, - uid, packageName) == MODE_IGNORED); + getAppOpsManager().checkOpNoThrow( + AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, uid, packageName) + == MODE_IGNORED); } private void verifyOptOutStatus(String packageName, int uid) @@ -676,23 +685,51 @@ public class PackageArchiver { PackageStateInternal ps; try { ps = getPackageState(packageName, snapshot, callingUid, userId); - snapshot.enforceCrossUserPermission(callingUid, userId, true, false, - "getArchivedAppIcon"); - verifyArchived(ps, userId); } catch (PackageManager.NameNotFoundException e) { - throw new ParcelableException(e); + Slog.e(TAG, TextUtils.formatSimple("Package %s couldn't be found.", packageName), e); + return null; } - List<ArchiveActivityInfo> activityInfos = ps.getUserStateOrDefault( - userId).getArchiveState().getActivityInfos(); - if (activityInfos.size() == 0) { + ArchiveState archiveState = getAnyArchiveState(ps, userId); + if (archiveState == null || archiveState.getActivityInfos().size() == 0) { return null; } // TODO(b/298452477) Handle monochrome icons. // In the rare case the archived app defined more than two launcher activities, we choose // the first one arbitrarily. - return includeCloudOverlay(decodeIcon(activityInfos.get(0))); + return includeCloudOverlay(decodeIcon(archiveState.getActivityInfos().get(0))); + } + + /** + * This method first checks the ArchiveState for the provided userId and then tries to fallback + * to other users if the current user is not archived. + * + * <p> This fallback behaviour is required for archived apps to fit into the multi-user world + * where APKs are shared across users. E.g. current ways of fetching icons for apps that are + * only installed on the work profile also work when executed on the personal profile if you're + * using {@link PackageManager#MATCH_UNINSTALLED_PACKAGES}. Resource fetching from APKs is for + * the most part userId-agnostic, which we need to mimic here in order for existing methods + * like {@link PackageManager#getApplicationIcon} to continue working. + * + * @return {@link ArchiveState} for {@code userId} if present. If not present, false back to an + * arbitrary userId. If no user is archived, returns null. + */ + @Nullable + private ArchiveState getAnyArchiveState(PackageStateInternal ps, int userId) { + PackageUserStateInternal userState = ps.getUserStateOrDefault(userId); + if (isArchived(userState)) { + return userState.getArchiveState(); + } + + for (int i = 0; i < ps.getUserStates().size(); i++) { + userState = ps.getUserStates().valueAt(i); + if (isArchived(userState)) { + return userState.getArchiveState(); + } + } + + return null; } @VisibleForTesting diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java index 1e7d0437d99e..c737b45ae885 100644 --- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java +++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java @@ -762,13 +762,14 @@ abstract class PackageManagerInternalBase extends PackageManagerInternal { } @Override - public boolean isPackageQuarantined(@NonNull String packageName, - @UserIdInt int userId) { + public boolean isPackageQuarantined(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException { return snapshot().isPackageQuarantinedForUser(packageName, userId); } @Override - public boolean isPackageStopped(@NonNull String packageName, @UserIdInt int userId) { + public boolean isPackageStopped(@NonNull String packageName, @UserIdInt int userId) + throws PackageManager.NameNotFoundException { return snapshot().isPackageStoppedForUser(packageName, userId); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index c5b006c4c77d..d36965832186 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5242,14 +5242,18 @@ public class PackageManagerService implements PackageSender, TestUtilityService @Override public String getSuspendingPackage(String packageName, int userId) { - final int callingUid = Binder.getCallingUid(); - final Computer snapshot = snapshot(); - // This will do visibility checks as well. - if (!snapshot.isPackageSuspendedForUser(packageName, userId)) { + try { + final int callingUid = Binder.getCallingUid(); + final Computer snapshot = snapshot(); + // This will do visibility checks as well. + if (!snapshot.isPackageSuspendedForUser(packageName, userId)) { + return null; + } + return mSuspendPackageHelper.getSuspendingPackage(snapshot, packageName, userId, + callingUid); + } catch (PackageManager.NameNotFoundException e) { return null; } - return mSuspendPackageHelper.getSuspendingPackage(snapshot, packageName, userId, - callingUid); } @Override diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java index fe8c12c8e232..c2a960a95394 100644 --- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java +++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java @@ -418,11 +418,24 @@ public final class SuspendPackageHelper { } String suspendingPackage = null; + String suspendedBySystem = null; + String qasPackage = null; for (int i = 0; i < userState.getSuspendParams().size(); i++) { suspendingPackage = userState.getSuspendParams().keyAt(i); + var suspendParams = userState.getSuspendParams().valueAt(i); if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) { - return suspendingPackage; + suspendedBySystem = suspendingPackage; } + if (suspendParams.isQuarantined() && qasPackage == null) { + qasPackage = suspendingPackage; + } + } + // Precedence: quarantined, then system, then suspending. + if (qasPackage != null) { + return qasPackage; + } + if (suspendedBySystem != null) { + return suspendedBySystem; } return suspendingPackage; } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a7b52f4e7a58..c48eccf2aac5 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -532,9 +532,10 @@ public class UserManagerService extends IUserManager.Stub { } final IntentSender target = intent.getParcelableExtra(Intent.EXTRA_INTENT, android.content.IntentSender.class); final int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL); + final String callingPackage = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); // Call setQuietModeEnabled on bg thread to avoid ANR BackgroundThread.getHandler().post(() -> - setQuietModeEnabled(userId, false, target, /* callingPackage */ null)); + setQuietModeEnabled(userId, false, target, callingPackage)); } }; @@ -1410,7 +1411,7 @@ public class UserManagerService extends IUserManager.Stub { if (onlyIfCredentialNotRequired) { return false; } - showConfirmCredentialToDisableQuietMode(userId, target); + showConfirmCredentialToDisableQuietMode(userId, target, callingPackage); return false; } } @@ -1434,7 +1435,7 @@ public class UserManagerService extends IUserManager.Stub { if (onlyIfCredentialNotRequired) { return false; } - showConfirmCredentialToDisableQuietMode(userId, target); + showConfirmCredentialToDisableQuietMode(userId, target, callingPackage); return false; } setQuietModeEnabled(userId, false /* enableQuietMode */, target, callingPackage); @@ -1604,7 +1605,7 @@ public class UserManagerService extends IUserManager.Stub { * Show confirm credential screen to unlock user in order to turn off quiet mode. */ private void showConfirmCredentialToDisableQuietMode( - @UserIdInt int userId, @Nullable IntentSender target) { + @UserIdInt int userId, @Nullable IntentSender target, @Nullable String callingPackage) { if (android.app.admin.flags.Flags.quietModeCredentialBugFix()) { // TODO (b/308121702) It may be brittle to rely on user states to check profile state int state; @@ -1635,6 +1636,7 @@ public class UserManagerService extends IUserManager.Stub { } callBackIntent.putExtra(Intent.EXTRA_USER_ID, userId); callBackIntent.setPackage(mContext.getPackageName()); + callBackIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, callingPackage); callBackIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); final PendingIntent pendingIntent = PendingIntent.getBroadcast( mContext, diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 4e5dc1dd76fa..938ed2329ffd 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -76,7 +76,6 @@ import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED; import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; -import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_UNCOVERED; @@ -97,7 +96,6 @@ import static com.android.server.wm.WindowManagerPolicyProto.SCREEN_ON_FULLY; import static com.android.server.wm.WindowManagerPolicyProto.WINDOW_MANAGER_DRAW_COMPLETE; import android.accessibilityservice.AccessibilityService; -import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -124,7 +122,6 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; @@ -204,8 +201,6 @@ import android.widget.Toast; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; -import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; -import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AssistUtils; import com.android.internal.display.BrightnessUtils; @@ -385,8 +380,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { public static final String TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD = "waitForAllWindowsDrawn"; - private static final String TALKBACK_LABEL = "TalkBack"; - private static final int POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS = 800; /** @@ -477,6 +470,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { /** Controller that supports enabling an AccessibilityService by holding down the volume keys */ private AccessibilityShortcutController mAccessibilityShortcutController; + private TalkbackShortcutController mTalkbackShortcutController; + boolean mSafeMode; // Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key. @@ -813,7 +808,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { handleScreenShot(msg.arg1); break; case MSG_SWITCH_KEYBOARD_LAYOUT: - handleSwitchKeyboardLayout(msg.arg1, msg.arg2); + SwitchKeyboardLayoutMessageObject object = + (SwitchKeyboardLayoutMessageObject) msg.obj; + handleSwitchKeyboardLayout(object.keyEvent, object.direction, + object.focusedToken); break; case MSG_LOG_KEYBOARD_SYSTEM_EVENT: handleKeyboardSystemEvent(KeyboardLogEvent.from(msg.arg1), (KeyEvent) msg.obj); @@ -934,6 +932,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + private record SwitchKeyboardLayoutMessageObject(KeyEvent keyEvent, IBinder focusedToken, + int direction) { + } + final IPersistentVrStateCallbacks mPersistentVrModeListener = new IPersistentVrStateCallbacks.Stub() { @Override @@ -1602,19 +1604,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (DEBUG_INPUT) { Slog.d(TAG, "Executing stem primary triple press action behavior."); } - - if (Settings.System.getIntForUser(mContext.getContentResolver(), - Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, - /* def= */ 0, UserHandle.USER_CURRENT) == 1) { - /** Toggle talkback begin */ - ComponentName componentName = getTalkbackComponent(); - if (componentName != null && toggleTalkBack(componentName)) { - /** log stem triple press telemetry if it's a talkback enabled event */ - logStemTriplePressAccessibilityTelemetry(componentName); - } - performHapticFeedback(HapticFeedbackConstants.CONFIRM, /* always = */ false, - /* reason = */ "Stem primary - Triple Press - Toggle Accessibility"); - /** Toggle talkback end */ + mTalkbackShortcutController.toggleTalkback(mCurrentUserId); + if (mTalkbackShortcutController.isTalkBackShortcutGestureEnabled()) { + performHapticFeedback(HapticFeedbackConstants.CONFIRM, /* always = */ + false, /* reason = */ + "Stem primary - Triple Press - Toggle Accessibility"); } break; } @@ -1640,61 +1634,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } /** - * A function that toggles talkback service - * - * @return {@code true} if talkback is enabled, {@code false} if talkback is disabled - */ - private boolean toggleTalkBack(ComponentName componentName) { - final Set<ComponentName> enabledServices = - AccessibilityUtils.getEnabledServicesFromSettings(mContext, mCurrentUserId); - - boolean isTalkbackAlreadyEnabled = enabledServices.contains(componentName); - AccessibilityUtils.setAccessibilityServiceState(mContext, componentName, - !isTalkbackAlreadyEnabled); - /** if isTalkbackAlreadyEnabled is true, then it's a disabled event so return false - * and if isTalkbackAlreadyEnabled is false, return true as it's an enabled event */ - return !isTalkbackAlreadyEnabled; - } - - /** - * A function that logs stem triple press accessibility telemetry - * If the user setup (Oobe) is not completed, set the - * WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE - * setting which will be later logged via Settings Snapshot - * else, log ACCESSIBILITY_SHORTCUT_REPORTED atom - */ - private void logStemTriplePressAccessibilityTelemetry(ComponentName componentName) { - if (!AccessibilityUtils.isUserSetupCompleted(mContext)) { - Settings.Secure.putInt(mContext.getContentResolver(), - Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE, 1); - } else { - AccessibilityStatsLogUtils.logAccessibilityShortcutActivated(mContext, componentName, - ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE, - /* serviceEnabled= */ true); - } - } - - private ComponentName getTalkbackComponent() { - AccessibilityManager accessibilityManager = mContext.getSystemService( - AccessibilityManager.class); - List<AccessibilityServiceInfo> serviceInfos = - accessibilityManager.getInstalledAccessibilityServiceList(); - - for (AccessibilityServiceInfo service : serviceInfos) { - final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; - if (isTalkback(serviceInfo)) { - return new ComponentName(serviceInfo.packageName, serviceInfo.name); - } - } - return null; - } - - private boolean isTalkback(ServiceInfo info) { - String label = info.loadLabel(mPackageManager).toString(); - return label.equals(TALKBACK_LABEL); - } - - /** * Load most recent task (expect current task) and bring it to the front. */ void performStemPrimaryDoublePressSwitchToRecentTask() { @@ -1731,12 +1670,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { case TRIPLE_PRESS_PRIMARY_NOTHING: break; case TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY: - if (Settings.System.getIntForUser( - mContext.getContentResolver(), - Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, - /* def= */ 0, - UserHandle.USER_CURRENT) - == 1) { + if (mTalkbackShortcutController.isTalkBackShortcutGestureEnabled()) { return 3; } break; @@ -2252,6 +2186,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { ButtonOverridePermissionChecker getButtonOverridePermissionChecker() { return new ButtonOverridePermissionChecker(); } + + TalkbackShortcutController getTalkbackShortcutController() { + return new TalkbackShortcutController(mContext); + } } /** {@inheritDoc} */ @@ -2515,6 +2453,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mKeyguardDrawnTimeout = mContext.getResources().getInteger( com.android.internal.R.integer.config_keyguardDrawnTimeout); mKeyguardDelegate = injector.getKeyguardServiceDelegate(); + mTalkbackShortcutController = injector.getTalkbackShortcutController(); initKeyCombinationRules(); initSingleKeyGestureRules(injector.getLooper()); mButtonOverridePermissionChecker = injector.getButtonOverridePermissionChecker(); @@ -3709,7 +3648,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_LANGUAGE_SWITCH: if (firstDown) { int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; - sendSwitchKeyboardLayout(event, direction); + sendSwitchKeyboardLayout(event, focusedToken, direction); logKeyboardSystemsEvent(event, KeyboardLogEvent.LANGUAGE_SWITCH); return true; } @@ -3978,7 +3917,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { + ", policyFlags=" + policyFlags); } - if (interceptUnhandledKey(event)) { + if (interceptUnhandledKey(event, focusedToken)) { return null; } @@ -4036,7 +3975,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { return fallbackEvent; } - private boolean interceptUnhandledKey(KeyEvent event) { + private boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) { final int keyCode = event.getKeyCode(); final int repeatCount = event.getRepeatCount(); final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; @@ -4049,7 +3988,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (KeyEvent.metaStateHasModifiers(metaState & ~KeyEvent.META_SHIFT_MASK, KeyEvent.META_CTRL_ON)) { int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; - sendSwitchKeyboardLayout(event, direction); + sendSwitchKeyboardLayout(event, focusedToken, direction); return true; } } @@ -4105,16 +4044,22 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - private void sendSwitchKeyboardLayout(@NonNull KeyEvent event, int direction) { - mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, event.getDeviceId(), - direction).sendToTarget(); + private void sendSwitchKeyboardLayout(@NonNull KeyEvent event, + @Nullable IBinder focusedToken, int direction) { + SwitchKeyboardLayoutMessageObject object = + new SwitchKeyboardLayoutMessageObject(event, focusedToken, direction); + mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, object).sendToTarget(); } - private void handleSwitchKeyboardLayout(int deviceId, int direction) { + private void handleSwitchKeyboardLayout(@NonNull KeyEvent event, int direction, + IBinder focusedToken) { if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI)) { - InputMethodManagerInternal.get().switchKeyboardLayout(direction); + IBinder targetWindowToken = + mWindowManagerInternal.getTargetWindowTokenFromInputToken(focusedToken); + InputMethodManagerInternal.get().onSwitchKeyboardLayoutShortcut(direction, + event.getDisplayId(), targetWindowToken); } else { - mWindowManagerFuncs.switchKeyboardLayout(deviceId, direction); + mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction); } } @@ -4124,7 +4069,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if ((actions & ACTION_PASS_TO_USER) != 0) { long delayMillis = interceptKeyBeforeDispatching( focusedToken, fallbackEvent, policyFlags); - if (delayMillis == 0 && !interceptUnhandledKey(fallbackEvent)) { + if (delayMillis == 0 && !interceptUnhandledKey(fallbackEvent, focusedToken)) { return true; } } diff --git a/services/core/java/com/android/server/policy/TalkbackShortcutController.java b/services/core/java/com/android/server/policy/TalkbackShortcutController.java new file mode 100644 index 000000000000..906da2f4cdf5 --- /dev/null +++ b/services/core/java/com/android/server/policy/TalkbackShortcutController.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.policy; + +import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.UserHandle; +import android.provider.Settings; +import android.view.accessibility.AccessibilityManager; + +import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; +import com.android.internal.accessibility.util.AccessibilityUtils; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; +import java.util.Set; + +/** + * This class controls talkback shortcut related operations such as toggling, quering and + * logging. + */ +@VisibleForTesting +class TalkbackShortcutController { + private static final String TALKBACK_LABEL = "TalkBack"; + private final Context mContext; + private final PackageManager mPackageManager; + + TalkbackShortcutController(Context context) { + mContext = context; + mPackageManager = mContext.getPackageManager(); + } + + /** + * A function that toggles talkback service. + * + * @return talkback state after toggle. {@code true} if talkback is enabled, {@code false} if + * talkback is disabled + */ + boolean toggleTalkback(int userId) { + final Set<ComponentName> enabledServices = + AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId); + ComponentName componentName = getTalkbackComponent(); + boolean isTalkbackAlreadyEnabled = enabledServices.contains(componentName); + + if (isTalkBackShortcutGestureEnabled()) { + isTalkbackAlreadyEnabled = !isTalkbackAlreadyEnabled; + AccessibilityUtils.setAccessibilityServiceState(mContext, componentName, + isTalkbackAlreadyEnabled); + + // log stem triple press telemetry if it's a talkback enabled event. + if (componentName != null && isTalkbackAlreadyEnabled) { + logStemTriplePressAccessibilityTelemetry(componentName); + } + } + return isTalkbackAlreadyEnabled; + } + + private ComponentName getTalkbackComponent() { + AccessibilityManager accessibilityManager = mContext.getSystemService( + AccessibilityManager.class); + List<AccessibilityServiceInfo> serviceInfos = + accessibilityManager.getInstalledAccessibilityServiceList(); + + for (AccessibilityServiceInfo service : serviceInfos) { + final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; + if (isTalkback(serviceInfo)) { + return new ComponentName(serviceInfo.packageName, serviceInfo.name); + } + } + return null; + } + + boolean isTalkBackShortcutGestureEnabled() { + return Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, + /* def= */ 0, UserHandle.USER_CURRENT) == 1; + } + + /** + * A function that logs stem triple press accessibility telemetry. If the user setup (Oobe) + * is not completed, set the WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE setting which + * will be later logged via Settings Snapshot else, log ACCESSIBILITY_SHORTCUT_REPORTED atom + */ + private void logStemTriplePressAccessibilityTelemetry(ComponentName componentName) { + if (!AccessibilityUtils.isUserSetupCompleted(mContext)) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE, 1); + return; + } + AccessibilityStatsLogUtils.logAccessibilityShortcutActivated(mContext, + componentName, + ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE, + /* serviceEnabled= */ true); + } + + private boolean isTalkback(ServiceInfo info) { + return TALKBACK_LABEL.equals(info.loadLabel(mPackageManager).toString()); + } +} diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index eac4fc00b667..9a85c42e1a10 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -1608,14 +1608,12 @@ public class TrustManagerService extends SystemService { user.name, user.id, user.flags); if (!user.supportsSwitchToByUser()) { final boolean locked; - if (user.isProfile()) { - if (mLockPatternUtils.isSeparateProfileChallengeEnabled(user.id)) { - fout.print(" (profile with separate challenge)"); - locked = isDeviceLockedInner(user.id); - } else { - fout.print(" (profile with unified challenge)"); - locked = isDeviceLockedInner(resolveProfileParent(user.id)); - } + if (mLockPatternUtils.isProfileWithUnifiedChallenge(user.id)) { + fout.print(" (profile with unified challenge)"); + locked = isDeviceLockedInner(resolveProfileParent(user.id)); + } else if (mLockPatternUtils.isSeparateProfileChallengeEnabled(user.id)) { + fout.print(" (profile with separate challenge)"); + locked = isDeviceLockedInner(user.id); } else { fout.println(" (user that cannot be switched to)"); locked = isDeviceLockedInner(user.id); diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLaunchObserver.java b/services/core/java/com/android/server/wm/ActivityMetricsLaunchObserver.java index 81e5fbd564e0..769f01c34e17 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLaunchObserver.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLaunchObserver.java @@ -142,8 +142,11 @@ public class ActivityMetricsLaunchObserver { * if the launching activity is started from an existing launch sequence (trampoline) * but cannot coalesce to the existing one, e.g. to a different display. * @param name The launching activity name. + * @param temperature The temperature at which a launch sequence had started. + * @param userId The id of the user the activity is being launched for. */ - public void onActivityLaunched(long id, ComponentName name, @Temperature int temperature) { + public void onActivityLaunched(long id, ComponentName name, @Temperature int temperature, + int userId) { } /** @@ -177,13 +180,15 @@ public class ActivityMetricsLaunchObserver { * @param timestampNanos the timestamp of ActivityLaunchFinished event in nanoseconds. * To compute the TotalTime duration, deduct the timestamp {@link #onIntentStarted} * from {@code timestampNanos}. + * @param launchMode The activity launch mode. * * @apiNote The finishing activity isn't necessarily the same as the starting activity; * in the case of a trampoline, multiple activities could've been started * and only the latest activity that was top-most during first-frame drawn * is reported here. */ - public void onActivityLaunchFinished(long id, ComponentName name, long timestampNanos) { + public void onActivityLaunchFinished(long id, ComponentName name, long timestampNanos, + int launchMode) { } /** diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 7b20529ce5e1..78f501ad9fed 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -1737,7 +1737,8 @@ class ActivityMetricsLogger { // Beginning a launch is timing sensitive and so should be observed as soon as possible. mLaunchObserver.onActivityLaunched(info.mLaunchingState.mStartUptimeNs, - info.mLastLaunchedActivity.mActivityComponent, temperature); + info.mLastLaunchedActivity.mActivityComponent, temperature, + info.mLastLaunchedActivity.mUserId); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } @@ -1774,7 +1775,8 @@ class ActivityMetricsLogger { "MetricsLogger:launchObserverNotifyActivityLaunchFinished"); mLaunchObserver.onActivityLaunchFinished(info.mLaunchingState.mStartUptimeNs, - info.mLastLaunchedActivity.mActivityComponent, timestampNs); + info.mLastLaunchedActivity.mActivityComponent, timestampNs, + info.mLastLaunchedActivity.launchMode); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 73edb4bed6ca..869bcc0f9b8f 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1053,6 +1053,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mWindowManager = wm; mRootWindowContainer = wm.mRoot; mWindowOrganizerController.mTransitionController.setWindowManager(wm); + mLifecycleManager.setWindowManager(wm); mTempConfig.setToDefaults(); mTempConfig.setLocales(LocaleList.getDefault()); mConfigurationSeq = mTempConfig.seq = 1; @@ -1274,7 +1275,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Nullable String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) { - final SafeActivityOptions opts = SafeActivityOptions.fromBundle(bOptions); assertPackageMatchesCallingUid(callingPackage); @@ -1315,7 +1315,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { .setActivityOptions(opts) .setUserId(userId) .execute(); - } @Override diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 92665af1075b..4929df8061b2 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static android.os.Process.SYSTEM_UID; import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; @@ -49,7 +50,6 @@ import android.compat.annotation.EnabledAfter; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; -import android.os.Build; import android.os.Process; import android.os.UserHandle; import android.provider.DeviceConfig; @@ -86,7 +86,7 @@ public class BackgroundActivityStartController { private static final int NO_PROCESS_UID = -1; /** If enabled the creator will not allow BAL on its behalf by default. */ @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @EnabledAfter(targetSdkVersion = UPSIDE_DOWN_CAKE) private static final long DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_CREATOR = 296478951; public static final ActivityOptions ACTIVITY_OPTIONS_SYSTEM_DEFINED = @@ -228,6 +228,7 @@ public class BackgroundActivityStartController { private final Intent mIntent; private final WindowProcessController mCallerApp; private final WindowProcessController mRealCallerApp; + private final boolean mIsCallForResult; private BalState(int callingUid, int callingPid, final String callingPackage, int realCallingUid, int realCallingPid, @@ -247,8 +248,10 @@ public class BackgroundActivityStartController { mOriginatingPendingIntent = originatingPendingIntent; mIntent = intent; mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid); - if (originatingPendingIntent == null // not a PendingIntent - || resultRecord != null // sent for result + mIsCallForResult = resultRecord != null; + if (balRequireOptInByPendingIntentCreator() // auto-opt in introduced with this feature + && (originatingPendingIntent == null // not a PendingIntent + || mIsCallForResult) // sent for result ) { // grant BAL privileges unless explicitly opted out mBalAllowedByPiCreatorWithHardening = mBalAllowedByPiCreator = @@ -351,6 +354,19 @@ public class BackgroundActivityStartController { return name + "[debugOnly]"; } + /** @return valid targetSdk or <code>-1</code> */ + private int getTargetSdk(String packageName) { + if (packageName == null) { + return -1; + } + try { + PackageManager pm = mService.mContext.getPackageManager(); + return pm.getTargetSdkVersion(packageName); + } catch (Exception e) { + return -1; + } + } + private boolean hasRealCaller() { return mRealCallingUid != NO_PROCESS_UID; } @@ -368,6 +384,7 @@ public class BackgroundActivityStartController { StringBuilder sb = new StringBuilder(2048); sb.append("[callingPackage: ") .append(getDebugPackageName(mCallingPackage, mCallingUid)); + sb.append("; callingPackageTargetSdk: ").append(getTargetSdk(mCallingPackage)); sb.append("; callingUid: ").append(mCallingUid); sb.append("; callingPid: ").append(mCallingPid); sb.append("; appSwitchState: ").append(mAppSwitchState); @@ -387,10 +404,13 @@ public class BackgroundActivityStartController { .append(mBalAllowedByPiCreatorWithHardening); sb.append("; resultIfPiCreatorAllowsBal: ").append(resultIfPiCreatorAllowsBal); sb.append("; hasRealCaller: ").append(hasRealCaller()); + sb.append("; isCallForResult: ").append(mIsCallForResult); sb.append("; isPendingIntent: ").append(isPendingIntent()); if (hasRealCaller()) { sb.append("; realCallingPackage: ") .append(getDebugPackageName(mRealCallingPackage, mRealCallingUid)); + sb.append("; realCallingPackageTargetSdk: ") + .append(getTargetSdk(mRealCallingPackage)); sb.append("; realCallingUid: ").append(mRealCallingUid); sb.append("; realCallingPid: ").append(mRealCallingPid); sb.append("; realCallingUidHasAnyVisibleWindow: ") diff --git a/services/core/java/com/android/server/wm/ClientLifecycleManager.java b/services/core/java/com/android/server/wm/ClientLifecycleManager.java index 8b282dd3e7a8..6b6388b10ce7 100644 --- a/services/core/java/com/android/server/wm/ClientLifecycleManager.java +++ b/services/core/java/com/android/server/wm/ClientLifecycleManager.java @@ -22,7 +22,13 @@ import android.app.servertransaction.ActivityLifecycleItem; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.ClientTransactionItem; import android.os.Binder; +import android.os.IBinder; import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; /** * Class that is able to combine multiple client lifecycle transition requests and/or callbacks, @@ -31,8 +37,18 @@ import android.os.RemoteException; * @see ClientTransaction */ class ClientLifecycleManager { - // TODO(lifecycler): Implement building transactions or global transaction. - // TODO(lifecycler): Use object pools for transactions and transaction items. + + private static final String TAG = "ClientLifecycleManager"; + + /** Mapping from client process binder to its pending transaction. */ + @VisibleForTesting + final ArrayMap<IBinder, ClientTransaction> mPendingTransactions = new ArrayMap<>(); + + private WindowManagerService mWms; + + void setWindowManager(@NonNull WindowManagerService wms) { + mWms = wms; + } /** * Schedules a transaction, which may consist of multiple callbacks and a lifecycle request. @@ -82,14 +98,24 @@ class ClientLifecycleManager { */ void scheduleTransactionItem(@NonNull IApplicationThread client, @NonNull ClientTransactionItem transactionItem) throws RemoteException { - // TODO(b/260873529): queue the transaction items. - final ClientTransaction clientTransaction = ClientTransaction.obtain(client); - if (transactionItem.isActivityLifecycleItem()) { - clientTransaction.setLifecycleStateRequest((ActivityLifecycleItem) transactionItem); + // The behavior is different depending on the flag. + // When flag is on, we wait until RootWindowContainer#performSurfacePlacementNoTrace to + // dispatch all pending transactions at once. + if (Flags.bundleClientTransactionFlag()) { + final ClientTransaction clientTransaction = getOrCreatePendingTransaction(client); + clientTransaction.addTransactionItem(transactionItem); + + onClientTransactionItemScheduledLocked(clientTransaction); } else { - clientTransaction.addCallback(transactionItem); + // TODO(b/260873529): cleanup after launch. + final ClientTransaction clientTransaction = ClientTransaction.obtain(client); + if (transactionItem.isActivityLifecycleItem()) { + clientTransaction.setLifecycleStateRequest((ActivityLifecycleItem) transactionItem); + } else { + clientTransaction.addCallback(transactionItem); + } + scheduleTransaction(clientTransaction); } - scheduleTransaction(clientTransaction); } /** @@ -100,10 +126,67 @@ class ClientLifecycleManager { void scheduleTransactionAndLifecycleItems(@NonNull IApplicationThread client, @NonNull ClientTransactionItem transactionItem, @NonNull ActivityLifecycleItem lifecycleItem) throws RemoteException { - // TODO(b/260873529): replace with #scheduleTransactionItem after launch for cleanup. - final ClientTransaction clientTransaction = ClientTransaction.obtain(client); - clientTransaction.addCallback(transactionItem); - clientTransaction.setLifecycleStateRequest(lifecycleItem); + // The behavior is different depending on the flag. + // When flag is on, we wait until RootWindowContainer#performSurfacePlacementNoTrace to + // dispatch all pending transactions at once. + if (Flags.bundleClientTransactionFlag()) { + final ClientTransaction clientTransaction = getOrCreatePendingTransaction(client); + clientTransaction.addTransactionItem(transactionItem); + clientTransaction.addTransactionItem(lifecycleItem); + + onClientTransactionItemScheduledLocked(clientTransaction); + } else { + // TODO(b/260873529): cleanup after launch. + final ClientTransaction clientTransaction = ClientTransaction.obtain(client); + clientTransaction.addCallback(transactionItem); + clientTransaction.setLifecycleStateRequest(lifecycleItem); + scheduleTransaction(clientTransaction); + } + } + + /** Executes all the pending transactions. */ + void dispatchPendingTransactions() { + final int size = mPendingTransactions.size(); + for (int i = 0; i < size; i++) { + final ClientTransaction transaction = mPendingTransactions.valueAt(i); + try { + scheduleTransaction(transaction); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to deliver transaction for " + transaction.getClient()); + } + } + mPendingTransactions.clear(); + } + + @NonNull + private ClientTransaction getOrCreatePendingTransaction(@NonNull IApplicationThread client) { + final IBinder clientBinder = client.asBinder(); + final ClientTransaction pendingTransaction = mPendingTransactions.get(clientBinder); + if (pendingTransaction != null) { + return pendingTransaction; + } + + // Create new transaction if there is no existing. + final ClientTransaction transaction = ClientTransaction.obtain(client); + mPendingTransactions.put(clientBinder, transaction); + return transaction; + } + + /** Must only be called with WM lock. */ + private void onClientTransactionItemScheduledLocked( + @NonNull ClientTransaction clientTransaction) throws RemoteException { + // TODO(b/260873529): make sure WindowSurfacePlacer#requestTraversal is called before + // ClientTransaction scheduled when needed. + + if (mWms != null && (mWms.mWindowPlacerLocked.isInLayout() + || mWms.mWindowPlacerLocked.isTraversalScheduled())) { + // The pending transactions will be dispatched when + // RootWindowContainer#performSurfacePlacementNoTrace. + return; + } + + // Dispatch the pending transaction immediately. + mPendingTransactions.remove(clientTransaction.getClient().asBinder()); scheduleTransaction(clientTransaction); } } diff --git a/services/core/java/com/android/server/wm/LaunchObserverRegistryImpl.java b/services/core/java/com/android/server/wm/LaunchObserverRegistryImpl.java index 9cbc1bdcbeeb..4c73a415d3f3 100644 --- a/services/core/java/com/android/server/wm/LaunchObserverRegistryImpl.java +++ b/services/core/java/com/android/server/wm/LaunchObserverRegistryImpl.java @@ -84,10 +84,10 @@ class LaunchObserverRegistryImpl extends ActivityMetricsLaunchObserver implement } @Override - public void onActivityLaunched(long id, ComponentName name, int temperature) { + public void onActivityLaunched(long id, ComponentName name, int temperature, int userId) { mHandler.sendMessage(PooledLambda.obtainMessage( LaunchObserverRegistryImpl::handleOnActivityLaunched, - this, id, name, temperature)); + this, id, name, temperature, userId)); } @Override @@ -97,10 +97,11 @@ class LaunchObserverRegistryImpl extends ActivityMetricsLaunchObserver implement } @Override - public void onActivityLaunchFinished(long id, ComponentName name, long timestampNs) { + public void onActivityLaunchFinished(long id, ComponentName name, long timestampNs, + int launchMode) { mHandler.sendMessage(PooledLambda.obtainMessage( LaunchObserverRegistryImpl::handleOnActivityLaunchFinished, - this, id, name, timestampNs)); + this, id, name, timestampNs, launchMode)); } @Override @@ -137,10 +138,10 @@ class LaunchObserverRegistryImpl extends ActivityMetricsLaunchObserver implement } private void handleOnActivityLaunched(long id, ComponentName name, - @Temperature int temperature) { + @Temperature int temperature, int userId) { // Traverse start-to-end to meet the registerLaunchObserver multi-cast order guarantee. for (int i = 0; i < mList.size(); i++) { - mList.get(i).onActivityLaunched(id, name, temperature); + mList.get(i).onActivityLaunched(id, name, temperature, userId); } } @@ -151,10 +152,11 @@ class LaunchObserverRegistryImpl extends ActivityMetricsLaunchObserver implement } } - private void handleOnActivityLaunchFinished(long id, ComponentName name, long timestampNs) { + private void handleOnActivityLaunchFinished(long id, ComponentName name, long timestampNs, + int launchMode) { // Traverse start-to-end to meet the registerLaunchObserver multi-cast order guarantee. for (int i = 0; i < mList.size(); i++) { - mList.get(i).onActivityLaunchFinished(id, name, timestampNs); + mList.get(i).onActivityLaunchFinished(id, name, timestampNs, launchMode); } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index d5aa2760d41a..9a75dae3569e 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -842,6 +842,9 @@ class RootWindowContainer extends WindowContainer<DisplayContent> handleResizingWindows(); clearFrameChangingWindows(); + // Called after #handleResizingWindows to include WindowStateResizeItem if any. + mWmService.mAtmService.getLifecycleManager().dispatchPendingTransactions(); + if (mWmService.mDisplayFrozen) { ProtoLog.v(WM_DEBUG_ORIENTATION, "With display frozen, orientationChangeComplete=%b", diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index fc92755c6550..5d019122d52e 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -73,6 +73,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityOptions; +import android.app.IApplicationThread; import android.app.ResultInfo; import android.app.WindowConfiguration; import android.app.servertransaction.ActivityResultItem; @@ -103,6 +104,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.server.am.HostingRecord; import com.android.server.pm.pkg.AndroidPackage; +import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -1509,23 +1511,38 @@ class TaskFragment extends WindowContainer<WindowContainer> { } try { - final ClientTransaction transaction = ClientTransaction.obtain( - next.app.getThread()); + final IApplicationThread appThread = next.app.getThread(); + final ClientTransaction transaction = Flags.bundleClientTransactionFlag() + ? null + : ClientTransaction.obtain(appThread); // Deliver all pending results. - ArrayList<ResultInfo> a = next.results; + final ArrayList<ResultInfo> a = next.results; if (a != null) { final int size = a.size(); if (!next.finishing && size > 0) { if (DEBUG_RESULTS) { Slog.v(TAG_RESULTS, "Delivering results to " + next + ": " + a); } - transaction.addCallback(ActivityResultItem.obtain(next.token, a)); + final ActivityResultItem activityResultItem = ActivityResultItem.obtain( + next.token, a); + if (transaction == null) { + mAtmService.getLifecycleManager().scheduleTransactionItem( + appThread, activityResultItem); + } else { + transaction.addCallback(activityResultItem); + } } } if (next.newIntents != null) { - transaction.addCallback( - NewIntentItem.obtain(next.token, next.newIntents, true /* resume */)); + final NewIntentItem newIntentItem = NewIntentItem.obtain( + next.token, next.newIntents, true /* resume */); + if (transaction == null) { + mAtmService.getLifecycleManager().scheduleTransactionItem( + appThread, newIntentItem); + } else { + transaction.addCallback(newIntentItem); + } } // Well the app will no longer be stopped. @@ -1539,10 +1556,16 @@ class TaskFragment extends WindowContainer<WindowContainer> { final int topProcessState = mAtmService.mTopProcessState; next.app.setPendingUiCleanAndForceProcessStateUpTo(topProcessState); next.abortAndClearOptionsAnimation(); - transaction.setLifecycleStateRequest( - ResumeActivityItem.obtain(next.token, topProcessState, - dc.isNextTransitionForward(), next.shouldSendCompatFakeFocus())); - mAtmService.getLifecycleManager().scheduleTransaction(transaction); + final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain( + next.token, topProcessState, dc.isNextTransitionForward(), + next.shouldSendCompatFakeFocus()); + if (transaction == null) { + mAtmService.getLifecycleManager().scheduleTransactionItem( + appThread, resumeActivityItem); + } else { + transaction.setLifecycleStateRequest(resumeActivityItem); + mAtmService.getLifecycleManager().scheduleTransaction(transaction); + } ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Resumed %s", next); } catch (Exception e) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 808a11d4adae..516d37c0136a 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -866,6 +866,11 @@ public abstract class WindowManagerInternal { public abstract ImeTargetInfo onToggleImeRequested(boolean show, @NonNull IBinder focusedToken, @NonNull IBinder requestToken, int displayId); + /** + * Returns the token to identify the target window that the IME is associated with. + */ + public abstract @Nullable IBinder getTargetWindowTokenFromInputToken(IBinder inputToken); + /** The information of input method target when IME is requested to show or hide. */ public static class ImeTargetInfo { public final String focusedWindowName; diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 10dd334ed50c..2125c63eed34 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8552,6 +8552,12 @@ public class WindowManagerService extends IWindowManager.Stub fromOrientations, toOrientations); } } + + @Override + public @Nullable IBinder getTargetWindowTokenFromInputToken(IBinder inputToken) { + InputTarget inputTarget = WindowManagerService.this.getInputTargetFromToken(inputToken); + return inputTarget == null ? null : inputTarget.getWindowToken(); + } } private final class ImeTargetVisibilityPolicyImpl extends ImeTargetVisibilityPolicy { diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java index aa589024fdc6..dff718a4b7d5 100644 --- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java +++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java @@ -210,6 +210,10 @@ class WindowSurfacePlacer { return mInLayout; } + boolean isTraversalScheduled() { + return mTraversalScheduled; + } + void requestTraversal() { if (mTraversalScheduled) { return; diff --git a/services/core/jni/com_android_server_pm_PackageManagerShellCommandDataLoader.cpp b/services/core/jni/com_android_server_pm_PackageManagerShellCommandDataLoader.cpp index 3d4f866948af..d66b9b956071 100644 --- a/services/core/jni/com_android_server_pm_PackageManagerShellCommandDataLoader.cpp +++ b/services/core/jni/com_android_server_pm_PackageManagerShellCommandDataLoader.cpp @@ -182,7 +182,7 @@ static inline IncFsSize verityTreeSizeForFile(IncFsSize fileSize) { auto block_count = 1 + (fileSize - 1) / INCFS_DATA_FILE_BLOCK_SIZE; auto hash_block_count = block_count; - for (auto i = 0; hash_block_count > 1; i++) { + while (hash_block_count > 1) { hash_block_count = (hash_block_count + hash_per_block - 1) / hash_per_block; total_tree_block_count += hash_block_count; } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java index 23886a17c890..00fe3d92413c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java @@ -25,6 +25,7 @@ import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BACKGROUND import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -167,8 +168,12 @@ public class BackgroundJobsControllerTest { } private void setStoppedState(int uid, String pkgName, boolean stopped) { - doReturn(stopped).when(mPackageManagerInternal).isPackageStopped(pkgName, uid); - sendPackageStoppedBroadcast(uid, pkgName, stopped); + try { + doReturn(stopped).when(mPackageManagerInternal).isPackageStopped(pkgName, uid); + sendPackageStoppedBroadcast(uid, pkgName, stopped); + } catch (PackageManager.NameNotFoundException e) { + fail("Unable to set stopped state for unknown package: " + pkgName); + } } private void sendPackageStoppedBroadcast(int uid, String pkgName, boolean stopped) { diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java index 2332988cc832..e5ecdc478df7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -62,6 +62,7 @@ import android.os.ParcelableException; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.text.TextUtils; +import android.util.SparseArray; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -174,6 +175,7 @@ public class PackageArchiverTest { mUserState = new PackageUserStateImpl().setInstalled(true); mPackageSetting.setUserState(mUserId, mUserState); when(mPackageState.getUserStateOrDefault(eq(mUserId))).thenReturn(mUserState); + when(mPackageState.getUserStates()).thenReturn(new SparseArray<>()); when(mContext.getSystemService(LauncherApps.class)).thenReturn(mLauncherApps); when(mContext.getSystemService(AppOpsManager.class)).thenReturn( @@ -343,7 +345,7 @@ public class PackageArchiverTest { @Test public void archiveApp_appOptedOutOfArchiving() { - when(mAppOpsManager.checkOp( + when(mAppOpsManager.checkOpNoThrow( eq(AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED), anyInt(), eq(PACKAGE))).thenReturn(MODE_IGNORED); @@ -430,7 +432,7 @@ public class PackageArchiverTest { @Test public void isAppArchivable_appOptedOutOfArchiving() throws PackageManager.NameNotFoundException { - when(mAppOpsManager.checkOp( + when(mAppOpsManager.checkOpNoThrow( eq(AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED), anyInt(), eq(PACKAGE))).thenReturn(MODE_IGNORED); @@ -551,22 +553,12 @@ public class PackageArchiverTest { when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn( null); - Exception e = assertThrows( - ParcelableException.class, - () -> mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)); - assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); - assertThat(e.getCause()).hasMessageThat().isEqualTo( - String.format("Package %s not found.", PACKAGE)); + assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull(); } @Test public void getArchivedAppIcon_notArchived() { - Exception e = assertThrows( - ParcelableException.class, - () -> mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)); - assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); - assertThat(e.getCause()).hasMessageThat().isEqualTo( - String.format("Package %s is not currently archived.", PACKAGE)); + assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull(); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt index a6ba5d4c3032..7b80aea80035 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt @@ -36,6 +36,7 @@ import org.mockito.MockitoAnnotations open class PackageHelperTestBase { companion object { + const val PLATFORM_PACKAGE_NAME = "android" const val TEST_PACKAGE_1 = "com.android.test.package1" const val TEST_PACKAGE_2 = "com.android.test.package2" const val DEVICE_OWNER_PACKAGE = "com.android.test.owner" diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt index 147303363200..ae53e707a7cc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt @@ -283,6 +283,72 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { } @Test + fun getSuspendingPackagePrecedence() { + val launcherExtras = PersistableBundle() + launcherExtras.putString(TEST_PACKAGE_2, TEST_PACKAGE_2) + val targetPackages = arrayOf(TEST_PACKAGE_2) + // Suspend. + var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), + targetPackages, true /* suspended */, null /* appExtras */, launcherExtras, + null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid, + false /* quarantined */) + assertThat(failedNames).isEmpty() + testHandler.flush() + + assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(), + TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE) + + // Suspend by system. + failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), + targetPackages, true /* suspended */, null /* appExtras */, launcherExtras, + null /* dialogInfo */, PLATFORM_PACKAGE_NAME, TEST_USER_ID, deviceOwnerUid, + false /* quarantined */) + assertThat(failedNames).isEmpty() + testHandler.flush() + + assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(), + TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(PLATFORM_PACKAGE_NAME) + + // QAS by package1. + failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), + targetPackages, true /* suspended */, null /* appExtras */, launcherExtras, + null /* dialogInfo */, TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid, + true /* quarantined */) + assertThat(failedNames).isEmpty() + testHandler.flush() + + assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(), + TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(TEST_PACKAGE_1) + + // Un-QAS by package1. + suspendPackageHelper.removeSuspensionsBySuspendingPackage(pms.snapshotComputer(), + targetPackages, { suspendingPackage -> suspendingPackage == TEST_PACKAGE_1 }, + TEST_USER_ID) + testHandler.flush() + + assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(), + TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(PLATFORM_PACKAGE_NAME) + + // Un-suspend by system. + suspendPackageHelper.removeSuspensionsBySuspendingPackage(pms.snapshotComputer(), + targetPackages, { suspendingPackage -> suspendingPackage == PLATFORM_PACKAGE_NAME }, + TEST_USER_ID) + testHandler.flush() + + assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(), + TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE) + + // Unsuspend. + suspendPackageHelper.removeSuspensionsBySuspendingPackage(pms.snapshotComputer(), + targetPackages, { suspendingPackage -> suspendingPackage == DEVICE_OWNER_PACKAGE }, + TEST_USER_ID) + testHandler.flush() + + assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(), + TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isNull() + } + + @Test fun getSuspendedDialogInfo() { val dialogInfo = SuspendDialogInfo.Builder() .setTitle(TEST_PACKAGE_1).build() diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index a2a8424881d4..0973d46283ed 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -23,6 +23,7 @@ import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1; import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2; import static com.android.server.hdmi.Constants.ADDR_RECORDER_1; import static com.android.server.hdmi.Constants.ADDR_TV; +import static com.android.server.hdmi.HdmiCecLocalDevice.ActiveSource; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; import static com.google.common.truth.Truth.assertThat; @@ -1712,13 +1713,14 @@ public class HdmiCecLocalDeviceTvTest { HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV); HdmiCecMessage activeSourceFromTv = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); - mHdmiControlService.getHdmiCecNetwork().clearLocalDevices(); mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS); mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); mTestLooper.dispatchAll(); assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource); + assertThat(mHdmiControlService.getLocalActiveSource()).isEqualTo( + new ActiveSource(Constants.ADDR_INVALID, Constants.INVALID_PHYSICAL_ADDRESS)); mNativeWrapper.clearResultMessages(); mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); mTestLooper.dispatchAll(); @@ -1728,8 +1730,64 @@ public class HdmiCecLocalDeviceTvTest { mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); mTestLooper.dispatchAll(); assertThat(mNativeWrapper.getResultMessages()).contains(activeSourceFromTv); + assertThat(mHdmiControlService.getLocalActiveSource()).isEqualTo( + new ActiveSource(mTvLogicalAddress, mTvPhysicalAddress)); + } + + @Test + public void requestActiveSourceActionComplete_validLocalActiveSource_doNotSendActiveSource() { + HdmiCecMessage requestActiveSource = + HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV); + HdmiCecMessage activeSourceFromTv = + HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + mHdmiControlService.getHdmiCecNetwork().clearLocalDevices(); + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource); + mHdmiControlService.setActiveSource(mTvLogicalAddress, mTvPhysicalAddress, + "HdmiCecLocalDeviceTvTest"); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSourceFromTv); + } + + @Test + public void onAddressAllocated_startRequestActiveSourceAction_cancelOnDeviceSelect() { + HdmiCecMessage requestActiveSource = + HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV); + HdmiCecMessage activeSourceFromTv = + HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + mHdmiControlService.getHdmiCecNetwork().clearLocalDevices(); + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + HdmiDeviceInfo playbackDevice = HdmiDeviceInfo.cecDeviceBuilder() + .setLogicalAddress(ADDR_PLAYBACK_1) + .setPhysicalAddress(0x1000) + .setPortId(PORT_1) + .setDeviceType(HdmiDeviceInfo.DEVICE_PLAYBACK) + .setVendorId(0x1234) + .setDisplayName("Playback 1") + .setDevicePowerStatus(HdmiControlManager.POWER_STATUS_ON) + .setCecVersion(HdmiControlManager.HDMI_CEC_VERSION_1_4_B) + .build(); + mHdmiControlService.getHdmiCecNetwork().addCecDevice(playbackDevice); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource); + mNativeWrapper.clearResultMessages(); + mHdmiCecLocalDeviceTv.deviceSelect(playbackDevice.getId(), null); + mTestLooper.dispatchAll(); + + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS * 2); + mTestLooper.dispatchAll(); + + // RequestActiveSourceAction should be cancelled and TV will not broadcast <Active Source>. + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSourceFromTv); } + @Test public void newDeviceConnectedIfOnlyOneGiveOsdNameSent() { mHdmiControlService.getHdmiCecNetwork().clearDeviceList(); @@ -1737,7 +1795,7 @@ public class HdmiCecLocalDeviceTvTest { .isEmpty(); HdmiCecMessage reportPhysicalAddress = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( - ADDR_PLAYBACK_2, 0x1000, HdmiDeviceInfo.DEVICE_PLAYBACK); + ADDR_PLAYBACK_2, 0x1000, HdmiDeviceInfo.DEVICE_PLAYBACK); HdmiCecMessage giveOsdName = HdmiCecMessageBuilder.buildGiveOsdNameCommand( ADDR_TV, ADDR_PLAYBACK_2); mNativeWrapper.onCecMessage(reportPhysicalAddress); diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java index 18961c0feef9..ee076c6bcf4b 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java @@ -140,9 +140,9 @@ public class LockSettingsServiceTestable extends LockSettingsService { } @Override - public ManagedProfilePasswordCache getManagedProfilePasswordCache( + public UnifiedProfilePasswordCache getUnifiedProfilePasswordCache( java.security.KeyStore ks) { - return mock(ManagedProfilePasswordCache.class); + return mock(UnifiedProfilePasswordCache.class); } @Override diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 6335b3bfade2..4e1c72af2727 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -46,6 +46,7 @@ android_test { "testng", "flag-junit", "notification_flags_lib", + "platform-test-rules", ], libs: [ diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 36d55a48346e..14b551ae0b22 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -211,6 +211,9 @@ import android.os.WorkSource; import android.permission.PermissionManager; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.platform.test.rule.DeniedDevices; +import android.platform.test.rule.DeviceProduct; +import android.platform.test.rule.LimitDevicesRule; import android.provider.DeviceConfig; import android.provider.MediaStore; import android.provider.Settings; @@ -281,6 +284,7 @@ import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -313,6 +317,7 @@ import java.util.function.Consumer; @RunWith(AndroidTestingRunner.class) @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. @RunWithLooper +@DeniedDevices(denied = {DeviceProduct.CF_AUTO}) public class NotificationManagerServiceTest extends UiServiceTestCase { private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId"; private static final String TEST_PACKAGE = "The.name.is.Package.Test.Package"; @@ -331,6 +336,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private final int mUid = Binder.getCallingUid(); private final @UserIdInt int mUserId = UserHandle.getUserId(mUid); + @ClassRule + public static final LimitDevicesRule sLimitDevicesRule = new LimitDevicesRule(); + @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 646ee3f6d206..1aea56cd8f9f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -80,6 +80,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.notNull; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -265,10 +266,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { .thenReturn(CUSTOM_PKG_UID); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( new String[] {pkg}); - ApplicationInfo mockAppInfo = mock(ApplicationInfo.class); - when(mockAppInfo.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL); + + ApplicationInfo appInfoSpy = spy(new ApplicationInfo()); + appInfoSpy.icon = ICON_RES_ID; + when(appInfoSpy.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL); when(mPackageManager.getApplicationInfo(eq(CUSTOM_PKG_NAME), anyInt())) - .thenReturn(mockAppInfo); + .thenReturn(appInfoSpy); mZenModeHelper.mPm = mPackageManager; mZenModeEventLogger.reset(); @@ -3753,6 +3756,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.zenPolicy = policy; rule.pkg = ownerPkg; rule.name = CUSTOM_APP_LABEL; + rule.iconResName = ICON_RES_NAME; + rule.triggerDescription = mContext.getString(R.string.zen_mode_implicit_trigger_description, + CUSTOM_APP_LABEL); + rule.type = AutomaticZenRule.TYPE_OTHER; rule.enabled = true; return rule; } diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java index 8d236eda5dc5..0382ca0d9fec 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java @@ -61,6 +61,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { META_SHORTCUTS.append(KEYCODE_P, Intent.CATEGORY_APP_MUSIC); META_SHORTCUTS.append(KEYCODE_S, Intent.CATEGORY_APP_MESSAGING); } + private static final int ANY_DISPLAY_ID = 123; @Before public void setUp() { @@ -96,8 +97,9 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { */ @Test public void testCtrlSpace() { - sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SPACE}, 0); - mPhoneWindowManager.assertSwitchKeyboardLayout(1); + sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SPACE}, /* duration= */ 0, + ANY_DISPLAY_ID); + mPhoneWindowManager.assertSwitchKeyboardLayout(/* direction= */ 1, ANY_DISPLAY_ID); } /** @@ -105,8 +107,9 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { */ @Test public void testCtrlShiftSpace() { - sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SHIFT_LEFT, KEYCODE_SPACE}, 0); - mPhoneWindowManager.assertSwitchKeyboardLayout(-1); + sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SHIFT_LEFT, KEYCODE_SPACE}, + /* duration= */ 0, ANY_DISPLAY_ID); + mPhoneWindowManager.assertSwitchKeyboardLayout(/* direction= */ -1, ANY_DISPLAY_ID); } /** diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java index 9cdec2588501..157d1627d993 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java @@ -114,7 +114,7 @@ class ShortcutKeyTestBase { } } - void sendKeyCombination(int[] keyCodes, long durationMillis, boolean longPress) { + void sendKeyCombination(int[] keyCodes, long durationMillis, boolean longPress, int displayId) { final long downTime = mPhoneWindowManager.getCurrentTime(); final int count = keyCodes.length; int metaState = 0; @@ -124,7 +124,7 @@ class ShortcutKeyTestBase { final KeyEvent event = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/, InputDevice.SOURCE_KEYBOARD); - event.setDisplayId(DEFAULT_DISPLAY); + event.setDisplayId(displayId); interceptKey(event); // The order is important here, metaState could be updated and applied to the next key. metaState |= MODIFIER.getOrDefault(keyCode, 0); @@ -142,7 +142,7 @@ class ShortcutKeyTestBase { KeyEvent.ACTION_DOWN, keyCode, 1 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, KeyEvent.FLAG_LONG_PRESS /*flags*/, InputDevice.SOURCE_KEYBOARD); - nextDownEvent.setDisplayId(DEFAULT_DISPLAY); + nextDownEvent.setDisplayId(displayId); interceptKey(nextDownEvent); } } @@ -153,18 +153,23 @@ class ShortcutKeyTestBase { final KeyEvent upEvent = new KeyEvent(downTime, eventTime, KeyEvent.ACTION_UP, keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/, InputDevice.SOURCE_KEYBOARD); - upEvent.setDisplayId(DEFAULT_DISPLAY); + upEvent.setDisplayId(displayId); interceptKey(upEvent); metaState &= ~MODIFIER.getOrDefault(keyCode, 0); } } void sendKeyCombination(int[] keyCodes, long durationMillis) { - sendKeyCombination(keyCodes, durationMillis, false /* longPress */); + sendKeyCombination(keyCodes, durationMillis, false /* longPress */, DEFAULT_DISPLAY); + } + + void sendKeyCombination(int[] keyCodes, long durationMillis, int displayId) { + sendKeyCombination(keyCodes, durationMillis, false /* longPress */, displayId); } void sendLongPressKeyCombination(int[] keyCodes) { - sendKeyCombination(keyCodes, ViewConfiguration.getLongPressTimeout(), true /* longPress */); + sendKeyCombination(keyCodes, ViewConfiguration.getLongPressTimeout(), true /* longPress */, + DEFAULT_DISPLAY); } void sendKey(int keyCode) { @@ -172,7 +177,7 @@ class ShortcutKeyTestBase { } void sendKey(int keyCode, boolean longPress) { - sendKeyCombination(new int[]{keyCode}, 0 /*durationMillis*/, longPress); + sendKeyCombination(new int[]{keyCode}, 0 /*durationMillis*/, longPress, DEFAULT_DISPLAY); } private void interceptKey(KeyEvent keyEvent) { diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index d057226836a3..0678210e1d2f 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -166,12 +166,34 @@ class TestPhoneWindowManager { @Mock private PhoneWindowManager.ButtonOverridePermissionChecker mButtonOverridePermissionChecker; + @Mock private IBinder mInputToken; + @Mock private IBinder mImeTargetWindowToken; + private StaticMockitoSession mMockitoSession; private OffsettableClock mClock = new OffsettableClock(); private TestLooper mTestLooper = new TestLooper(() -> mClock.now()); private HandlerThread mHandlerThread; private Handler mHandler; + private boolean mIsTalkBackEnabled; + + class TestTalkbackShortcutController extends TalkbackShortcutController { + TestTalkbackShortcutController(Context context) { + super(context); + } + + @Override + boolean toggleTalkback(int currentUserId) { + mIsTalkBackEnabled = !mIsTalkBackEnabled; + return mIsTalkBackEnabled; + } + + @Override + boolean isTalkBackShortcutGestureEnabled() { + return true; + } + } + private class TestInjector extends PhoneWindowManager.Injector { TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) { super(context, funcs, mTestLooper.getLooper()); @@ -197,6 +219,10 @@ class TestPhoneWindowManager { PhoneWindowManager.ButtonOverridePermissionChecker getButtonOverridePermissionChecker() { return mButtonOverridePermissionChecker; } + + TalkbackShortcutController getTalkbackShortcutController() { + return new TestTalkbackShortcutController(mContext); + } } TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) { @@ -304,6 +330,9 @@ class TestPhoneWindowManager { doNothing().when(mPhoneWindowManager).finishedWakingUp(anyInt(), anyInt()); doNothing().when(mPhoneWindowManager).lockNow(any()); + doReturn(mImeTargetWindowToken) + .when(mWindowManagerInternal).getTargetWindowTokenFromInputToken(mInputToken); + mPhoneWindowManager.init(new TestInjector(mContext, mWindowManagerFuncsImpl)); mPhoneWindowManager.systemReady(); mPhoneWindowManager.systemBooted(); @@ -342,12 +371,12 @@ class TestPhoneWindowManager { } long interceptKeyBeforeDispatching(KeyEvent event) { - return mPhoneWindowManager.interceptKeyBeforeDispatching(null /*focusedToken*/, - event, FLAG_INTERACTIVE); + return mPhoneWindowManager.interceptKeyBeforeDispatching(mInputToken, event, + FLAG_INTERACTIVE); } void dispatchUnhandledKey(KeyEvent event) { - mPhoneWindowManager.dispatchUnhandledKey(null /*focusedToken*/, event, FLAG_INTERACTIVE); + mPhoneWindowManager.dispatchUnhandledKey(mInputToken, event, FLAG_INTERACTIVE); } long getCurrentTime() { @@ -623,14 +652,16 @@ class TestPhoneWindowManager { verify(mStatusBarManagerInternal).startAssist(any()); } - void assertSwitchKeyboardLayout(int direction) { + void assertSwitchKeyboardLayout(int direction, int displayId) { mTestLooper.dispatchAll(); if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI)) { - verify(mInputMethodManagerInternal).switchKeyboardLayout(eq(direction)); + verify(mInputMethodManagerInternal).onSwitchKeyboardLayoutShortcut(eq(direction), + eq(displayId), eq(mImeTargetWindowToken)); verify(mWindowManagerFuncsImpl, never()).switchKeyboardLayout(anyInt(), anyInt()); } else { verify(mWindowManagerFuncsImpl).switchKeyboardLayout(anyInt(), eq(direction)); - verify(mInputMethodManagerInternal, never()).switchKeyboardLayout(anyInt()); + verify(mInputMethodManagerInternal, never()) + .onSwitchKeyboardLayoutShortcut(anyInt(), anyInt(), any()); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java index 65e77dcd4ca9..d4ba3b25178d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java @@ -124,7 +124,7 @@ public class ActivityMetricsLaunchObserverTests extends WindowTestsBase { private void verifyOnActivityLaunched(ActivityRecord activity) { final ArgumentCaptor<Long> idCaptor = ArgumentCaptor.forClass(Long.class); verifyAsync(mLaunchObserver).onActivityLaunched(idCaptor.capture(), - eq(activity.mActivityComponent), anyInt()); + eq(activity.mActivityComponent), anyInt(), anyInt()); final long id = idCaptor.getValue(); setExpectedStartedId(id, activity); mLastLaunchedIds.put(activity.mActivityComponent, id); @@ -132,7 +132,7 @@ public class ActivityMetricsLaunchObserverTests extends WindowTestsBase { private void verifyOnActivityLaunchFinished(ActivityRecord activity) { verifyAsync(mLaunchObserver).onActivityLaunchFinished(eq(mExpectedStartedId), - eq(activity.mActivityComponent), anyLong()); + eq(activity.mActivityComponent), anyLong(), anyInt()); } private void setExpectedStartedId(long id, Object reason) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 7aa46a62b0f1..85c6f9e9b2fe 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -123,6 +123,7 @@ import android.app.ICompatCameraControlCallback; import android.app.PictureInPictureParams; import android.app.servertransaction.ActivityConfigurationChangeItem; import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.ClientTransactionItem; import android.app.servertransaction.DestroyActivityItem; import android.app.servertransaction.PauseActivityItem; import android.app.servertransaction.WindowStateResizeItem; @@ -169,6 +170,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import java.util.ArrayList; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -260,8 +262,18 @@ public class ActivityRecordTests extends WindowTestsBase { final MutableBoolean pauseFound = new MutableBoolean(false); doAnswer((InvocationOnMock invocationOnMock) -> { final ClientTransaction transaction = invocationOnMock.getArgument(0); - if (transaction.getLifecycleStateRequest() instanceof PauseActivityItem) { - pauseFound.value = true; + final List<ClientTransactionItem> items = transaction.getTransactionItems(); + if (items != null) { + for (ClientTransactionItem item : items) { + if (item instanceof PauseActivityItem) { + pauseFound.value = true; + break; + } + } + } else { + if (transaction.getLifecycleStateRequest() instanceof PauseActivityItem) { + pauseFound.value = true; + } } return null; }).when(mClientLifecycleManager).scheduleTransaction(any()); @@ -279,6 +291,8 @@ public class ActivityRecordTests extends WindowTestsBase { // If the activity is not focusable, it should move to paused. activity.makeVisibleIfNeeded(null /* starting */, true /* reportToClient */); + mClientLifecycleManager.dispatchPendingTransactions(); + assertTrue(activity.isState(PAUSING)); assertTrue(pauseFound.value); @@ -1239,7 +1253,7 @@ public class ActivityRecordTests extends WindowTestsBase { } @Test - public void testFinishActivityIfPossible_sendResultImmediately() { + public void testFinishActivityIfPossible_sendResultImmediately() throws RemoteException { // Create activity representing the source of the activity result. final ComponentName sourceComponent = ComponentName.createRelative( DEFAULT_COMPONENT_PACKAGE_NAME, ".SourceActivity"); @@ -1270,12 +1284,8 @@ public class ActivityRecordTests extends WindowTestsBase { targetActivity.finishIfPossible(0, new Intent(), null, "test", false /* oomAdj */); - try { - verify(mClientLifecycleManager, atLeastOnce()).scheduleTransaction( - any(ClientTransaction.class)); - } catch (RemoteException ignored) { - } - + verify(mClientLifecycleManager, atLeastOnce()).scheduleTransactionItem( + any(), any(ClientTransactionItem.class)); assertNull(targetActivity.results); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java index 04aa9815e698..7fdc5fc2cff6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java @@ -17,13 +17,15 @@ package com.android.server.wm; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; @@ -31,12 +33,15 @@ import android.app.IApplicationThread; import android.app.servertransaction.ActivityLifecycleItem; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.ClientTransactionItem; +import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -47,30 +52,47 @@ import org.mockito.MockitoAnnotations; * Build/Install/Run: * atest WmTests:ClientLifecycleManagerTests */ +// Suppress GuardedBy warning on unit tests +@SuppressWarnings("GuardedBy") @SmallTest @Presubmit public class ClientLifecycleManagerTests { + @Rule(order = 0) + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Rule(order = 1) + public final SystemServicesTestRule mSystemServices = new SystemServicesTestRule(); + + @Mock + private IBinder mClientBinder; @Mock private IApplicationThread mClient; @Mock private IApplicationThread.Stub mNonBinderClient; @Mock + private ClientTransaction mTransaction; + @Mock private ClientTransactionItem mTransactionItem; @Mock private ActivityLifecycleItem mLifecycleItem; @Captor private ArgumentCaptor<ClientTransaction> mTransactionCaptor; + private WindowManagerService mWms; private ClientLifecycleManager mLifecycleManager; @Before public void setup() { MockitoAnnotations.initMocks(this); + mWms = mSystemServices.getWindowManagerService(); mLifecycleManager = spy(new ClientLifecycleManager()); + mLifecycleManager.setWindowManager(mWms); doReturn(true).when(mLifecycleItem).isActivityLifecycleItem(); + doReturn(mClientBinder).when(mClient).asBinder(); + doReturn(mNonBinderClient).when(mNonBinderClient).asBinder(); } @Test @@ -92,9 +114,11 @@ public class ClientLifecycleManagerTests { } @Test - public void testScheduleTransactionItem() throws RemoteException { - doNothing().when(mLifecycleManager).scheduleTransaction(any()); - mLifecycleManager.scheduleTransactionItem(mClient, mTransactionItem); + public void testScheduleTransactionItem_notBundle() throws RemoteException { + mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG); + + // Use non binder client to get non-recycled ClientTransaction. + mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mTransactionItem); verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture()); ClientTransaction transaction = mTransactionCaptor.getValue(); @@ -104,7 +128,7 @@ public class ClientLifecycleManagerTests { assertNull(transaction.getTransactionItems()); clearInvocations(mLifecycleManager); - mLifecycleManager.scheduleTransactionItem(mClient, mLifecycleItem); + mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mLifecycleItem); verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture()); transaction = mTransactionCaptor.getValue(); @@ -113,9 +137,54 @@ public class ClientLifecycleManagerTests { } @Test - public void testScheduleTransactionAndLifecycleItems() throws RemoteException { - doNothing().when(mLifecycleManager).scheduleTransaction(any()); - mLifecycleManager.scheduleTransactionAndLifecycleItems(mClient, mTransactionItem, + public void testScheduleTransactionItem() throws RemoteException { + mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG); + spyOn(mWms.mWindowPlacerLocked); + doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled(); + + // Use non binder client to get non-recycled ClientTransaction. + mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mTransactionItem); + + // When there is traversal scheduled, add transaction items to pending. + assertEquals(1, mLifecycleManager.mPendingTransactions.size()); + ClientTransaction transaction = + mLifecycleManager.mPendingTransactions.get(mNonBinderClient); + assertEquals(1, transaction.getTransactionItems().size()); + assertEquals(mTransactionItem, transaction.getTransactionItems().get(0)); + assertNull(transaction.getCallbacks()); + assertNull(transaction.getLifecycleStateRequest()); + verify(mLifecycleManager, never()).scheduleTransaction(any()); + + // Add new transaction item to the existing pending. + clearInvocations(mLifecycleManager); + mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mLifecycleItem); + + assertEquals(1, mLifecycleManager.mPendingTransactions.size()); + transaction = mLifecycleManager.mPendingTransactions.get(mNonBinderClient); + assertEquals(2, transaction.getTransactionItems().size()); + assertEquals(mTransactionItem, transaction.getTransactionItems().get(0)); + assertEquals(mLifecycleItem, transaction.getTransactionItems().get(1)); + assertNull(transaction.getCallbacks()); + assertNull(transaction.getLifecycleStateRequest()); + verify(mLifecycleManager, never()).scheduleTransaction(any()); + } + + @Test + public void testScheduleTransactionItemUnlocked() throws RemoteException { + // Use non binder client to get non-recycled ClientTransaction. + mLifecycleManager.scheduleTransactionItemUnlocked(mNonBinderClient, mTransactionItem); + + // Dispatch immediately. + assertTrue(mLifecycleManager.mPendingTransactions.isEmpty()); + verify(mLifecycleManager).scheduleTransaction(any()); + } + + @Test + public void testScheduleTransactionAndLifecycleItems_notBundle() throws RemoteException { + mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG); + + // Use non binder client to get non-recycled ClientTransaction. + mLifecycleManager.scheduleTransactionAndLifecycleItems(mNonBinderClient, mTransactionItem, mLifecycleItem); verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture()); @@ -124,4 +193,36 @@ public class ClientLifecycleManagerTests { assertEquals(mTransactionItem, transaction.getCallbacks().get(0)); assertEquals(mLifecycleItem, transaction.getLifecycleStateRequest()); } + + @Test + public void testScheduleTransactionAndLifecycleItems() throws RemoteException { + mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG); + spyOn(mWms.mWindowPlacerLocked); + doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled(); + + // Use non binder client to get non-recycled ClientTransaction. + mLifecycleManager.scheduleTransactionAndLifecycleItems(mNonBinderClient, mTransactionItem, + mLifecycleItem); + + assertEquals(1, mLifecycleManager.mPendingTransactions.size()); + final ClientTransaction transaction = + mLifecycleManager.mPendingTransactions.get(mNonBinderClient); + assertEquals(2, transaction.getTransactionItems().size()); + assertEquals(mTransactionItem, transaction.getTransactionItems().get(0)); + assertEquals(mLifecycleItem, transaction.getTransactionItems().get(1)); + assertNull(transaction.getCallbacks()); + assertNull(transaction.getLifecycleStateRequest()); + verify(mLifecycleManager, never()).scheduleTransaction(any()); + } + + @Test + public void testDispatchPendingTransactions() throws RemoteException { + mLifecycleManager.mPendingTransactions.put(mClientBinder, mTransaction); + + mLifecycleManager.dispatchPendingTransactions(); + + assertTrue(mLifecycleManager.mPendingTransactions.isEmpty()); + verify(mTransaction).schedule(); + verify(mTransaction).recycle(); + } } diff --git a/telephony/java/android/telephony/satellite/NtnSignalStrength.java b/telephony/java/android/telephony/satellite/NtnSignalStrength.java index 16d765455d21..2fec423d1d65 100644 --- a/telephony/java/android/telephony/satellite/NtnSignalStrength.java +++ b/telephony/java/android/telephony/satellite/NtnSignalStrength.java @@ -86,6 +86,9 @@ public final class NtnSignalStrength implements Parcelable { readFromParcel(in); } + /** + * Returns notified non-terrestrial network signal strength level. + */ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @NtnSignalStrengthLevel public int getLevel() { return mLevel; diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 71786b308937..e09bd201f93e 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -34,6 +34,7 @@ import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.RemoteException; import android.os.ResultReceiver; +import android.os.ServiceSpecificException; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyFrameworkInitializer; @@ -1919,7 +1920,6 @@ public final class SatelliteManager { */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) - @NonNull public void requestNtnSignalStrength(@NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<NtnSignalStrength, SatelliteException> callback) { Objects.requireNonNull(executor); @@ -1962,6 +1962,8 @@ public final class SatelliteManager { /** * Registers for NTN signal strength changed from satellite modem. + * If the registration operation is not successful, a {@link SatelliteException} that contains + * {@link SatelliteResult} will be thrown. * * <p> * Note: This API is specifically designed for OEM enabled satellite connectivity only. @@ -1973,16 +1975,14 @@ public final class SatelliteManager { * @param executor The executor on which the callback will be called. * @param callback The callback to handle the NTN signal strength changed event. * - * @return The {@link SatelliteResult} result of the operation. - * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * @throws SatelliteException if the callback registration operation fails. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) - @SatelliteResult public int registerForNtnSignalStrengthChanged( - @NonNull @CallbackExecutor Executor executor, - @NonNull NtnSignalStrengthCallback callback) { + public void registerForNtnSignalStrengthChanged(@NonNull @CallbackExecutor Executor executor, + @NonNull NtnSignalStrengthCallback callback) throws SatelliteException { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -1999,16 +1999,18 @@ public final class SatelliteManager { ntnSignalStrength))); } }; + telephony.registerForNtnSignalStrengthChanged(mSubId, internalCallback); sNtnSignalStrengthCallbackMap.put(callback, internalCallback); - return telephony.registerForNtnSignalStrengthChanged(mSubId, internalCallback); } else { throw new IllegalStateException("Telephony service is null."); } + } catch (ServiceSpecificException ex) { + logd("registerForNtnSignalStrengthChanged() registration fails: " + ex.errorCode); + throw new SatelliteException(ex.errorCode); } catch (RemoteException ex) { loge("registerForNtnSignalStrengthChanged() RemoteException: " + ex); ex.rethrowFromSystemServer(); } - return SATELLITE_RESULT_REQUEST_FAILED; } /** @@ -2025,6 +2027,8 @@ public final class SatelliteManager { * {@link #registerForNtnSignalStrengthChanged(Executor, NtnSignalStrengthCallback)}. * * @throws SecurityException if the caller doesn't have required permission. + * @throws IllegalArgumentException if the callback is not valid or has already been + * unregistered. * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @@ -2041,6 +2045,7 @@ public final class SatelliteManager { telephony.unregisterForNtnSignalStrengthChanged(mSubId, internalCallback); } else { loge("unregisterForNtnSignalStrengthChanged: No internal callback."); + throw new IllegalArgumentException("callback is not valid"); } } else { throw new IllegalStateException("Telephony service is null."); diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index d7d28a17e7f2..397fb2d6db96 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3101,16 +3101,21 @@ interface ITelephony { void requestNtnSignalStrength(int subId, in ResultReceiver receiver); /** - * Registers for NTN signal strength changed from satellite modem. + * Registers for NTN signal strength changed from satellite modem. If the registration operation + * is not successful, a {@link SatelliteException} that contains {@link SatelliteResult} will be + * thrown. * * @param subId The subId of the subscription to request for. - * @param callback The callback to handle the NTN signal strength changed event. - * - * @return The {@link SatelliteResult} result of the operation. + * @param callback The callback to handle the NTN signal strength changed event. If the + * operation is successful, {@link NtnSignalStrengthCallback#onNtnSignalStrengthChanged( + * NtnSignalStrength)} will return an instance of {@link NtnSignalStrength} with a value of + * {@link NtnSignalStrength.NtnSignalStrengthLevel} when the signal strength of non-terrestrial + * network has changed. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - int registerForNtnSignalStrengthChanged(int subId, in INtnSignalStrengthCallback callback); + void registerForNtnSignalStrengthChanged(int subId, + in INtnSignalStrengthCallback callback); /** * Unregisters for NTN signal strength changed from satellite modem. |