diff options
249 files changed, 7134 insertions, 1544 deletions
diff --git a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java index 4bcc8c499f0d..f302033dee0f 100644 --- a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java +++ b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java @@ -31,6 +31,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; +import android.permission.PermissionManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; @@ -107,6 +108,8 @@ public class PackageManagerPerfTest { public void setup() { PackageManager.disableApplicationInfoCache(); PackageManager.disablePackageInfoCache(); + PermissionManager.disablePermissionCache(); + PermissionManager.disablePackageNamePermissionCache(); } @Test diff --git a/core/api/current.txt b/core/api/current.txt index 0f237212c768..d610f4c8d4ed 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -26569,7 +26569,7 @@ package android.media.midi { package android.media.projection { public final class MediaProjection { - method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler); + method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler); method public void registerCallback(@NonNull android.media.projection.MediaProjection.Callback, @Nullable android.os.Handler); method public void stop(); method public void unregisterCallback(@NonNull android.media.projection.MediaProjection.Callback); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 44c4ab4e1b57..e0c3230f8c27 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -618,6 +618,7 @@ package android.app.admin { method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void resetDefaultCrossProfileIntentFilters(int); method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void resetShouldAllowBypassingDevicePolicyManagementRoleQualificationState(); method @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int); + method @FlaggedApi("android.app.admin.flags.provisioning_context_parameter") @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int, @Nullable String); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwner(@NonNull android.content.ComponentName, int); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwnerOnly(@NonNull android.content.ComponentName, int); method public void setDeviceOwnerType(@NonNull android.content.ComponentName, int); diff --git a/core/java/android/animation/OWNERS b/core/java/android/animation/OWNERS index f3b330a02116..5223c870824c 100644 --- a/core/java/android/animation/OWNERS +++ b/core/java/android/animation/OWNERS @@ -3,3 +3,4 @@ romainguy@google.com tianliu@google.com adamp@google.com +mount@google.com diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index c6a1546fb931..65acd49d44fa 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -104,7 +104,9 @@ public class ActivityOptions extends ComponentOptions { MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED, MODE_BACKGROUND_ACTIVITY_START_ALLOWED, MODE_BACKGROUND_ACTIVITY_START_DENIED, - MODE_BACKGROUND_ACTIVITY_START_COMPAT}) + MODE_BACKGROUND_ACTIVITY_START_COMPAT, + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS, + MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE}) public @interface BackgroundActivityStartMode {} /** * No explicit value chosen. The system will decide whether to grant privileges. @@ -119,6 +121,20 @@ public class ActivityOptions extends ComponentOptions { */ public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; /** + * Allow the {@link PendingIntent} to use ALL background activity start privileges, including + * special permissions that will allow starts at any time. + * + * @hide + */ + public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3; + /** + * Allow the {@link PendingIntent} to use background activity start privileges based on + * visibility of the app. + * + * @hide + */ + public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE = 4; + /** * Special behavior for compatibility. * Similar to {@link #MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED} * diff --git a/core/java/android/app/ComponentOptions.java b/core/java/android/app/ComponentOptions.java index 0e8e2e30c26f..b3fc0588022b 100644 --- a/core/java/android/app/ComponentOptions.java +++ b/core/java/android/app/ComponentOptions.java @@ -18,9 +18,11 @@ package android.app; import static android.app.ActivityOptions.BackgroundActivityStartMode; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,15 +50,7 @@ public class ComponentOptions { public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED = "android.pendingIntent.backgroundActivityAllowed"; - /** - * PendingIntent caller allows activity to be started if caller has BAL permission. - * @hide - */ - public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION = - "android.pendingIntent.backgroundActivityAllowedByPermission"; - private Integer mPendingIntentBalAllowed = MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; - private boolean mPendingIntentBalAllowedByPermission = false; ComponentOptions() { } @@ -69,9 +63,6 @@ public class ComponentOptions { mPendingIntentBalAllowed = opts.getInt(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); - setPendingIntentBackgroundActivityLaunchAllowedByPermission( - opts.getBoolean( - KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, false)); } /** @@ -114,10 +105,19 @@ public class ComponentOptions { public @NonNull ComponentOptions setPendingIntentBackgroundActivityStartMode( @BackgroundActivityStartMode int state) { switch (state) { + case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: + if (mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) { + // do not overwrite ALWAYS with ALLOWED for backwards compatibility, + // if setPendingIntentBackgroundActivityLaunchAllowedByPermission is used + // before this method. + mPendingIntentBalAllowed = state; + } + break; case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: case MODE_BACKGROUND_ACTIVITY_START_DENIED: case MODE_BACKGROUND_ACTIVITY_START_COMPAT: - case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: + case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS: + case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE: mPendingIntentBalAllowed = state; break; default: @@ -140,20 +140,32 @@ public class ComponentOptions { } /** - * Set PendingIntent activity can be launched from background if caller has BAL permission. + * Get PendingIntent activity is allowed to be started in the background if the caller + * has BAL permission. * @hide + * @deprecated check for #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS */ - public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) { - mPendingIntentBalAllowedByPermission = allowed; + @Deprecated + public boolean isPendingIntentBackgroundActivityLaunchAllowedByPermission() { + return mPendingIntentBalAllowed == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; } /** - * Get PendingIntent activity is allowed to be started in the background if the caller - * has BAL permission. + * Set PendingIntent activity can be launched from background if caller has BAL permission. * @hide + * @deprecated use #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS */ - public boolean isPendingIntentBackgroundActivityLaunchAllowedByPermission() { - return mPendingIntentBalAllowedByPermission; + @Deprecated + public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) { + if (allowed) { + setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + } else { + if (getPendingIntentBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) { + setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + } + } } /** @hide */ @@ -162,10 +174,6 @@ public class ComponentOptions { if (mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) { b.putInt(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, mPendingIntentBalAllowed); } - if (mPendingIntentBalAllowedByPermission) { - b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, - mPendingIntentBalAllowedByPermission); - } return b; } diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS index 0fad979e27cf..1200b4b45712 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -118,6 +118,8 @@ per-file *Task* = file:/services/core/java/com/android/server/wm/OWNERS per-file Window* = file:/services/core/java/com/android/server/wm/OWNERS per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS +per-file ComponentOptions.java = file:/services/core/java/com/android/server/wm/OWNERS + # Multitasking per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index fb0ce0d2d077..a7070b910f17 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -9202,6 +9202,14 @@ public class DevicePolicyManager { /** * @hide */ + @UnsupportedAppUsage + public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) { + setActiveAdmin(policyReceiver, refreshing, myUserId()); + } + + /** + * @hide + */ @TestApi @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @RequiresPermission(allOf = { @@ -9210,21 +9218,45 @@ public class DevicePolicyManager { }) public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing, int userHandle) { - if (mService != null) { - try { - mService.setActiveAdmin(policyReceiver, refreshing, userHandle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } + setActiveAdminInternal(policyReceiver, refreshing, userHandle, null); } /** * @hide */ - @UnsupportedAppUsage - public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) { - setActiveAdmin(policyReceiver, refreshing, myUserId()); + @TestApi + @RequiresPermission(allOf = { + MANAGE_DEVICE_ADMINS, + INTERACT_ACROSS_USERS_FULL + }) + @FlaggedApi(Flags.FLAG_PROVISIONING_CONTEXT_PARAMETER) + public void setActiveAdmin( + @NonNull ComponentName policyReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { + setActiveAdminInternal(policyReceiver, refreshing, userHandle, provisioningContext); + } + + private void setActiveAdminInternal( + @NonNull ComponentName policyReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { + if (mService != null) { + try { + mService.setActiveAdmin( + policyReceiver, + refreshing, + userHandle, + provisioningContext + ); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -9678,7 +9710,7 @@ public class DevicePolicyManager { if (mService != null) { try { final int myUserId = myUserId(); - mService.setActiveAdmin(admin, false, myUserId); + mService.setActiveAdmin(admin, false, myUserId, null); return mService.setProfileOwner(admin, myUserId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index d1837132e1a4..381f9963789b 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -160,7 +160,8 @@ interface IDevicePolicyManager { void setKeyguardDisabledFeatures(in ComponentName who, String callerPackageName, int which, boolean parent); int getKeyguardDisabledFeatures(in ComponentName who, int userHandle, boolean parent); - void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, int userHandle); + void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, + int userHandle, String provisioningContext); boolean isAdminActive(in ComponentName policyReceiver, int userHandle); List<ComponentName> getActiveAdmins(int userHandle); @UnsupportedAppUsage diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 82271129c69e..c789af32e2b1 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -393,3 +393,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "provisioning_context_parameter" + namespace: "enterprise" + description: "Add provisioningContext to store metadata about when the admin was set" + bug: "326525847" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 97404dcdea0c..111e6a8e93ef 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -5277,12 +5277,28 @@ public class Intent implements Parcelable, Cloneable { * through {@link #getData()}. User interaction is required to return the edited screenshot to * the calling activity. * + * <p>The response {@link Intent} may include additional data to "backlink" directly back to the + * application for which the screenshot was captured. If present, the application "backlink" can + * be retrieved via {@link #getClipData()}. The data is present only if the user accepted to + * include the link information with the screenshot. The data can contain one of the following: + * <ul> + * <li>A deeplinking {@link Uri} or an {@link Intent} if the captured app integrates with + * {@link android.app.assist.AssistContent}.</li> + * <li>Otherwise, a main launcher intent that launches the screenshotted application to + * its home screen.</li> + * </ul> + * The "backlink" to the screenshotted application will be set within {@link ClipData}, either + * as a {@link Uri} or an {@link Intent} if present. + * * <p>This intent action requires the permission * {@link android.Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}. * * <p>Callers should query * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} before showing a UI * element that allows users to trigger this flow. + * + * <p>Callers should query for {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} in the + * response {@link Intent} to check if the request was a success. */ @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index c2c7b81871df..5a3970295ac2 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -327,3 +327,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "fix_large_display_private_space_settings" + namespace: "profile_experiences" + description: "Fix tablet and foldable specific bugs for private space" + bug: "342563741" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java index a3beaf427226..209f323d7b34 100644 --- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java +++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java @@ -216,7 +216,11 @@ public final class NavigationBarView extends FrameLayout { oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection(); if (densityChange || dirChange) { - mImeSwitcherIcon = getDrawable(com.android.internal.R.drawable.ic_ime_switcher); + final int switcherResId = Flags.imeSwitcherRevamp() + ? com.android.internal.R.drawable.ic_ime_switcher_new + : com.android.internal.R.drawable.ic_ime_switcher; + + mImeSwitcherIcon = getDrawable(switcherResId); } if (orientationChange || densityChange || dirChange) { mBackIcon = getBackDrawable(); diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index f0e673b3e3ac..7e247493e35c 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -41,6 +41,7 @@ import android.util.TimeUtils; import android.view.animation.AnimationUtils; import java.io.PrintWriter; +import java.util.Locale; /** * Coordinates the timing of animations, input and drawing. @@ -200,6 +201,7 @@ public final class Choreographer { private final DisplayEventReceiver.VsyncEventData mLastVsyncEventData = new DisplayEventReceiver.VsyncEventData(); private final FrameData mFrameData = new FrameData(); + private volatile boolean mInDoFrameCallback = false; /** * Contains information about the current frame for jank-tracking, @@ -818,6 +820,11 @@ public final class Choreographer { * @hide */ public long getVsyncId() { + if (!mInDoFrameCallback && Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + String message = String.format(Locale.getDefault(), "unsync-vsync-id=%d isSfChoreo=%s", + mLastVsyncEventData.preferredFrameTimeline().vsyncId, this == getSfInstance()); + Trace.instant(Trace.TRACE_TAG_VIEW, message); + } return mLastVsyncEventData.preferredFrameTimeline().vsyncId; } @@ -853,6 +860,7 @@ public final class Choreographer { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin( Trace.TRACE_TAG_VIEW, "Choreographer#doFrame " + timeline.mVsyncId); + mInDoFrameCallback = true; } synchronized (mLock) { if (!mFrameScheduled) { @@ -947,6 +955,7 @@ public final class Choreographer { doCallbacks(Choreographer.CALLBACK_COMMIT, frameIntervalNanos); } finally { AnimationUtils.unlockAnimationClock(); + mInDoFrameCallback = false; if (resynced) { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index 1d950dc44e46..6343313b2e01 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -119,9 +119,11 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { @Override public boolean applyLocalVisibilityOverride() { - ImeTracing.getInstance().triggerClientDump( - "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", - mController.getHost().getInputMethodManager(), null /* icProto */); + if (!Flags.refactorInsetsController()) { + ImeTracing.getInstance().triggerClientDump( + "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", + mController.getHost().getInputMethodManager(), null /* icProto */); + } return super.applyLocalVisibilityOverride(); } @@ -205,9 +207,13 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { @Override public void removeSurface() { - final IBinder window = mController.getHost().getWindowToken(); - if (window != null) { - getImm().removeImeSurface(window); + if (Flags.refactorInsetsController()) { + super.removeSurface(); + } else { + final IBinder window = mController.getHost().getWindowToken(); + if (window != null) { + getImm().removeImeSurface(window); + } } } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index df2af731037e..f166b89a1d13 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -765,7 +765,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation public InsetsController(Host host) { this(host, (controller, id, type) -> { - if (type == ime()) { + if (!Flags.refactorInsetsController() && type == ime()) { return new ImeInsetsSourceConsumer(id, controller.mState, Transaction::new, controller); } else { diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index c73cbc6e9a57..477e35b6e655 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -43,6 +43,7 @@ import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.inputmethod.ImeTracing; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -296,6 +297,13 @@ public class InsetsSourceConsumer { @VisibleForTesting(visibility = PACKAGE) public boolean applyLocalVisibilityOverride() { + if (Flags.refactorInsetsController()) { + if (mType == WindowInsets.Type.ime()) { + ImeTracing.getInstance().triggerClientDump( + "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", + mController.getHost().getInputMethodManager(), null /* icProto */); + } + } final InsetsSource source = mState.peekSource(mId); if (source == null) { return false; @@ -396,6 +404,14 @@ public class InsetsSourceConsumer { */ public void removeSurface() { // no-op for types that always return ShowResult#SHOW_IMMEDIATELY. + if (Flags.refactorInsetsController()) { + if (mType == WindowInsets.Type.ime()) { + final IBinder window = mController.getHost().getWindowToken(); + if (window != null) { + mController.getHost().getInputMethodManager().removeImeSurface(window); + } + } + } } @VisibleForTesting(visibility = PACKAGE) diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 82a7e162dc2d..a23e3839c348 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -30,6 +30,7 @@ import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; +import static android.view.accessibility.Flags.removeChildHoverCheckForTouchExploration; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN; @@ -17486,9 +17487,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Dispatching hover events to {@link TouchDelegate} to improve accessibility. * <p> * This method is dispatching hover events to the delegate target to support explore by touch. - * Similar to {@link ViewGroup#dispatchTouchEvent}, this method send proper hover events to + * Similar to {@link ViewGroup#dispatchTouchEvent}, this method sends proper hover events to * the delegate target according to the pointer and the touch area of the delegate while touch - * exploration enabled. + * exploration is enabled. * </p> * * @param event The motion event dispatch to the delegate target. @@ -17520,17 +17521,33 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // hover events but receive accessibility focus, it should also not delegate to these // views when hovered. if (!oldHoveringTouchDelegate) { - if ((action == MotionEvent.ACTION_HOVER_ENTER - || action == MotionEvent.ACTION_HOVER_MOVE) - && !pointInHoveredChild(event) - && pointInDelegateRegion) { - mHoveringTouchDelegate = true; + if (removeChildHoverCheckForTouchExploration()) { + if ((action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE) && pointInDelegateRegion) { + mHoveringTouchDelegate = true; + } + } else { + if ((action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE) + && !pointInHoveredChild(event) + && pointInDelegateRegion) { + mHoveringTouchDelegate = true; + } } } else { - if (action == MotionEvent.ACTION_HOVER_EXIT - || (action == MotionEvent.ACTION_HOVER_MOVE + if (removeChildHoverCheckForTouchExploration()) { + if (action == MotionEvent.ACTION_HOVER_EXIT + || (action == MotionEvent.ACTION_HOVER_MOVE)) { + if (!pointInDelegateRegion) { + mHoveringTouchDelegate = false; + } + } + } else { + if (action == MotionEvent.ACTION_HOVER_EXIT + || (action == MotionEvent.ACTION_HOVER_MOVE && (pointInHoveredChild(event) || !pointInDelegateRegion))) { - mHoveringTouchDelegate = false; + mHoveringTouchDelegate = false; + } } } switch (action) { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index d0bc57b9afbe..44c1acc34273 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -128,6 +128,13 @@ flag { } flag { + namespace: "accessibility" + name: "remove_child_hover_check_for_touch_exploration" + description: "Remove a check for a hovered child that prevents touch events from being delegated to non-direct descendants" + bug: "304770837" +} + +flag { name: "skip_accessibility_warning_dialog_for_trusted_services" namespace: "accessibility" description: "Skips showing the accessibility warning dialog for trusted services." diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 098f65575928..0e66f7ac47b7 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -891,12 +891,13 @@ public final class InputMethodInfo implements Parcelable { @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) @Nullable public Intent createImeLanguageSettingsActivityIntent() { - if (TextUtils.isEmpty(mLanguageSettingsActivityName)) { + final var activityName = !TextUtils.isEmpty(mLanguageSettingsActivityName) + ? mLanguageSettingsActivityName : mSettingsActivityName; + if (TextUtils.isEmpty(activityName)) { return null; } return new Intent(ACTION_IME_LANGUAGE_SETTINGS).setComponent( - new ComponentName(getServiceInfo().packageName, - mLanguageSettingsActivityName) + new ComponentName(getServiceInfo().packageName, activityName) ); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index c9d2eecfb9b0..fed8eea97688 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1352,12 +1352,16 @@ public final class InputMethodManager { case MSG_SET_VISIBILITY: final boolean visible = msg.arg1 != 0; synchronized (mH) { - if (visible) { - showSoftInput(mServedView, /* flags */ 0); - } else { - if (mCurRootView != null - && mCurRootView.getInsetsController() != null) { - mCurRootView.getInsetsController().hide(WindowInsets.Type.ime()); + if (mCurRootView != null) { + final var insetsController = mCurRootView.getInsetsController(); + if (insetsController != null) { + if (visible) { + insetsController.show(WindowInsets.Type.ime(), + false /* fromIme */, null /* statsToken */); + } else { + insetsController.hide(WindowInsets.Type.ime(), + false /* fromIme */, null /* statsToken */); + } } } } @@ -2334,16 +2338,18 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); if (Flags.refactorInsetsController()) { + final var viewRootImpl = view.getViewRootImpl(); // In case of a running show IME animation, it should not be requested visible, // otherwise the animation would jump and not be controlled by the user anymore - if ((mCurRootView.getInsetsController().computeUserAnimatingTypes() - & WindowInsets.Type.ime()) == 0) { + if (viewRootImpl != null + && (viewRootImpl.getInsetsController().computeUserAnimatingTypes() + & WindowInsets.Type.ime()) == 0) { // TODO(b/322992891) handle case of SHOW_IMPLICIT - view.getWindowInsetsController().show(WindowInsets.Type.ime()); + viewRootImpl.getInsetsController().show(WindowInsets.Type.ime(), + false /* fromIme */, statsToken); return true; - } else { - return false; } + return false; } else { // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread. // TODO(b/229426865): call WindowInsetsController#show instead. @@ -2497,7 +2503,10 @@ public final class InputMethodManager { if (Flags.refactorInsetsController()) { // TODO(b/322992891) handle case of HIDE_IMPLICIT_ONLY - servedView.getWindowInsetsController().hide(WindowInsets.Type.ime()); + final var viewRootImpl = servedView.getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime()); + } return true; } else { return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java index 6629fdc4cdee..16727c30dfd4 100644 --- a/core/java/android/webkit/WebViewProviderInfo.java +++ b/core/java/android/webkit/WebViewProviderInfo.java @@ -23,6 +23,9 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.Base64; +import java.util.Arrays; +import java.util.Objects; + /** * @hide */ @@ -80,6 +83,35 @@ public final class WebViewProviderInfo implements Parcelable { out.writeTypedArray(signatures, 0); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof WebViewProviderInfo that) { + return this.packageName.equals(that.packageName) + && this.description.equals(that.description) + && this.availableByDefault == that.availableByDefault + && this.isFallback == that.isFallback + && Arrays.equals(this.signatures, that.signatures); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(packageName, description, availableByDefault, + isFallback, Arrays.hashCode(signatures)); + } + + @Override + public String toString() { + return "WebViewProviderInfo; packageName=" + packageName + + " description=\"" + description + + "\" availableByDefault=" + availableByDefault + + " isFallback=" + isFallback + + " signatures=" + Arrays.toString(signatures); + } + // fields read from framework resource public final String packageName; public final String description; diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 3f1c06ac7e10..91ac4ff05687 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -101,6 +101,13 @@ flag { } flag { + name: "enable_cascading_windows" + namespace: "lse_desktop_experience" + description: "Whether to apply cascading effect for placing multiple windows when first launched" + bug: "325240051" +} + +flag { name: "enable_camera_compat_for_desktop_windowing" namespace: "lse_desktop_experience" description: "Whether to apply Camera Compat treatment to fixed-orientation apps in desktop windowing mode" @@ -129,6 +136,13 @@ flag { } flag { + name: "enable_caption_compat_inset_force_consumption_always" + namespace: "lse_desktop_experience" + description: "Enables force-consumption of caption bar insets for all apps in freeform" + bug: "352563889" +} + +flag { name: "show_desktop_windowing_dev_option" namespace: "lse_desktop_experience" description: "Whether to show developer option for enabling desktop windowing mode" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 5397e91bd249..c451cc880a8c 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -19,6 +19,16 @@ flag { } flag { + name: "do_not_skip_ime_by_target_visibility" + namespace: "windowing_frontend" + description: "Avoid window traversal missing IME" + bug: "339375944" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "apply_lifecycle_on_pip_change" namespace: "windowing_frontend" description: "Make pip activity lifecyle change with windowing mode" diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 652cba7ed00d..4d717239b1b2 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -66,6 +66,7 @@ import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogDataType; import com.android.internal.protolog.common.LogLevel; +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -100,6 +101,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { ); @Nullable private final ProtoLogViewerConfigReader mViewerConfigReader; + @Nullable private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>(); private final Runnable mCacheUpdater; @@ -111,13 +113,12 @@ public class PerfettoProtoLogImpl implements IProtoLog { private final Lock mBackgroundServiceLock = new ReentrantLock(); private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); - public PerfettoProtoLogImpl(String viewerConfigFilePath, Runnable cacheUpdater) { + public PerfettoProtoLogImpl(@NonNull String viewerConfigFilePath, Runnable cacheUpdater) { this(() -> { try { return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); } catch (FileNotFoundException e) { - Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e); - return null; + throw new RuntimeException("Failed to load viewer config file " + viewerConfigFilePath, e); } }, cacheUpdater); } @@ -127,7 +128,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { } public PerfettoProtoLogImpl( - @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, Runnable cacheUpdater ) { this(viewerConfigInputStreamProvider, @@ -242,6 +243,15 @@ public class PerfettoProtoLogImpl implements IProtoLog { for (IProtoLogGroup protoLogGroup : protoLogGroups) { mLogGroups.put(protoLogGroup.name(), protoLogGroup); } + + final String[] groupsLoggingToLogcat = Arrays.stream(protoLogGroups) + .filter(IProtoLogGroup::isLogToLogcat) + .map(IProtoLogGroup::name) + .toArray(String[]::new); + + if (mViewerConfigReader != null) { + mViewerConfigReader.loadViewerConfig(groupsLoggingToLogcat); + } } /** diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 3082295a522c..77ca7ce91b22 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -30,6 +30,7 @@ import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLogToolInjected; +import java.io.File; import java.util.TreeMap; /** @@ -105,7 +106,15 @@ public class ProtoLogImpl { public static synchronized IProtoLog getSingleInstance() { if (sServiceInstance == null) { if (android.tracing.Flags.perfettoProtologTracing()) { - sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater); + File f = new File(sViewerConfigPath); + if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) { + // TODO(b/353530422): Remove - temporary fix to unblock b/352290057 + // In so tests the viewer config file might not exist in which we don't + // want to provide config path to the user + sServiceInstance = new PerfettoProtoLogImpl(null, null, sCacheUpdater); + } else { + sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater); + } } else { sServiceInstance = new LegacyProtoLogImpl( sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater); diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java index bb6c8b7a9698..38ca0d8f75e8 100644 --- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java +++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java @@ -24,12 +24,13 @@ import java.util.Set; import java.util.TreeMap; public class ProtoLogViewerConfigReader { + @NonNull private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; private final Map<String, Set<Long>> mGroupHashes = new TreeMap<>(); private final LongSparseArray<String> mLogMessageMap = new LongSparseArray<>(); public ProtoLogViewerConfigReader( - ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; } diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java index 334f5488425a..14bc8e4782f2 100644 --- a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java +++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java @@ -16,11 +16,13 @@ package com.android.internal.protolog; +import android.annotation.NonNull; import android.util.proto.ProtoInputStream; public interface ViewerConfigInputStreamProvider { /** * @return a ProtoInputStream. */ + @NonNull ProtoInputStream getInputStream(); } diff --git a/core/java/com/android/internal/widget/MaxHeightFrameLayout.java b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java new file mode 100644 index 000000000000..d65dddd9c5b1 --- /dev/null +++ b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import android.annotation.AttrRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.Px; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import com.android.internal.R; + +/** + * This custom subclass of FrameLayout enforces that its calculated height be no larger than the + * given maximum height (if any). + * + * @hide + */ +public class MaxHeightFrameLayout extends FrameLayout { + + private int mMaxHeight = Integer.MAX_VALUE; + + public MaxHeightFrameLayout(@NonNull Context context) { + this(context, null); + } + + public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public MaxHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.MaxHeightFrameLayout, defStyleAttr, defStyleRes); + saveAttributeDataForStyleable(context, R.styleable.MaxHeightFrameLayout, + attrs, a, defStyleAttr, defStyleRes); + + setMaxHeight(a.getDimensionPixelSize(R.styleable.MaxHeightFrameLayout_maxHeight, + Integer.MAX_VALUE)); + } + + /** + * Gets the maximum height of this view, in pixels. + * + * @see #setMaxHeight(int) + * + * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight + */ + @Px + public int getMaxHeight() { + return mMaxHeight; + } + + /** + * Sets the maximum height this view can have. + * + * @param maxHeight the maximum height, in pixels + * + * @see #getMaxHeight() + * + * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight + */ + public void setMaxHeight(@Px int maxHeight) { + mMaxHeight = maxHeight; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (MeasureSpec.getSize(heightMeasureSpec) > mMaxHeight) { + final int mode = MeasureSpec.getMode(heightMeasureSpec); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, mode); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index c07fd3838837..7c62615cdc42 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -27,6 +27,7 @@ #include <android_media_audiopolicy.h> #include <android_os_Parcel.h> #include <audiomanager/AudioManager.h> +#include <android-base/properties.h> #include <binder/IBinder.h> #include <jni.h> #include <media/AidlConversion.h> @@ -41,8 +42,10 @@ #include <system/audio_policy.h> #include <utils/Log.h> +#include <thread> #include <optional> #include <sstream> +#include <memory> #include <vector> #include "android_media_AudioAttributes.h" @@ -261,6 +264,13 @@ static struct { jfieldID mMixerBehavior; } gAudioMixerAttributesField; +static struct { + jclass clazz; + jmethodID run; +} gRunnableClassInfo; + +static JavaVM* gVm; + static Mutex gLock; enum AudioError { @@ -3362,6 +3372,55 @@ static jboolean android_media_AudioSystem_isBluetoothVariableLatencyEnabled(JNIE return enabled; } +class JavaSystemPropertyListener { + public: + JavaSystemPropertyListener(JNIEnv* env, jobject javaCallback, std::string sysPropName) : + mCallback(env->NewGlobalRef(javaCallback)), + mCachedProperty(android::base::CachedProperty{std::move(sysPropName)}) { + mListenerThread = std::thread([this]() mutable { + JNIEnv* threadEnv = GetOrAttachJNIEnvironment(gVm); + while (!mCleanupSignal.load()) { + using namespace std::chrono_literals; + // 1s timeout so this thread can read the cleanup signal to (slowly) be able to + // be destroyed. + std::string newVal = mCachedProperty.WaitForChange(1000ms) ?: ""; + if (newVal != "" && mLastVal != newVal) { + threadEnv->CallVoidMethod(mCallback, gRunnableClassInfo.run); + mLastVal = std::move(newVal); + } + } + }); + } + + ~JavaSystemPropertyListener() { + mCleanupSignal.store(true); + mListenerThread.join(); + JNIEnv* env = GetOrAttachJNIEnvironment(gVm); + env->DeleteGlobalRef(mCallback); + } + + private: + jobject mCallback; + android::base::CachedProperty mCachedProperty; + std::thread mListenerThread; + std::atomic<bool> mCleanupSignal{false}; + std::string mLastVal = ""; +}; + +std::vector<std::unique_ptr<JavaSystemPropertyListener>> gSystemPropertyListeners; +std::mutex gSysPropLock{}; + +static void android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env, jobject thiz, + jstring sysProp, + jobject javaCallback) { + ScopedUtfChars sysPropChars{env, sysProp}; + auto listener = std::make_unique<JavaSystemPropertyListener>(env, javaCallback, + std::string{sysPropChars.c_str()}); + std::unique_lock _l{gSysPropLock}; + gSystemPropertyListeners.push_back(std::move(listener)); +} + + // ---------------------------------------------------------------------------- #define MAKE_AUDIO_SYSTEM_METHOD(x) \ @@ -3534,7 +3593,12 @@ static const JNINativeMethod gMethods[] = android_media_AudioSystem_clearPreferredMixerAttributes), MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency), MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled), - MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled)}; + MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled), + MAKE_JNI_NATIVE_METHOD("listenForSystemPropertyChange", + "(Ljava/lang/String;Ljava/lang/Runnable;)V", + android_media_AudioSystem_listenForSystemPropertyChange), + + }; static const JNINativeMethod gEventHandlerMethods[] = {MAKE_JNI_NATIVE_METHOD("native_setup", "(Ljava/lang/Object;)V", @@ -3816,6 +3880,12 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixerAttributesField.mMixerBehavior = GetFieldIDOrDie(env, audioMixerAttributesClass, "mMixerBehavior", "I"); + jclass runnableClazz = FindClassOrDie(env, "java/lang/Runnable"); + gRunnableClassInfo.clazz = MakeGlobalRefOrDie(env, runnableClazz); + gRunnableClassInfo.run = GetMethodIDOrDie(env, runnableClazz, "run", "()V"); + + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&gVm) != 0); + AudioSystem::addErrorCallback(android_media_AudioSystem_error_callback); RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp index d32486c73db2..240be3fe5534 100644 --- a/core/jni/android_view_MotionEvent.cpp +++ b/core/jni/android_view_MotionEvent.cpp @@ -415,7 +415,7 @@ static void android_view_MotionEvent_nativeAddBatch(JNIEnv* env, jclass clazz, env->DeleteLocalRef(pointerCoordsObj); } - event->addSample(eventTimeNanos, rawPointerCoords.data()); + event->addSample(eventTimeNanos, rawPointerCoords.data(), event->getId()); event->setMetaState(event->getMetaState() | metaState); } diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index 30c926c57693..7e2a5ace7e64 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -17,6 +17,8 @@ #include <android-base/logging.h> #include <android-base/properties.h> #include <android/graphics/jni_runtime.h> +#include <android_runtime/AndroidRuntime.h> +#include <jni_wrappers.h> #include <nativehelper/JNIHelp.h> #include <nativehelper/jni_macros.h> #include <unicode/putil.h> @@ -27,9 +29,6 @@ #include <unordered_map> #include <vector> -#include "android_view_InputDevice.h" -#include "core_jni_helpers.h" -#include "jni.h" #ifdef _WIN32 #include <windows.h> #else @@ -38,8 +37,6 @@ #include <sys/stat.h> #endif -#include <iostream> - using namespace std; /* @@ -49,12 +46,6 @@ using namespace std; * (see AndroidRuntime.cpp). */ -static JavaVM* javaVM; -static jclass bridge; -static jclass layoutLog; -static jmethodID getLogId; -static jmethodID logMethodId; - extern int register_android_os_Binder(JNIEnv* env); extern int register_libcore_util_NativeAllocationRegistry_Delegate(JNIEnv* env); @@ -168,28 +159,9 @@ static int register_jni_procs(const std::unordered_map<std::string, RegJNIRec>& } } - if (register_android_graphics_classes(env) < 0) { - return -1; - } - return 0; } -int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, - const JNINativeMethod* gMethods, int numMethods) { - return jniRegisterNativeMethods(env, className, gMethods, numMethods); -} - -JNIEnv* AndroidRuntime::getJNIEnv() { - JNIEnv* env; - if (javaVM->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) return nullptr; - return env; -} - -JavaVM* AndroidRuntime::getJavaVM() { - return javaVM; -} - static vector<string> parseCsv(const string& csvString) { vector<string> result; istringstream stream(csvString); @@ -200,29 +172,6 @@ static vector<string> parseCsv(const string& csvString) { return result; } -void LayoutlibLogger(base::LogId, base::LogSeverity severity, const char* tag, const char* file, - unsigned int line, const char* message) { - JNIEnv* env = AndroidRuntime::getJNIEnv(); - jint logPrio = severity; - jstring tagString = env->NewStringUTF(tag); - jstring messageString = env->NewStringUTF(message); - - jobject bridgeLog = env->CallStaticObjectMethod(bridge, getLogId); - - env->CallVoidMethod(bridgeLog, logMethodId, logPrio, tagString, messageString); - - env->DeleteLocalRef(tagString); - env->DeleteLocalRef(messageString); - env->DeleteLocalRef(bridgeLog); -} - -void LayoutlibAborter(const char* abort_message) { - // Layoutlib should not call abort() as it would terminate Studio. - // Throw an exception back to Java instead. - JNIEnv* env = AndroidRuntime::getJNIEnv(); - jniThrowRuntimeException(env, "The Android framework has encountered a fatal error"); -} - // This method has been copied/adapted from system/core/init/property_service.cpp // If the ro.product.cpu.abilist* properties have not been explicitly // set, derive them from ro.system.product.cpu.abilist* properties. @@ -311,62 +260,49 @@ static void* mmapFile(const char* dataFilePath) { #endif } -static bool init_icu(const char* dataPath) { - void* addr = mmapFile(dataPath); - UErrorCode err = U_ZERO_ERROR; - udata_setCommonData(addr, &err); - if (err != U_ZERO_ERROR) { - return false; +// Loads the ICU data file from the location specified in the system property ro.icu.data.path +static void loadIcuData() { + string icuPath = base::GetProperty("ro.icu.data.path", ""); + if (!icuPath.empty()) { + // Set the location of ICU data + void* addr = mmapFile(icuPath.c_str()); + UErrorCode err = U_ZERO_ERROR; + udata_setCommonData(addr, &err); + if (err != U_ZERO_ERROR) { + ALOGE("Unable to load ICU data\n"); + } } - return true; } -// Creates an array of InputDevice from key character map files -static void init_keyboard(JNIEnv* env, const vector<string>& keyboardPaths) { - jclass inputDevice = FindClassOrDie(env, "android/view/InputDevice"); - jobjectArray inputDevicesArray = - env->NewObjectArray(keyboardPaths.size(), inputDevice, nullptr); - int keyboardId = 1; - - for (const string& path : keyboardPaths) { - base::Result<std::shared_ptr<KeyCharacterMap>> charMap = - KeyCharacterMap::load(path, KeyCharacterMap::Format::BASE); - - InputDeviceInfo info = InputDeviceInfo(); - info.initialize(keyboardId, 0, 0, InputDeviceIdentifier(), - "keyboard " + std::to_string(keyboardId), true, false, - ui::LogicalDisplayId::DEFAULT); - info.setKeyboardType(AINPUT_KEYBOARD_TYPE_ALPHABETIC); - info.setKeyCharacterMap(*charMap); - - jobject inputDeviceObj = android_view_InputDevice_create(env, info); - if (inputDeviceObj) { - env->SetObjectArrayElement(inputDevicesArray, keyboardId - 1, inputDeviceObj); - env->DeleteLocalRef(inputDeviceObj); - } - keyboardId++; - } +static int register_android_core_classes(JNIEnv* env) { + jclass system = FindClassOrDie(env, "java/lang/System"); + jmethodID getPropertyMethod = + GetStaticMethodIDOrDie(env, system, "getProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); - if (bridge == nullptr) { - bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge"); - bridge = MakeGlobalRefOrDie(env, bridge); - } - jmethodID setInputManager = GetStaticMethodIDOrDie(env, bridge, "setInputManager", - "([Landroid/view/InputDevice;)V"); - env->CallStaticVoidMethod(bridge, setInputManager, inputDevicesArray); - env->DeleteLocalRef(inputDevicesArray); -} + // Get the names of classes that need to register their native methods + auto nativesClassesJString = + (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, + env->NewStringUTF("core_native_classes"), + env->NewStringUTF("")); + const char* nativesClassesArray = env->GetStringUTFChars(nativesClassesJString, nullptr); + string nativesClassesString(nativesClassesArray); + vector<string> classesToRegister = parseCsv(nativesClassesString); + env->ReleaseStringUTFChars(nativesClassesJString, nativesClassesArray); -} // namespace android + if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) { + return JNI_ERR; + } -using namespace android; + return 0; +} // Called right before aborting by LOG_ALWAYS_FATAL. Print the pending exception. void abort_handler(const char* abort_message) { ALOGE("About to abort the process..."); - JNIEnv* env = NULL; - if (javaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + if (env == nullptr) { ALOGE("vm->GetEnv() failed"); return; } @@ -377,107 +313,98 @@ void abort_handler(const char* abort_message) { ALOGE("Aborting because: %s", abort_message); } -JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { - javaVM = vm; - JNIEnv* env = nullptr; - if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { - return JNI_ERR; - } - - __android_log_set_aborter(abort_handler); +// ------------------ Host implementation of AndroidRuntime ------------------ - init_android_graphics(); +/*static*/ JavaVM* AndroidRuntime::mJavaVM; - // Configuration is stored as java System properties. - // Get a reference to System.getProperty - jclass system = FindClassOrDie(env, "java/lang/System"); - jmethodID getPropertyMethod = - GetStaticMethodIDOrDie(env, system, "getProperty", - "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); +/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, + const JNINativeMethod* gMethods, + int numMethods) { + return jniRegisterNativeMethods(env, className, gMethods, numMethods); +} - // Java system properties that contain LayoutLib config. The initial values in the map - // are the default values if the property is not specified. - std::unordered_map<std::string, std::string> systemProperties = - {{"core_native_classes", ""}, - {"register_properties_during_load", ""}, - {"icu.data.path", ""}, - {"use_bridge_for_logging", ""}, - {"keyboard_paths", ""}}; - - for (auto& [name, defaultValue] : systemProperties) { - jstring propertyString = - (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, - env->NewStringUTF(name.c_str()), - env->NewStringUTF(defaultValue.c_str())); - const char* propertyChars = env->GetStringUTFChars(propertyString, 0); - systemProperties[name] = string(propertyChars); - env->ReleaseStringUTFChars(propertyString, propertyChars); +/*static*/ JNIEnv* AndroidRuntime::getJNIEnv() { + JNIEnv* env; + JavaVM* vm = AndroidRuntime::getJavaVM(); + if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return nullptr; } - // Get the names of classes that need to register their native methods - vector<string> classesToRegister = parseCsv(systemProperties["core_native_classes"]); + return env; +} - if (systemProperties["register_properties_during_load"] == "true") { - // Set the system properties first as they could be used in the static initialization of - // other classes - if (register_android_os_SystemProperties(env) < 0) { - return JNI_ERR; - } - classesToRegister.erase(find(classesToRegister.begin(), classesToRegister.end(), - "android.os.SystemProperties")); - bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge"); - bridge = MakeGlobalRefOrDie(env, bridge); - jmethodID setSystemPropertiesMethod = - GetStaticMethodIDOrDie(env, bridge, "setSystemProperties", "()V"); - env->CallStaticVoidMethod(bridge, setSystemPropertiesMethod); - property_initialize_ro_cpu_abilist(); - } +/*static*/ JavaVM* AndroidRuntime::getJavaVM() { + return mJavaVM; +} - if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) { +/*static*/ int AndroidRuntime::startReg(JNIEnv* env) { + if (register_android_core_classes(env) < 0) { return JNI_ERR; } - - if (!systemProperties["icu.data.path"].empty()) { - // Set the location of ICU data - bool icuInitialized = init_icu(systemProperties["icu.data.path"].c_str()); - if (!icuInitialized) { - return JNI_ERR; - } + if (register_android_graphics_classes(env) < 0) { + return JNI_ERR; } + return 0; +} - if (systemProperties["use_bridge_for_logging"] == "true") { - layoutLog = FindClassOrDie(env, "com/android/ide/common/rendering/api/ILayoutLog"); - layoutLog = MakeGlobalRefOrDie(env, layoutLog); - logMethodId = GetMethodIDOrDie(env, layoutLog, "logAndroidFramework", - "(ILjava/lang/String;Ljava/lang/String;)V"); - if (bridge == nullptr) { - bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge"); - bridge = MakeGlobalRefOrDie(env, bridge); - } - getLogId = GetStaticMethodIDOrDie(env, bridge, "getLog", - "()Lcom/android/ide/common/rendering/api/ILayoutLog;"); - android::base::SetLogger(LayoutlibLogger); - android::base::SetAborter(LayoutlibAborter); - } else { - // initialize logging, so ANDROD_LOG_TAGS env variable is respected - android::base::InitLogging(nullptr, android::base::StderrLogger); - } +void AndroidRuntime::onVmCreated(JNIEnv* env) { + env->GetJavaVM(&mJavaVM); +} + +void AndroidRuntime::onStarted() { + property_initialize_ro_cpu_abilist(); + loadIcuData(); // Use English locale for number format to ensure correct parsing of floats when using strtof setlocale(LC_NUMERIC, "en_US.UTF-8"); +} - if (!systemProperties["keyboard_paths"].empty()) { - vector<string> keyboardPaths = parseCsv(systemProperties["keyboard_paths"]); - init_keyboard(env, keyboardPaths); - } else { - fprintf(stderr, "Skip initializing keyboard\n"); +void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + // Register native functions. + if (startReg(env) < 0) { + ALOGE("Unable to register all android native methods\n"); } + onStarted(); +} - return JNI_VERSION_1_6; +AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength) + : mExitWithoutCleanup(false), mArgBlockStart(argBlockStart), mArgBlockLength(argBlockLength) { + init_android_graphics(); } -JNIEXPORT void JNI_OnUnload(JavaVM* vm, void*) { +AndroidRuntime::~AndroidRuntime() {} + +// Version of AndroidRuntime to run on host +class HostRuntime : public AndroidRuntime { +public: + HostRuntime() : AndroidRuntime(nullptr, 0) {} + + void onVmCreated(JNIEnv* env) override { + AndroidRuntime::onVmCreated(env); + // initialize logging, so ANDROD_LOG_TAGS env variable is respected + android::base::InitLogging(nullptr, android::base::StderrLogger, abort_handler); + } + + void onStarted() override { + AndroidRuntime::onStarted(); + } +}; + +} // namespace android + +using namespace android; + +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv* env = nullptr; - vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); - env->DeleteGlobalRef(bridge); - env->DeleteGlobalRef(layoutLog); + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + + Vector<String8> args; + HostRuntime runtime; + + runtime.onVmCreated(env); + runtime.start("HostRuntime", args, false); + + return JNI_VERSION_1_6; } diff --git a/core/res/res/drawable/ic_ime_switcher_new.xml b/core/res/res/drawable/ic_ime_switcher_new.xml new file mode 100644 index 000000000000..04f4a250b3ec --- /dev/null +++ b/core/res/res/drawable/ic_ime_switcher_new.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z" + android:fillColor="#FFFFFFFF"/> +</vector> diff --git a/core/res/res/drawable/input_method_switch_button.xml b/core/res/res/drawable/input_method_switch_button.xml new file mode 100644 index 000000000000..396d81ed87f6 --- /dev/null +++ b/core/res/res/drawable/input_method_switch_button.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:insetTop="6dp" + android:insetBottom="6dp"> + <ripple android:color="?android:attr/colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners android:radius="28dp"/> + <solid android:color="@color/white"/> + </shape> + </item> + + <item> + <shape android:shape="rectangle"> + <corners android:radius="28dp"/> + <solid android:color="@color/transparent"/> + <stroke android:color="?attr/materialColorPrimary" + android:width="1dp"/> + <padding android:left="16dp" + android:top="8dp" + android:right="16dp" + android:bottom="8dp"/> + </shape> + </item> + </ripple> +</inset> diff --git a/core/res/res/drawable/input_method_switch_item_background.xml b/core/res/res/drawable/input_method_switch_item_background.xml new file mode 100644 index 000000000000..eb7a24691f37 --- /dev/null +++ b/core/res/res/drawable/input_method_switch_item_background.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/list_highlight_material"> + <item android:id="@id/mask"> + <shape android:shape="rectangle"> + <corners android:radius="28dp"/> + <solid android:color="@color/white"/> + </shape> + </item> + + <item> + <selector> + <item android:state_activated="true"> + <shape android:shape="rectangle"> + <corners android:radius="28dp"/> + <solid android:color="?attr/materialColorSecondaryContainer"/> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/core/res/res/layout/input_method_switch_dialog_new.xml b/core/res/res/layout/input_method_switch_dialog_new.xml new file mode 100644 index 000000000000..5a4d6b14a52b --- /dev/null +++ b/core/res/res/layout/input_method_switch_dialog_new.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <com.android.internal.widget.MaxHeightFrameLayout + android:layout_width="320dp" + android:layout_height="0dp" + android:layout_weight="1" + android:maxHeight="373dp"> + + <com.android.internal.widget.RecyclerView + android:id="@+id/list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingVertical="8dp" + android:clipToPadding="false" + android:layoutManager="com.android.internal.widget.LinearLayoutManager"/> + + </com.android.internal.widget.MaxHeightFrameLayout> + + <LinearLayout + style="?android:attr/buttonBarStyle" + android:id="@+id/button_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingHorizontal="16dp" + android:paddingTop="8dp" + android:paddingBottom="16dp" + android:visibility="gone"> + + <Space + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1"/> + + <Button + style="?attr/buttonBarButtonStyle" + android:id="@+id/button1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/input_method_switch_button" + android:layout_gravity="end" + android:text="@string/input_method_language_settings" + android:fontFamily="google-sans-text" + android:textAppearance="?attr/textAppearance" + android:visibility="gone"/> + + </LinearLayout> + +</LinearLayout> diff --git a/core/res/res/layout/input_method_switch_item_new.xml b/core/res/res/layout/input_method_switch_item_new.xml new file mode 100644 index 000000000000..16a97c4b39ee --- /dev/null +++ b/core/res/res/layout/input_method_switch_item_new.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingBottom="8dp"> + + <View + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?attr/materialColorSurfaceVariant" + android:layout_marginStart="20dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:layout_marginBottom="12dp" + android:visibility="gone"/> + + <TextView + android:id="@+id/header_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:ellipsize="end" + android:singleLine="true" + android:fontFamily="google-sans-text" + android:textAppearance="?attr/textAppearance" + android:textColor="?attr/materialColorPrimary" + android:visibility="gone"/> + + <LinearLayout + android:id="@+id/list_item" + android:layout_width="match_parent" + android:layout_height="72dp" + android:background="@drawable/input_method_switch_item_background" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="20dp" + android:paddingEnd="24dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="start|center_vertical" + android:orientation="vertical"> + + <TextView + android:id="@+id/text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="marquee" + android:singleLine="true" + android:fontFamily="google-sans-text" + android:textAppearance="?attr/textAppearanceListItem"/> + + </LinearLayout> + + <ImageView + android:id="@+id/image" + android:layout_width="24dp" + android:layout_height="24dp" + android:gravity="center_vertical" + android:layout_marginStart="12dp" + android:src="@drawable/ic_check_24dp" + android:tint="?attr/materialColorOnSurface" + android:visibility="gone"/> + + </LinearLayout> + +</LinearLayout> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 0975eda3f9ff..7cc9e13db5cf 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -5243,6 +5243,11 @@ the VISIBLE or INVISIBLE state when measuring. Defaults to false. --> <attr name="measureAllChildren" format="boolean" /> </declare-styleable> + <!-- @hide --> + <declare-styleable name="MaxHeightFrameLayout"> + <!-- An optional argument to supply a maximum height for this view. --> + <attr name="maxHeight" format="dimension" /> + </declare-styleable> <declare-styleable name="ExpandableListView"> <!-- Indicator shown beside the group View. This can be a stateful Drawable. --> <attr name="groupIndicator" format="reference" /> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 46b154163224..ec865f6c376f 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3880,6 +3880,8 @@ <!-- Title of the pop-up dialog in which the user switches keyboard, also known as input method. --> <string name="select_input_method">Choose input method</string> + <!-- Button to access the language settings of the current input method. [CHAR LIMIT=50]--> + <string name="input_method_language_settings">Language Settings</string> <!-- Summary text of a toggle switch to enable/disable use of the IME while a physical keyboard is connected --> <string name="show_ime">Keep it on screen while physical keyboard is active</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c50b961f74cd..fcafdaed8d1a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1577,6 +1577,8 @@ <java-symbol type="layout" name="input_method" /> <java-symbol type="layout" name="input_method_extract_view" /> <java-symbol type="layout" name="input_method_switch_item" /> + <java-symbol type="layout" name="input_method_switch_item_new" /> + <java-symbol type="layout" name="input_method_switch_dialog_new" /> <java-symbol type="layout" name="input_method_switch_dialog_title" /> <java-symbol type="layout" name="js_prompt" /> <java-symbol type="layout" name="list_content_simple" /> @@ -2552,6 +2554,7 @@ <java-symbol type="dimen" name="input_method_nav_key_button_ripple_max_width" /> <java-symbol type="drawable" name="ic_ime_nav_back" /> <java-symbol type="drawable" name="ic_ime_switcher" /> + <java-symbol type="drawable" name="ic_ime_switcher_new" /> <java-symbol type="id" name="input_method_nav_back" /> <java-symbol type="id" name="input_method_nav_buttons" /> <java-symbol type="id" name="input_method_nav_center_group" /> @@ -5400,6 +5403,7 @@ <java-symbol type="style" name="Theme.DeviceDefault.DialogWhenLarge" /> <java-symbol type="style" name="Theme.DeviceDefault.DocumentsUI" /> <java-symbol type="style" name="Theme.DeviceDefault.InputMethod" /> + <java-symbol type="style" name="Theme.DeviceDefault.InputMethodSwitcherDialog" /> <java-symbol type="style" name="Theme.DeviceDefault.Light.DarkActionBar" /> <java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.FixedSize" /> <java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.MinWidth" /> diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml index 382ff0441fd2..f5c67387cb92 100644 --- a/core/res/res/values/themes_device_defaults.xml +++ b/core/res/res/values/themes_device_defaults.xml @@ -6179,4 +6179,10 @@ easier. <item name="colorListDivider">@color/list_divider_opacity_device_default_light</item> <item name="opacityListDivider">@color/list_divider_opacity_device_default_light</item> </style> + + <!-- Device default theme for the Input Method Switcher dialog. --> + <style name="Theme.DeviceDefault.InputMethodSwitcherDialog" parent="Theme.DeviceDefault.Dialog.Alert.DayNight"> + <item name="windowMinWidthMajor">@null</item> + <item name="windowMinWidthMinor">@null</item> + </style> </resources> diff --git a/core/tests/coretests/src/android/animation/OWNERS b/core/tests/coretests/src/android/animation/OWNERS new file mode 100644 index 000000000000..1eefb3a3dc65 --- /dev/null +++ b/core/tests/coretests/src/android/animation/OWNERS @@ -0,0 +1 @@ +include /core/java/android/animation/OWNERS diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java index 47b288993d11..248db65d7435 100644 --- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java @@ -37,8 +37,12 @@ import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.widget.TextView; @@ -46,6 +50,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -61,6 +66,9 @@ import org.mockito.Spy; @RunWith(AndroidJUnit4.class) public class ImeInsetsSourceConsumerTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); InsetsSourceConsumer mImeConsumer; @Spy InsetsController mController; @@ -112,6 +120,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeRequestedVisibleAwaitingControl() { // Set null control and then request show. mController.onControlsChanged(new InsetsSourceControl[] { null }); @@ -141,6 +150,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeRequestedVisibleAwaitingLeash() { // Set null control, then request show. mController.onControlsChanged(new InsetsSourceControl[] { null }); @@ -185,6 +195,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeGetAndClearSkipAnimationOnce_expectSkip() { // Expect IME animation will skipped when the IME is visible at first place. verifyImeGetAndClearSkipAnimationOnce(true /* hasWindowFocus */, true /* hasViewFocus */, @@ -192,6 +203,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeGetAndClearSkipAnimationOnce_expectNoSkip() { // Expect IME animation will not skipped if previously no view focused when gained the // window focus and requesting the IME visible next time. diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java index 7c7cb18afc4d..9887c272e7f8 100644 --- a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java +++ b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java @@ -55,9 +55,9 @@ import com.sun.source.util.TreeScanner; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; -import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.ClassType; +import com.sun.tools.javac.tree.JCTree.JCNewClass; import java.util.ArrayList; import java.util.Arrays; @@ -67,7 +67,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Predicate; import java.util.regex.Pattern; import javax.lang.model.element.Name; @@ -125,6 +124,12 @@ public final class RequiresPermissionChecker extends BugChecker instanceMethod() .onDescendantOf("android.content.Context") .withNameMatching(Pattern.compile("^send(Ordered|Sticky)?Broadcast.*$"))); + private static final Matcher<ExpressionTree> SEND_BROADCAST_AS_USER = + methodInvocation( + instanceMethod() + .onDescendantOf("android.content.Context") + .withNameMatching( + Pattern.compile("^send(Ordered|Sticky)?Broadcast.*AsUser.*$"))); private static final Matcher<ExpressionTree> SEND_PENDING_INTENT = methodInvocation( instanceMethod() .onDescendantOf("android.app.PendingIntent") @@ -306,18 +311,6 @@ public final class RequiresPermissionChecker extends BugChecker } } - private static ExpressionTree findArgumentByParameterName(MethodInvocationTree tree, - Predicate<String> paramName) { - final MethodSymbol sym = ASTHelpers.getSymbol(tree); - final List<VarSymbol> params = sym.getParameters(); - for (int i = 0; i < params.size(); i++) { - if (paramName.test(params.get(i).name.toString())) { - return tree.getArguments().get(i); - } - } - return null; - } - private static Name resolveName(ExpressionTree tree) { if (tree instanceof IdentifierTree) { return ((IdentifierTree) tree).getName(); @@ -345,76 +338,85 @@ public final class RequiresPermissionChecker extends BugChecker private static ParsedRequiresPermission parseBroadcastSourceRequiresPermission( MethodInvocationTree methodTree, VisitorState state) { - final ExpressionTree arg = findArgumentByParameterName(methodTree, - (name) -> name.toLowerCase().contains("intent")); - if (arg instanceof IdentifierTree) { - final Name argName = ((IdentifierTree) arg).getName(); - final MethodTree method = state.findEnclosing(MethodTree.class); - final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>(); - method.accept(new TreeScanner<Void, Void>() { - private ParsedRequiresPermission last; - - @Override - public Void visitMethodInvocation(MethodInvocationTree tree, Void param) { - if (Objects.equal(methodTree, tree)) { - res.set(last); - } else { - final Name name = resolveName(tree.getMethodSelect()); - if (Objects.equal(argName, name) - && INTENT_SET_ACTION.matches(tree, state)) { - last = parseIntentAction(tree); + if (methodTree.getArguments().size() < 1) { + return null; + } + final ExpressionTree arg = methodTree.getArguments().get(0); + if (!(arg instanceof IdentifierTree)) { + return null; + } + final Name argName = ((IdentifierTree) arg).getName(); + final MethodTree method = state.findEnclosing(MethodTree.class); + final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>(); + method.accept(new TreeScanner<Void, Void>() { + private ParsedRequiresPermission mLast; + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void param) { + if (Objects.equal(methodTree, tree)) { + res.set(mLast); + } else { + final Name name = resolveName(tree.getMethodSelect()); + if (Objects.equal(argName, name) && INTENT_SET_ACTION.matches(tree, state)) { + mLast = parseIntentAction(tree); + } else if (name == null && tree.getMethodSelect() instanceof MemberSelectTree) { + ExpressionTree innerTree = + ((MemberSelectTree) tree.getMethodSelect()).getExpression(); + if (innerTree instanceof JCNewClass) { + mLast = parseIntentAction((NewClassTree) innerTree); } } - return super.visitMethodInvocation(tree, param); } + return super.visitMethodInvocation(tree, param); + } - @Override - public Void visitAssignment(AssignmentTree tree, Void param) { - final Name name = resolveName(tree.getVariable()); - final Tree init = tree.getExpression(); - if (Objects.equal(argName, name) - && init instanceof NewClassTree) { - last = parseIntentAction((NewClassTree) init); - } - return super.visitAssignment(tree, param); + @Override + public Void visitAssignment(AssignmentTree tree, Void param) { + final Name name = resolveName(tree.getVariable()); + final Tree init = tree.getExpression(); + if (Objects.equal(argName, name) && init instanceof NewClassTree) { + mLast = parseIntentAction((NewClassTree) init); } + return super.visitAssignment(tree, param); + } - @Override - public Void visitVariable(VariableTree tree, Void param) { - final Name name = tree.getName(); - final ExpressionTree init = tree.getInitializer(); - if (Objects.equal(argName, name) - && init instanceof NewClassTree) { - last = parseIntentAction((NewClassTree) init); - } - return super.visitVariable(tree, param); + @Override + public Void visitVariable(VariableTree tree, Void param) { + final Name name = tree.getName(); + final ExpressionTree init = tree.getInitializer(); + if (Objects.equal(argName, name) && init instanceof NewClassTree) { + mLast = parseIntentAction((NewClassTree) init); } - }, null); - return res.get(); - } - return null; + return super.visitVariable(tree, param); + } + }, null); + return res.get(); } private static ParsedRequiresPermission parseBroadcastTargetRequiresPermission( MethodInvocationTree tree, VisitorState state) { - final ExpressionTree arg = findArgumentByParameterName(tree, - (name) -> name.toLowerCase().contains("permission")); final ParsedRequiresPermission res = new ParsedRequiresPermission(); - if (arg != null) { - arg.accept(new TreeScanner<Void, Void>() { - @Override - public Void visitIdentifier(IdentifierTree tree, Void param) { - res.addConstValue(tree); - return super.visitIdentifier(tree, param); - } - - @Override - public Void visitMemberSelect(MemberSelectTree tree, Void param) { - res.addConstValue(tree); - return super.visitMemberSelect(tree, param); - } - }, null); + int permission_position = 1; + if (SEND_BROADCAST_AS_USER.matches(tree, state)) { + permission_position = 2; } + if (tree.getArguments().size() < permission_position + 1) { + return res; + } + final ExpressionTree arg = tree.getArguments().get(permission_position); + arg.accept(new TreeScanner<Void, Void>() { + @Override + public Void visitIdentifier(IdentifierTree tree, Void param) { + res.addConstValue(tree); + return super.visitIdentifier(tree, param); + } + + @Override + public Void visitMemberSelect(MemberSelectTree tree, Void param) { + res.addConstValue(tree); + return super.visitMemberSelect(tree, param); + } + }, null); return res; } diff --git a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java index e53372d97f3d..05fde7c4fe57 100644 --- a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java +++ b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java @@ -412,6 +412,19 @@ public class RequiresPermissionCheckerTest { " context.sendBroadcast(intent);", " }", " }", + " public void exampleWithChainedMethod(Context context) {", + " Intent intent = new Intent(FooManager.ACTION_RED)", + " .putExtra(\"foo\", 42);", + " context.sendBroadcast(intent, FooManager.PERMISSION_RED);", + " context.sendBroadcastWithMultiplePermissions(intent,", + " new String[] { FooManager.PERMISSION_RED });", + " }", + " public void exampleWithAsUser(Context context) {", + " Intent intent = new Intent(FooManager.ACTION_RED);", + " context.sendBroadcastAsUser(intent, 42, FooManager.PERMISSION_RED);", + " context.sendBroadcastAsUserMultiplePermissions(intent, 42,", + " new String[] { FooManager.PERMISSION_RED });", + " }", "}") .doTest(); } diff --git a/errorprone/tests/res/android/content/Context.java b/errorprone/tests/res/android/content/Context.java index efc4fb122435..9d622ffaf120 100644 --- a/errorprone/tests/res/android/content/Context.java +++ b/errorprone/tests/res/android/content/Context.java @@ -36,4 +36,15 @@ public class Context { public void sendBroadcastWithMultiplePermissions(Intent intent, String[] receiverPermissions) { throw new UnsupportedOperationException(); } + + /* Fake user type for test purposes */ + public void sendBroadcastAsUser(Intent intent, int user, String receiverPermission) { + throw new UnsupportedOperationException(); + } + + /* Fake user type for test purposes */ + public void sendBroadcastAsUserMultiplePermissions( + Intent intent, int user, String[] receiverPermissions) { + throw new UnsupportedOperationException(); + } } diff --git a/errorprone/tests/res/android/content/Intent.java b/errorprone/tests/res/android/content/Intent.java index 288396e60577..7ccea784754a 100644 --- a/errorprone/tests/res/android/content/Intent.java +++ b/errorprone/tests/res/android/content/Intent.java @@ -24,4 +24,8 @@ public class Intent { public Intent setAction(String action) { throw new UnsupportedOperationException(); } + + public Intent putExtra(String extra, int value) { + throw new UnsupportedOperationException(); + } } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt index 235b9bf7b9fd..fc3dc1465dff 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt @@ -168,6 +168,16 @@ object PhysicsAnimatorTestUtils { } } + /** Whether any animation is currently running. */ + @JvmStatic + fun isAnyAnimationRunning(): Boolean { + for (target in allAnimatedObjects) { + val animator = PhysicsAnimator.getInstance(target) + if (animator.isRunning()) return true + } + return false + } + /** * Blocks the calling thread until the first animation frame in which predicate returns true. If * the given object isn't animating, returns without blocking. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index f7a5c271a729..d4d9d003bc0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -16,7 +16,7 @@ package com.android.wm.shell.bubbles; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -225,8 +225,7 @@ public class BubbleExpandedView extends LinearLayout { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index c79d9c4942bf..5e2141aa639e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -15,7 +15,7 @@ */ package com.android.wm.shell.bubbles; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -103,8 +103,7 @@ public class BubbleTaskViewHelper { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 9a1a8a20ae0e..31f797a222a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -818,9 +818,8 @@ class DesktopTasksController( val intent = Intent(context, DesktopWallpaperActivity::class.java) val options = ActivityOptions.makeBasic().apply { - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true pendingIntentBackgroundActivityStartMode = - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS } val pendingIntent = PendingIntent.getActivity( @@ -983,6 +982,7 @@ class DesktopTasksController( ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: skip keyguard is locked") return null } + val wct = WindowContainerTransaction() if (!isDesktopModeShowing(task.displayId)) { ProtoLog.d( WM_SHELL_DESKTOP_MODE, @@ -990,12 +990,17 @@ class DesktopTasksController( " taskId=%d", task.taskId ) - return WindowContainerTransaction().also { wct -> - bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) - wct.reorder(task.token, true) + // We are outside of desktop mode and already existing desktop task is being launched. + // We should make this task go to fullscreen instead of freeform. Note that this means + // any re-launch of a freeform window outside of desktop will be in fullscreen. + if (desktopModeTaskRepository.isActiveTask(task.taskId)) { + addMoveToFullscreenChanges(wct, task) + return wct } + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) + wct.reorder(task.token, true) + return wct } - val wct = WindowContainerTransaction() if (useDesktopOverrideDensity()) { wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) } @@ -1420,7 +1425,6 @@ class DesktopTasksController( setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED ) - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true } val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, null, opts.toBundle()) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 95fe8b6f1f4e..7e0362475f21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -16,7 +16,7 @@ package com.android.wm.shell.draganddrop; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -280,8 +280,7 @@ public class DragAndDropPolicy { baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); // Put BAL flags to avoid activity start aborted. baseActivityOpts.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final Bundle opts = baseActivityOpts.toBundle(); if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 9539a456502f..d001b2c09f85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -466,7 +466,7 @@ public class RecentTasksController implements TaskStackListenerCallback, @Nullable WindowContainerToken ignoreTaskToken) { List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(2, false /* filterOnlyVisibleRecents */); - for (int i = tasks.size() - 1; i >= 0; i--) { + for (int i = 0; i < tasks.size(); i++) { final ActivityManager.RunningTaskInfo task = tasks.get(i); if (task.token.equals(ignoreTaskToken)) { continue; 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 9bcd9b0a11c8..dbeee3b0a450 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 @@ -16,7 +16,7 @@ package com.android.wm.shell.splitscreen; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; @@ -1909,8 +1909,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split // will be canceled. - options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); // TODO (b/336477473): Disallow enter PiP when launching a task in split by default; // this might have to be changed as more split-to-pip cujs are defined. @@ -3739,8 +3739,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow", - "app package " + taskInfo.baseActivity.getPackageName() - + " does not support splitscreen, or is a controlled activity type")); + "app package " + taskInfo.baseIntent.getComponent() + + " does not support splitscreen, or is a controlled activity" + + " type")); if (splitScreenVisible) { handleUnsupportedSplitStart(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java index dd4595a70211..287e779d8e24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java @@ -48,6 +48,7 @@ public class ShellInit { public ShellInit(ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; + ProtoLog.registerGroups(ShellProtoLogGroup.values()); } /** @@ -76,7 +77,6 @@ public class ShellInit { */ @VisibleForTesting public void init() { - ProtoLog.registerGroups(ShellProtoLogGroup.values()); ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size()); SurfaceControl.setDebugUsageAfterRelease(true); // Init in order of registration diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index bce233fb0b52..b51b700bc315 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -15,19 +15,15 @@ */ package com.android.wm.shell.windowdecor +import android.annotation.ColorInt import android.annotation.DimenRes import android.app.ActivityManager -import android.app.WindowConfiguration -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN -import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW -import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.content.Context import android.content.res.ColorStateList -import android.content.res.Configuration import android.content.res.Resources import android.graphics.Bitmap -import android.graphics.Color +import android.graphics.BlendMode +import android.graphics.BlendModeColorFilter import android.graphics.Point import android.graphics.PointF import android.graphics.Rect @@ -40,6 +36,8 @@ import android.widget.ImageView import android.widget.TextView import android.window.SurfaceSyncGroup import androidx.annotation.VisibleForTesting +import androidx.compose.ui.graphics.toArgb +import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController @@ -47,7 +45,10 @@ import com.android.wm.shell.common.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.extension.isFullscreen +import com.android.wm.shell.windowdecor.extension.isMultiWindow +import com.android.wm.shell.windowdecor.extension.isPinned /** * Handle menu opened when the appropriate button is clicked on. @@ -72,6 +73,7 @@ class HandleMenu( ) { private val context: Context = parentDecor.mDecorWindowContext private val taskInfo: ActivityManager.RunningTaskInfo = parentDecor.mTaskInfo + private val decorThemeUtil = DecorThemeUtil(context) private val isViewAboveStatusBar: Boolean get() = (Flags.enableAdditionalWindowsAboveStatusBar() && !taskInfo.isFreeform) @@ -102,31 +104,6 @@ class HandleMenu( // those as well. private val globalMenuPosition: Point = Point() - /** - * An a array of windowing icon color based on current UI theme. First element of the - * array is for inactive icons and the second is for active icons. - */ - private val windowingIconColor: Array<ColorStateList> - get() { - val mode = (context.resources.configuration.uiMode - and Configuration.UI_MODE_NIGHT_MASK) - val isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES) - val typedArray = context.obtainStyledAttributes( - intArrayOf( - com.android.internal.R.attr.materialColorOnSurface, - com.android.internal.R.attr.materialColorPrimary - ) - ) - val inActiveColor = - typedArray.getColor(0, if (isNightMode) Color.WHITE else Color.BLACK) - val activeColor = typedArray.getColor(1, if (isNightMode) Color.WHITE else Color.BLACK) - typedArray.recycle() - return arrayOf( - ColorStateList.valueOf(inActiveColor), - ColorStateList.valueOf(activeColor) - ) - } - init { updateHandleMenuPillPositions() } @@ -175,9 +152,8 @@ class HandleMenu( * Animates the appearance of the handle menu and its three pills. */ private fun animateHandleMenu() { - when (taskInfo.windowingMode) { - WindowConfiguration.WINDOWING_MODE_FULLSCREEN, - WINDOWING_MODE_MULTI_WINDOW -> { + when { + taskInfo.isFullscreen || taskInfo.isMultiWindow -> { handleMenuAnimator?.animateCaptionHandleExpandToOpen() } else -> { @@ -193,85 +169,94 @@ class HandleMenu( private fun setupHandleMenu() { val handleMenu = handleMenuViewContainer?.view ?: return handleMenu.setOnTouchListener(onTouchListener) - setupAppInfoPill(handleMenu) + + val style = calculateMenuStyle() + setupAppInfoPill(handleMenu, style) if (shouldShowWindowingPill) { - setupWindowingPill(handleMenu) + setupWindowingPill(handleMenu, style) } - setupMoreActionsPill(handleMenu) - setupOpenInBrowserPill(handleMenu) + setupMoreActionsPill(handleMenu, style) + setupOpenInBrowserPill(handleMenu, style) } /** * Set up interactive elements of handle menu's app info pill. */ - private fun setupAppInfoPill(handleMenu: View) { - val collapseBtn = handleMenu.findViewById<HandleMenuImageButton>(R.id.collapse_menu_button) - val appIcon = handleMenu.findViewById<ImageView>(R.id.application_icon) - val appName = handleMenu.findViewById<TextView>(R.id.application_name) - collapseBtn.setOnClickListener(onClickListener) - collapseBtn.taskInfo = taskInfo - appIcon.setImageBitmap(appIconBitmap) - appName.text = this.appName + private fun setupAppInfoPill(handleMenu: View, style: MenuStyle) { + val pill = handleMenu.requireViewById<View>(R.id.app_info_pill).apply { + background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + } + + pill.requireViewById<HandleMenuImageButton>(R.id.collapse_menu_button) + .let { collapseBtn -> + collapseBtn.imageTintList = ColorStateList.valueOf(style.textColor) + collapseBtn.setOnClickListener(onClickListener) + collapseBtn.taskInfo = taskInfo + } + pill.requireViewById<ImageView>(R.id.application_icon).let { appIcon -> + appIcon.setImageBitmap(appIconBitmap) + } + pill.requireViewById<TextView>(R.id.application_name).let { appNameView -> + appNameView.text = appName + appNameView.setTextColor(style.textColor) + } } /** * Set up interactive elements and color of handle menu's windowing pill. */ - private fun setupWindowingPill(handleMenu: View) { - val fullscreenBtn = handleMenu.findViewById<ImageButton>(R.id.fullscreen_button) - val splitscreenBtn = handleMenu.findViewById<ImageButton>(R.id.split_screen_button) - val floatingBtn = handleMenu.findViewById<ImageButton>(R.id.floating_button) + private fun setupWindowingPill(handleMenu: View, style: MenuStyle) { + val pill = handleMenu.requireViewById<View>(R.id.windowing_pill).apply { + background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + } + + val fullscreenBtn = pill.requireViewById<ImageButton>(R.id.fullscreen_button) + val splitscreenBtn = pill.requireViewById<ImageButton>(R.id.split_screen_button) + val floatingBtn = pill.requireViewById<ImageButton>(R.id.floating_button) // TODO: Remove once implemented. floatingBtn.visibility = View.GONE + val desktopBtn = handleMenu.requireViewById<ImageButton>(R.id.desktop_button) - val desktopBtn = handleMenu.findViewById<ImageButton>(R.id.desktop_button) fullscreenBtn.setOnClickListener(onClickListener) splitscreenBtn.setOnClickListener(onClickListener) floatingBtn.setOnClickListener(onClickListener) desktopBtn.setOnClickListener(onClickListener) - // The button corresponding to the windowing mode that the task is currently in uses a - // different color than the others. - val iconColors = windowingIconColor - val inActiveColorStateList = iconColors[0] - val activeColorStateList = iconColors[1] - fullscreenBtn.imageTintList = if (taskInfo.isFullscreen) { - activeColorStateList - } else { - inActiveColorStateList - } - splitscreenBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { - activeColorStateList - } else { - inActiveColorStateList - } - floatingBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_PINNED) { - activeColorStateList - } else { - inActiveColorStateList - } - desktopBtn.imageTintList = if (taskInfo.isFreeform) { - activeColorStateList - } else { - inActiveColorStateList - } + + fullscreenBtn.isSelected = taskInfo.isFullscreen + fullscreenBtn.imageTintList = style.windowingButtonColor + splitscreenBtn.isSelected = taskInfo.isMultiWindow + splitscreenBtn.imageTintList = style.windowingButtonColor + floatingBtn.isSelected = taskInfo.isPinned + floatingBtn.imageTintList = style.windowingButtonColor + desktopBtn.isSelected = taskInfo.isFreeform + desktopBtn.imageTintList = style.windowingButtonColor } /** * Set up interactive elements & height of handle menu's more actions pill */ - private fun setupMoreActionsPill(handleMenu: View) { - if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { - handleMenu.findViewById<View>(R.id.more_actions_pill).visibility = View.GONE + private fun setupMoreActionsPill(handleMenu: View, style: MenuStyle) { + val pill = handleMenu.requireViewById<View>(R.id.more_actions_pill).apply { + isGone = !SHOULD_SHOW_MORE_ACTIONS_PILL + background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + } + pill.requireViewById<Button>(R.id.screenshot_button).let { screenshotBtn -> + screenshotBtn.setTextColor(style.textColor) + screenshotBtn.compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } } - private fun setupOpenInBrowserPill(handleMenu: View) { - if (!shouldShowBrowserPill) { - handleMenu.findViewById<View>(R.id.open_in_browser_pill).visibility = View.GONE - return + private fun setupOpenInBrowserPill(handleMenu: View, style: MenuStyle) { + val pill = handleMenu.requireViewById<View>(R.id.open_in_browser_pill).apply { + isGone = !shouldShowBrowserPill + background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + } + + pill.requireViewById<Button>(R.id.open_in_browser_button).let { browserButton -> + browserButton.setOnClickListener(onClickListener) + browserButton.setTextColor(style.textColor) + browserButton.compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } - val browserButton = handleMenu.findViewById<Button>(R.id.open_in_browser_button) - browserButton.setOnClickListener(onClickListener) } /** @@ -303,20 +288,20 @@ class HandleMenu( } private fun updateGlobalMenuPosition(taskBounds: Rect) { - when (taskInfo.windowingMode) { - WINDOWING_MODE_FREEFORM -> { + when { + taskInfo.isFreeform -> { globalMenuPosition.set( /* x = */ taskBounds.left + marginMenuStart, /* y = */ taskBounds.top + marginMenuTop ) } - WINDOWING_MODE_FULLSCREEN -> { + taskInfo.isFullscreen -> { globalMenuPosition.set( /* x = */ taskBounds.width() / 2 - (menuWidth / 2), /* y = */ marginMenuTop ) } - WINDOWING_MODE_MULTI_WINDOW -> { + taskInfo.isMultiWindow -> { val splitPosition = splitScreenController.getSplitPosition(taskInfo.taskId) val leftOrTopStageBounds = Rect() val rightOrBottomStageBounds = Rect() @@ -469,14 +454,41 @@ class HandleMenu( handleMenuViewContainer?.releaseView() handleMenuViewContainer = null } - if (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || - taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + if (taskInfo.isFullscreen || taskInfo.isMultiWindow) { handleMenuAnimator?.animateCollapseIntoHandleClose(after) } else { handleMenuAnimator?.animateClose(after) } } + private fun calculateMenuStyle(): MenuStyle { + val colorScheme = decorThemeUtil.getColorScheme(taskInfo) + return MenuStyle( + backgroundColor = colorScheme.surfaceBright.toArgb(), + textColor = colorScheme.onSurface.toArgb(), + windowingButtonColor = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_focused), + intArrayOf(android.R.attr.state_selected), + intArrayOf(), + ), + intArrayOf( + colorScheme.onSurface.toArgb(), + colorScheme.onSurface.toArgb(), + colorScheme.primary.toArgb(), + colorScheme.onSurface.toArgb(), + ) + ), + ) + } + + private data class MenuStyle( + @ColorInt val backgroundColor: Int, + @ColorInt val textColor: Int, + val windowingButtonColor: ColorStateList, + ) + companion object { private const val TAG = "HandleMenu" private const val SHOULD_SHOW_MORE_ACTIONS_PILL = false diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt index 7ade9876d28a..6f8e00143848 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt @@ -18,6 +18,8 @@ package com.android.wm.shell.windowdecor.extension import android.app.TaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND @@ -33,5 +35,14 @@ val TaskInfo.isLightCaptionBarAppearance: Boolean return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0 } +/** Whether the task is in fullscreen windowing mode. */ val TaskInfo.isFullscreen: Boolean get() = windowingMode == WINDOWING_MODE_FULLSCREEN + +/** Whether the task is in pinned windowing mode. */ +val TaskInfo.isPinned: Boolean + get() = windowingMode == WINDOWING_MODE_PINNED + +/** Whether the task is in multi-window windowing mode. */ +val TaskInfo.isMultiWindow: Boolean + get() = windowingMode == WINDOWING_MODE_MULTI_WINDOW diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt index db962e717a3b..2406bdeebdf2 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt @@ -48,7 +48,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @Before fun setup() { - tapl.workspace.switchToOverview().dismissAllTasks() + val overview = tapl.workspace.switchToOverview() + if (overview.hasTasks()) { + overview.dismissAllTasks() + } tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 078694df3f5d..6002c21ccb24 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -1326,6 +1326,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + + val wct = + controller.handleRequest(Binder(), createTransition(freeformTask)) + + // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA. + assertNotNull(wct, "should handle request") + assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index d18fec2f24ad..e7b4c50b9871 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -347,8 +347,7 @@ public class StageCoordinatorTests extends ShellTestCase { assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token); assertThat(options.getPendingIntentBackgroundActivityStartMode()) - .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue(); + .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); } @Test diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 2d0e7abbe890..a255f730b0f3 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -2651,4 +2651,11 @@ public class AudioSystem * @hide */ public static native boolean isBluetoothVariableLatencyEnabled(); + + /** + * Register a native listener for system property sysprop + * @param callback the listener which fires when the property changes + * @hide + */ + public static native void listenForSystemPropertyChange(String sysprop, Runnable callback); } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 999f40e53952..1c5049e891e9 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -23,6 +23,7 @@ import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.Context; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.VirtualDisplayFlag; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.os.Build; @@ -140,6 +141,7 @@ public final class MediaProjection { /** * @hide */ + @Nullable public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, boolean isSecure, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { @@ -192,6 +194,11 @@ public final class MediaProjection { * <li>If attempting to create a new virtual display * associated with this MediaProjection instance after it has * been stopped by invoking {@link #stop()}. + * <li>If attempting to create a new virtual display + * associated with this MediaProjection instance after a + * {@link MediaProjection.Callback#onStop()} callback has been + * received due to the user or the system stopping the + * MediaProjection session. * <li>If the target SDK is {@link * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, * and if this instance has already taken a recording through @@ -208,12 +215,17 @@ public final class MediaProjection { * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}. * Instead, recording doesn't begin until the user re-grants * consent in the dialog. + * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay} + * could be created. * @see VirtualDisplay * @see VirtualDisplay.Callback */ + @SuppressWarnings("RequiresPermission") + @Nullable public VirtualDisplay createVirtualDisplay(@NonNull String name, - int width, int height, int dpi, int flags, @Nullable Surface surface, - @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { + int width, int height, int dpi, @VirtualDisplayFlag int flags, + @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, + @Nullable Handler handler) { if (shouldMediaProjectionRequireCallback()) { if (mCallbacks.isEmpty()) { final IllegalStateException e = new IllegalStateException( diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index 88770d487a83..186b69b4107b 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -912,8 +912,10 @@ class InstallRepository(private val context: Context) { "message: $message" ) } + + val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) + if (statusCode == PackageInstaller.STATUS_SUCCESS) { - val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) val resultIntent = if (shouldReturnResult) { Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED) } else { @@ -922,12 +924,34 @@ class InstallRepository(private val context: Context) { } _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent)) } else { - if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) { + // TODO (b/346655018): Use INSTALL_FAILED_ABORTED legacyCode in the condition + // statusCode can be STATUS_FAILURE_ABORTED if: + // 1. GPP blocks an install. + // 2. User denies ownership update explicitly. + // InstallFailed dialog must not be shown only when the user denies ownership update. We + // must show this dialog for all other install failures. + + val userDenied = + statusCode == PackageInstaller.STATUS_FAILURE_ABORTED && + legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_TIMEOUT && + legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE + + if (shouldReturnResult) { + val resultIntent = Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus) _installResult.setValue( - InstallFailed(appSnippet, statusCode, legacyStatus, message) + InstallFailed( + legacyCode = legacyStatus, + statusCode = statusCode, + shouldReturnResult = true, + resultIntent = resultIntent + ) ) - } else { + } else if (userDenied) { _installResult.setValue(InstallAborted(ABORT_REASON_INTERNAL_ERROR)) + } else { + _installResult.setValue( + InstallFailed(appSnippet, legacyStatus, statusCode, message) + ) } } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt index 5dd4d2905f47..8de8fbb3e688 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt @@ -19,6 +19,7 @@ package com.android.packageinstaller.v2.model import android.app.Activity import android.content.Intent import android.content.pm.PackageManager +import android.content.pm.PackageInstaller import android.graphics.drawable.Drawable sealed class InstallStage(val stageCode: Int) { @@ -77,11 +78,10 @@ data class InstallSuccess( val shouldReturnResult: Boolean = false, /** * - * * If the caller is requesting a result back, this will hold the Intent with - * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED] which is sent - * back to the caller. + * * If the caller is requesting a result back, this will hold an Intent with + * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED]. * - * * If the caller doesn't want the result back, this will hold the Intent that launches + * * If the caller doesn't want the result back, this will hold an Intent that launches * the newly installed / updated app if a launchable activity exists. */ val resultIntent: Intent? = null, @@ -95,17 +95,23 @@ data class InstallSuccess( } data class InstallFailed( - private val appSnippet: PackageUtil.AppSnippet, + private val appSnippet: PackageUtil.AppSnippet? = null, val legacyCode: Int, val statusCode: Int, - val message: String?, + val message: String? = null, + val shouldReturnResult: Boolean = false, + /** + * If the caller is requesting a result back, this will hold an Intent with + * [Intent.EXTRA_INSTALL_RESULT] set to the [PackageInstaller.EXTRA_LEGACY_STATUS]. + */ + val resultIntent: Intent? = null ) : InstallStage(STAGE_FAILED) { val appIcon: Drawable? - get() = appSnippet.icon + get() = appSnippet?.icon val appLabel: String? - get() = appSnippet.label as String? + get() = appSnippet?.label as String? } data class InstallAborted( diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt index 31b9ccbdb838..e2ab31662380 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt @@ -171,15 +171,20 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val successIntent = success.resultIntent setResult(Activity.RESULT_OK, successIntent, true) } else { - val successFragment = InstallSuccessFragment(success) - showDialogInner(successFragment) + val successDialog = InstallSuccessFragment(success) + showDialogInner(successDialog) } } InstallStage.STAGE_FAILED -> { val failed = installStage as InstallFailed - val failedDialog = InstallFailedFragment(failed) - showDialogInner(failedDialog) + if (failed.shouldReturnResult) { + val failureIntent = failed.resultIntent + setResult(Activity.RESULT_FIRST_USER, failureIntent, true) + } else { + val failureDialog = InstallFailedFragment(failed) + showDialogInner(failureDialog) + } } else -> { diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java index 4028b73a2c71..714f9519f378 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java @@ -18,9 +18,7 @@ package com.android.settingslib.mobile.dataservice; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; -import android.telephony.UiccCardInfo; import android.telephony.UiccPortInfo; -import android.telephony.UiccSlotInfo; import android.telephony.UiccSlotMapping; public class DataServiceUtils { @@ -71,53 +69,9 @@ public class DataServiceUtils { public static final String COLUMN_ID = "sudId"; /** - * The name of the physical slot index column, see - * {@link UiccSlotMapping#getPhysicalSlotIndex()}. - */ - public static final String COLUMN_PHYSICAL_SLOT_INDEX = "physicalSlotIndex"; - - /** - * The name of the logical slot index column, see - * {@link UiccSlotMapping#getLogicalSlotIndex()}. - */ - public static final String COLUMN_LOGICAL_SLOT_INDEX = "logicalSlotIndex"; - - /** - * The name of the card ID column, see {@link UiccCardInfo#getCardId()}. - */ - public static final String COLUMN_CARD_ID = "cardId"; - - /** - * The name of the eUICC state column, see {@link UiccCardInfo#isEuicc()}. - */ - public static final String COLUMN_IS_EUICC = "isEuicc"; - - /** - * The name of the multiple enabled profiles supported state column, see - * {@link UiccCardInfo#isMultipleEnabledProfilesSupported()}. - */ - public static final String COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED = - "isMultipleEnabledProfilesSupported"; - - /** - * The name of the card state column, see {@link UiccSlotInfo#getCardStateInfo()}. - */ - public static final String COLUMN_CARD_STATE = "cardState"; - - /** - * The name of the removable state column, see {@link UiccSlotInfo#isRemovable()}. - */ - public static final String COLUMN_IS_REMOVABLE = "isRemovable"; - - /** * The name of the active state column, see {@link UiccPortInfo#isActive()}. */ public static final String COLUMN_IS_ACTIVE = "isActive"; - - /** - * The name of the port index column, see {@link UiccPortInfo#getPortIndex()}. - */ - public static final String COLUMN_PORT_INDEX = "portIndex"; } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java index c92204fa1f39..5f7fa278082b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java @@ -19,14 +19,13 @@ package com.android.settingslib.mobile.dataservice; import android.content.Context; import android.util.Log; -import java.util.List; -import java.util.Objects; - import androidx.lifecycle.LiveData; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; -import androidx.sqlite.db.SupportSQLiteDatabase; + +import java.util.List; +import java.util.Objects; @Database(entities = {SubscriptionInfoEntity.class, UiccInfoEntity.class, MobileNetworkInfoEntity.class}, exportSchema = false, version = 1) @@ -132,13 +131,6 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { } /** - * Query the UICC info by the subscription ID from the UiccInfoEntity table. - */ - public LiveData<UiccInfoEntity> queryUiccInfoById(String id) { - return mUiccInfoDao().queryUiccInfoById(id); - } - - /** * Delete the subscriptionInfo info by the subscription ID from the SubscriptionInfoEntity * table. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java index 7e60421d0ab4..90e5189fdf1d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java @@ -16,14 +16,14 @@ package com.android.settingslib.mobile.dataservice; -import java.util.List; - import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; +import java.util.List; + @Dao public interface UiccInfoDao { @@ -34,14 +34,6 @@ public interface UiccInfoDao { + DataServiceUtils.UiccInfoData.COLUMN_ID) LiveData<List<UiccInfoEntity>> queryAllUiccInfos(); - @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE " - + DataServiceUtils.UiccInfoData.COLUMN_ID + " = :subId") - LiveData<UiccInfoEntity> queryUiccInfoById(String subId); - - @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE " - + DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC + " = :isEuicc") - LiveData<List<UiccInfoEntity>> queryUiccInfosByEuicc(boolean isEuicc); - @Query("SELECT COUNT(*) FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME) int count(); diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java index 2ccf295007dc..0f80edf52d80 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java @@ -26,20 +26,9 @@ import androidx.room.PrimaryKey; @Entity(tableName = DataServiceUtils.UiccInfoData.TABLE_NAME) public class UiccInfoEntity { - public UiccInfoEntity(@NonNull String subId, @NonNull String physicalSlotIndex, - int logicalSlotIndex, int cardId, boolean isEuicc, - boolean isMultipleEnabledProfilesSupported, int cardState, boolean isRemovable, - boolean isActive, int portIndex) { + public UiccInfoEntity(@NonNull String subId, boolean isActive) { this.subId = subId; - this.physicalSlotIndex = physicalSlotIndex; - this.logicalSlotIndex = logicalSlotIndex; - this.cardId = cardId; - this.isEuicc = isEuicc; - this.isMultipleEnabledProfilesSupported = isMultipleEnabledProfilesSupported; - this.cardState = cardState; - this.isRemovable = isRemovable; this.isActive = isActive; - this.portIndex = portIndex; } @PrimaryKey @@ -47,48 +36,14 @@ public class UiccInfoEntity { @NonNull public String subId; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PHYSICAL_SLOT_INDEX) - @NonNull - public String physicalSlotIndex; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_LOGICAL_SLOT_INDEX) - public int logicalSlotIndex; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_ID) - public int cardId; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC) - public boolean isEuicc; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED) - public boolean isMultipleEnabledProfilesSupported; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_STATE) - public int cardState; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_REMOVABLE) - public boolean isRemovable; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_ACTIVE) public boolean isActive; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PORT_INDEX) - public int portIndex; - - @Override public int hashCode() { int result = 17; result = 31 * result + subId.hashCode(); - result = 31 * result + physicalSlotIndex.hashCode(); - result = 31 * result + logicalSlotIndex; - result = 31 * result + cardId; - result = 31 * result + Boolean.hashCode(isEuicc); - result = 31 * result + Boolean.hashCode(isMultipleEnabledProfilesSupported); - result = 31 * result + cardState; - result = 31 * result + Boolean.hashCode(isRemovable); result = 31 * result + Boolean.hashCode(isActive); - result = 31 * result + portIndex; return result; } @@ -102,40 +57,15 @@ public class UiccInfoEntity { } UiccInfoEntity info = (UiccInfoEntity) obj; - return TextUtils.equals(subId, info.subId) - && TextUtils.equals(physicalSlotIndex, info.physicalSlotIndex) - && logicalSlotIndex == info.logicalSlotIndex - && cardId == info.cardId - && isEuicc == info.isEuicc - && isMultipleEnabledProfilesSupported == info.isMultipleEnabledProfilesSupported - && cardState == info.cardState - && isRemovable == info.isRemovable - && isActive == info.isActive - && portIndex == info.portIndex; + return TextUtils.equals(subId, info.subId) && isActive == info.isActive; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append(" {UiccInfoEntity(subId = ") .append(subId) - .append(", logicalSlotIndex = ") - .append(physicalSlotIndex) - .append(", logicalSlotIndex = ") - .append(logicalSlotIndex) - .append(", cardId = ") - .append(cardId) - .append(", isEuicc = ") - .append(isEuicc) - .append(", isMultipleEnabledProfilesSupported = ") - .append(isMultipleEnabledProfilesSupported) - .append(", cardState = ") - .append(cardState) - .append(", isRemovable = ") - .append(isRemovable) .append(", isActive = ") .append(isActive) - .append(", portIndex = ") - .append(portIndex) .append(")}"); return builder.toString(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 7886e85cbad8..49b974fa3f00 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -20,6 +20,7 @@ import android.app.NotificationManager import android.provider.Settings import com.android.settingslib.notification.modes.TestModeBuilder import com.android.settingslib.notification.modes.ZenMode +import java.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -35,8 +36,7 @@ class FakeZenModeRepository : ZenModeRepository { override val globalZenMode: StateFlow<Int> get() = mutableZenMode.asStateFlow() - private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = - MutableStateFlow(listOf(TestModeBuilder.EXAMPLE)) + private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf()) override val modes: Flow<List<ZenMode>> get() = mutableModesFlow.asStateFlow() @@ -52,6 +52,10 @@ class FakeZenModeRepository : ZenModeRepository { mutableZenMode.value = zenMode } + fun addModes(zenModes: List<ZenMode>) { + mutableModesFlow.value += zenModes + } + fun addMode(id: String, active: Boolean = false) { mutableModesFlow.value += newMode(id, active) } @@ -60,6 +64,20 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id } } + override fun activateMode(zenMode: ZenMode, duration: Duration?) { + activateMode(zenMode.id) + } + + override fun deactivateMode(zenMode: ZenMode) { + deactivateMode(zenMode.id) + } + + fun activateMode(id: String) { + val oldMode = mutableModesFlow.value.find { it.id == id } ?: return + removeMode(id) + mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build() + } + fun deactivateMode(id: String) { val oldMode = mutableModesFlow.value.find { it.id == id } ?: return removeMode(id) diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index b2fcb5f6da41..0ff7f84a08b9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -30,6 +30,7 @@ import android.provider.Settings import com.android.settingslib.flags.Flags import com.android.settingslib.notification.modes.ZenMode import com.android.settingslib.notification.modes.ZenModesBackend +import java.time.Duration import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose @@ -57,6 +58,10 @@ interface ZenModeRepository { /** A list of all existing priority modes. */ val modes: Flow<List<ZenMode>> + + fun activateMode(zenMode: ZenMode, duration: Duration? = null) + + fun deactivateMode(zenMode: ZenMode) } @SuppressLint("SharedFlowCreation") @@ -178,4 +183,12 @@ class ZenModeRepositoryImpl( flowOf(emptyList()) } } + + override fun activateMode(zenMode: ZenMode, duration: Duration?) { + backend.activateMode(zenMode, duration) + } + + override fun deactivateMode(zenMode: ZenMode) { + backend.deactivateMode(zenMode) + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index 7b994d59d963..2f7cdd617081 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -37,6 +37,13 @@ public class TestModeBuilder { private ZenModeConfig.ZenRule mConfigZenRule; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); + public static final ZenMode MANUAL_DND = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("Manual DND", Uri.parse("rule://dnd")) + .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(), + true /* isActive */ + ); public TestModeBuilder() { // Reasonable defaults diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index a17076b525f4..4e01a71df113 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -539,6 +539,7 @@ android_library { "androidx.preference_preference", "androidx.appcompat_appcompat", "androidx.concurrent_concurrent-futures", + "androidx.concurrent_concurrent-futures-ktx", "androidx.mediarouter_mediarouter", "androidx.palette_palette", "androidx.legacy_legacy-preference-v14", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8e2f7c186d8c..7032c73f798f 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -870,6 +870,13 @@ flag { } flag { + name: "qs_ui_refactor_compose_fragment" + namespace: "systemui" + description: "Uses a different QS fragment in NPVC that uses the new compose UI and recommended architecture. This flag depends on qs_ui_refactor flag." + bug: "325099249" +} + +flag { name: "remove_dream_overlay_hide_on_touch" namespace: "systemui" description: "Removes logic to hide the dream overlay on user interaction, as it conflicts with various transitions" @@ -1189,6 +1196,16 @@ flag { } flag { + namespace: "systemui" + name: "remove_update_listener_in_qs_icon_view_impl" + description: "Remove update listeners in QsIconViewImpl class to avoid memory leak." + bug: "327078684" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "sim_pin_race_condition_on_restart" namespace: "systemui" description: "The SIM PIN screen may be shown incorrectly on reboot" @@ -1196,4 +1213,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "sim_pin_talkback_fix_for_double_submit" + namespace: "systemui" + description: "The SIM PIN entry screens show the wrong message due" + bug: "346932439" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 92f03d792554..35db9e0c2bb8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -209,7 +209,6 @@ fun CommunalContainer( backgroundType = backgroundType, colors = colors, content = content, - modifier = Modifier.horizontalNestedScrollToScene(), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 768e6533ac7d..1c02d3f7662b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -969,6 +969,8 @@ private fun WidgetContent( val clickActionLabel = stringResource(R.string.accessibility_action_label_select_widget) val removeWidgetActionLabel = stringResource(R.string.accessibility_action_label_remove_widget) val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget) + val unselectWidgetActionLabel = + stringResource(R.string.accessibility_action_label_unselect_widget) val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle() val selectedIndex = selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } } @@ -1009,18 +1011,7 @@ private fun WidgetContent( contentListState.onSaveList() true } - val selectWidgetAction = - CustomAccessibilityAction(clickActionLabel) { - val currentWidgetKey = - index?.let { - keyAtIndexIfEditable(contentListState.list, index) - } - viewModel.setSelectedKey(currentWidgetKey) - true - } - - val actions = mutableListOf(selectWidgetAction, deleteAction) - + val actions = mutableListOf(deleteAction) if (selectedIndex != null && selectedIndex != index) { actions.add( CustomAccessibilityAction(placeWidgetActionLabel) { @@ -1032,6 +1023,21 @@ private fun WidgetContent( ) } + if (!selected) { + actions.add( + CustomAccessibilityAction(clickActionLabel) { + viewModel.setSelectedKey(model.key) + true + } + ) + } else { + actions.add( + CustomAccessibilityAction(unselectWidgetActionLabel) { + viewModel.setSelectedKey(null) + true + } + ) + } customActions = actions } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 42ec2d253421..3cf8e70d458f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -305,8 +305,7 @@ private fun SceneScope.QuickSettingsScene( if (isCustomizerShowing) { Modifier.fillMaxHeight().align(Alignment.TopCenter) } else { - Modifier.verticalNestedScrollToScene() - .verticalScroll( + Modifier.verticalScroll( scrollState, enabled = isScrollable, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 805351ea8bbe..ece8b40ad332 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -514,8 +514,7 @@ private fun SceneScope.SplitShade( .sysuiResTag("expanded_qs_scroll_view") .weight(1f) .thenIf(!isCustomizerShowing) { - Modifier.verticalNestedScrollToScene() - .verticalScroll( + Modifier.verticalScroll( quickSettingsScrollState, enabled = isScrollable ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 20b1303ae6bd..78ba7defe77c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -64,6 +64,7 @@ internal class DraggableHandlerImpl( internal val orientation: Orientation, internal val coroutineScope: CoroutineScope, ) : DraggableHandler { + internal val nestedScrollKey = Any() /** The [DraggableHandler] can only have one active [DragController] at a time. */ private var dragController: DragControllerImpl? = null @@ -912,9 +913,9 @@ private class Swipes( internal class NestedScrollHandlerImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val orientation: Orientation, - private val topOrLeftBehavior: NestedScrollBehavior, - private val bottomOrRightBehavior: NestedScrollBehavior, - private val isExternalOverscrollGesture: () -> Boolean, + internal var topOrLeftBehavior: NestedScrollBehavior, + internal var bottomOrRightBehavior: NestedScrollBehavior, + internal var isExternalOverscrollGesture: () -> Boolean, private val pointersInfoOwner: PointersInfoOwner, ) { private val layoutState = layoutImpl.state diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 615d393f8bee..2b78b5adaa62 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.PointerInputModifierNode -import androidx.compose.ui.node.TraversableNode import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.node.findNearestAncestor import androidx.compose.ui.node.observeReads @@ -139,16 +138,12 @@ internal class MultiPointerDraggableNode( DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode, - TraversableNode, - PointersInfoOwner, ObserverModifierNode { private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() } private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) private val velocityTracker = VelocityTracker() private var previousEnabled: Boolean = false - override val traverseKey: Any = TRAVERSE_KEY - var enabled: () -> Boolean = enabled set(value) { // Reset the pointer input whenever enabled changed. @@ -208,7 +203,7 @@ internal class MultiPointerDraggableNode( private var startedPosition: Offset? = null private var pointersDown: Int = 0 - override fun pointersInfo(): PointersInfo { + internal fun pointersInfo(): PointersInfo { return PointersInfo( startedPosition = startedPosition, // Note: We could have 0 pointers during fling or for other reasons. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt index ddff2f709082..945043d8fe95 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt @@ -18,12 +18,13 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode -import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.InspectorInfo -import com.android.compose.nestedscroll.PriorityNestedScrollConnection /** * Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled. @@ -67,7 +68,11 @@ enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) { * In addition, during scene transitions, scroll events are consumed by the * [SceneTransitionLayout] instead of the scrollable component. */ - EdgeAlways(canStartOnPostFling = true), + EdgeAlways(canStartOnPostFling = true); + + companion object { + val Default = EdgeNoPreview + } } internal fun Modifier.nestedScrollToScene( @@ -122,37 +127,60 @@ private data class NestedScrollToSceneElement( } private class NestedScrollToSceneNode( - layoutImpl: SceneTransitionLayoutImpl, - orientation: Orientation, - topOrLeftBehavior: NestedScrollBehavior, - bottomOrRightBehavior: NestedScrollBehavior, - isExternalOverscrollGesture: () -> Boolean, + private var layoutImpl: SceneTransitionLayoutImpl, + private var orientation: Orientation, + private var topOrLeftBehavior: NestedScrollBehavior, + private var bottomOrRightBehavior: NestedScrollBehavior, + private var isExternalOverscrollGesture: () -> Boolean, ) : DelegatingNode() { - lateinit var pointersInfoOwner: PointersInfoOwner - private var priorityNestedScrollConnection: PriorityNestedScrollConnection = - scenePriorityNestedScrollConnection( - layoutImpl = layoutImpl, - orientation = orientation, - topOrLeftBehavior = topOrLeftBehavior, - bottomOrRightBehavior = bottomOrRightBehavior, - isExternalOverscrollGesture = isExternalOverscrollGesture, - pointersInfoOwner = { pointersInfoOwner.pointersInfo() } - ) - - private var nestedScrollNode: DelegatableNode = - nestedScrollModifierNode( - connection = priorityNestedScrollConnection, - dispatcher = null, - ) + private var scrollBehaviorOwner: ScrollBehaviorOwner? = null + + private fun requireScrollBehaviorOwner(): ScrollBehaviorOwner { + var behaviorOwner = scrollBehaviorOwner + if (behaviorOwner == null) { + behaviorOwner = requireScrollBehaviorOwner(layoutImpl.draggableHandler(orientation)) + scrollBehaviorOwner = behaviorOwner + } + return behaviorOwner + } - override fun onAttach() { - pointersInfoOwner = requireAncestorPointersInfoOwner() - delegate(nestedScrollNode) + private val updateScrollBehaviorsConnection = + object : NestedScrollConnection { + /** + * When using [NestedScrollConnection.onPostScroll], we can specify the desired behavior + * before our parent components. This gives them the option to override our behavior if + * they choose. + * + * The behavior can be communicated at every scroll gesture to ensure that the hierarchy + * is respected, even if one of our descendant nodes changes behavior after we set it. + */ + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + // If we have some remaining scroll, that scroll can be used to initiate a + // transition between scenes. We can assume that the behavior is only needed if + // there is some remaining amount. + if (available != Offset.Zero) { + requireScrollBehaviorOwner() + .updateScrollBehaviors( + topOrLeftBehavior = topOrLeftBehavior, + bottomOrRightBehavior = bottomOrRightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, + ) + } + + return Offset.Zero + } + } + + init { + delegate(nestedScrollModifierNode(updateScrollBehaviorsConnection, dispatcher = null)) } override fun onDetach() { - // Make sure we reset the scroll connection when this modifier is removed from composition - priorityNestedScrollConnection.reset() + scrollBehaviorOwner = null } fun update( @@ -162,43 +190,10 @@ private class NestedScrollToSceneNode( bottomOrRightBehavior: NestedScrollBehavior, isExternalOverscrollGesture: () -> Boolean, ) { - // Clean up the old nested scroll connection - priorityNestedScrollConnection.reset() - undelegate(nestedScrollNode) - - // Create a new nested scroll connection - priorityNestedScrollConnection = - scenePriorityNestedScrollConnection( - layoutImpl = layoutImpl, - orientation = orientation, - topOrLeftBehavior = topOrLeftBehavior, - bottomOrRightBehavior = bottomOrRightBehavior, - isExternalOverscrollGesture = isExternalOverscrollGesture, - pointersInfoOwner = pointersInfoOwner, - ) - nestedScrollNode = - nestedScrollModifierNode( - connection = priorityNestedScrollConnection, - dispatcher = null, - ) - delegate(nestedScrollNode) + this.layoutImpl = layoutImpl + this.orientation = orientation + this.topOrLeftBehavior = topOrLeftBehavior + this.bottomOrRightBehavior = bottomOrRightBehavior + this.isExternalOverscrollGesture = isExternalOverscrollGesture } } - -private fun scenePriorityNestedScrollConnection( - layoutImpl: SceneTransitionLayoutImpl, - orientation: Orientation, - topOrLeftBehavior: NestedScrollBehavior, - bottomOrRightBehavior: NestedScrollBehavior, - isExternalOverscrollGesture: () -> Boolean, - pointersInfoOwner: PointersInfoOwner, -) = - NestedScrollHandlerImpl( - layoutImpl = layoutImpl, - orientation = orientation, - topOrLeftBehavior = topOrLeftBehavior, - bottomOrRightBehavior = bottomOrRightBehavior, - isExternalOverscrollGesture = isExternalOverscrollGesture, - pointersInfoOwner = pointersInfoOwner, - ) - .connection diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 0c467b181cd8..82275a9ac0a6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -207,8 +207,8 @@ interface BaseSceneScope : ElementStateScope { * @param rightBehavior when we should perform the overscroll animation at the right. */ fun Modifier.horizontalNestedScrollToScene( - leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, - rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, + leftBehavior: NestedScrollBehavior = NestedScrollBehavior.Default, + rightBehavior: NestedScrollBehavior = NestedScrollBehavior.Default, isExternalOverscrollGesture: () -> Boolean = { false }, ): Modifier @@ -220,8 +220,8 @@ interface BaseSceneScope : ElementStateScope { * @param bottomBehavior when we should perform the overscroll animation at the bottom. */ fun Modifier.verticalNestedScrollToScene( - topBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, - bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, + topBehavior: NestedScrollBehavior = NestedScrollBehavior.Default, + bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.Default, isExternalOverscrollGesture: () -> Boolean = { false }, ): Modifier diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index aeb62628a8f4..b8010f25f9a4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -20,11 +20,15 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.PointerInputModifierNode +import androidx.compose.ui.node.TraversableNode +import androidx.compose.ui.node.findNearestAncestor import androidx.compose.ui.unit.IntSize /** @@ -53,7 +57,7 @@ private class SwipeToSceneNode( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ) : DelegatingNode(), PointerInputModifierNode { - private val delegate = + private val multiPointerDraggableNode = delegate( MultiPointerDraggableNode( orientation = draggableHandler.orientation, @@ -74,21 +78,41 @@ private class SwipeToSceneNode( // Make sure to update the delegate orientation. Note that this will automatically // reset the underlying pointer input handler, so previous gestures will be // cancelled. - delegate.orientation = value.orientation + multiPointerDraggableNode.orientation = value.orientation } } + private val nestedScrollHandlerImpl = + NestedScrollHandlerImpl( + layoutImpl = draggableHandler.layoutImpl, + orientation = draggableHandler.orientation, + topOrLeftBehavior = NestedScrollBehavior.Default, + bottomOrRightBehavior = NestedScrollBehavior.Default, + isExternalOverscrollGesture = { false }, + pointersInfoOwner = { multiPointerDraggableNode.pointersInfo() }, + ) + + init { + delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher = null)) + delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey, nestedScrollHandlerImpl)) + } + + override fun onDetach() { + // Make sure we reset the scroll connection when this modifier is removed from composition + nestedScrollHandlerImpl.connection.reset() + } + override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, - ) = delegate.onPointerEvent(pointerEvent, pass, bounds) + ) = multiPointerDraggableNode.onPointerEvent(pointerEvent, pass, bounds) - override fun onCancelPointerInput() = delegate.onCancelPointerInput() + override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput() private fun enabled(): Boolean { return draggableHandler.isDrivingTransition || - currentScene().shouldEnableSwipes(delegate.orientation) + currentScene().shouldEnableSwipes(multiPointerDraggableNode.orientation) } private fun currentScene(): Scene { @@ -118,3 +142,43 @@ private class SwipeToSceneNode( return currentScene().shouldEnableSwipes(oppositeOrientation) } } + +/** Find the [ScrollBehaviorOwner] for the current orientation. */ +internal fun DelegatableNode.requireScrollBehaviorOwner( + draggableHandler: DraggableHandlerImpl +): ScrollBehaviorOwner { + val ancestorNode = + checkNotNull(findNearestAncestor(draggableHandler.nestedScrollKey)) { + "This should never happen! Couldn't find a ScrollBehaviorOwner. " + + "Are we inside an SceneTransitionLayout?" + } + return ancestorNode as ScrollBehaviorOwner +} + +internal fun interface ScrollBehaviorOwner { + fun updateScrollBehaviors( + topOrLeftBehavior: NestedScrollBehavior, + bottomOrRightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean, + ) +} + +/** + * We need a node that receives the desired behavior. + * + * TODO(b/353234530) move this logic into [SwipeToSceneNode] + */ +private class ScrollBehaviorOwnerNode( + override val traverseKey: Any, + val nestedScrollHandlerImpl: NestedScrollHandlerImpl +) : Modifier.Node(), TraversableNode, ScrollBehaviorOwner { + override fun updateScrollBehaviors( + topOrLeftBehavior: NestedScrollBehavior, + bottomOrRightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean + ) { + nestedScrollHandlerImpl.topOrLeftBehavior = topOrLeftBehavior + nestedScrollHandlerImpl.bottomOrRightBehavior = bottomOrRightBehavior + nestedScrollHandlerImpl.isExternalOverscrollGesture = isExternalOverscrollGesture + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 7988e0e4e416..c91151e41605 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -797,8 +797,6 @@ class ElementTest { scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) { Box( Modifier - // Unconsumed scroll gesture will be intercepted by STL - .verticalNestedScrollToScene() // A scrollable that does not consume the scroll gesture .scrollable( rememberScrollableState(consumeScrollDelta = { 0f }), @@ -875,8 +873,6 @@ class ElementTest { ) { Box( Modifier - // Unconsumed scroll gesture will be intercepted by STL - .verticalNestedScrollToScene() // A scrollable that does not consume the scroll gesture .scrollable( rememberScrollableState(consumeScrollDelta = { 0f }), diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt new file mode 100644 index 000000000000..311a58018840 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt @@ -0,0 +1,269 @@ +/* + * 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.compose.animation.scene + +import androidx.compose.foundation.gestures.Orientation.Vertical +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.animation.scene.TestScenes.SceneB +import com.android.compose.animation.scene.subjects.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class NestedScrollToSceneTest { + @get:Rule val rule = createComposeRule() + + private var touchSlop = 0f + private val layoutWidth: Dp = 200.dp + private val layoutHeight = 400.dp + + private fun setup2ScenesAndScrollTouchSlop( + modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier }, + ): MutableSceneTransitionLayoutState { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState(SceneA, transitions = EmptyTestTransitions) + } + + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout( + state = state, + modifier = Modifier.size(layoutWidth, layoutHeight) + ) { + scene(SceneA, userActions = mapOf(Swipe.Up to SceneB)) { + Spacer(modifierSceneA().fillMaxSize()) + } + scene(SceneB, userActions = mapOf(Swipe.Down to SceneA)) { + Spacer(Modifier.fillMaxSize()) + } + } + } + + pointerDownAndScrollTouchSlop() + + assertThat(state.transitionState).isIdle() + + return state + } + + private fun pointerDownAndScrollTouchSlop() { + rule.onRoot().performTouchInput { + val middleTop = Offset((layoutWidth / 2).toPx(), 0f) + down(middleTop) + // Scroll touchSlop + moveBy(Offset(0f, touchSlop), delayMillis = 1_000) + } + } + + private fun scrollDown(percent: Float = 1f) { + rule.onRoot().performTouchInput { + moveBy(Offset(0f, layoutHeight.toPx() * percent), delayMillis = 1_000) + } + } + + private fun scrollUp(percent: Float = 1f) = scrollDown(-percent) + + private fun pointerUp() { + rule.onRoot().performTouchInput { up() } + } + + @Test + fun scrollableElementsInSTL_shouldHavePriority() { + val state = setup2ScenesAndScrollTouchSlop { + Modifier + // A scrollable that consumes the scroll gesture + .scrollable(rememberScrollableState { it }, Vertical) + } + + scrollUp(percent = 0.5f) + + // Consumed by the scrollable element + assertThat(state.transitionState).isIdle() + } + + @Test + fun unconsumedScrollEvents_canBeConsumedBySTLByDefault() { + val state = setup2ScenesAndScrollTouchSlop { + Modifier + // A scrollable that does not consume the scroll gesture + .scrollable(rememberScrollableState { 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + // STL will start a transition with the remaining scroll + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) + + scrollUp(percent = 1f) + assertThat(transition).hasProgress(1.5f) + } + + @Test + fun customizeStlNestedScrollBehavior_DuringTransitionBetweenScenes() { + var canScroll = true + val state = setup2ScenesAndScrollTouchSlop { + Modifier.verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes + ) + .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + // Reach the end of the scrollable element + canScroll = false + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + pointerUp() + assertThat(state.transitionState).isIdle() + + // Start a new gesture + pointerDownAndScrollTouchSlop() + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + } + + @Test + fun customizeStlNestedScrollBehavior_EdgeNoPreview() { + var canScroll = true + val state = setup2ScenesAndScrollTouchSlop { + Modifier.verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.EdgeNoPreview + ) + .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + // Reach the end of the scrollable element + canScroll = false + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + pointerUp() + assertThat(state.transitionState).isIdle() + + // Start a new gesture + pointerDownAndScrollTouchSlop() + scrollUp(percent = 0.5f) + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) + + pointerUp() + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneB) + } + + @Test + fun customizeStlNestedScrollBehavior_EdgeWithPreview() { + var canScroll = true + val state = setup2ScenesAndScrollTouchSlop { + Modifier.verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.EdgeWithPreview + ) + .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + // Reach the end of the scrollable element + canScroll = false + scrollUp(percent = 0.5f) + val transition1 = assertThat(state.transitionState).isTransition() + assertThat(transition1).hasProgress(0.5f) + + pointerUp() + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + + // Start a new gesture + pointerDownAndScrollTouchSlop() + scrollUp(percent = 0.5f) + val transition2 = assertThat(state.transitionState).isTransition() + assertThat(transition2).hasProgress(0.5f) + + pointerUp() + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneB) + } + + @Test + fun customizeStlNestedScrollBehavior_EdgeAlways() { + var canScroll = true + val state = setup2ScenesAndScrollTouchSlop { + Modifier.verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways) + .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + // Reach the end of the scrollable element + canScroll = false + scrollUp(percent = 0.5f) + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) + + pointerUp() + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneB) + } + + @Test + fun customizeStlNestedScrollBehavior_multipleRequests() { + val state = setup2ScenesAndScrollTouchSlop { + Modifier + // This verticalNestedScrollToScene is closer the STL (an ancestor node) + .verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways) + // Another verticalNestedScrollToScene modifier + .verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes + ) + .scrollable(rememberScrollableState { 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + // EdgeAlways always consume the remaining scroll, DuringTransitionBetweenScenes does not. + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt index a8bdc7c632d2..1f5e30ca4a0d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -88,24 +88,6 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { } @Test - fun isDisclaimerDismissed_byDefault_isFalse() = - testScope.runTest { - val isDisclaimerDismissed by - collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER)) - assertThat(isDisclaimerDismissed).isFalse() - } - - @Test - fun isDisclaimerDismissed_onSet_isTrue() = - testScope.runTest { - val isDisclaimerDismissed by - collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER)) - - underTest.setDisclaimerDismissed(MAIN_USER) - assertThat(isDisclaimerDismissed).isTrue() - } - - @Test fun getSharedPreferences_whenFileRestored() = testScope.runTest { val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 5cdbe9ce5856..9539c0492056 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -84,6 +84,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -1059,6 +1060,25 @@ class CommunalInteractorTest : SysuiTestCase() { ) } + @Test + fun dismissDisclaimerSetsDismissedFlag() = + testScope.runTest { + val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) + assertThat(disclaimerDismissed).isFalse() + underTest.setDisclaimerDismissed() + assertThat(disclaimerDismissed).isTrue() + } + + @Test + fun dismissDisclaimerTimeoutResetsDismissedFlag() = + testScope.runTest { + val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) + underTest.setDisclaimerDismissed() + assertThat(disclaimerDismissed).isTrue() + advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + assertThat(disclaimerDismissed).isFalse() + } + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt index 7b79d2817478..9a92f76f90c6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt @@ -74,40 +74,6 @@ class CommunalPrefsInteractorTest : SysuiTestCase() { assertThat(isCtaDismissed).isFalse() } - @Test - fun setDisclaimerDismissed_currentUser() = - testScope.runTest { - setSelectedUser(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isFalse() - underTest.setDisclaimerDismissed(MAIN_USER) - assertThat(isDisclaimerDismissed).isTrue() - } - - @Test - fun setDisclaimerDismissed_anotherUser() = - testScope.runTest { - setSelectedUser(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isFalse() - underTest.setDisclaimerDismissed(SECONDARY_USER) - assertThat(isDisclaimerDismissed).isFalse() - } - - @Test - fun isDisclaimerDismissed_userSwitch() = - testScope.runTest { - setSelectedUser(MAIN_USER) - underTest.setDisclaimerDismissed(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isTrue() - setSelectedUser(SECONDARY_USER) - assertThat(isDisclaimerDismissed).isFalse() - } - private suspend fun setSelectedUser(user: UserInfo) { with(kosmos.fakeUserRepository) { setUserInfos(listOf(user)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index b138fb3b779a..f8906adc33d4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -65,6 +65,7 @@ import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -352,6 +353,21 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test + fun showDisclaimer_trueWhenTimeout() = + testScope.runTest { + underTest.setEditModeState(EditModeState.SHOWING) + kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + + val showDisclaimer by collectLastValue(underTest.showDisclaimer) + + assertThat(showDisclaimer).isTrue() + underTest.onDisclaimerDismissed() + assertThat(showDisclaimer).isFalse() + advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + assertThat(showDisclaimer).isTrue() + } + + @Test fun scrollPosition_persistedOnEditCleanup() { val index = 2 val offset = 30 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 9b9e584a936e..d5c910248942 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -21,14 +21,23 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.google.common.truth.Truth +import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -36,7 +45,33 @@ import org.junit.runner.RunWith class ModesTileUserActionInteractorTest : SysuiTestCase() { private val inputHandler = FakeQSTileIntentUserInputHandler() - val underTest = ModesTileUserActionInteractor(inputHandler) + @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator + @Mock private lateinit var dialogDelegate: ModesDialogDelegate + @Mock private lateinit var mockDialog: SystemUIDialog + + private lateinit var underTest: ModesTileUserActionInteractor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(dialogDelegate.createDialog()).thenReturn(mockDialog) + + underTest = + ModesTileUserActionInteractor( + EmptyCoroutineContext, + inputHandler, + dialogTransitionAnimator, + dialogDelegate, + ) + } + + @Test + fun handleClick() = runTest { + underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false))) + + verify(mockDialog).show() + } @Test fun handleLongClick() = runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt new file mode 100644 index 000000000000..3baf2f40f175 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.modes.ui + +import android.graphics.drawable.TestStubDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig +import com.android.systemui.res.R +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesTileMapperTest : SysuiTestCase() { + val config = + QSTileConfigTestBuilder.build { + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_dnd_icon_off, + labelRes = R.string.quick_settings_modes_label, + ) + } + + val underTest = + ModesTileMapper( + context.orCreateTestableResources + .apply { + addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable()) + addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable()) + } + .resources, + context.theme, + ) + + @Test + fun inactiveState() { + val model = ModesTileModel(isActivated = false) + + val state = underTest.map(config, model) + + assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE) + assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_off) + } + + @Test + fun activeState() { + val model = ModesTileModel(isActivated = true) + + val state = underTest.map(config, model) + + assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE) + assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_on) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index fd1b21332973..a120bdc0b743 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -1559,6 +1559,63 @@ class SceneContainerStartableTest : SysuiTestCase() { verify(dismissCallback).onDismissCancelled() } + @Test + fun refreshLockscreenEnabled() = + testScope.runTest { + val transitionState = + prepareState( + isDeviceUnlocked = true, + initialSceneKey = Scenes.Gone, + ) + underTest.start() + val isLockscreenEnabled by + collectLastValue(kosmos.deviceEntryInteractor.isLockscreenEnabled) + assertThat(isLockscreenEnabled).isTrue() + + kosmos.fakeDeviceEntryRepository.setPendingLockscreenEnabled(false) + runCurrent() + // Pending value didn't propagate yet. + assertThat(isLockscreenEnabled).isTrue() + + // Starting a transition to Lockscreen should refresh the value, causing the pending + // value + // to propagate to the real flow: + transitionState.value = + ObservableTransitionState.Transition( + fromScene = Scenes.Gone, + toScene = Scenes.Lockscreen, + currentScene = flowOf(Scenes.Gone), + progress = flowOf(0.1f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + runCurrent() + assertThat(isLockscreenEnabled).isFalse() + + kosmos.fakeDeviceEntryRepository.setPendingLockscreenEnabled(true) + runCurrent() + // Pending value didn't propagate yet. + assertThat(isLockscreenEnabled).isFalse() + transitionState.value = ObservableTransitionState.Idle(Scenes.Gone) + runCurrent() + assertThat(isLockscreenEnabled).isFalse() + + // Starting another transition to Lockscreen should refresh the value, causing the + // pending + // value to propagate to the real flow: + transitionState.value = + ObservableTransitionState.Transition( + fromScene = Scenes.Gone, + toScene = Scenes.Lockscreen, + currentScene = flowOf(Scenes.Gone), + progress = flowOf(0.1f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + runCurrent() + assertThat(isLockscreenEnabled).isTrue() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt index 5e87f4663d76..61873ad294e3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row.ui.viewmodel +import android.app.Notification import android.app.PendingIntent import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -90,7 +91,8 @@ class TimerViewModelTest : SysuiTestCase() { name: String = "example", timeRemaining: Duration = Duration.ofMinutes(3), resumeIntent: PendingIntent? = null, - resetIntent: PendingIntent? = null + addMinuteAction: Notification.Action? = null, + resetAction: Notification.Action? = null ) = TimerContentModel( icon = icon, @@ -99,7 +101,8 @@ class TimerViewModelTest : SysuiTestCase() { Paused( timeRemaining = timeRemaining, resumeIntent = resumeIntent, - resetIntent = resetIntent, + addMinuteAction = addMinuteAction, + resetAction = resetAction, ) ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt index 495ab61600f9..8f9da3b2e1e3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt @@ -180,6 +180,23 @@ class AvalancheControllerTest : SysuiTestCase() { } @Test + fun testDelete_untracked_runnableRuns() { + val headsUpEntry = createHeadsUpEntry(id = 0) + + // None showing + mAvalancheController.headsUpEntryShowing = null + + // Nothing is next + mAvalancheController.clearNext() + + // Delete + mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel") + + // Runnable was run + Mockito.verify(runnableMock, Mockito.times(1)).run() + } + + @Test fun testDelete_isNext_removedFromNext_runnableNotRun() { // Entry is next val headsUpEntry = createHeadsUpEntry(id = 0) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt index d0ddbffecf9a..5dadc4caf0f6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.phone.ConfigurationControllerImpl @@ -42,6 +43,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.mockExecutorHandler import com.android.systemui.util.kotlin.JavaAdapter +import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.time.SystemClock import junit.framework.Assert @@ -237,6 +239,34 @@ class HeadsUpManagerPhoneTest(flags: FlagsParameterization) : BaseHeadsUpManager } @Test + fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() { + whenever(mVSProvider.isReorderingAllowed).thenReturn(false) + val hmp = createHeadsUpManagerPhone() + + val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + val row = mock<ExpandableNotificationRow>() + whenever(row.showingPulsing()).thenReturn(false) + notifEntry.row = row + + hmp.showNotification(notifEntry) + Assert.assertTrue(notifEntry.isSeenInShade) + } + + @Test + fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() { + whenever(mVSProvider.isReorderingAllowed).thenReturn(true) + val hmp = createHeadsUpManagerPhone() + + val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + val row = mock<ExpandableNotificationRow>() + whenever(row.showingPulsing()).thenReturn(false) + notifEntry.row = row + + hmp.showNotification(notifEntry) + Assert.assertFalse(notifEntry.isSeenInShade) + } + + @Test fun shouldHeadsUpBecomePinned_shadeNotExpanded_true() = testScope.runTest { // GIVEN diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt new file mode 100644 index 000000000000..fdfc7f13abf7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.policy.ui.dialog.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesDialogViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + val repository = kosmos.fakeZenModeRepository + val interactor = kosmos.zenModeInteractor + + val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher) + + @Test + fun tiles_filtersOutDisabledModes() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder().setName("Disabled").setEnabled(false).build(), + TestModeBuilder.MANUAL_DND, + TestModeBuilder() + .setName("Enabled") + .setEnabled(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Disabled with manual") + .setEnabled(false) + .setManualInvocationAllowed(true) + .build(), + )) + runCurrent() + + assertThat(tiles?.size).isEqualTo(2) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Manual DND") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(1)!!) { + assertThat(this.text).isEqualTo("Enabled") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun tiles_filtersOutInactiveModesWithoutManualInvocation() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build(), + TestModeBuilder() + .setName("Active with manual") + .setTriggerDescription("trigger description") + .setActive(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive with manual") + .setActive(false) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive without manual") + .setActive(false) + .setManualInvocationAllowed(false) + .build(), + )) + runCurrent() + + assertThat(tiles?.size).isEqualTo(3) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Active without manual") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(1)!!) { + assertThat(this.text).isEqualTo("Active with manual") + assertThat(this.subtext).isEqualTo("trigger description") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(2)!!) { + assertThat(this.text).isEqualTo("Inactive with manual") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun onClick_togglesTileState() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + val modeId = "id" + repository.addModes( + listOf( + TestModeBuilder() + .setId(modeId) + .setName("Test") + .setManualInvocationAllowed(true) + .build() + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles?.elementAt(0)?.enabled).isFalse() + + // Trigger onClick + tiles?.first()?.onClick?.let { it() } + runCurrent() + + assertThat(tiles?.first()?.enabled).isTrue() + + // Trigger onClick + tiles?.first()?.onClick?.let { it() } + runCurrent() + + assertThat(tiles?.first()?.enabled).isFalse() + } +} diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml index f2bfbe5c960d..3a679e3c16cb 100644 --- a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml +++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml @@ -33,7 +33,6 @@ android:id="@+id/icon" android:layout_width="24dp" android:layout_height="24dp" - android:src="@drawable/ic_close" app:tint="@android:color/white" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/label" @@ -88,11 +87,10 @@ /> <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" android:id="@+id/mainButton" android:layout_width="124dp" android:layout_height="wrap_content" - tools:text="Reset" - tools:drawableStart="@android:drawable/ic_menu_add" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/altButton" app:layout_constraintTop_toBottomOf="@id/bottomOfTop" @@ -101,15 +99,23 @@ /> <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" android:id="@+id/altButton" - tools:text="Reset" - tools:drawableStart="@android:drawable/ic_menu_add" - android:drawablePadding="2dp" - android:drawableTint="@android:color/white" android:layout_width="124dp" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/bottomOfTop" app:layout_constraintStart_toEndOf="@id/mainButton" + app:layout_constraintEnd_toEndOf="@id/resetButton" + android:paddingEnd="4dp" + /> + + <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" + android:id="@+id/resetButton" + android:layout_width="124dp" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/bottomOfTop" + app:layout_constraintStart_toEndOf="@id/altButton" app:layout_constraintEnd_toEndOf="parent" android:paddingEnd="4dp" /> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 1af5be54503f..68c83c747d73 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1084,6 +1084,21 @@ <!-- QuickStep: Accessibility to toggle overview [CHAR LIMIT=40] --> <string name="quick_step_accessibility_toggle_overview">Toggle Overview</string> + <!-- Priority modes dialog title [CHAR LIMIT=35] --> + <string name="zen_modes_dialog_title">Priority modes</string> + + <!-- Priority modes dialog confirmation button [CHAR LIMIT=15] --> + <string name="zen_modes_dialog_done">Done</string> + + <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] --> + <string name="zen_modes_dialog_settings">Settings</string> + + <!-- Priority modes: label for an active mode [CHAR LIMIT=35] --> + <string name="zen_mode_on">On</string> + + <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] --> + <string name="zen_mode_off">Off</string> + <!-- Zen mode: Priority only introduction message on first use --> <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string> @@ -1249,6 +1264,8 @@ <string name="communal_widget_picker_title">Lock screen widgets</string> <!-- Text displayed below the title in the communal widget picker providing additional details about the communal surface. [CHAR LIMIT=80] --> <string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string> + <!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_unselect_widget">unselect widget</string> <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] --> <string name="communal_widgets_disclaimer_title">Lock screen widgets</string> <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index 845ca5e8b9ec..3019fe796d7d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -344,7 +344,7 @@ public class ActivityManagerWrapper { * Shows a voice session identified by {@code token} * @return true if the session was shown, false otherwise */ - public boolean showVoiceSession(@NonNull IBinder token, @NonNull Bundle args, int flags, + public boolean showVoiceSession(IBinder token, @NonNull Bundle args, int flags, @Nullable String attributionTag) { IVoiceInteractionManagerService service = IVoiceInteractionManagerService.Stub.asInterface( ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 10d1891c8405..0f61233ac64f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -34,6 +34,7 @@ import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor; +import com.android.systemui.Flags; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; @@ -130,7 +131,10 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB verifyPasswordAndUnlock(); } }); - okButton.setOnHoverListener(mLiftToActivateListener); + + if (!Flags.simPinTalkbackFixForDoubleSubmit()) { + okButton.setOnHoverListener(mLiftToActivateListener); + } } if (pinInputFieldStyledFocusState()) { collectFlow(mPasswordEntry, mKeyguardKeyboardInteractor.isAnyKeyboardConnected(), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index c95a94e5e388..b10d37e5c27a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -34,7 +34,9 @@ import com.android.settingslib.Utils import com.android.systemui.CoreStartable import com.android.systemui.Flags.lightRevealMigration import com.android.systemui.biometrics.data.repository.FacePropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams +import com.android.systemui.biometrics.shared.model.toSensorType import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.AuthRippleInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor @@ -102,6 +104,7 @@ constructor( private var udfpsController: UdfpsController? = null private var udfpsRadius: Float = -1f + private var udfpsType: FingerprintSensorType = FingerprintSensorType.UNKNOWN override fun start() { init() @@ -370,8 +373,11 @@ constructor( private val udfpsControllerCallback = object : UdfpsController.Callback { override fun onFingerDown() { - // only show dwell ripple for device entry - if (keyguardUpdateMonitor.isFingerprintDetectionRunning) { + // only show dwell ripple for device entry non-ultrasonic udfps + if ( + keyguardUpdateMonitor.isFingerprintDetectionRunning && + udfpsType != FingerprintSensorType.UDFPS_ULTRASONIC + ) { showDwellRipple() } } @@ -397,6 +403,7 @@ constructor( if (it.size > 0) { udfpsController = udfpsControllerProvider.get() udfpsRadius = authController.udfpsRadius + udfpsType = it[0].sensorType.toSensorType() if (mView.isAttachedToWindow) { udfpsController?.addCallback(udfpsControllerCallback) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt index d8067b887c67..4de39c457f3b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt @@ -49,14 +49,8 @@ interface CommunalPrefsRepository { /** Whether the CTA tile has been dismissed. */ fun isCtaDismissed(user: UserInfo): Flow<Boolean> - /** Whether the lock screen widget disclaimer has been dismissed by the user. */ - fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> - /** Save the CTA tile dismissed state for the current user. */ suspend fun setCtaDismissed(user: UserInfo) - - /** Save the lock screen widget disclaimer dismissed state for the current user. */ - suspend fun setDisclaimerDismissed(user: UserInfo) } @OptIn(ExperimentalCoroutinesApi::class) @@ -74,9 +68,6 @@ constructor( override fun isCtaDismissed(user: UserInfo): Flow<Boolean> = readKeyForUser(user, CTA_DISMISSED_STATE) - override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> = - readKeyForUser(user, DISCLAIMER_DISMISSED_STATE) - /** * Emits an event each time a Backup & Restore restoration job is completed, and once at the * start of collection. @@ -97,12 +88,6 @@ constructor( logger.i("Dismissed CTA tile") } - override suspend fun setDisclaimerDismissed(user: UserInfo) = - withContext(bgDispatcher) { - getSharedPrefsForUser(user).edit().putBoolean(DISCLAIMER_DISMISSED_STATE, true).apply() - logger.i("Dismissed widget disclaimer") - } - private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences { return userFileManager.getSharedPreferences( FILE_NAME, @@ -124,6 +109,5 @@ constructor( const val TAG = "CommunalPrefsRepository" const val FILE_NAME = "communal_hub_prefs" const val CTA_DISMISSED_STATE = "cta_dismissed" - const val DISCLAIMER_DISMISSED_STATE = "disclaimer_dismissed" } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 3fffd76ab6a9..e13161f91f16 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -23,6 +23,7 @@ import android.content.pm.UserInfo import android.os.UserHandle import android.os.UserManager import android.provider.Settings +import com.android.app.tracing.coroutines.launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -64,10 +65,12 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart import javax.inject.Inject +import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -94,6 +97,7 @@ class CommunalInteractor @Inject constructor( @Application val applicationScope: CoroutineScope, + @Background private val bgScope: CoroutineScope, @Background val bgDispatcher: CoroutineDispatcher, broadcastDispatcher: BroadcastDispatcher, private val widgetRepository: CommunalWidgetRepository, @@ -148,6 +152,17 @@ constructor( replay = 1, ) + private val _isDisclaimerDismissed = MutableStateFlow(false) + val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow() + + fun setDisclaimerDismissed() { + bgScope.launch("$TAG#setDisclaimerDismissed") { + _isDisclaimerDismissed.value = true + delay(DISCLAIMER_RESET_MILLIS) + _isDisclaimerDismissed.value = false + } + } + /** Whether to show communal when exiting the occluded state. */ val showCommunalFromOccluded: Flow<Boolean> = keyguardTransitionInteractor.startedKeyguardTransitionStep @@ -510,6 +525,14 @@ constructor( } companion object { + const val TAG = "CommunalInteractor" + + /** + * The amount of time between showing the widget disclaimer to the user as measured from the + * moment the disclaimer is dimsissed. + */ + val DISCLAIMER_RESET_MILLIS = 30.minutes + /** * The user activity timeout which should be used when the communal hub is opened. A value * of -1 means that the user's chosen screen timeout will be used instead. diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt index 3517650c8cf3..0b5f40d8041e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt @@ -17,7 +17,6 @@ package com.android.systemui.communal.domain.interactor import android.content.pm.UserInfo -import com.android.app.tracing.coroutines.launch import com.android.systemui.communal.data.repository.CommunalPrefsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -43,7 +42,7 @@ constructor( private val repository: CommunalPrefsRepository, userInteractor: SelectedUserInteractor, private val userTracker: UserTracker, - @CommunalTableLog tableLogBuffer: TableLogBuffer + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { val isCtaDismissed: Flow<Boolean> = @@ -64,25 +63,6 @@ constructor( suspend fun setCtaDismissed(user: UserInfo = userTracker.userInfo) = repository.setCtaDismissed(user) - val isDisclaimerDismissed: Flow<Boolean> = - userInteractor.selectedUserInfo - .flatMapLatest { user -> repository.isDisclaimerDismissed(user) } - .logDiffsForTable( - tableLogBuffer = tableLogBuffer, - columnPrefix = "", - columnName = "isDisclaimerDismissed", - initialValue = false, - ) - .stateIn( - scope = bgScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) - - fun setDisclaimerDismissed(user: UserInfo = userTracker.userInfo) { - bgScope.launch("$TAG#setDisclaimerDismissed") { repository.setDisclaimerDismissed(user) } - } - private companion object { const val TAG = "CommunalPrefsInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 7b0aadfdcebd..0353d2c043e8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -82,10 +82,10 @@ constructor( communalSceneInteractor.editModeState.map { it == EditModeState.SHOWING } val showDisclaimer: Flow<Boolean> = - allOf(isCommunalContentVisible, not(communalPrefsInteractor.isDisclaimerDismissed)) + allOf(isCommunalContentVisible, not(communalInteractor.isDisclaimerDismissed)) fun onDisclaimerDismissed() { - communalPrefsInteractor.setDisclaimerDismissed() + communalInteractor.setDisclaimerDismissed() } /** diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt index e2ad7741557f..3f937bba46d4 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt @@ -13,8 +13,10 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -25,7 +27,7 @@ interface DeviceEntryRepository { * chosen any secure authentication method and even if they set the lockscreen to be dismissed * when the user swipes on it. */ - suspend fun isLockscreenEnabled(): Boolean + val isLockscreenEnabled: StateFlow<Boolean> /** * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically @@ -39,6 +41,13 @@ interface DeviceEntryRepository { * the lockscreen. */ val isBypassEnabled: StateFlow<Boolean> + + /** + * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has + * chosen any secure authentication method and even if they set the lockscreen to be dismissed + * when the user swipes on it. + */ + suspend fun isLockscreenEnabled(): Boolean } /** Encapsulates application state for device entry. */ @@ -53,12 +62,8 @@ constructor( private val keyguardBypassController: KeyguardBypassController, ) : DeviceEntryRepository { - override suspend fun isLockscreenEnabled(): Boolean { - return withContext(backgroundDispatcher) { - val selectedUserId = userRepository.getSelectedUserInfo().id - !lockPatternUtils.isLockScreenDisabled(selectedUserId) - } - } + private val _isLockscreenEnabled = MutableStateFlow(true) + override val isLockscreenEnabled: StateFlow<Boolean> = _isLockscreenEnabled.asStateFlow() override val isBypassEnabled: StateFlow<Boolean> = conflatedCallbackFlow { @@ -78,6 +83,15 @@ constructor( SharingStarted.Eagerly, initialValue = keyguardBypassController.bypassEnabled, ) + + override suspend fun isLockscreenEnabled(): Boolean { + return withContext(backgroundDispatcher) { + val selectedUserId = userRepository.getSelectedUserInfo().id + val isEnabled = !lockPatternUtils.isLockScreenDisabled(selectedUserId) + _isLockscreenEnabled.value = isEnabled + isEnabled + } + } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index ea0e59bb6ccc..9b95ac4797c0 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -28,12 +28,14 @@ import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -101,6 +103,10 @@ constructor( initialValue = false, ) + val isLockscreenEnabled: Flow<Boolean> by lazy { + repository.isLockscreenEnabled.onStart { refreshLockscreenEnabled() } + } + /** * Whether it's currently possible to swipe up to enter the device without requiring * authentication or when the device is already authenticated using a passive authentication @@ -115,14 +121,14 @@ constructor( */ val canSwipeToEnter: StateFlow<Boolean?> = combine( - // This is true when the user has chosen to show the lockscreen but has not made it - // secure. authenticationInteractor.authenticationMethod.map { - it == AuthenticationMethodModel.None && repository.isLockscreenEnabled() + it == AuthenticationMethodModel.None }, + isLockscreenEnabled, deviceUnlockedInteractor.deviceUnlockStatus, isDeviceEntered - ) { isSwipeAuthMethod, deviceUnlockStatus, isDeviceEntered -> + ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered -> + val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled (isSwipeAuthMethod || (deviceUnlockStatus.isUnlocked && deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) && @@ -186,6 +192,17 @@ constructor( } /** + * Forces a refresh of the value of [isLockscreenEnabled] such that the flow emits the latest + * value. + * + * Without calling this method, the flow will have a stale value unless the collector is removed + * and re-added. + */ + suspend fun refreshLockscreenEnabled() { + isLockscreenEnabled() + } + + /** * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically * dismissed once the authentication challenge is completed. For example, completing a biometric * authentication challenge via face unlock or fingerprint sensor can automatically bypass the diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index af7ecf66d107..1ba274ff4e76 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -28,6 +28,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.ComposeLockscreen +import com.android.systemui.qs.flags.NewQsUI +import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag @@ -66,14 +68,20 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() + + // QS Fragment using Compose dependencies + QSComposeFragment.token dependsOn NewQsUI.token } private inline val politeNotifications get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications()) + private inline val crossAppPoliteNotifications get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications()) + private inline val vibrateWhileUnlockedToken: FlagToken get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) + private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index cd28bec938b8..8f50b03eafec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -25,7 +25,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock @@ -59,7 +59,7 @@ constructor( private val communalInteractor: CommunalInteractor, private val communalSceneInteractor: CommunalSceneInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, - val deviceEntryRepository: DeviceEntryRepository, + val deviceEntryInteractor: DeviceEntryInteractor, private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, private val dreamManager: DreamManager, ) : @@ -146,7 +146,7 @@ constructor( isIdleOnCommunal, canTransitionToGoneOnWake, primaryBouncerShowing) -> - if (!deviceEntryRepository.isLockscreenEnabled()) { + if (!deviceEntryInteractor.isLockscreenEnabled()) { if (SceneContainerFlag.isEnabled) { // TODO(b/336576536): Check if adaptation for scene framework is needed } else { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 46c5c188344e..c5d7b25827ea 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -888,6 +888,8 @@ constructor( heightInSceneContainerPx = height mediaCarouselScrollHandler.playerWidthPlusPadding = width + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + mediaContent.minimumWidth = widthInSceneContainerPx + mediaContent.minimumHeight = heightInSceneContainerPx updatePlayers(recreateMedia = true) } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java index 1dbd500c15f1..c4abcd2afc4f 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java @@ -54,6 +54,7 @@ import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import android.view.inputmethod.Flags; import android.widget.FrameLayout; import androidx.annotation.Nullable; @@ -285,8 +286,11 @@ public class NavigationBarView extends FrameLayout { // Set up the context group of buttons mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container); + final int switcherResId = Flags.imeSwitcherRevamp() + ? com.android.internal.R.drawable.ic_ime_switcher_new + : R.drawable.ic_ime_switcher_default; final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher, - mLightContext, R.drawable.ic_ime_switcher_default); + mLightContext, switcherResId); final ContextualButton accessibilityButton = new ContextualButton(R.id.accessibility_button, mLightContext, R.drawable.ic_sysbar_accessibility_button); diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt index 8af566523b67..ee709c4cf41a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt @@ -20,7 +20,7 @@ import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils -/** Helper for reading or using the notification avalanche suppression flag state. */ +/** Helper for reading or using the new QS UI flag state. */ @Suppress("NOTHING_TO_INLINE") object NewQsUI { /** The aconfig flag name */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt new file mode 100644 index 000000000000..664d49607f89 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.flags + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the new QS UI in NPVC flag state. */ +@Suppress("NOTHING_TO_INLINE") +object QSComposeFragment { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.qsUiRefactorComposeFragment() && NewQsUI.isEnabled + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java index 720120b630d5..5ea8c2183295 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java @@ -14,6 +14,8 @@ package com.android.systemui.qs.tileimpl; +import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ArgbEvaluator; @@ -204,6 +206,9 @@ public class QSIconViewImpl extends QSIconView { values.setEvaluator(ArgbEvaluator.getInstance()); mColorAnimator.setValues(values); mColorAnimator.removeAllListeners(); + if (removeUpdateListenerInQsIconViewImpl()) { + mColorAnimator.removeAllUpdateListeners(); + } mColorAnimator.addUpdateListener(animation -> { setTint(iv, (int) animation.getAnimatedValue()); }); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt index b91891cf7be0..a3000316057f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt @@ -44,6 +44,7 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class ModesTile @Inject @@ -91,8 +92,8 @@ constructor( override fun newTileState() = BooleanState() - override fun handleClick(expandable: Expandable?) { - // TODO(b/346519570) open dialog + override fun handleClick(expandable: Expandable?) = runBlocking { + userActionInteractor.handleClick(expandable) } override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent @@ -107,6 +108,7 @@ constructor( label = tileLabel secondaryLabel = tileState.secondaryLabel contentDescription = tileState.contentDescription + forceExpandIcon = true } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt index fd1f3d8fb23a..4c6563d6c143 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt @@ -16,19 +16,31 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor +//noinspection CleanArchitectureDependencyViolation: dialog needs to be opened on click import android.content.Intent import android.provider.Settings +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext class ModesTileUserActionInteractor @Inject constructor( + @Main private val coroutineContext: CoroutineContext, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val dialogDelegate: ModesDialogDelegate, ) : QSTileUserActionInteractor<ModesTileModel> { val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) @@ -36,7 +48,7 @@ constructor( with(input) { when (action) { is QSTileUserAction.Click -> { - // TODO(b/346519570) open dialog + handleClick(action.expandable) } is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent) @@ -44,4 +56,24 @@ constructor( } } } + + suspend fun handleClick(expandable: Expandable?) { + // Show a dialog with the list of modes to configure. Dialogs shown by the + // DialogTransitionAnimator must be created and shown on the main thread, so we post it to + // the UI handler. + withContext(coroutineContext) { + val dialog = dialogDelegate.createDialog() + + expandable + ?.dialogTransitionController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) + ) + ?.let { controller -> dialogTransitionAnimator.show(dialog, controller) } + ?: dialog.show() + } + } + + companion object { + private const val INTERACTION_JANK_TAG = "configure_priority_modes" + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 26b9a4c7f416..7048adab329d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -59,5 +59,6 @@ constructor( QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK, ) + sideViewIcon = QSTileState.SideViewIcon.Chevron } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 8711e8878525..51447cc6f373 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -149,6 +149,7 @@ constructor( resetShadeSessions() handleKeyguardEnabledness() notifyKeyguardDismissCallbacks() + refreshLockscreenEnabled() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -735,4 +736,22 @@ constructor( } } } + + /** + * Keeps the value of [DeviceEntryInteractor.isLockscreenEnabled] fresh. + * + * This is needed because that value is sourced from a non-observable data source + * (`LockPatternUtils`, which doesn't expose a listener or callback for this value). Therefore, + * every time a transition to the `Lockscreen` scene is started, the value is re-fetched and + * cached. + */ + private fun refreshLockscreenEnabled() { + applicationScope.launch { + sceneInteractor.transitionState + .map { it.isTransitioning(to = Scenes.Lockscreen) } + .distinctUntilChanged() + .filter { it } + .collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 4f6a64f043d2..bd0868530cba 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -1265,20 +1265,20 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mTranslationForFullShadeTransition = qsTranslation; updateQsFrameTranslation(); float currentTranslation = mQsFrame.getTranslationY(); - int clipTop = mEnableClipping - ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0; - int clipBottom = mEnableClipping - ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0; + int clipTop = (int) (top - currentTranslation - mQsFrame.getTop()); + int clipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop()); mVisible = qsVisible; mQs.setQsVisible(qsVisible); - mQs.setFancyClipping( - mDisplayLeftInset, - clipTop, - mDisplayRightInset, - clipBottom, - radius, - qsVisible && !mSplitShadeEnabled, - mIsFullWidth); + if (mEnableClipping) { + mQs.setFancyClipping( + mDisplayLeftInset, + clipTop, + mDisplayRightInset, + clipBottom, + radius, + qsVisible && !mSplitShadeEnabled, + mIsFullWidth); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index 11ccdff687a1..59fd0ca4513e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -57,7 +57,7 @@ constructor( interactor.ongoingCallState .map { state -> when (state) { - is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden + is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden() is OngoingCallModel.InCall -> { // This block mimics OngoingCallController#updateChip. if (state.startTimeMs <= 0L) { @@ -82,7 +82,7 @@ constructor( } } } - .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden) + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? { if (state.intent == null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt index bafec38efe9d..6ea72b97cb3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt @@ -44,9 +44,10 @@ class EndCastScreenToOtherDeviceDialogDelegate( // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) - setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ -> - stopAction.invoke() - } + setPositiveButton( + R.string.cast_to_other_device_stop_dialog_button, + endMediaProjectionDialogHelper.wrapStopAction(stopAction), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt index 7dc9b255badc..b0c832172776 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt @@ -55,9 +55,10 @@ class EndGenericCastToOtherDeviceDialogDelegate( // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) - setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ -> - stopAction.invoke() - } + setPositiveButton( + R.string.cast_to_other_device_stop_dialog_button, + endMediaProjectionDialogHelper.wrapStopAction(stopAction), + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt index afa9ccefab86..d9b0504308f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes +import com.android.internal.jank.Cuj +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -35,6 +38,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.model.Project import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener import com.android.systemui.util.time.SystemClock @@ -60,6 +64,7 @@ constructor( private val mediaProjectionChipInteractor: MediaProjectionChipInteractor, private val mediaRouterChipInteractor: MediaRouterChipInteractor, private val systemClock: SystemClock, + private val dialogTransitionAnimator: DialogTransitionAnimator, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { @@ -74,18 +79,18 @@ constructor( mediaProjectionChipInteractor.projection .map { projectionModel -> when (projectionModel) { - is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden + is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden() is ProjectionChipModel.Projecting -> { if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) { - OngoingActivityChipModel.Hidden + OngoingActivityChipModel.Hidden() } else { createCastScreenToOtherDeviceChip(projectionModel) } } } } - // See b/347726238. - .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden) + // See b/347726238 for [SharingStarted.Lazily] reasoning. + .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden()) /** * The cast chip to show, based only on MediaRouter API events. @@ -109,7 +114,7 @@ constructor( mediaRouterChipInteractor.mediaRouterCastingState .map { routerModel -> when (routerModel) { - is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden + is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden() is MediaRouterCastModel.Casting -> { // A consequence of b/269975671 is that MediaRouter will mark a device as // casting before casting has actually started. To alleviate this bug a bit, @@ -123,9 +128,9 @@ constructor( } } } - .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden) + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) - override val chip: StateFlow<OngoingActivityChipModel> = + private val internalChip: StateFlow<OngoingActivityChipModel> = combine(projectionChip, routerChip) { projection, router -> logger.log( TAG, @@ -159,17 +164,24 @@ constructor( router } } - .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden) + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) + + private val hideChipDuringDialogTransitionHelper = ChipTransitionHelper(scope) + + override val chip: StateFlow<OngoingActivityChipModel> = + hideChipDuringDialogTransitionHelper.createChipFlow(internalChip) /** Stops the currently active projection. */ - private fun stopProjecting() { - logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested (projection)" }) + private fun stopProjectingFromDialog() { + logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested from dialog (projection)" }) + hideChipDuringDialogTransitionHelper.onActivityStoppedFromDialog() mediaProjectionChipInteractor.stopProjecting() } /** Stops the currently active media route. */ - private fun stopMediaRouterCasting() { - logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested (router)" }) + private fun stopMediaRouterCastingFromDialog() { + logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested from dialog (router)" }) + hideChipDuringDialogTransitionHelper.onActivityStoppedFromDialog() mediaRouterChipInteractor.stopCasting() } @@ -190,6 +202,8 @@ constructor( startTimeMs = systemClock.elapsedRealtime(), createDialogLaunchOnClickListener( createCastScreenToOtherDeviceDialogDelegate(state), + dialogTransitionAnimator, + DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Cast to other device"), logger, TAG, ), @@ -207,6 +221,11 @@ constructor( colors = ColorsModel.Red, createDialogLaunchOnClickListener( createGenericCastToOtherDeviceDialogDelegate(deviceName), + dialogTransitionAnimator, + DialogCuj( + Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, + tag = "Cast to other device audio only", + ), logger, TAG, ), @@ -219,7 +238,7 @@ constructor( EndCastScreenToOtherDeviceDialogDelegate( endMediaProjectionDialogHelper, context, - stopAction = this::stopProjecting, + stopAction = this::stopProjectingFromDialog, state, ) @@ -228,7 +247,7 @@ constructor( endMediaProjectionDialogHelper, context, deviceName, - stopAction = this::stopMediaRouterCasting, + stopAction = this::stopMediaRouterCastingFromDialog, ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt index 600436557efb..2d9ccb7b09b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt @@ -17,7 +17,9 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view import android.app.ActivityManager +import android.content.DialogInterface import android.content.pm.PackageManager +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.statusbar.phone.SystemUIDialog @@ -29,6 +31,7 @@ class EndMediaProjectionDialogHelper @Inject constructor( private val dialogFactory: SystemUIDialog.Factory, + private val dialogTransitionAnimator: DialogTransitionAnimator, private val packageManager: PackageManager, ) { /** Creates a new [SystemUIDialog] using the given delegate. */ @@ -36,6 +39,28 @@ constructor( return dialogFactory.create(delegate) } + /** + * Returns the click listener that should be invoked if a user clicks "Stop" on the end media + * projection dialog. + * + * The click listener will invoke [stopAction] and also do some UI manipulation. + * + * @param stopAction an action that, when invoked, should notify system API(s) that the media + * projection should be stopped. + */ + fun wrapStopAction(stopAction: () -> Unit): DialogInterface.OnClickListener { + return DialogInterface.OnClickListener { _, _ -> + // If the projection is stopped, then the chip will disappear, so we don't want the + // dialog to animate back into the chip just for the chip to disappear in a few frames. + dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() + stopAction.invoke() + // TODO(b/332662551): If the projection is stopped, there's a brief moment where the + // dialog closes and the chip re-shows because the system APIs haven't come back and + // told SysUI that the projection has officially stopped. It would be great for the chip + // to not re-show at all. + } + } + fun getAppName(state: MediaProjectionState.Projecting): CharSequence? { val specificTaskInfo = if (state is MediaProjectionState.Projecting.SingleTask) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt index 1eca827d55c4..72656ca1934c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt @@ -52,9 +52,10 @@ class EndScreenRecordingDialogDelegate( // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) - setPositiveButton(R.string.screenrecord_stop_dialog_button) { _, _ -> - stopAction.invoke() - } + setPositiveButton( + R.string.screenrecord_stop_dialog_button, + endMediaProjectionDialogHelper.wrapStopAction(stopAction), + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt index 0c349810257a..fcf3de42eb32 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt @@ -19,6 +19,9 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.app.ActivityManager import android.content.Context import androidx.annotation.DrawableRes +import com.android.internal.jank.Cuj +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -32,8 +35,10 @@ import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProj import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener import com.android.systemui.util.time.SystemClock @@ -52,15 +57,18 @@ constructor( @Application private val scope: CoroutineScope, private val context: Context, private val interactor: ScreenRecordChipInteractor, + private val shareToAppChipViewModel: ShareToAppChipViewModel, private val systemClock: SystemClock, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, + private val dialogTransitionAnimator: DialogTransitionAnimator, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { - override val chip: StateFlow<OngoingActivityChipModel> = + + private val internalChip = interactor.screenRecordState .map { state -> when (state) { - is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden + is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden() is ScreenRecordChipModel.Starting -> { OngoingActivityChipModel.Shown.Countdown( colors = ColorsModel.Red, @@ -80,6 +88,11 @@ constructor( startTimeMs = systemClock.elapsedRealtime(), createDialogLaunchOnClickListener( createDelegate(state.recordedTask), + dialogTransitionAnimator, + DialogCuj( + Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, + tag = "Screen record", + ), logger, TAG, ), @@ -87,8 +100,13 @@ constructor( } } } - // See b/347726238. - .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden) + // See b/347726238 for [SharingStarted.Lazily] reasoning. + .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden()) + + private val chipTransitionHelper = ChipTransitionHelper(scope) + + override val chip: StateFlow<OngoingActivityChipModel> = + chipTransitionHelper.createChipFlow(internalChip) private fun createDelegate( recordedTask: ActivityManager.RunningTaskInfo? @@ -96,13 +114,15 @@ constructor( return EndScreenRecordingDialogDelegate( endMediaProjectionDialogHelper, context, - stopAction = this::stopRecording, + stopAction = this::stopRecordingFromDialog, recordedTask, ) } - private fun stopRecording() { - logger.log(TAG, LogLevel.INFO, {}, { "Stop recording requested" }) + private fun stopRecordingFromDialog() { + logger.log(TAG, LogLevel.INFO, {}, { "Stop recording requested from dialog" }) + chipTransitionHelper.onActivityStoppedFromDialog() + shareToAppChipViewModel.onRecordingStoppedFromDialog() interactor.stopRecording() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt index 564f20e4b596..d10bd7705ce9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt @@ -44,9 +44,10 @@ class EndShareToAppDialogDelegate( // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) - setPositiveButton(R.string.share_to_app_stop_dialog_button) { _, _ -> - stopAction.invoke() - } + setPositiveButton( + R.string.share_to_app_stop_dialog_button, + endMediaProjectionDialogHelper.wrapStopAction(stopAction), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index ddebd3a0e3c2..85973fca4326 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes +import com.android.internal.jank.Cuj +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -32,6 +35,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProj import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener import com.android.systemui.util.time.SystemClock @@ -55,28 +59,49 @@ constructor( private val mediaProjectionChipInteractor: MediaProjectionChipInteractor, private val systemClock: SystemClock, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, + private val dialogTransitionAnimator: DialogTransitionAnimator, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { - override val chip: StateFlow<OngoingActivityChipModel> = + private val internalChip = mediaProjectionChipInteractor.projection .map { projectionModel -> when (projectionModel) { - is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden + is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden() is ProjectionChipModel.Projecting -> { if (projectionModel.type != ProjectionChipModel.Type.SHARE_TO_APP) { - OngoingActivityChipModel.Hidden + OngoingActivityChipModel.Hidden() } else { createShareToAppChip(projectionModel) } } } } - // See b/347726238. - .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden) + // See b/347726238 for [SharingStarted.Lazily] reasoning. + .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden()) + + private val chipTransitionHelper = ChipTransitionHelper(scope) + + override val chip: StateFlow<OngoingActivityChipModel> = + chipTransitionHelper.createChipFlow(internalChip) + + /** + * Notifies this class that the user just stopped a screen recording from the dialog that's + * shown when you tap the recording chip. + */ + fun onRecordingStoppedFromDialog() { + // When a screen recording is active, share-to-app is also active (screen recording is just + // a special case of share-to-app, where the specific app receiving the share is System UI). + // When a screen recording is stopped, we immediately hide the screen recording chip in + // [com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel]. + // We *also* need to immediately hide the share-to-app chip so it doesn't briefly show. + // See b/350891338. + chipTransitionHelper.onActivityStoppedFromDialog() + } /** Stops the currently active projection. */ - private fun stopProjecting() { - logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested" }) + private fun stopProjectingFromDialog() { + logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested from dialog" }) + chipTransitionHelper.onActivityStoppedFromDialog() mediaProjectionChipInteractor.stopProjecting() } @@ -92,7 +117,16 @@ constructor( colors = ColorsModel.Red, // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time. startTimeMs = systemClock.elapsedRealtime(), - createDialogLaunchOnClickListener(createShareToAppDialogDelegate(state), logger, TAG), + createDialogLaunchOnClickListener( + createShareToAppDialogDelegate(state), + dialogTransitionAnimator, + DialogCuj( + Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, + tag = "Share to app", + ), + logger, + TAG, + ), ) } @@ -100,7 +134,7 @@ constructor( EndShareToAppDialogDelegate( endMediaProjectionDialogHelper, context, - stopAction = this::stopProjecting, + stopAction = this::stopProjectingFromDialog, state, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index 40f86f924cd5..17cf60bf2dc5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -24,9 +24,15 @@ sealed class OngoingActivityChipModel { /** Condensed name representing the model, used for logs. */ abstract val logName: String - /** This chip shouldn't be shown. */ - data object Hidden : OngoingActivityChipModel() { - override val logName = "Hidden" + /** + * This chip shouldn't be shown. + * + * @property shouldAnimate true if the transition from [Shown] to [Hidden] should be animated, + * and false if that transition should *not* be animated (i.e. the chip view should + * immediately disappear). + */ + data class Hidden(val shouldAnimate: Boolean = true) : OngoingActivityChipModel() { + override val logName = "Hidden(anim=$shouldAnimate)" } /** This chip should be shown with the given information. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt new file mode 100644 index 000000000000..92e72c29519a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.ui.viewmodel + +import android.annotation.SuppressLint +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch + +/** + * A class that can help [OngoingActivityChipViewModel] instances with various transition states. + * + * For now, this class's only functionality is immediately hiding the chip if the user has tapped an + * activity chip and then clicked "Stop" on the resulting dialog. There's a bit of a delay between + * when the user clicks "Stop" and when the system services notify SysUI that the activity has + * indeed stopped. We don't want the chip to briefly show for a few frames during that delay, so + * this class helps us immediately hide the chip as soon as the user clicks "Stop" in the dialog. + * See b/353249803#comment4. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ChipTransitionHelper(@Application private val scope: CoroutineScope) { + /** A flow that emits each time the user has clicked "Stop" on the dialog. */ + @SuppressLint("SharedFlowCreation") + private val activityStoppedFromDialogEvent = MutableSharedFlow<Unit>() + + /** True if the user recently stopped the activity from the dialog. */ + private val wasActivityRecentlyStoppedFromDialog: Flow<Boolean> = + activityStoppedFromDialogEvent + .transformLatest { + // Give system services 500ms to stop the activity and notify SysUI. Once more than + // 500ms has elapsed, we should go back to using the current system service + // information as the source of truth. + emit(true) + delay(500) + emit(false) + } + // Use stateIn so that the flow created in [createChipFlow] is guaranteed to + // emit. (`combine`s require that all input flows have emitted.) + .stateIn(scope, SharingStarted.Lazily, false) + + /** + * Notifies this class that the user just clicked "Stop" on the stop dialog that's shown when + * the chip is tapped. + * + * Call this method in order to immediately hide the chip. + */ + fun onActivityStoppedFromDialog() { + // Because this event causes UI changes, make sure it's launched on the main thread scope. + scope.launch { activityStoppedFromDialogEvent.emit(Unit) } + } + + /** + * Creates a flow that will forcibly hide the chip if the user recently stopped the activity + * (see [onActivityStoppedFromDialog]). In general, this flow just uses value in [chip]. + */ + fun createChipFlow(chip: Flow<OngoingActivityChipModel>): StateFlow<OngoingActivityChipModel> { + return combine( + chip, + wasActivityRecentlyStoppedFromDialog, + ) { chipModel, activityRecentlyStopped -> + if (activityRecentlyStopped) { + // There's a bit of a delay between when the user stops an activity via + // SysUI and when the system services notify SysUI that the activity has + // indeed stopped. Prevent the chip from showing during this delay by + // immediately hiding it without any animation. + OngoingActivityChipModel.Hidden(shouldAnimate = false) + } else { + chipModel + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt index ee010f7a818b..2fc366b7f078 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt @@ -17,10 +17,14 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.view.View +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel +import com.android.systemui.res.R import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.phone.SystemUIDialog import kotlinx.coroutines.flow.StateFlow @@ -36,13 +40,19 @@ interface OngoingActivityChipViewModel { /** Creates a chip click listener that launches a dialog created by [dialogDelegate]. */ fun createDialogLaunchOnClickListener( dialogDelegate: SystemUIDialog.Delegate, + dialogTransitionAnimator: DialogTransitionAnimator, + cuj: DialogCuj, @StatusBarChipsLog logger: LogBuffer, tag: String, ): View.OnClickListener { - return View.OnClickListener { _ -> + return View.OnClickListener { view -> logger.log(tag, LogLevel.INFO, {}, { "Chip clicked" }) val dialog = dialogDelegate.createDialog() - dialog.show() + val launchableView = + view.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + dialogTransitionAnimator.showFromView(dialog, launchableView, cuj) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 15c348ed2f67..b0d897def53f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -26,11 +26,14 @@ import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastT import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** @@ -50,49 +53,132 @@ constructor( callChipViewModel: CallChipViewModel, @StatusBarChipsLog private val logger: LogBuffer, ) { + private enum class ChipType { + ScreenRecord, + ShareToApp, + CastToOtherDevice, + Call, + } + + /** Model that helps us internally track the various chip states from each of the types. */ + private sealed interface InternalChipModel { + /** + * Represents that we've internally decided to show the chip with type [type] with the given + * [model] information. + */ + data class Shown(val type: ChipType, val model: OngoingActivityChipModel.Shown) : + InternalChipModel + + /** + * Represents that all chip types would like to be hidden. Each value specifies *how* that + * chip type should get hidden. + */ + data class Hidden( + val screenRecord: OngoingActivityChipModel.Hidden, + val shareToApp: OngoingActivityChipModel.Hidden, + val castToOtherDevice: OngoingActivityChipModel.Hidden, + val call: OngoingActivityChipModel.Hidden, + ) : InternalChipModel + } + + private val internalChip: Flow<InternalChipModel> = + combine( + screenRecordChipViewModel.chip, + shareToAppChipViewModel.chip, + castToOtherDeviceChipViewModel.chip, + callChipViewModel.chip, + ) { screenRecord, shareToApp, castToOtherDevice, call -> + logger.log( + TAG, + LogLevel.INFO, + { + str1 = screenRecord.logName + str2 = shareToApp.logName + str3 = castToOtherDevice.logName + }, + { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." }, + ) + logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" }) + // This `when` statement shows the priority order of the chips. + when { + // Screen recording also activates the media projection APIs, so whenever the + // screen recording chip is active, the media projection chip would also be + // active. We want the screen-recording-specific chip shown in this case, so we + // give the screen recording chip priority. See b/296461748. + screenRecord is OngoingActivityChipModel.Shown -> + InternalChipModel.Shown(ChipType.ScreenRecord, screenRecord) + shareToApp is OngoingActivityChipModel.Shown -> + InternalChipModel.Shown(ChipType.ShareToApp, shareToApp) + castToOtherDevice is OngoingActivityChipModel.Shown -> + InternalChipModel.Shown(ChipType.CastToOtherDevice, castToOtherDevice) + call is OngoingActivityChipModel.Shown -> + InternalChipModel.Shown(ChipType.Call, call) + else -> { + // We should only get here if all chip types are hidden + check(screenRecord is OngoingActivityChipModel.Hidden) + check(shareToApp is OngoingActivityChipModel.Hidden) + check(castToOtherDevice is OngoingActivityChipModel.Hidden) + check(call is OngoingActivityChipModel.Hidden) + InternalChipModel.Hidden( + screenRecord = screenRecord, + shareToApp = shareToApp, + castToOtherDevice = castToOtherDevice, + call = call, + ) + } + } + } + /** * A flow modeling the chip that should be shown in the status bar after accounting for possibly - * multiple ongoing activities. + * multiple ongoing activities and animation requirements. * * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] is responsible for * actually displaying the chip. */ val chip: StateFlow<OngoingActivityChipModel> = - combine( - screenRecordChipViewModel.chip, - shareToAppChipViewModel.chip, - castToOtherDeviceChipViewModel.chip, - callChipViewModel.chip, - ) { screenRecord, shareToApp, castToOtherDevice, call -> - logger.log( - TAG, - LogLevel.INFO, - { - str1 = screenRecord.logName - str2 = shareToApp.logName - str3 = castToOtherDevice.logName - }, - { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." }, - ) - logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" }) - // This `when` statement shows the priority order of the chips - when { - // Screen recording also activates the media projection APIs, so whenever the - // screen recording chip is active, the media projection chip would also be - // active. We want the screen-recording-specific chip shown in this case, so we - // give the screen recording chip priority. See b/296461748. - screenRecord is OngoingActivityChipModel.Shown -> screenRecord - shareToApp is OngoingActivityChipModel.Shown -> shareToApp - castToOtherDevice is OngoingActivityChipModel.Shown -> castToOtherDevice - else -> call + internalChip + .pairwise(initialValue = DEFAULT_INTERNAL_HIDDEN_MODEL) + .map { (old, new) -> + if (old is InternalChipModel.Shown && new is InternalChipModel.Hidden) { + // If we're transitioning from showing the chip to hiding the chip, different + // chips require different animation behaviors. For example, the screen share + // chips shouldn't animate if the user stopped the screen share from the dialog + // (see b/353249803#comment4), but the call chip should always animate. + // + // This `when` block makes sure that when we're transitioning from Shown to + // Hidden, we check what chip type was previously showing and we use that chip + // type's hide animation behavior. + when (old.type) { + ChipType.ScreenRecord -> new.screenRecord + ChipType.ShareToApp -> new.shareToApp + ChipType.CastToOtherDevice -> new.castToOtherDevice + ChipType.Call -> new.call + } + } else if (new is InternalChipModel.Shown) { + // If we have a chip to show, always show it. + new.model + } else { + // In the Hidden -> Hidden transition, it shouldn't matter which hidden model we + // choose because no animation should happen regardless. + OngoingActivityChipModel.Hidden() } } // Some of the chips could have timers in them and we don't want the start time // for those timers to get reset for any reason. So, as soon as any subscriber has - // requested the chip information, we need to maintain it forever. See b/347726238. - .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden) + // requested the chip information, we maintain it forever by using + // [SharingStarted.Lazily]. See b/347726238. + .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden()) companion object { private const val TAG = "ChipsViewModel" + + private val DEFAULT_INTERNAL_HIDDEN_MODEL = + InternalChipModel.Hidden( + screenRecord = OngoingActivityChipModel.Hidden(), + shareToApp = OngoingActivityChipModel.Hidden(), + castToOtherDevice = OngoingActivityChipModel.Hidden(), + call = OngoingActivityChipModel.Hidden(), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index e48c28d3f3ee..cb133ecadab2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -1005,6 +1005,16 @@ public final class NotificationEntry extends ListEntry implements NotificationRo mIsMarkedForUserTriggeredMovement = marked; } + private boolean mSeenInShade = false; + + public void setSeenInShade(boolean seen) { + mSeenInShade = seen; + } + + public boolean isSeenInShade() { + return mSeenInShade; + } + public void setIsHeadsUpEntry(boolean isHeadsUpEntry) { mIsHeadsUpEntry = isHeadsUpEntry; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt index 5adf31b75fa7..0c7ba15baa92 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt @@ -13,12 +13,18 @@ class VisualStabilityProvider @Inject constructor() { /** The subset of active listeners which are temporary (will be removed after called) */ private val temporaryListeners = ArraySet<OnReorderingAllowedListener>() + private val banListeners = ListenerSet<OnReorderingBannedListener>() + var isReorderingAllowed = true set(value) { if (field != value) { field = value if (value) { notifyReorderingAllowed() + } else { + banListeners.forEach { listener -> + listener.onReorderingBanned() + } } } } @@ -38,6 +44,10 @@ class VisualStabilityProvider @Inject constructor() { allListeners.addIfAbsent(listener) } + fun addPersistentReorderingBannedListener(listener: OnReorderingBannedListener) { + banListeners.addIfAbsent(listener) + } + /** Add a listener which will be removed when it is called. */ fun addTemporaryReorderingAllowedListener(listener: OnReorderingAllowedListener) { // Only add to the temporary set if it was added to the global set @@ -57,3 +67,7 @@ class VisualStabilityProvider @Inject constructor() { fun interface OnReorderingAllowedListener { fun onReorderingAllowed() } + +fun interface OnReorderingBannedListener { + fun onReorderingBanned() +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt index b8af3698fb63..fe86375d628e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt @@ -122,12 +122,15 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : val timeRemaining = parseTimeDelta(remaining) TimerContentModel( icon = icon, - name = total, + // TODO: b/352142761 - define and use a string resource rather than " Timer". + // (The UX isn't final so using " Timer" for now). + name = total.replace("Σ", "") + " Timer", state = TimerContentModel.TimerState.Paused( timeRemaining = timeRemaining, - resumeIntent = notification.findActionWithName("Resume"), - resetIntent = notification.findActionWithName("Reset"), + resumeIntent = notification.findStartIntent(), + addMinuteAction = notification.findAddMinuteAction(), + resetAction = notification.findResetAction(), ) ) } @@ -136,12 +139,15 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis() TimerContentModel( icon = icon, - name = total, + // TODO: b/352142761 - define and use a string resource rather than " Timer". + // (The UX isn't final so using " Timer" for now). + name = total.replace("Σ", "") + " Timer", state = TimerContentModel.TimerState.Running( finishTime = finishTime, - pauseIntent = notification.findActionWithName("Pause"), - addOneMinuteIntent = notification.findActionWithName("Add 1 min"), + pauseIntent = notification.findPauseIntent(), + addMinuteAction = notification.findAddMinuteAction(), + resetAction = notification.findResetAction(), ) ) } @@ -149,8 +155,34 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : } } - private fun Notification.findActionWithName(name: String): PendingIntent? { - return actions.firstOrNull { name == it.title?.toString() }?.actionIntent + private fun Notification.findPauseIntent(): PendingIntent? { + return actions + .firstOrNull { it.actionIntent.intent?.action?.endsWith(".PAUSE_TIMER") == true } + ?.actionIntent + } + + private fun Notification.findStartIntent(): PendingIntent? { + return actions + .firstOrNull { it.actionIntent.intent?.action?.endsWith(".START_TIMER") == true } + ?.actionIntent + } + + // TODO: b/352142761 - switch to system attributes for label and icon. + // - We probably want a consistent look for the Reset button. (Double check with UX.) + // - Using the custom assets now since I couldn't an existing "Reset" icon. + private fun Notification.findResetAction(): Notification.Action? { + return actions.firstOrNull { + it.actionIntent.intent?.action?.endsWith(".RESET_TIMER") == true + } + } + + // TODO: b/352142761 - check with UX on whether this should be required. + // - Alternative is to allow for optional actions in addition to main and reset. + // - For optional actions, we should take the custom label and icon. + private fun Notification.findAddMinuteAction(): Notification.Action? { + return actions.firstOrNull { + it.actionIntent.intent?.action?.endsWith(".ADD_MINUTE_TIMER") == true + } } private fun parseCurrentTime(current: String): Long { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt index 558470175e8d..33b256456ca3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row.shared +import android.app.Notification import android.app.PendingIntent import java.time.Duration @@ -32,6 +33,9 @@ data class TimerContentModel( ) : RichOngoingContentModel { /** The state (paused or running) of the timer, and relevant time */ sealed interface TimerState { + val addMinuteAction: Notification.Action? + val resetAction: Notification.Action? + /** * Indicates a running timer * @@ -41,7 +45,8 @@ data class TimerContentModel( data class Running( val finishTime: Long, val pauseIntent: PendingIntent?, - val addOneMinuteIntent: PendingIntent?, + override val addMinuteAction: Notification.Action?, + override val resetAction: Notification.Action?, ) : TimerState /** @@ -53,7 +58,8 @@ data class TimerContentModel( data class Paused( val timeRemaining: Duration, val resumeIntent: PendingIntent?, - val resetIntent: PendingIntent?, + override val addMinuteAction: Notification.Action?, + override val resetAction: Notification.Action?, ) : TimerState } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt index 0d83aced6d07..8c951877544c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt @@ -18,8 +18,9 @@ package com.android.systemui.statusbar.notification.row.ui.view import android.annotation.DrawableRes import android.content.Context +import android.graphics.BlendMode import android.util.AttributeSet -import android.widget.Button +import com.android.internal.widget.EmphasizedNotificationButton class TimerButtonView @JvmOverloads @@ -28,14 +29,19 @@ constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0, -) : Button(context, attrs, defStyleAttr, defStyleRes) { +) : EmphasizedNotificationButton(context, attrs, defStyleAttr, defStyleRes) { private val Int.dp: Int get() = (this * context.resources.displayMetrics.density).toInt() fun setIcon(@DrawableRes icon: Int) { val drawable = context.getDrawable(icon) + + drawable?.mutate() + drawable?.setTintList(textColors) + drawable?.setTintBlendMode(BlendMode.SRC_IN) drawable?.setBounds(0, 0, 24.dp, 24.dp) + setCompoundDrawablesRelative(drawable, null, null, null) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt index 2e164d60431d..d481b50101c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.notification.row.ui.view import android.content.Context -import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon import android.os.SystemClock import android.util.AttributeSet import android.widget.Chronometer @@ -48,6 +48,9 @@ constructor( lateinit var altButton: TimerButtonView private set + lateinit var resetButton: TimerButtonView + private set + override fun onFinishInflate() { super.onFinishInflate() icon = requireViewById(R.id.icon) @@ -56,13 +59,14 @@ constructor( pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining) mainButton = requireViewById(R.id.mainButton) altButton = requireViewById(R.id.altButton) + resetButton = requireViewById(R.id.resetButton) } /** the resources configuration has changed such that the view needs to be reinflated */ fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange() - fun setIcon(iconDrawable: Drawable?) { - this.icon.setImageDrawable(iconDrawable) + fun setIcon(icon: Icon?) { + this.icon.setImageIcon(icon) } fun setLabel(label: String) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt index c9ff58961582..042d1bcfb2ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row.ui.viewbinder +import android.content.res.ColorStateList +import android.graphics.drawable.Icon import android.view.View import androidx.core.view.isGone import androidx.lifecycle.lifecycleScope @@ -46,12 +48,43 @@ object TimerViewBinder { launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } } launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } } launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } } + launch { viewModel.resetButtonModel.collect { bind(view.resetButton, it) } } } fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) { if (model != null) { - buttonView.setIcon(model.iconRes) - buttonView.setText(model.labelRes) + buttonView.setButtonBackground( + ColorStateList.valueOf( + buttonView.context.getColor(com.android.internal.R.color.system_accent2_100) + ) + ) + buttonView.setTextColor( + buttonView.context.getColor( + com.android.internal.R.color.notification_primary_text_color_light + ) + ) + + when (model) { + is TimerViewModel.ButtonViewModel.WithSystemAttrs -> { + buttonView.setIcon(model.iconRes) + buttonView.setText(model.labelRes) + } + is TimerViewModel.ButtonViewModel.WithCustomAttrs -> { + // TODO: b/352142761 - is there a better way to deal with TYPE_RESOURCE icons + // with empty resPackage? RemoteViews handles this by using a different + // `contextForResources` for inflation. + val icon = + if (model.icon.type == Icon.TYPE_RESOURCE && model.icon.resPackage == "") + Icon.createWithResource( + "com.google.android.deskclock", + model.icon.resId + ) + else model.icon + buttonView.setImageIcon(icon) + buttonView.text = model.label + } + } + buttonView.setOnClickListener( model.pendingIntent?.let { pendingIntent -> View.OnClickListener { pendingIntent.send() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt index a85c87f288d3..768a093e0b65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.row.ui.viewmodel import android.annotation.DrawableRes import android.annotation.StringRes import android.app.PendingIntent -import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag @@ -44,7 +44,7 @@ constructor( private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state } - val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable } + val icon: Flow<Icon?> = rowInteractor.timerContentModel.mapNotNull { it.icon.icon } val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name } @@ -57,13 +57,13 @@ constructor( state.map { when (it) { is TimerState.Paused -> - ButtonViewModel( + ButtonViewModel.WithSystemAttrs( it.resumeIntent, com.android.systemui.res.R.string.controls_media_resume, // "Resume", com.android.systemui.res.R.drawable.ic_media_play ) is TimerState.Running -> - ButtonViewModel( + ButtonViewModel.WithSystemAttrs( it.pauseIntent, com.android.systemui.res.R.string.controls_media_button_pause, // "Pause", com.android.systemui.res.R.drawable.ic_media_pause @@ -73,31 +73,41 @@ constructor( val altButtonModel: Flow<ButtonViewModel?> = state.map { - when (it) { - is TimerState.Paused -> - it.resetIntent?.let { resetIntent -> - ButtonViewModel( - resetIntent, - com.android.systemui.res.R.string.reset, // "Reset", - com.android.systemui.res.R.drawable.ic_close_white_rounded - ) - } - is TimerState.Running -> - it.addOneMinuteIntent?.let { addOneMinuteIntent -> - ButtonViewModel( - addOneMinuteIntent, - com.android.systemui.res.R.string.add, // "Add 1 minute", - com.android.systemui.res.R.drawable.ic_add - ) - } + it.addMinuteAction?.let { action -> + ButtonViewModel.WithCustomAttrs( + action.actionIntent, + action.title, // "1:00", + action.getIcon() + ) + } + } + + val resetButtonModel: Flow<ButtonViewModel?> = + state.map { + it.resetAction?.let { action -> + ButtonViewModel.WithCustomAttrs( + action.actionIntent, + action.title, // "Reset", + action.getIcon() + ) } } - data class ButtonViewModel( - val pendingIntent: PendingIntent?, - @StringRes val labelRes: Int, - @DrawableRes val iconRes: Int, - ) + sealed interface ButtonViewModel { + val pendingIntent: PendingIntent? + + data class WithSystemAttrs( + override val pendingIntent: PendingIntent?, + @StringRes val labelRes: Int, + @DrawableRes val iconRes: Int, + ) : ButtonViewModel + + data class WithCustomAttrs( + override val pendingIntent: PendingIntent?, + val label: CharSequence, + val icon: Icon, + ) : ButtonViewModel + } } private fun Duration.format(): String { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index cec1ef3f1e7b..4a447b7439b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -4862,14 +4862,20 @@ public class NotificationStackScrollLayout * @param isHeadsUp true for appear, false for disappear animations */ public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { - final boolean add = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); + final boolean closedAndSeenInShade = !mIsExpanded && row.getEntry() != null + && row.getEntry().isSeenInShade(); + final boolean addAnimation = mAnimationsEnabled && !closedAndSeenInShade && + (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); if (SPEW) { Log.v(TAG, "generateHeadsUpAnimation:" - + " willAdd=" + add - + " isHeadsUp=" + isHeadsUp - + " row=" + row.getEntry().getKey()); - } - if (add) { + + " addAnimation=" + addAnimation + + (row.getEntry() == null ? " entry NULL " + : " isSeenInShade=" + row.getEntry().isSeenInShade() + + " row=" + row.getEntry().getKey()) + + " mIsExpanded=" + mIsExpanded + + " isHeadsUp=" + isHeadsUp); + } + if (addAnimation) { // If we're hiding a HUN we just started showing THIS FRAME, then remove that event, // and do not add the disappear event either. if (!isHeadsUp && mHeadsUpChangeAnimations.remove(new Pair<>(row, true))) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index a2e44dffb767..8577d48b6679 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -39,6 +39,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; @@ -86,7 +87,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); private final VisualStabilityProvider mVisualStabilityProvider; - private final AvalancheController mAvalancheController; + private AvalancheController mAvalancheController; // TODO(b/328393698) move the topHeadsUpRow logic to an interactor private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow = @@ -173,6 +174,9 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements }); javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), this::onShadeOrQsExpanded); + mVisualStabilityProvider.addPersistentReorderingBannedListener(mOnReorderingBannedListener); + mVisualStabilityProvider.addPersistentReorderingAllowedListener( + mOnReorderingAllowedListener); } public void setAnimationStateHandler(AnimationStateHandler handler) { @@ -379,6 +383,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); + mAvalancheController.setEnableAtRuntime(true); for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { if (isHeadsUpEntry(entry.getKey())) { // Maybe the heads-up was removed already @@ -389,6 +394,22 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); }; + private final OnReorderingBannedListener mOnReorderingBannedListener = () -> { + if (mAvalancheController != null) { + // In open shade the first HUN is pinned, and visual stability logic prevents us from + // unpinning this first HUN as long as the shade remains open. AvalancheController only + // shows the next HUN when the currently showing HUN is unpinned, so we must disable + // throttling here so that the incoming HUN stream is not forever paused. This is reset + // when reorder becomes allowed. + mAvalancheController.setEnableAtRuntime(false); + + // Note that we cannot do the above when + // 1) The remove runnable runs because its delay means it may not run before shade close + // 2) Reordering is allowed again (when shade closes) because the HUN appear animation + // will have started by then + } + }; + /////////////////////////////////////////////////////////////////////////////////////////////// // HeadsUpManager utility (protected) methods overrides: @@ -561,18 +582,26 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements } @Override + protected void setEntry(@androidx.annotation.NonNull NotificationEntry entry, + @androidx.annotation.Nullable Runnable removeRunnable) { + super.setEntry(entry, removeRunnable); + + if (!mVisualStabilityProvider.isReorderingAllowed() + // We don't want to allow reordering while pulsing, but headsup need to + // time out anyway + && !entry.showingPulsing()) { + mEntriesToRemoveWhenReorderingAllowed.add(entry); + entry.setSeenInShade(true); + } + } + + @Override protected Runnable createRemoveRunnable(NotificationEntry entry) { - return () -> { - if (!mVisualStabilityProvider.isReorderingAllowed() - // We don't want to allow reordering while pulsing, but headsup need to - // time out anyway - && !entry.showingPulsing()) { - mEntriesToRemoveWhenReorderingAllowed.add(entry); - mVisualStabilityProvider.addTemporaryReorderingAllowedListener( - mOnReorderingAllowedListener); - } else if (mTrackingHeadsUp) { + return () -> { + if (mTrackingHeadsUp) { mEntriesToRemoveAfterExpand.add(entry); - } else { + } else if (mVisualStabilityProvider.isReorderingAllowed() + || entry.showingPulsing()) { removeEntry(entry.getKey(), "createRemoveRunnable"); } }; @@ -585,9 +614,6 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements if (mEntriesToRemoveAfterExpand.contains(mEntry)) { mEntriesToRemoveAfterExpand.remove(mEntry); } - if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) { - mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); - } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index aced0be4cc46..0320a7ae103b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -528,9 +528,10 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } @Override - public void onOngoingActivityStatusChanged(boolean hasOngoingActivity) { + public void onOngoingActivityStatusChanged( + boolean hasOngoingActivity, boolean shouldAnimate) { mHasOngoingActivity = hasOngoingActivity; - updateStatusBarVisibilities(/* animate= */ true); + updateStatusBarVisibilities(shouldAnimate); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt index ae1898bc479c..4c97854bb5c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt @@ -122,7 +122,8 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa // Notify listeners listener.onOngoingActivityStatusChanged( - hasOngoingActivity = true + hasOngoingActivity = true, + shouldAnimate = true, ) } is OngoingActivityChipModel.Hidden -> { @@ -130,7 +131,8 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa // b/192243808 and [Chronometer.start]. chipTimeView.stop() listener.onOngoingActivityStatusChanged( - hasOngoingActivity = false + hasOngoingActivity = false, + shouldAnimate = chipModel.shouldAnimate, ) } } @@ -266,8 +268,13 @@ interface StatusBarVisibilityChangeListener { /** Called when a transition from lockscreen to dream has started. */ fun onTransitionFromLockscreenToDreamStarted() - /** Called when the status of the ongoing activity chip (active or not active) has changed. */ - fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean) + /** + * Called when the status of the ongoing activity chip (active or not active) has changed. + * + * @param shouldAnimate true if the chip should animate in/out, and false if the chip should + * immediately appear/disappear. + */ + fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean, shouldAnimate: Boolean) /** * Called when the scene state has changed such that the home status bar is newly allowed or no diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt index 40799583a7b9..645a3619a7e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt @@ -44,6 +44,16 @@ constructor(dumpManager: DumpManager, private val tag = "AvalancheController" private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG) + var enableAtRuntime = true + set(value) { + if (!value) { + // Waiting HUNs in AvalancheController are shown in the HUN section in open shade. + // Clear them so we don't show them again when the shade closes and reordering is + // allowed again. + logDroppedHunsInBackground(getWaitingKeys().size) + clearNext() + } + } // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null @@ -90,6 +100,10 @@ constructor(dumpManager: DumpManager, return getKey(headsUpEntryShowing) } + fun isEnabled() : Boolean { + return NotificationThrottleHun.isEnabled && enableAtRuntime + } + /** Run or delay Runnable for given HeadsUpEntry */ fun update(entry: HeadsUpEntry?, runnable: Runnable?, label: String) { if (runnable == null) { @@ -185,7 +199,8 @@ constructor(dumpManager: DumpManager, showNext() runnable.run() } else { - log { "$fn => removing untracked ${getKey(entry)}" } + log { "$fn => run runnable for untracked shown ${getKey(entry)}" } + runnable.run() } logState("after $fn") } @@ -197,7 +212,7 @@ constructor(dumpManager: DumpManager, * BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration. */ fun getDurationMs(entry: HeadsUpEntry, autoDismissMs: Int): Int { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { // Use default duration, like we did before AvalancheController existed return autoDismissMs } @@ -246,7 +261,7 @@ constructor(dumpManager: DumpManager, /** Return true if entry is waiting to show. */ fun isWaiting(key: String): Boolean { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return false } for (entry in nextMap.keys) { @@ -259,7 +274,7 @@ constructor(dumpManager: DumpManager, /** Return list of keys for huns waiting */ fun getWaitingKeys(): MutableList<String> { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return mutableListOf() } val keyList = mutableListOf<String>() @@ -270,7 +285,7 @@ constructor(dumpManager: DumpManager, } fun getWaitingEntry(key: String): HeadsUpEntry? { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return null } for (headsUpEntry in nextMap.keys) { @@ -282,7 +297,7 @@ constructor(dumpManager: DumpManager, } fun getWaitingEntryList(): List<HeadsUpEntry> { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return mutableListOf() } return nextMap.keys.toList() @@ -340,7 +355,7 @@ constructor(dumpManager: DumpManager, showNow(headsUpEntryShowing!!, headsUpEntryShowingRunnableList) } - fun logDroppedHunsInBackground(numDropped: Int) { + private fun logDroppedHunsInBackground(numDropped: Int) { bgHandler.post(Runnable { // Do this in the background to avoid missing frames when closing the shade for (n in 1..numDropped) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 220e729625af..a0eb989a57bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -756,7 +756,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { setEntry(entry, createRemoveRunnable(entry)); } - private void setEntry(@NonNull final NotificationEntry entry, + protected void setEntry(@NonNull final NotificationEntry entry, @Nullable Runnable removeRunnable) { mEntry = entry; mRemoveRunnable = removeRunnable; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index e4d06681d439..7a521a6ba28f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -16,8 +16,14 @@ package com.android.systemui.statusbar.policy.domain.interactor +import android.content.Context import android.provider.Settings +import androidx.concurrent.futures.await import com.android.settingslib.notification.data.repository.ZenModeRepository +import com.android.settingslib.notification.modes.ZenIconLoader +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.common.shared.model.Icon +import java.time.Duration import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -28,7 +34,9 @@ import kotlinx.coroutines.flow.map * An interactor that performs business logic related to the status and configuration of Zen Mode * (or Do Not Disturb/DND Mode). */ -class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) { +class ZenModeInteractor @Inject constructor(private val repository: ZenModeRepository) { + private val iconLoader: ZenIconLoader = ZenIconLoader.getInstance() + val isZenModeEnabled: Flow<Boolean> = repository.globalZenMode .map { @@ -52,4 +60,18 @@ class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) { } } .distinctUntilChanged() + + val modes: Flow<List<ZenMode>> = repository.modes + + suspend fun getModeIcon(mode: ZenMode, context: Context): Icon { + return Icon.Loaded(mode.getIcon(context, iconLoader).await(), contentDescription = null) + } + + fun activateMode(zenMode: ZenMode, duration: Duration? = null) { + repository.activateMode(zenMode, duration) + } + + fun deactivateMode(zenMode: ZenMode) { + repository.deactivateMode(zenMode) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt new file mode 100644 index 000000000000..2b094d6b4922 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog + +import android.content.Intent +import android.provider.Settings +import androidx.compose.material3.Text +import androidx.compose.ui.res.stringResource +import com.android.compose.PlatformButton +import com.android.compose.PlatformOutlinedButton +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.dialog.ui.composable.AlertDialogContent +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.statusbar.phone.create +import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel +import javax.inject.Inject + +class ModesDialogDelegate +@Inject +constructor( + private val sysuiDialogFactory: SystemUIDialogFactory, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val activityStarter: ActivityStarter, + private val viewModel: ModesDialogViewModel, +) : SystemUIDialog.Delegate { + override fun createDialog(): SystemUIDialog { + return sysuiDialogFactory.create { dialog -> + AlertDialogContent( + title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, + content = { ModeTileGrid(viewModel) }, + neutralButton = { + PlatformOutlinedButton( + onClick = { + val animationController = + dialogTransitionAnimator.createActivityTransitionController( + dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL) + ) + if (animationController == null) { + // The controller will take care of dismissing for us after the + // animation, but let's make sure we dismiss the dialog if we don't + // animate it. + dialog.dismiss() + } + activityStarter.startActivity( + ZEN_MODE_SETTINGS_INTENT, + true /* dismissShade */, + animationController + ) + } + ) { + Text(stringResource(R.string.zen_modes_dialog_settings)) + } + }, + positiveButton = { + PlatformButton(onClick = { dialog.dismiss() }) { + Text(stringResource(R.string.zen_modes_dialog_done)) + } + }, + ) + } + } + + companion object { + private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt new file mode 100644 index 000000000000..91bfdff1095e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.composable + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewModel + +@Composable +fun ModeTile(viewModel: ModeTileViewModel) { + val tileColor = + if (viewModel.enabled) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceVariant + val contentColor = + if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurfaceVariant + + CompositionLocalProvider(LocalContentColor provides contentColor) { + Surface( + color = tileColor, + shape = RoundedCornerShape(16.dp), + modifier = + Modifier.combinedClickable( + onClick = viewModel.onClick, + onLongClick = viewModel.onLongClick + ), + ) { + Row( + modifier = Modifier.padding(20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = + Arrangement.spacedBy( + space = 10.dp, + alignment = Alignment.Start, + ), + ) { + Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp)) + Column { + Text( + viewModel.text, + fontWeight = FontWeight.W500, + modifier = Modifier.tileMarquee() + ) + Text( + viewModel.subtext, + fontWeight = FontWeight.W400, + modifier = Modifier.tileMarquee() + ) + } + } + } + } +} + +private fun Modifier.tileMarquee(): Modifier { + return this.basicMarquee( + iterations = 1, + initialDelayMillis = 200, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt new file mode 100644 index 000000000000..73d361f69eac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel + +@Composable +fun ModeTileGrid(viewModel: ModesDialogViewModel) { + val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList()) + + // TODO(b/346519570): Handle what happens when we have more than a few modes. + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + tiles.size, + key = { index -> tiles[index].id }, + ) { index -> + ModeTile(viewModel = tiles[index]) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt new file mode 100644 index 000000000000..5bd26ccc965f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.viewmodel + +import com.android.systemui.common.shared.model.Icon + +/** + * Viewmodel for a tile representing a single priority ("zen") mode, for use within the modes + * dialog. Not to be confused with ModesTile, which is the Quick Settings tile that opens the + * dialog. + */ +data class ModeTileViewModel( + val id: String, + val icon: Icon, + val text: String, + val subtext: String, + val enabled: Boolean, + val contentDescription: String, + val onClick: () -> Unit, + val onLongClick: () -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt new file mode 100644 index 000000000000..e84c8b61ff54 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.viewmodel + +import android.content.Context +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows + * the user to quickly toggle modes. + */ +@SysUISingleton +class ModesDialogViewModel +@Inject +constructor( + val context: Context, + zenModeInteractor: ZenModeInteractor, + @Background val bgDispatcher: CoroutineDispatcher, +) { + // Modes that should be displayed in the dialog + // TODO(b/346519570): Include modes that have not been set up yet. + private val visibleModes: Flow<List<ZenMode>> = + zenModeInteractor.modes.map { + it.filter { mode -> + mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed) + } + } + + val tiles: Flow<List<ModeTileViewModel>> = + visibleModes + .map { modesList -> + modesList.map { mode -> + ModeTileViewModel( + id = mode.id, + icon = zenModeInteractor.getModeIcon(mode, context), + text = mode.rule.name, + subtext = getTileSubtext(mode), + enabled = mode.isActive, + // TODO(b/346519570): This should be some combination of the above, e.g. + // "ON: Do Not Disturb, Until Mon 08:09"; see DndTile. + contentDescription = "", + onClick = { + if (mode.isActive) { + zenModeInteractor.deactivateMode(mode) + } else { + // TODO(b/346519570): Handle duration for DND mode. + zenModeInteractor.activateMode(mode) + } + }, + onLongClick = { + // TODO(b/346519570): Open settings page for mode. + } + ) + } + } + .flowOn(bgDispatcher) + + private fun getTileSubtext(mode: ZenMode): String { + // TODO(b/346519570): Use ZenModeConfig.getDescription for manual DND + val on = context.resources.getString(R.string.zen_mode_on) + val off = context.resources.getString(R.string.zen_mode_off) + return mode.rule.triggerDescription ?: if (mode.isActive) on else off + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index c45f98e5f4f5..066bfc5c588d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -18,6 +18,8 @@ package com.android.systemui.volume; import static android.media.AudioManager.RINGER_MODE_NORMAL; +import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix; + import android.app.ActivityManager; import android.app.KeyguardManager; import android.app.NotificationManager; @@ -59,6 +61,8 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.CaptioningManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; @@ -76,6 +80,8 @@ import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.RingerModeLiveData; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.concurrency.ThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor; import dalvik.annotation.optimization.NeverCompile; @@ -102,7 +108,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int TOUCH_FEEDBACK_TIMEOUT_MS = 1000; - private static final int DYNAMIC_STREAM_START_INDEX = 100; + // We only need one dynamic stream for broadcast because at most two headsets are allowed + // to join local broadcast in current stage. + // It is safe to use 99 as the broadcast stream now. There are only 10+ default audio + // streams defined in AudioSystem for now and audio team is in the middle of restructure, + // no new default stream is preferred. + @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99; + private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100; private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) @@ -145,6 +157,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final State mState = new State(); protected final MediaSessionsCallbacks mMediaSessionsCallbacksW; private final VibratorHelper mVibrator; + private final AudioSharingInteractor mAudioSharingInteractor; + private final JavaAdapter mJavaAdapter; private final boolean mHasVibrator; private boolean mShowA11yStream; private boolean mShowVolumeDialog; @@ -188,7 +202,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa KeyguardManager keyguardManager, ActivityManager activityManager, UserTracker userTracker, - DumpManager dumpManager + DumpManager dumpManager, + AudioSharingInteractor audioSharingInteractor, + JavaAdapter javaAdapter ) { mContext = context.getApplicationContext(); mPackageManager = packageManager; @@ -200,6 +216,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mRouter2Manager = MediaRouter2Manager.getInstance(mContext); mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext); mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW); + mAudioSharingInteractor = audioSharingInteractor; + mJavaAdapter = javaAdapter; mAudio = audioManager; mNoMan = notificationManager; mObserver = new SettingObserver(mWorker); @@ -272,6 +290,12 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } catch (SecurityException e) { Log.w(TAG, "No access to media sessions", e); } + if (volumeDialogAudioSharingFix()) { + Slog.d(TAG, "Start collect volume changes in audio sharing"); + mJavaAdapter.alwaysCollectFlow( + mAudioSharingInteractor.getVolume(), + this::handleAudioSharingStreamVolumeChanges); + } } public void setVolumePolicy(VolumePolicy policy) { @@ -545,7 +569,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mState.activeStream = activeStream; Events.writeEvent(Events.EVENT_ACTIVE_STREAM_CHANGED, activeStream); if (D.BUG) Log.d(TAG, "updateActiveStreamW " + activeStream); - final int s = activeStream < DYNAMIC_STREAM_START_INDEX ? activeStream : -1; + final int s = + activeStream + < (volumeDialogAudioSharingFix() + ? DYNAMIC_STREAM_BROADCAST + : DYNAMIC_STREAM_REMOTE_START_INDEX) + ? activeStream + : -1; if (D.BUG) Log.d(TAG, "forceVolumeControlStream " + s); mAudio.forceVolumeControlStream(s); return true; @@ -726,7 +756,12 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private void onSetStreamVolumeW(int stream, int level) { if (D.BUG) Log.d(TAG, "onSetStreamVolume " + stream + " level=" + level); - if (stream >= DYNAMIC_STREAM_START_INDEX) { + if (volumeDialogAudioSharingFix() && stream == DYNAMIC_STREAM_BROADCAST) { + Slog.d(TAG, "onSetStreamVolumeW set broadcast stream level = " + level); + mAudioSharingInteractor.setStreamVolume(level); + return; + } + if (stream >= DYNAMIC_STREAM_REMOTE_START_INDEX) { mMediaSessionsCallbacksW.setStreamVolume(stream, level); return; } @@ -758,6 +793,40 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa DndTile.setVisible(mContext, true); } + void handleAudioSharingStreamVolumeChanges(@Nullable Integer volume) { + if (volume == null) { + if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) { + mState.states.remove(DYNAMIC_STREAM_BROADCAST); + Slog.d(TAG, "Remove audio sharing stream"); + mCallbacks.onStateChanged(mState); + } + } else { + if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) { + StreamState ss = mState.states.get(DYNAMIC_STREAM_BROADCAST); + if (ss.level != volume) { + ss.level = volume; + Slog.d(TAG, "updateState, audio sharing stream volume = " + volume); + mCallbacks.onStateChanged(mState); + } + } else { + StreamState ss = streamStateW(DYNAMIC_STREAM_BROADCAST); + ss.dynamic = true; + ss.levelMin = mAudioSharingInteractor.getVolumeMin(); + ss.levelMax = mAudioSharingInteractor.getVolumeMax(); + if (ss.level != volume) { + ss.level = volume; + } + String label = mContext.getString(R.string.audio_sharing_description); + if (!Objects.equals(ss.remoteLabel, label)) { + ss.name = -1; + ss.remoteLabel = label; + } + Slog.d(TAG, "updateState, new audio sharing stream volume = " + volume); + mCallbacks.onStateChanged(mState); + } + } + } + private final class VC extends IVolumeController.Stub { private final String TAG = VolumeDialogControllerImpl.TAG + ".VC"; @@ -1256,7 +1325,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks { private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); - private int mNextStream = DYNAMIC_STREAM_START_INDEX; + private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX; private final boolean mVolumeAdjustmentForRemoteGroupSessions; public MediaSessionsCallbacks(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 6b02e1ada491..0770d8926389 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -34,6 +34,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL; import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder; +import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix; import static com.android.systemui.Flags.hapticVolumeSlider; import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED; import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; @@ -1678,6 +1679,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, return true; } + // Always show the stream for audio sharing if it exists. + if (volumeDialogAudioSharingFix() + && row.ss != null + && mContext.getString(R.string.audio_sharing_description) + .equals(row.ss.remoteLabel)) { + return true; + } + if (row.defaultStream) { return activeRow.stream == STREAM_RING || activeRow.stream == STREAM_ALARM @@ -1880,10 +1889,25 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, if (!ss.dynamic) continue; mDynamic.put(stream, true); if (findRow(stream) == null) { - addRow(stream, - com.android.settingslib.R.drawable.ic_volume_remote, - com.android.settingslib.R.drawable.ic_volume_remote_mute, - true, false, true); + if (volumeDialogAudioSharingFix() + && mContext.getString(R.string.audio_sharing_description) + .equals(ss.remoteLabel)) { + addRow( + stream, + R.drawable.ic_volume_media, + R.drawable.ic_volume_media_mute, + true, + false, + true); + } else { + addRow( + stream, + com.android.settingslib.R.drawable.ic_volume_remote, + com.android.settingslib.R.drawable.ic_volume_remote_mute, + true, + false, + true); + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index 6dc4b10a57da..bbff5392f59c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -18,6 +18,10 @@ package com.android.systemui.biometrics import android.graphics.Point import android.hardware.biometrics.BiometricSourceType +import android.hardware.biometrics.ComponentInfoInternal +import android.hardware.biometrics.SensorLocationInternal +import android.hardware.biometrics.SensorProperties +import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.testing.TestableLooper.RunWithLooper import android.util.DisplayMetrics @@ -43,6 +47,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.leak.RotationUtils import com.android.systemui.util.mockito.any +import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.assertFalse @@ -62,8 +67,6 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.MockitoSession import org.mockito.quality.Strictness -import javax.inject.Provider - @ExperimentalCoroutinesApi @SmallTest @@ -79,35 +82,28 @@ class AuthRippleControllerTest : SysuiTestCase() { @Mock private lateinit var authController: AuthController @Mock private lateinit var authRippleInteractor: AuthRippleInteractor @Mock private lateinit var keyguardStateController: KeyguardStateController - @Mock - private lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock - private lateinit var notificationShadeWindowController: NotificationShadeWindowController - @Mock - private lateinit var biometricUnlockController: BiometricUnlockController - @Mock - private lateinit var udfpsControllerProvider: Provider<UdfpsController> - @Mock - private lateinit var udfpsController: UdfpsController - @Mock - private lateinit var statusBarStateController: StatusBarStateController - @Mock - private lateinit var lightRevealScrim: LightRevealScrim - @Mock - private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal + @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle + @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController + @Mock private lateinit var biometricUnlockController: BiometricUnlockController + @Mock private lateinit var udfpsControllerProvider: Provider<UdfpsController> + @Mock private lateinit var udfpsController: UdfpsController + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var lightRevealScrim: LightRevealScrim + @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal private val facePropertyRepository = FakeFacePropertyRepository() private val displayMetrics = DisplayMetrics() @Captor private lateinit var biometricUnlockListener: - ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener> + ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener> @Before fun setUp() { mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) MockitoAnnotations.initMocks(this) - staticMockSession = mockitoSession() + staticMockSession = + mockitoSession() .mockStatic(RotationUtils::class.java) .strictness(Strictness.LENIENT) .startMocking() @@ -116,25 +112,26 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(authController.udfpsProps).thenReturn(listOf(fpSensorProp)) `when`(udfpsControllerProvider.get()).thenReturn(udfpsController) - controller = AuthRippleController( - context, - authController, - configurationController, - keyguardUpdateMonitor, - keyguardStateController, - wakefulnessLifecycle, - commandRegistry, - notificationShadeWindowController, - udfpsControllerProvider, - statusBarStateController, - displayMetrics, - KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), - biometricUnlockController, - lightRevealScrim, - authRippleInteractor, - facePropertyRepository, - rippleView, - ) + controller = + AuthRippleController( + context, + authController, + configurationController, + keyguardUpdateMonitor, + keyguardStateController, + wakefulnessLifecycle, + commandRegistry, + notificationShadeWindowController, + udfpsControllerProvider, + statusBarStateController, + displayMetrics, + KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), + biometricUnlockController, + lightRevealScrim, + authRippleInteractor, + facePropertyRepository, + rippleView, + ) controller.init() } @@ -150,13 +147,18 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - eq(BiometricSourceType.FINGERPRINT))).thenReturn(true) + `when`( + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT) + ) + ) + .thenReturn(true) // WHEN fingerprint authenticated verify(biometricUnlockController).addListener(biometricUnlockListener.capture()) - biometricUnlockListener.value - .onBiometricUnlockedWithKeyguardDismissal(BiometricSourceType.FINGERPRINT) + biometricUnlockListener.value.onBiometricUnlockedWithKeyguardDismissal( + BiometricSourceType.FINGERPRINT + ) // THEN update sensor location and show ripple verify(rippleView).setFingerprintSensorLocation(fpsLocation, 0f) @@ -169,8 +171,12 @@ class AuthRippleControllerTest : SysuiTestCase() { val fpsLocation = Point(5, 5) `when`(authController.udfpsLocation).thenReturn(fpsLocation) controller.onViewAttached() - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - eq(BiometricSourceType.FINGERPRINT))).thenReturn(true) + `when`( + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT) + ) + ) + .thenReturn(true) // WHEN keyguard is NOT showing & fingerprint authenticated `when`(keyguardStateController.isShowing).thenReturn(false) @@ -179,7 +185,8 @@ class AuthRippleControllerTest : SysuiTestCase() { captor.value.onBiometricAuthenticated( 0 /* userId */, BiometricSourceType.FINGERPRINT /* type */, - false /* isStrongBiometric */) + false /* isStrongBiometric */ + ) // THEN no ripple verify(rippleView, never()).startUnlockedRipple(any()) @@ -194,14 +201,19 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(keyguardStateController.isShowing).thenReturn(true) // WHEN unlocking with fingerprint is NOT allowed & fingerprint authenticated - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - eq(BiometricSourceType.FINGERPRINT))).thenReturn(false) + `when`( + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT) + ) + ) + .thenReturn(false) val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) verify(keyguardUpdateMonitor).registerCallback(captor.capture()) captor.value.onBiometricAuthenticated( 0 /* userId */, BiometricSourceType.FINGERPRINT /* type */, - false /* isStrongBiometric */) + false /* isStrongBiometric */ + ) // THEN no ripple verify(rippleView, never()).startUnlockedRipple(any()) @@ -218,7 +230,8 @@ class AuthRippleControllerTest : SysuiTestCase() { captor.value.onBiometricAuthenticated( 0 /* userId */, BiometricSourceType.FACE /* type */, - false /* isStrongBiometric */) + false /* isStrongBiometric */ + ) verify(rippleView, never()).startUnlockedRipple(any()) } @@ -233,18 +246,17 @@ class AuthRippleControllerTest : SysuiTestCase() { captor.value.onBiometricAuthenticated( 0 /* userId */, BiometricSourceType.FINGERPRINT /* type */, - false /* isStrongBiometric */) + false /* isStrongBiometric */ + ) verify(rippleView, never()).startUnlockedRipple(any()) } @Test fun registersAndDeregisters() { controller.onViewAttached() - val captor = ArgumentCaptor - .forClass(KeyguardStateController.Callback::class.java) + val captor = ArgumentCaptor.forClass(KeyguardStateController.Callback::class.java) verify(keyguardStateController).addCallback(captor.capture()) - val captor2 = ArgumentCaptor - .forClass(WakefulnessLifecycle.Observer::class.java) + val captor2 = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java) verify(wakefulnessLifecycle).addObserver(captor2.capture()) controller.onViewDetached() verify(keyguardStateController).removeCallback(any()) @@ -259,17 +271,25 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - BiometricSourceType.FINGERPRINT)).thenReturn(true) + `when`( + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT + ) + ) + .thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) controller.showUnlockRipple(BiometricSourceType.FINGERPRINT) - assertTrue("reveal didn't start on keyguardFadingAway", - controller.startLightRevealScrimOnKeyguardFadingAway) + assertTrue( + "reveal didn't start on keyguardFadingAway", + controller.startLightRevealScrimOnKeyguardFadingAway + ) `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true) controller.onKeyguardFadingAwayChanged() - assertFalse("reveal triggers multiple times", - controller.startLightRevealScrimOnKeyguardFadingAway) + assertFalse( + "reveal triggers multiple times", + controller.startLightRevealScrimOnKeyguardFadingAway + ) } @Test @@ -282,23 +302,27 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(keyguardStateController.isShowing).thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) `when`(authController.isUdfpsFingerDown).thenReturn(true) - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - eq(BiometricSourceType.FACE))).thenReturn(true) + `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(eq(BiometricSourceType.FACE))) + .thenReturn(true) controller.showUnlockRipple(BiometricSourceType.FACE) - assertTrue("reveal didn't start on keyguardFadingAway", - controller.startLightRevealScrimOnKeyguardFadingAway) + assertTrue( + "reveal didn't start on keyguardFadingAway", + controller.startLightRevealScrimOnKeyguardFadingAway + ) `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true) controller.onKeyguardFadingAwayChanged() - assertFalse("reveal triggers multiple times", - controller.startLightRevealScrimOnKeyguardFadingAway) + assertFalse( + "reveal triggers multiple times", + controller.startLightRevealScrimOnKeyguardFadingAway + ) } @Test fun testUpdateRippleColor() { controller.onViewAttached() - val captor = ArgumentCaptor - .forClass(ConfigurationController.ConfigurationListener::class.java) + val captor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) verify(configurationController).addCallback(captor.capture()) reset(rippleView) @@ -333,6 +357,40 @@ class AuthRippleControllerTest : SysuiTestCase() { } @Test + fun testUltrasonicUdfps_onFingerDown_runningForDeviceEntry_doNotShowDwellRipple() { + // GIVEN UDFPS is ultrasonic + `when`(authController.udfpsProps) + .thenReturn( + listOf( + FingerprintSensorPropertiesInternal( + 0 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + listOf<ComponentInfoInternal>(), + FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC, + false /* halControlsIllumination */, + true /* resetLockoutRequiresHardwareAuthToken */, + listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT), + ) + ) + ) + + // GIVEN fingerprint detection is running on keyguard + `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(true) + + // GIVEN view is already attached + controller.onViewAttached() + val captor = ArgumentCaptor.forClass(UdfpsController.Callback::class.java) + verify(udfpsController).addCallback(captor.capture()) + + // WHEN finger is down + captor.value.onFingerDown() + + // THEN never show dwell ripple + verify(rippleView, never()).startDwellRipple(false) + } + + @Test fun testUdfps_onFingerDown_notDeviceEntry_doesNotShowDwellRipple() { // GIVEN fingerprint detection is NOT running on keyguard `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt index 10b3ce31a895..0489d815b074 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt @@ -89,7 +89,8 @@ class DetailDialogTest : SysuiTestCase() { verify(taskView).startActivity(any(), any(), capture(optionsCaptor), any()) assertThat(optionsCaptor.value.pendingIntentBackgroundActivityStartMode) - .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .isAnyOf(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED, + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) assertThat(optionsCaptor.value.isPendingIntentBackgroundActivityLaunchAllowedByPermission) .isTrue() assertThat(optionsCaptor.value.taskAlwaysOnTop).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt index 4c77fb84d8ce..27b6ea61a922 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.settingslib.notification.data.repository.FakeZenModeRepository import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -41,10 +42,12 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.util.mockito.any import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.SecureSettings import com.google.common.truth.Truth.assertThat +import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -79,6 +82,10 @@ class ModesTileTest : SysuiTestCase() { @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider + @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator + + @Mock private lateinit var dialogDelegate: ModesDialogDelegate + private val inputHandler = FakeQSTileIntentUserInputHandler() private val zenModeRepository = FakeZenModeRepository() private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository) @@ -122,7 +129,13 @@ class ModesTileTest : SysuiTestCase() { } ) - userActionInteractor = ModesTileUserActionInteractor(inputHandler) + userActionInteractor = + ModesTileUserActionInteractor( + EmptyCoroutineContext, + inputHandler, + dialogTransitionAnimator, + dialogDelegate, + ) underTest = ModesTile( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index c9a7c82d6b3f..02764f8a15fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt @@ -16,9 +16,13 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel +import android.content.DialogInterface import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue @@ -37,6 +41,8 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.policy.CastDevice @@ -45,7 +51,10 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -60,6 +69,16 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { private val mockScreenCastDialog = mock<SystemUIDialog>() private val mockGenericCastDialog = mock<SystemUIDialog>() + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } private val underTest = kosmos.castToOtherDeviceChipViewModel @@ -193,6 +212,63 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test + fun chip_projectionStoppedFromDialog_chipImmediatelyHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // WHEN the stop action on the dialog is clicked + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockScreenCastDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden... + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + // ...even though the repo still says it's projecting + assertThat(mediaProjectionRepo.mediaProjectionState.value) + .isInstanceOf(MediaProjectionState.Projecting::class.java) + + // AND we specify no animation + assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse() + } + + @Test + fun chip_routeStoppedFromDialog_chipImmediatelyHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaRouterRepo.castDevices.value = + listOf( + CastDevice( + state = CastDevice.CastState.Connected, + id = "id", + name = "name", + description = "desc", + origin = CastDevice.CastOrigin.MediaRouter, + ) + ) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // WHEN the stop action on the dialog is clicked + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockGenericCastDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden... + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + // ...even though the repo still says it's projecting + assertThat(mediaRouterRepo.castDevices.value).isNotEmpty() + + // AND we specify no animation + assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse() + } + + @Test fun chip_colorsAreRed() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -297,8 +373,14 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockScreenCastDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockScreenCastDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -316,8 +398,14 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockScreenCastDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockScreenCastDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -339,7 +427,70 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockGenericCastDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockGenericCastDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) + } + + @Test + fun chip_projectionStateCasting_clickListenerHasCuj() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE) + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + val cujCaptor = argumentCaptor<DialogCuj>() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + any(), + any(), + cujCaptor.capture(), + anyBoolean(), + ) + + assertThat(cujCaptor.firstValue.cujType) + .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) + assertThat(cujCaptor.firstValue.tag).contains("Cast") + } + + @Test + fun chip_routerStateCasting_clickListenerHasCuj() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaRouterRepo.castDevices.value = + listOf( + CastDevice( + state = CastDevice.CastState.Connected, + id = "id", + name = "name", + description = "desc", + origin = CastDevice.CastOrigin.MediaRouter, + ) + ) + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + val cujCaptor = argumentCaptor<DialogCuj>() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + any(), + any(), + cujCaptor.capture(), + anyBoolean(), + ) + + assertThat(cujCaptor.firstValue.cujType) + .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) + assertThat(cujCaptor.firstValue.tag).contains("Cast") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index 4728c649b9a7..b4a37ee1a55e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -16,9 +16,13 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel +import android.content.DialogInterface import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos @@ -30,9 +34,13 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager import com.android.systemui.res.R import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository +import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.util.time.fakeSystemClock @@ -40,7 +48,10 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -53,11 +64,22 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository private val systemClock = kosmos.fakeSystemClock private val mockSystemUIDialog = mock<SystemUIDialog>() + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } private val underTest = kosmos.screenRecordChipViewModel @Before fun setUp() { + setUpPackageManagerForMediaProjection(kosmos) whenever(kosmos.mockSystemUIDialogFactory.create(any<EndScreenRecordingDialogDelegate>())) .thenReturn(mockSystemUIDialog) } @@ -132,6 +154,40 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test + fun chip_recordingStoppedFromDialog_screenRecordAndShareToAppChipImmediatelyHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + val latestShareToApp by collectLastValue(kosmos.shareToAppChipViewModel.chip) + + // On real devices, when screen recording is active then share-to-app is also active + // because screen record is just a special case of share-to-app where the app receiving + // the share is SysUI + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen("fake.package") + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + assertThat(latestShareToApp).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // WHEN the stop action on the dialog is clicked + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN both the screen record chip and the share-to-app chip are immediately hidden... + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + assertThat(latestShareToApp).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + // ...even though the repos still say it's recording + assertThat(screenRecordRepo.screenRecordState.value) + .isEqualTo(ScreenRecordModel.Recording) + assertThat(mediaProjectionRepo.mediaProjectionState.value) + .isInstanceOf(MediaProjectionState.Projecting::class.java) + + // AND we specify no animation + assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse() + } + + @Test fun chip_startingState_colorsAreRed() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -182,9 +238,15 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) + clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(mockSystemUIDialog).show() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockSystemUIDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -198,9 +260,15 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) + clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(mockSystemUIDialog).show() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockSystemUIDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -218,8 +286,39 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) + clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(mockSystemUIDialog).show() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockSystemUIDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) + } + + @Test + fun chip_clickListenerHasCuj() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen("host.package") + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + val cujCaptor = argumentCaptor<DialogCuj>() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + any(), + any(), + cujCaptor.capture(), + anyBoolean(), + ) + + assertThat(cujCaptor.firstValue.cujType) + .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) + assertThat(cujCaptor.firstValue.tag).contains("Screen record") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index f87b17dc92d1..2658679dee08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -16,9 +16,13 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel +import android.content.DialogInterface import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos @@ -34,6 +38,8 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.util.time.fakeSystemClock @@ -41,7 +47,10 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -54,6 +63,16 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { private val systemClock = kosmos.fakeSystemClock private val mockShareDialog = mock<SystemUIDialog>() + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } private val underTest = kosmos.shareToAppChipViewModel @@ -134,6 +153,31 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test + fun chip_shareStoppedFromDialog_chipImmediatelyHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // WHEN the stop action on the dialog is clicked + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockShareDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden... + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + // ...even though the repo still says it's projecting + assertThat(mediaProjectionRepo.mediaProjectionState.value) + .isInstanceOf(MediaProjectionState.Projecting::class.java) + + // AND we specify no animation + assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse() + } + + @Test fun chip_colorsAreRed() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -181,8 +225,14 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockShareDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockShareDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -199,7 +249,41 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockShareDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockShareDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) + } + + @Test + fun chip_clickListenerHasCuj() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + NORMAL_PACKAGE, + hostDeviceName = null, + createTask(taskId = 1), + ) + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + val cujCaptor = argumentCaptor<DialogCuj>() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + any(), + any(), + cujCaptor.capture(), + anyBoolean(), + ) + + assertThat(cujCaptor.firstValue.cujType) + .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) + assertThat(cujCaptor.firstValue.tag).contains("Share") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt new file mode 100644 index 000000000000..b9049e8f76b6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.model.ColorsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +class ChipTransitionHelperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + + @Test + fun createChipFlow_typicallyFollowsInputFlow() = + testScope.runTest { + val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope) + val inputChipFlow = + MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden()) + val latest by collectLastValue(underTest.createChipFlow(inputChipFlow)) + + val newChip = + OngoingActivityChipModel.Shown.Timer( + icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null), + colors = ColorsModel.Themed, + startTimeMs = 100L, + onClickListener = null, + ) + + inputChipFlow.value = newChip + + assertThat(latest).isEqualTo(newChip) + + val newerChip = + OngoingActivityChipModel.Shown.IconOnly( + icon = Icon.Resource(R.drawable.ic_hotspot, contentDescription = null), + colors = ColorsModel.Themed, + onClickListener = null, + ) + + inputChipFlow.value = newerChip + + assertThat(latest).isEqualTo(newerChip) + } + + @Test + fun activityStopped_chipHiddenWithoutAnimationFor500ms() = + testScope.runTest { + val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope) + val inputChipFlow = + MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden()) + val latest by collectLastValue(underTest.createChipFlow(inputChipFlow)) + + val shownChip = + OngoingActivityChipModel.Shown.Timer( + icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null), + colors = ColorsModel.Themed, + startTimeMs = 100L, + onClickListener = null, + ) + + inputChipFlow.value = shownChip + + assertThat(latest).isEqualTo(shownChip) + + // WHEN #onActivityStopped is invoked + underTest.onActivityStoppedFromDialog() + runCurrent() + + // THEN the chip is hidden and has no animation + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + + // WHEN only 250ms have elapsed + advanceTimeBy(250) + + // THEN the chip is still hidden + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + + // WHEN over 500ms have elapsed + advanceTimeBy(251) + + // THEN the chip returns to the original input flow value + assertThat(latest).isEqualTo(shownChip) + } + + @Test + fun activityStopped_stoppedAgainBefore500ms_chipReshownAfterSecond500ms() = + testScope.runTest { + val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope) + val inputChipFlow = + MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden()) + val latest by collectLastValue(underTest.createChipFlow(inputChipFlow)) + + val shownChip = + OngoingActivityChipModel.Shown.Timer( + icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null), + colors = ColorsModel.Themed, + startTimeMs = 100L, + onClickListener = null, + ) + + inputChipFlow.value = shownChip + + assertThat(latest).isEqualTo(shownChip) + + // WHEN #onActivityStopped is invoked + underTest.onActivityStoppedFromDialog() + runCurrent() + + // THEN the chip is hidden and has no animation + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + + // WHEN 250ms have elapsed, get another stop event + advanceTimeBy(250) + underTest.onActivityStoppedFromDialog() + runCurrent() + + // THEN the chip is still hidden for another 500ms afterwards + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + advanceTimeBy(499) + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + advanceTimeBy(2) + assertThat(latest).isEqualTo(shownChip) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt index ca043f163854..6e4d8863fee2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt @@ -18,32 +18,58 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener import com.android.systemui.statusbar.phone.SystemUIDialog import kotlin.test.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest class OngoingActivityChipViewModelTest : SysuiTestCase() { private val mockSystemUIDialog = mock<SystemUIDialog>() private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog } + private val dialogTransitionAnimator = mock<DialogTransitionAnimator>() + + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } @Test fun createDialogLaunchOnClickListener_showsDialogOnClick() { + val cuj = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Test") val clickListener = createDialogLaunchOnClickListener( dialogDelegate, + dialogTransitionAnimator, + cuj, logcatLogBuffer("OngoingActivityChipViewModelTest"), "tag", ) - // Dialogs must be created on the main thread - context.mainExecutor.execute { - clickListener.onClick(mock<View>()) - verify(mockSystemUIDialog).show() - } + clickListener.onClick(chipView) + verify(dialogTransitionAnimator) + .showFromView( + eq(mockSystemUIDialog), + eq(chipBackgroundView), + eq(cuj), + anyBoolean(), + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index b1a8d0beab34..ee249f0f8a2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -16,6 +16,10 @@ package com.android.systemui.statusbar.chips.ui.viewmodel +import android.content.DialogInterface +import android.content.packageManager +import android.content.pm.PackageManager +import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -33,10 +37,14 @@ import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runCurrent @@ -44,9 +52,17 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) class OngoingActivityChipsViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope @@ -56,6 +72,18 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState private val callRepo = kosmos.ongoingCallRepository + private val mockSystemUIDialog = mock<SystemUIDialog>() + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } + private val underTest = kosmos.ongoingActivityChipsViewModel @Before @@ -72,7 +100,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chip) - assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden) + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) } @Test @@ -230,7 +258,81 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { job2.cancel() } + @Test + fun chip_screenRecordStoppedViaDialog_chipHiddenWithoutAnimation() = + testScope.runTest { + screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionState.value = MediaProjectionState.NotProjecting + callRepo.setOngoingCallState(OngoingCallModel.NoCall) + + val latest by collectLastValue(underTest.chip) + + assertIsScreenRecordChip(latest) + + // WHEN screen record gets stopped via dialog + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden with no animation + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + } + + @Test + fun chip_projectionStoppedViaDialog_chipHiddenWithoutAnimation() = + testScope.runTest { + mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + screenRecordState.value = ScreenRecordModel.DoingNothing + callRepo.setOngoingCallState(OngoingCallModel.NoCall) + + val latest by collectLastValue(underTest.chip) + + assertIsShareToAppChip(latest) + + // WHEN media projection gets stopped via dialog + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden with no animation + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + } + companion object { + /** + * Assuming that the click listener in [latest] opens a dialog, this fetches the action + * associated with the positive button, which we assume is the "Stop sharing" action. + */ + fun getStopActionFromDialog( + latest: OngoingActivityChipModel?, + chipView: View, + dialog: SystemUIDialog, + kosmos: Kosmos + ): DialogInterface.OnClickListener { + // Capture the action that would get invoked when the user clicks "Stop" on the dialog + lateinit var dialogStopAction: DialogInterface.OnClickListener + Mockito.doAnswer { + val delegate = it.arguments[0] as SystemUIDialog.Delegate + delegate.beforeCreate(dialog, /* savedInstanceState= */ null) + + val stopActionCaptor = argumentCaptor<DialogInterface.OnClickListener>() + verify(dialog).setPositiveButton(any(), stopActionCaptor.capture()) + dialogStopAction = stopActionCaptor.firstValue + + return@doAnswer dialog + } + .whenever(kosmos.mockSystemUIDialogFactory) + .create(any<SystemUIDialog.Delegate>()) + whenever(kosmos.packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + // Click the chip so that we open the dialog and we fill in [dialogStopAction] + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + return dialogStopAction + } + fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) val icon = (latest as OngoingActivityChipModel.Shown).icon diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index a925ccfe174b..c9710370c2c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -1197,6 +1197,26 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) + public void testGenerateHeadsUpAnimation_isSeenInShade_noAnimation() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + // Entry was seen in shade + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isSeenInShade()).thenReturn(true); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + when(row.getEntry()).thenReturn(entry); + + // WHEN we generate an add event + mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + + // THEN nothing happens + assertThat(mStackScroller.isAddOrRemoveAnimationPending()).isFalse(); + } + + @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) public void testOnChildAnimationsFinished_resetsheadsUpAnimatingAway() { // GIVEN NSSL is ready for HUN animations Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 49e3f04cb44e..31f93b402a75 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -1059,6 +1059,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test @DisableSceneContainer + @DisableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART) public void testShowBouncerOrKeyguard_needsFullScreen() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 01540e7584a3..58ad83546e01 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -536,7 +536,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // WHEN there's *no* ongoing activity via new callback mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ false); + /* hasOngoingActivity= */ false, /* shouldAnimate= */ false); // THEN the old callback value is used, so the view is shown assertEquals(View.VISIBLE, @@ -548,7 +548,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // WHEN there *is* an ongoing activity via new callback mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); // THEN the old callback value is used, so the view is hidden assertEquals(View.GONE, @@ -565,7 +565,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // listener, but I'm unable to get the fragment to get attached so that the binder starts // listening to flows. mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ false); + /* hasOngoingActivity= */ false, /* shouldAnimate= */ false); assertEquals(View.GONE, mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); @@ -577,7 +577,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { resumeAndGetFragment(); mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); @@ -590,7 +590,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false); @@ -605,7 +605,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true); fragment.disable(DEFAULT_DISPLAY, 0, 0, false); @@ -621,14 +621,14 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // Ongoing activity started mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); // Ongoing activity ended mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ false); + /* hasOngoingActivity= */ false, /* shouldAnimate= */ false); assertEquals(View.GONE, mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); @@ -643,7 +643,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // Ongoing call started mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); // Notification area is hidden without delay assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01); @@ -661,7 +661,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // WHEN there's *no* ongoing activity via new callback mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ false); + /* hasOngoingActivity= */ false, /* shouldAnimate= */ false); // THEN the new callback value is used, so the view is hidden assertEquals(View.GONE, @@ -673,7 +673,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // WHEN there *is* an ongoing activity via new callback mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); // THEN the new callback value is used, so the view is shown assertEquals(View.VISIBLE, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt index 94159bcebf47..60750cf96e67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt @@ -425,7 +425,7 @@ class CollapsedStatusBarViewModelImplTest : SysuiTestCase() { kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing - assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden) + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) kosmos.fakeMediaProjectionRepository.mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt index d3f11253fc09..cefdf7e43fae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt @@ -29,7 +29,7 @@ class FakeCollapsedStatusBarViewModel : CollapsedStatusBarViewModel { override val transitionFromLockscreenToDreamStartedEvent = MutableSharedFlow<Unit>() override val ongoingActivityChip: MutableStateFlow<OngoingActivityChipModel> = - MutableStateFlow(OngoingActivityChipModel.Hidden) + MutableStateFlow(OngoingActivityChipModel.Hidden()) override val isHomeStatusBarAllowedByScene = MutableStateFlow(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index f7371487a7c5..3f5dc82ec5cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -18,6 +18,8 @@ package com.android.systemui.volume; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -37,16 +39,19 @@ import android.media.IAudioService; import android.media.session.MediaSession; import android.os.Handler; import android.os.Process; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import android.view.accessibility.AccessibilityManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.settingslib.flags.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.RingerModeLiveData; @@ -54,7 +59,9 @@ import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.concurrency.FakeThreadFactory; import com.android.systemui.util.concurrency.ThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor; import org.junit.Before; import org.junit.Test; @@ -63,6 +70,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Objects; import java.util.concurrent.Executor; @RunWith(AndroidJUnit4.class) @@ -104,6 +112,10 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { private UserTracker mUserTracker; @Mock private DumpManager mDumpManager; + @Mock + private AudioSharingInteractor mAudioSharingInteractor; + @Mock + private JavaAdapter mJavaAdapter; @Before @@ -124,11 +136,26 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { mCallback = mock(VolumeDialogControllerImpl.C.class); mThreadFactory.setLooper(TestableLooper.get(this).getLooper()); - mVolumeController = new TestableVolumeDialogControllerImpl(mContext, - mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager, - mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager, - mPackageManager, mWakefullnessLifcycle, mKeyguardManager, - mActivityManager, mUserTracker, mDumpManager, mCallback); + mVolumeController = + new TestableVolumeDialogControllerImpl( + mContext, + mBroadcastDispatcher, + mRingerModeTracker, + mThreadFactory, + mAudioManager, + mNotificationManager, + mVibrator, + mIAudioService, + mAccessibilityManager, + mPackageManager, + mWakefullnessLifcycle, + mKeyguardManager, + mActivityManager, + mUserTracker, + mDumpManager, + mCallback, + mAudioSharingInteractor, + mJavaAdapter); mVolumeController.setEnableDialogs(true, true); } @@ -224,6 +251,41 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { verify(mUserTracker).addCallback(any(UserTracker.Callback.class), any(Executor.class)); } + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void handleAudioSharingStreamVolumeChanges_updateState() { + ArgumentCaptor<VolumeDialogController.State> stateCaptor = + ArgumentCaptor.forClass(VolumeDialogController.State.class); + int broadcastStream = VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST; + + mVolumeController.handleAudioSharingStreamVolumeChanges(100); + + verify(mCallback).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue(); + assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(100); + + mVolumeController.handleAudioSharingStreamVolumeChanges(200); + + verify(mCallback, times(2)).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue(); + assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(200); + + mVolumeController.handleAudioSharingStreamVolumeChanges(null); + + verify(mCallback, times(3)).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void testSetStreamVolume_setSecondaryDeviceVolume() { + mVolumeController.setStreamVolume( + VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST, /* level= */ 100); + Objects.requireNonNull(TestableLooper.get(this)).processAllMessages(); + + verify(mAudioSharingInteractor).setStreamVolume(100); + } + static class TestableVolumeDialogControllerImpl extends VolumeDialogControllerImpl { private final WakefulnessLifecycle.Observer mWakefullessLifecycleObserver; @@ -243,11 +305,27 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { ActivityManager activityManager, UserTracker userTracker, DumpManager dumpManager, - C callback) { - super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager, - notificationManager, optionalVibrator, iAudioService, accessibilityManager, - packageManager, wakefulnessLifecycle, keyguardManager, - activityManager, userTracker, dumpManager); + C callback, + AudioSharingInteractor audioSharingInteractor, + JavaAdapter javaAdapter) { + super( + context, + broadcastDispatcher, + ringerModeTracker, + theadFactory, + audioManager, + notificationManager, + optionalVibrator, + iAudioService, + accessibilityManager, + packageManager, + wakefulnessLifecycle, + keyguardManager, + activityManager, + userTracker, + dumpManager, + audioSharingInteractor, + javaAdapter); mCallbacks = callback; ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index cdfcca6c7065..b5cbf598bd05 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -23,6 +23,7 @@ import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER; import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN; import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN; +import static com.android.systemui.volume.VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST; import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS; import static junit.framework.Assert.assertEquals; @@ -72,6 +73,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.settingslib.flags.Flags; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; @@ -794,6 +796,38 @@ public class VolumeDialogImplTest extends SysuiTestCase { verify(mVolumeDialogInteractor, atLeastOnce()).onDialogDismissed(); // dismiss by timeout } + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void testDynamicStreamForBroadcast_createRow() { + State state = createShellState(); + VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState(); + ss.dynamic = true; + ss.levelMin = 0; + ss.levelMax = 255; + ss.level = 20; + ss.name = -1; + ss.remoteLabel = mContext.getString(R.string.audio_sharing_description); + state.states.append(DYNAMIC_STREAM_BROADCAST, ss); + + mDialog.onStateChangedH(state); + mTestableLooper.processAllMessages(); + + ViewGroup volumeDialogRows = mDialog.getDialogView().findViewById(R.id.volume_dialog_rows); + assumeNotNull(volumeDialogRows); + View broadcastRow = null; + final int rowCount = volumeDialogRows.getChildCount(); + // we don't make assumptions about the position of the dnd row + for (int i = 0; i < rowCount; i++) { + View volumeRow = volumeDialogRows.getChildAt(i); + if (volumeRow.getId() == DYNAMIC_STREAM_BROADCAST) { + broadcastRow = volumeRow; + break; + } + } + assertNotNull(broadcastRow); + assertEquals(broadcastRow.getVisibility(), View.VISIBLE); + } + /** * @return true if at least one volume row has the DND icon */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index ac42319c7b25..60b5b5d39b9b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -86,6 +86,7 @@ import android.service.dreams.IDreamManager; import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; import android.testing.TestableLooper; +import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.Display; @@ -183,6 +184,7 @@ import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -192,7 +194,6 @@ import com.android.wm.shell.transition.Transitions; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -216,6 +217,9 @@ import platform.test.runner.parameterized.Parameters; @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class BubblesTest extends SysuiTestCase { + + private static final String TAG = "BubblesTest"; + @Mock private CommonNotifCollection mCommonNotifCollection; @Mock @@ -241,8 +245,6 @@ public class BubblesTest extends SysuiTestCase { @Mock private KeyguardBypassController mKeyguardBypassController; @Mock - private FloatingContentCoordinator mFloatingContentCoordinator; - @Mock private BubbleDataRepository mDataRepository; @Mock private NotificationShadeWindowView mNotificationShadeWindowView; @@ -372,6 +374,7 @@ public class BubblesTest extends SysuiTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + PhysicsAnimatorTestUtils.prepareForTest(); if (Transitions.ENABLE_SHELL_TRANSITIONS) { doReturn(true).when(mTransitions).isRegistered(); @@ -494,7 +497,7 @@ public class BubblesTest extends SysuiTestCase { mShellCommandHandler, mShellController, mBubbleData, - mFloatingContentCoordinator, + new FloatingContentCoordinator(), mDataRepository, mStatusBarService, mWindowManager, @@ -571,12 +574,32 @@ public class BubblesTest extends SysuiTestCase { } @After - public void tearDown() { + public void tearDown() throws Exception { ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles()); for (int i = 0; i < bubbles.size(); i++) { mBubbleController.removeBubble(bubbles.get(i).getKey(), Bubbles.DISMISS_NO_LONGER_BUBBLE); } + mTestableLooper.processAllMessages(); + + // check that no animations are running before finishing the test to make sure that the + // state gets cleaned up correctly between tests. + int retryCount = 0; + while (PhysicsAnimatorTestUtils.isAnyAnimationRunning() && retryCount <= 10) { + Log.d( + TAG, + String.format("waiting for animations to complete. attempt %d", retryCount)); + // post a message to the looper and wait for it to be processed + mTestableLooper.runWithLooper(() -> {}); + retryCount++; + } + mTestableLooper.processAllMessages(); + if (PhysicsAnimatorTestUtils.isAnyAnimationRunning()) { + Log.d(TAG, "finished waiting for animations to complete but animations are still " + + "running"); + } else { + Log.d(TAG, "no animations are running"); + } } @Test @@ -1853,7 +1876,6 @@ public class BubblesTest extends SysuiTestCase { any(Bubble.class), anyBoolean(), anyBoolean()); } - @Ignore("reason = b/351977103") @Test public void testShowStackEdu_isNotConversationBubble() { // Setup diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt index 1da1fb2ee52f..5e870b19681b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt @@ -25,20 +25,11 @@ import kotlinx.coroutines.flow.map /** Fake implementation of [CommunalPrefsRepository] */ class FakeCommunalPrefsRepository : CommunalPrefsRepository { private val _isCtaDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) - private val _isDisclaimerDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) override fun isCtaDismissed(user: UserInfo): Flow<Boolean> = _isCtaDismissed.map { it.contains(user) } - override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> = - _isDisclaimerDismissed.map { it.contains(user) } - override suspend fun setCtaDismissed(user: UserInfo) { _isCtaDismissed.value = _isCtaDismissed.value.toMutableSet().apply { add(user) } } - - override suspend fun setDisclaimerDismissed(user: UserInfo) { - _isDisclaimerDismissed.value = - _isDisclaimerDismissed.value.toMutableSet().apply { add(user) } - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index eb9278537db5..4ad046cc095e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -42,6 +43,7 @@ val Kosmos.communalInteractor by Fixture { CommunalInteractor( applicationScope = applicationCoroutineScope, bgDispatcher = testDispatcher, + bgScope = testScope.backgroundScope, broadcastDispatcher = broadcastDispatcher, communalSceneInteractor = communalSceneInteractor, widgetRepository = communalWidgetRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt index 045bd5d286df..2dcd275f0103 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt @@ -21,21 +21,32 @@ import dagger.Module import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow /** Fake implementation of [DeviceEntryRepository] */ @SysUISingleton class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository { - private var isLockscreenEnabled = true + + private val _isLockscreenEnabled = MutableStateFlow(true) + override val isLockscreenEnabled: StateFlow<Boolean> = _isLockscreenEnabled.asStateFlow() private val _isBypassEnabled = MutableStateFlow(false) override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled + private var pendingLockscreenEnabled = _isLockscreenEnabled.value + override suspend fun isLockscreenEnabled(): Boolean { - return isLockscreenEnabled + _isLockscreenEnabled.value = pendingLockscreenEnabled + return isLockscreenEnabled.value } fun setLockscreenEnabled(isLockscreenEnabled: Boolean) { - this.isLockscreenEnabled = isLockscreenEnabled + _isLockscreenEnabled.value = isLockscreenEnabled + pendingLockscreenEnabled = _isLockscreenEnabled.value + } + + fun setPendingLockscreenEnabled(isLockscreenEnabled: Boolean) { + pendingLockscreenEnabled = isLockscreenEnabled } fun setBypassEnabled(isBypassEnabled: Boolean) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt index 126d85890531..4634a7fd009f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.service.dream.dreamManager import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor -import com.android.systemui.deviceentry.data.repository.deviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -41,7 +41,7 @@ var Kosmos.fromDozingTransitionInteractor by communalSceneInteractor = communalSceneInteractor, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, - deviceEntryRepository = deviceEntryRepository, + deviceEntryInteractor = deviceEntryInteractor, wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, dreamManager = dreamManager ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt index 3d85a4abbd68..c7dfd5cc93b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.applicationContext +import com.android.systemui.animation.dialogTransitionAnimator +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor @@ -34,6 +36,7 @@ val Kosmos.castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel by mediaRouterChipInteractor = mediaRouterChipInteractor, systemClock = fakeSystemClock, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, + dialogTransitionAnimator = mockDialogTransitionAnimator, logger = statusBarChipsLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt index 1ed7a4702e2c..651a0f7639d8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view import android.content.packageManager +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory @@ -24,6 +25,7 @@ val Kosmos.endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper by Kosmos.Fixture { EndMediaProjectionDialogHelper( dialogFactory = mockSystemUIDialogFactory, + dialogTransitionAnimator = mockDialogTransitionAnimator, packageManager = packageManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt index e4bb1665a432..c2a6f7d91eb0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.content.applicationContext +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.screenRecordChipInteractor +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.statusBarChipsLogger import com.android.systemui.util.time.fakeSystemClock @@ -30,7 +32,9 @@ val Kosmos.screenRecordChipViewModel: ScreenRecordChipViewModel by scope = applicationCoroutineScope, context = applicationContext, interactor = screenRecordChipInteractor, + shareToAppChipViewModel = shareToAppChipViewModel, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, + dialogTransitionAnimator = mockDialogTransitionAnimator, systemClock = fakeSystemClock, logger = statusBarChipsLogger, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt index 8ed7f9684d86..0770009f9998 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.applicationContext +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor @@ -32,6 +33,7 @@ val Kosmos.shareToAppChipViewModel: ShareToAppChipViewModel by mediaProjectionChipInteractor = mediaProjectionChipInteractor, systemClock = fakeSystemClock, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, + dialogTransitionAnimator = mockDialogTransitionAnimator, logger = statusBarChipsLogger, ) } diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java new file mode 100644 index 000000000000..db95fad2a3ad --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail01_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail01_Test() { + mThrown.expectMessage("Method setUp() doesn't have @Before"); + } + + @SuppressWarnings("JUnit4SetUpNotRun") + public void setUp() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java new file mode 100644 index 000000000000..ddc66c73a7c0 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail02_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail02_Test() { + mThrown.expectMessage("Method tearDown() doesn't have @After"); + } + + @SuppressWarnings("JUnit4TearDownNotRun") + public void tearDown() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java new file mode 100644 index 000000000000..ec8e907dcdb3 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail03_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail03_Test() { + mThrown.expectMessage("Method testFoo() doesn't have @Test"); + } + + @SuppressWarnings("JUnit4TestNotRun") + public void testFoo() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java new file mode 100644 index 000000000000..d952d07b3817 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_OkTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Before + public void setUp() { + } + + @Before + public void testSetUp() { + } + + @After + public void tearDown() { + } + + @After + public void testTearDown() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 49e793fcbddf..4357f2b8660a 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -33,14 +33,17 @@ import com.android.internal.os.RuntimeInit; import com.android.server.LocalServices; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.Description; import org.junit.runner.RunWith; import org.junit.runners.model.Statement; import java.io.PrintStream; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -230,6 +233,18 @@ public class RavenwoodRuleImpl { } } + /** + * @return if a method has any of annotations. + */ + private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) { + for (var anno : annotations) { + if (m.getAnnotation(anno) != null) { + return true; + } + } + return false; + } + private static void validateTestAnnotations(Statement base, Description description, boolean enableOptionalValidation) { final var testClass = description.getTestClass(); @@ -239,13 +254,14 @@ public class RavenwoodRuleImpl { boolean hasErrors = false; for (Method m : collectMethods(testClass)) { if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) { - if (m.getAnnotation(Test.class) == null) { + if (!hasAnyAnnotations(m, Test.class, Before.class, After.class, + BeforeClass.class, AfterClass.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @Test"); hasErrors = true; } } if ("setUp".equals(m.getName())) { - if (m.getAnnotation(Before.class) == null) { + if (!hasAnyAnnotations(m, Before.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @Before"); hasErrors = true; } @@ -255,7 +271,7 @@ public class RavenwoodRuleImpl { } } if ("tearDown".equals(m.getName())) { - if (m.getAnnotation(After.class) == null) { + if (!hasAnyAnnotations(m, After.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @After"); hasErrors = true; } diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index b4efae3a05e4..8e2e0ad76d15 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -114,6 +114,13 @@ flag { } flag { + name: "enable_magnification_follows_mouse" + namespace: "accessibility" + description: "Whether to enable mouse following for fullscreen magnification" + bug: "335494097" +} + +flag { name: "fix_drag_pointer_when_ending_drag" namespace: "accessibility" description: "Send the correct pointer id when transitioning from dragging to delegating states." diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index d7da2f0052d3..a5ec2ba2f267 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -804,7 +804,15 @@ public final class PresentationStatsEventLogger { + event.mSuggestionPresentedLastTimestampMs + " event.mFocusedVirtualAutofillId=" + event.mFocusedVirtualAutofillId + " event.mFieldFirstLength=" + event.mFieldFirstLength - + " event.mFieldLastLength=" + event.mFieldLastLength); + + " event.mFieldLastLength=" + event.mFieldLastLength + + " event.mViewFailedPriorToRefillCount=" + event.mViewFailedPriorToRefillCount + + " event.mViewFilledSuccessfullyOnRefillCount=" + + event.mViewFilledSuccessfullyOnRefillCount + + " event.mViewFailedOnRefillCount=" + event.mViewFailedOnRefillCount + + " event.notExpiringResponseDuringAuthCount=" + + event.mFixExpireResponseDuringAuthCount + + " event.notifyViewEnteredIgnoredDuringAuthCount=" + + event.mNotifyViewEnteredIgnoredDuringAuthCount); } // TODO(b/234185326): Distinguish empty responses from other no presentation reasons. @@ -859,7 +867,12 @@ public final class PresentationStatsEventLogger { event.mSuggestionPresentedLastTimestampMs, event.mFocusedVirtualAutofillId, event.mFieldFirstLength, - event.mFieldLastLength); + event.mFieldLastLength, + event.mViewFailedPriorToRefillCount, + event.mViewFilledSuccessfullyOnRefillCount, + event.mViewFailedOnRefillCount, + event.mFixExpireResponseDuringAuthCount, + event.mNotifyViewEnteredIgnoredDuringAuthCount); mEventInternal = Optional.empty(); } @@ -912,6 +925,12 @@ public final class PresentationStatsEventLogger { // uninitialized doesn't help much, as this would be non-zero only if callback is received. int mViewFillSuccessCount = 0; int mViewFilledButUnexpectedCount = 0; + int mViewFailedPriorToRefillCount = 0; + int mViewFailedOnRefillCount = 0; + int mViewFilledSuccessfullyOnRefillCount = 0; + + int mFixExpireResponseDuringAuthCount = 0; + int mNotifyViewEnteredIgnoredDuringAuthCount = 0; ArraySet<AutofillId> mAutofillIdsAttemptedAutofill; ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>(); diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 7d9d660af536..ee7d0aef2189 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -28,9 +28,9 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; -import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery; import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix; +import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import android.annotation.EnforcePermission; import android.annotation.NonNull; @@ -561,8 +561,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private void sendPendingIntent(int displayId, PendingIntent pendingIntent) throws PendingIntent.CanceledException { final ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId); - options.setPendingIntentBackgroundActivityLaunchAllowed(true); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); pendingIntent.send( mContext, /* code= */ 0, diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index b058bd8eb3d6..504c54aefc44 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -1708,6 +1708,11 @@ public class OomAdjuster { // priority for this non-top split. schedGroup = SCHED_GROUP_TOP_APP; mAdjType = "resumed-split-screen-activity"; + } else if ((flags + & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) { + // The recently used non-top visible freeform app. + schedGroup = SCHED_GROUP_TOP_APP; + mAdjType = "perceptible-freeform-activity"; } foregroundActivities = true; mHasVisibleActivities = true; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index fab0a56af2a8..ed22b4ce7827 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -739,6 +739,8 @@ public class AudioService extends IAudioService.Stub // Broadcast receiver for device connections intent broadcasts private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver(); + private final Executor mAudioServerLifecycleExecutor; + private IMediaProjectionManager mProjectionService; // to validate projection token /** Interface for UserManagerService. */ @@ -1059,7 +1061,8 @@ public class AudioService extends IAudioService.Stub audioserverPermissions() ? initializeAudioServerPermissionProvider( context, audioPolicyFacade, audioserverLifecycleExecutor) : - null + null, + audioserverLifecycleExecutor ); } @@ -1145,13 +1148,16 @@ public class AudioService extends IAudioService.Stub * {@link AudioSystemThread} is created as the messaging thread instead. * @param appOps {@link AppOpsManager} system service * @param enforcer Used for permission enforcing + * @param permissionProvider Used to push permissions to audioserver + * @param audioserverLifecycleExecutor Used for tasks managing audioserver lifecycle */ @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public AudioService(Context context, AudioSystemAdapter audioSystem, SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer, - /* @NonNull */ AudioServerPermissionProvider permissionProvider) { + /* @NonNull */ AudioServerPermissionProvider permissionProvider, + Executor audioserverLifecycleExecutor) { super(enforcer); sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()")); mContext = context; @@ -1159,6 +1165,7 @@ public class AudioService extends IAudioService.Stub mAppOps = appOps; mPermissionProvider = permissionProvider; + mAudioServerLifecycleExecutor = audioserverLifecycleExecutor; mAudioSystem = audioSystem; mSystemServer = systemServer; @@ -1170,6 +1177,34 @@ public class AudioService extends IAudioService.Stub mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast"); mBroadcastHandlerThread.start(); + // Listen to permission invalidations for the PermissionProvider + if (audioserverPermissions()) { + final Handler broadcastHandler = mBroadcastHandlerThread.getThreadHandler(); + mAudioSystem.listenForSystemPropertyChange(PermissionManager.CACHE_KEY_PACKAGE_INFO, + new Runnable() { + // Roughly chosen to be long enough to suppress the autocork behavior + // of the permission cache (50ms), and longer than the task could reasonably + // take, even with many packages and users, while not introducing visible + // permission leaks - since the app needs to restart, and trigger an action + // which requires permissions from audioserver before this delay. + // For RECORD_AUDIO, we are additionally protected by appops. + final long UPDATE_DELAY_MS = 110; + final AtomicLong scheduledUpdateTimestamp = new AtomicLong(0); + @Override + public void run() { + var currentTime = SystemClock.uptimeMillis(); + if (currentTime > scheduledUpdateTimestamp.get()) { + scheduledUpdateTimestamp.set(currentTime + UPDATE_DELAY_MS); + broadcastHandler.postAtTime( () -> + mAudioServerLifecycleExecutor.execute(mPermissionProvider + ::onPermissionStateChanged), + currentTime + UPDATE_DELAY_MS + ); + } + } + }); + } + mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem); mIsSingleVolume = AudioSystem.isSingleVolume(context); @@ -11974,29 +12009,6 @@ public class AudioService extends IAudioService.Stub provider.onServiceStart(audioPolicy.getPermissionController()); }); - // Set up event listeners - // Must be kept in sync with PermissionManager - Runnable cacheSysPropHandler = new Runnable() { - private AtomicReference<SystemProperties.Handle> mHandle = new AtomicReference(); - private AtomicLong mNonce = new AtomicLong(); - @Override - public void run() { - if (mHandle.get() == null) { - // Cache the handle - mHandle.compareAndSet(null, SystemProperties.find( - PermissionManager.CACHE_KEY_PACKAGE_INFO)); - } - long nonce; - SystemProperties.Handle ref; - if ((ref = mHandle.get()) != null && (nonce = ref.getLong(0)) != 0 && - mNonce.getAndSet(nonce) != nonce) { - audioserverExecutor.execute(() -> provider.onPermissionStateChanged()); - } - } - }; - - SystemProperties.addChangeCallback(cacheSysPropHandler); - IntentFilter packageUpdateFilter = new IntentFilter(); packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED); packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED); diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index 7f4bc74bd59e..d083c68c4c2c 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -748,6 +748,10 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, return AudioSystem.setMasterMute(mute); } + public void listenForSystemPropertyChange(String systemPropertyName, Runnable callback) { + AudioSystem.listenForSystemPropertyChange(systemPropertyName, callback); + } + /** * Part of AudioService dump * @param pw diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 5c1e783c0f52..6992580e4df8 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -129,9 +129,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked"; private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked"; - private static final String TAG = "DisplayPowerController2"; + private static final String TAG = "DisplayPowerController"; // To enable these logs, run: - // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot' + // 'adb shell setprop persist.log.tag.DisplayPowerController DEBUG && adb reboot' private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); private static final String SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME = "Screen on blocked by displayoffload"; @@ -263,6 +263,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // The unique ID of the primary display device currently tied to this logical display private String mUniqueDisplayId; + private String mPhysicalDisplayName; // Tracker for brightness changes. @Nullable @@ -371,10 +372,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // If the last recorded screen state was dozing or not. private boolean mDozing; - private boolean mAppliedDimming; - - private boolean mAppliedThrottling; - // Reason for which the brightness was last changed. See {@link BrightnessReason} for more // information. // At the time of this writing, this value is changed within updatePowerState() only, which is @@ -483,7 +480,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // DPCs following the brightness of this DPC. This is used in concurrent displays mode - there // is one lead display, the additional displays follow the brightness value of the lead display. @GuardedBy("mLock") - private SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers = + private final SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers = new SparseArray(); private boolean mBootCompleted; @@ -525,8 +522,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mThermalBrightnessThrottlingDataId = logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId; mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); - mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(); + mUniqueDisplayId = mDisplayDevice.getUniqueId(); mDisplayStatsId = mUniqueDisplayId.hashCode(); + mPhysicalDisplayName = mDisplayDevice.getNameLocked(); mLastBrightnessEvent = new BrightnessEvent(mDisplayId); mTempBrightnessEvent = new BrightnessEvent(mDisplayId); @@ -544,8 +542,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessTracker = brightnessTracker; mOnBrightnessChangeRunnable = onBrightnessChangeRunnable; - PowerManager pm = context.getSystemService(PowerManager.class); - final Resources resources = context.getResources(); // DOZE AND DIM SETTINGS @@ -840,6 +836,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } final String uniqueId = device.getUniqueId(); + final String displayName = device.getNameLocked(); final DisplayDeviceConfig config = device.getDisplayDeviceConfig(); final IBinder token = device.getDisplayTokenLocked(); final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); @@ -866,6 +863,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call changed = true; mDisplayDevice = device; mUniqueDisplayId = uniqueId; + mPhysicalDisplayName = displayName; mDisplayStatsId = mUniqueDisplayId.hashCode(); mDisplayDeviceConfig = config; mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId; @@ -1552,10 +1550,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // unthrottled (unclamped/ideal) and throttled brightness levels for subsequent operations. // Note throttling effectively changes the allowed brightness range, so, similarly to HBM, // we broadcast this change through setting. - final float unthrottledBrightnessState = brightnessState; + final float unthrottledBrightnessState = rawBrightnessState; DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest, brightnessState, slowChange, /* displayState= */ state); - brightnessState = clampedState.getBrightness(); slowChange = clampedState.isSlowChange(); // faster rate wins, at this point customAnimationRate == -1, strategy does not control @@ -1744,11 +1741,23 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // brightness cap, RBC state, etc. mTempBrightnessEvent.setTime(System.currentTimeMillis()); mTempBrightnessEvent.setBrightness(brightnessState); + mTempBrightnessEvent.setNits( + mDisplayBrightnessController.convertToAdjustedNits(brightnessState)); + final float hbmMax = mBrightnessRangeController.getCurrentBrightnessMax(); + final float clampedMax = Math.min(clampedState.getMaxBrightness(), hbmMax); + final float brightnessOnAvailableScale = MathUtils.constrainedMap(0.0f, 1.0f, + clampedState.getMinBrightness(), clampedMax, + brightnessState); + mTempBrightnessEvent.setPercent(Math.round( + 1000.0f * com.android.internal.display.BrightnessUtils.convertLinearToGamma( + brightnessOnAvailableScale) / 10)); // rounded to one dp + mTempBrightnessEvent.setUnclampedBrightness(unthrottledBrightnessState); mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId); + mTempBrightnessEvent.setPhysicalDisplayName(mPhysicalDisplayName); mTempBrightnessEvent.setDisplayState(state); mTempBrightnessEvent.setDisplayPolicy(mPowerRequest.policy); mTempBrightnessEvent.setReason(mBrightnessReason); - mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax()); + mTempBrightnessEvent.setHbmMax(hbmMax); mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode()); mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags() | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0) @@ -2648,8 +2657,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call pw.println("Display Power Controller Thread State:"); pw.println(" mPowerRequest=" + mPowerRequest); pw.println(" mBrightnessReason=" + mBrightnessReason); - pw.println(" mAppliedDimming=" + mAppliedDimming); - pw.println(" mAppliedThrottling=" + mAppliedThrottling); pw.println(" mDozing=" + mDozing); pw.println(" mSkipRampState=" + skipRampStateToString(mSkipRampState)); pw.println(" mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime); diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java index 82b401a7cc83..5cc603c5018c 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java @@ -20,6 +20,8 @@ import static android.hardware.display.DisplayManagerInternal.DisplayPowerReques import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.policyToString; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT; +import static com.android.server.display.BrightnessMappingStrategy.INVALID_LUX; +import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS; import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessModeToString; import android.hardware.display.BrightnessInfo; @@ -48,13 +50,17 @@ public final class BrightnessEvent { private BrightnessReason mReason = new BrightnessReason(); private int mDisplayId; private String mPhysicalDisplayId; + private String mPhysicalDisplayName; private int mDisplayState; private int mDisplayPolicy; private long mTime; private float mLux; + private float mNits; + private float mPercent; private float mPreThresholdLux; private float mInitialBrightness; private float mBrightness; + private float mUnclampedBrightness; private float mRecommendedBrightness; private float mPreThresholdBrightness; private int mHbmMode; @@ -88,15 +94,19 @@ public final class BrightnessEvent { mReason.set(that.getReason()); mDisplayId = that.getDisplayId(); mPhysicalDisplayId = that.getPhysicalDisplayId(); + mPhysicalDisplayName = that.getPhysicalDisplayName(); mDisplayState = that.mDisplayState; mDisplayPolicy = that.mDisplayPolicy; mTime = that.getTime(); // Lux values mLux = that.getLux(); mPreThresholdLux = that.getPreThresholdLux(); + mNits = that.getNits(); + mPercent = that.getPercent(); // Brightness values mInitialBrightness = that.getInitialBrightness(); mBrightness = that.getBrightness(); + mUnclampedBrightness = that.getUnclampedBrightness(); mRecommendedBrightness = that.getRecommendedBrightness(); mPreThresholdBrightness = that.getPreThresholdBrightness(); // Different brightness modulations @@ -121,14 +131,18 @@ public final class BrightnessEvent { mReason = new BrightnessReason(); mTime = SystemClock.uptimeMillis(); mPhysicalDisplayId = ""; + mPhysicalDisplayName = ""; mDisplayState = Display.STATE_UNKNOWN; mDisplayPolicy = POLICY_OFF; // Lux values - mLux = 0; + mLux = INVALID_LUX; mPreThresholdLux = 0; + mNits = INVALID_NITS; + mPercent = -1f; // Brightness values mInitialBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mUnclampedBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; mRecommendedBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; mPreThresholdBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; // Different brightness modulations @@ -160,13 +174,18 @@ public final class BrightnessEvent { return mReason.equals(that.mReason) && mDisplayId == that.mDisplayId && mPhysicalDisplayId.equals(that.mPhysicalDisplayId) + && mPhysicalDisplayName.equals(that.mPhysicalDisplayName) && mDisplayState == that.mDisplayState && mDisplayPolicy == that.mDisplayPolicy && Float.floatToRawIntBits(mLux) == Float.floatToRawIntBits(that.mLux) && Float.floatToRawIntBits(mPreThresholdLux) == Float.floatToRawIntBits(that.mPreThresholdLux) + && Float.floatToRawIntBits(mNits) == Float.floatToRawIntBits(that.mNits) + && Float.floatToRawIntBits(mPercent) == Float.floatToRawIntBits(that.mPercent) && Float.floatToRawIntBits(mBrightness) == Float.floatToRawIntBits(that.mBrightness) + && Float.floatToRawIntBits(mUnclampedBrightness) + == Float.floatToRawIntBits(that.mUnclampedBrightness) && Float.floatToRawIntBits(mRecommendedBrightness) == Float.floatToRawIntBits(that.mRecommendedBrightness) && Float.floatToRawIntBits(mPreThresholdBrightness) @@ -195,27 +214,34 @@ public final class BrightnessEvent { public String toString(boolean includeTime) { return (includeTime ? FORMAT.format(new Date(mTime)) + " - " : "") + "BrightnessEvent: " - + "disp=" + mDisplayId - + ", physDisp=" + mPhysicalDisplayId - + ", displayState=" + Display.stateToString(mDisplayState) - + ", displayPolicy=" + policyToString(mDisplayPolicy) - + ", brt=" + mBrightness + ((mFlags & FLAG_USER_SET) != 0 ? "(user_set)" : "") + + "brt=" + mBrightness + ((mFlags & FLAG_USER_SET) != 0 ? "(user_set)" : "") + " (" + + mPercent + "%)" + + ", nits= " + mNits + + ", lux=" + mLux + + ", reason=" + mReason.toString(mAdjustmentFlags) + + ", strat=" + mDisplayBrightnessStrategyName + + ", state=" + Display.stateToString(mDisplayState) + + ", policy=" + policyToString(mDisplayPolicy) + + ", flags=" + flagsToString() + // Autobrightness + ", initBrt=" + mInitialBrightness + ", rcmdBrt=" + mRecommendedBrightness + ", preBrt=" + mPreThresholdBrightness - + ", lux=" + mLux + ", preLux=" + mPreThresholdLux + + ", wasShortTermModelActive=" + mWasShortTermModelActive + + ", autoBrightness=" + mAutomaticBrightnessEnabled + " (" + + autoBrightnessModeToString(mAutoBrightnessMode) + ")" + // Throttling info + + ", unclampedBrt=" + mUnclampedBrightness + ", hbmMax=" + mHbmMax + ", hbmMode=" + BrightnessInfo.hbmToString(mHbmMode) - + ", rbcStrength=" + mRbcStrength + ", thrmMax=" + mThermalMax + // Modifiers + + ", rbcStrength=" + mRbcStrength + ", powerFactor=" + mPowerFactor - + ", wasShortTermModelActive=" + mWasShortTermModelActive - + ", flags=" + flagsToString() - + ", reason=" + mReason.toString(mAdjustmentFlags) - + ", autoBrightness=" + mAutomaticBrightnessEnabled - + ", strategy=" + mDisplayBrightnessStrategyName - + ", autoBrightnessMode=" + autoBrightnessModeToString(mAutoBrightnessMode); + // Meta + + ", physDisp=" + mPhysicalDisplayName + "(" + mPhysicalDisplayId + ")" + + ", logicalId=" + mDisplayId; } @Override @@ -255,6 +281,14 @@ public final class BrightnessEvent { this.mPhysicalDisplayId = mPhysicalDisplayId; } + public String getPhysicalDisplayName() { + return mPhysicalDisplayName; + } + + public void setPhysicalDisplayName(String mPhysicalDisplayName) { + this.mPhysicalDisplayName = mPhysicalDisplayName; + } + public void setDisplayState(int state) { mDisplayState = state; } @@ -295,6 +329,29 @@ public final class BrightnessEvent { this.mBrightness = brightness; } + public float getUnclampedBrightness() { + return mUnclampedBrightness; + } + + public void setUnclampedBrightness(float unclampedBrightness) { + this.mUnclampedBrightness = unclampedBrightness; + } + + public void setPercent(float percent) { + this.mPercent = percent; + } + public float getPercent() { + return mPercent; + } + + public void setNits(float nits) { + this.mNits = nits; + } + + public float getNits() { + return mNits; + } + public float getRecommendedBrightness() { return mRecommendedBrightness; } diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java index 8ca045834981..99f4747227ae 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java @@ -20,9 +20,9 @@ import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.annotation.WorkerThread; -import android.os.Handler; import android.os.Process; import android.util.IntArray; +import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -36,7 +36,10 @@ import java.util.concurrent.locks.ReentrantLock; * persistent storages. */ final class AdditionalSubtypeMapRepository { - @GuardedBy("ImfLock.class") + private static final String TAG = "AdditionalSubtypeMapRepository"; + + // TODO(b/352594784): Should we user other lock primitives? + @GuardedBy("sPerUserMap") @NonNull private static final SparseArray<AdditionalSubtypeMap> sPerUserMap = new SparseArray<>(); @@ -192,29 +195,77 @@ final class AdditionalSubtypeMapRepository { private AdditionalSubtypeMapRepository() { } + /** + * Returns {@link AdditionalSubtypeMap} for the given user. + * + * <p>This method is expected be called after {@link #ensureInitializedAndGet(int)}. Otherwise + * {@link AdditionalSubtypeMap#EMPTY_MAP} will be returned.</p> + * + * @param userId the user to be queried about + * @return {@link AdditionalSubtypeMap} for the given user + */ + @AnyThread @NonNull - @GuardedBy("ImfLock.class") static AdditionalSubtypeMap get(@UserIdInt int userId) { - final AdditionalSubtypeMap map = sPerUserMap.get(userId); - if (map != null) { - return map; + final AdditionalSubtypeMap map; + synchronized (sPerUserMap) { + map = sPerUserMap.get(userId); + } + if (map == null) { + Slog.e(TAG, "get(userId=" + userId + ") is called before loadInitialDataAndGet()." + + " Returning an empty map"); + return AdditionalSubtypeMap.EMPTY_MAP; + } + return map; + } + + /** + * Ensures that {@link AdditionalSubtypeMap} is initialized for the given user. Load it from + * the persistent storage if {@link #putAndSave(int, AdditionalSubtypeMap, InputMethodMap)} has + * not been called yet. + * + * @param userId the user to be initialized + * @return {@link AdditionalSubtypeMap} that is associated with the given user. If + * {@link #putAndSave(int, AdditionalSubtypeMap, InputMethodMap)} is already called + * then the given {@link AdditionalSubtypeMap}. + */ + @AnyThread + @NonNull + static AdditionalSubtypeMap ensureInitializedAndGet(@UserIdInt int userId) { + final var map = AdditionalSubtypeUtils.load(userId); + synchronized (sPerUserMap) { + final AdditionalSubtypeMap previous = sPerUserMap.get(userId); + // If putAndSave() has already been called, then use it. + if (previous != null) { + return previous; + } + sPerUserMap.put(userId, map); } - final AdditionalSubtypeMap newMap = AdditionalSubtypeUtils.load(userId); - sPerUserMap.put(userId, newMap); - return newMap; + return map; } - @GuardedBy("ImfLock.class") + /** + * Puts {@link AdditionalSubtypeMap} for the given user then schedule an I/O task to save it + * to the storage. + * + * @param userId the user for the given {@link AdditionalSubtypeMap} is to be saved + * @param map {@link AdditionalSubtypeMap} to be saved + * @param inputMethodMap {@link InputMethodMap} to be used while saving the data + */ + @AnyThread static void putAndSave(@UserIdInt int userId, @NonNull AdditionalSubtypeMap map, @NonNull InputMethodMap inputMethodMap) { - final AdditionalSubtypeMap previous = sPerUserMap.get(userId); - if (previous == map) { - return; + synchronized (sPerUserMap) { + final AdditionalSubtypeMap previous = sPerUserMap.get(userId); + if (previous == map) { + return; + } + sPerUserMap.put(userId, map); + sWriter.scheduleWriteTask(userId, map, inputMethodMap); } - sPerUserMap.put(userId, map); - sWriter.scheduleWriteTask(userId, map, inputMethodMap); } + @AnyThread static void startWriterThread() { sWriter.startThread(); } @@ -225,12 +276,10 @@ final class AdditionalSubtypeMapRepository { } @AnyThread - static void remove(@UserIdInt int userId, @NonNull Handler ioHandler) { - sWriter.onUserRemoved(userId); - ioHandler.post(() -> { - synchronized (ImfLock.class) { - sPerUserMap.remove(userId); - } - }); + static void remove(@UserIdInt int userId) { + synchronized (sPerUserMap) { + sWriter.onUserRemoved(userId); + sPerUserMap.remove(userId); + } } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index f61ca61c1e04..c82e5be7c643 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -16,6 +16,8 @@ package com.android.server.inputmethod; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; + import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.NonNull; @@ -110,7 +112,7 @@ public abstract class InputMethodManagerInternal { InlineSuggestionsRequestInfo requestInfo, InlineSuggestionsRequestCallback cb); /** - * Force switch to the enabled input method by {@code imeId} for current user. If the input + * Force switch to the enabled input method by {@code imeId} for the current user. If the input * method with {@code imeId} is not enabled or not installed, do nothing. * * @param imeId the input method ID to be switched to @@ -119,7 +121,25 @@ public abstract class InputMethodManagerInternal { * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available * to be switched. */ - public abstract boolean switchToInputMethod(String imeId, @UserIdInt int userId); + public boolean switchToInputMethod(@NonNull String imeId, @UserIdInt int userId) { + return switchToInputMethod(imeId, NOT_A_SUBTYPE_ID, userId); + } + + /** + * Force switch to the enabled input method by {@code imeId} for the current user. If the input + * method with {@code imeId} is not enabled or not installed, do nothing. If {@code subtypeId} + * is also supplied (not {@link InputMethodUtils#NOT_A_SUBTYPE_ID}) and valid, also switches to + * it, otherwise the system decides the most sensible default subtype to use. + * + * @param imeId the input method ID to be switched to + * @param subtypeId the input method subtype ID to be switched to + * @param userId the user ID to be queried + * @return {@code true} if the current input method was successfully switched to the input + * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available + * to be switched. + */ + public abstract boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + @UserIdInt int userId); /** * Force enable or disable the input method associated with {@code imeId} for given user. If @@ -211,6 +231,15 @@ public abstract class InputMethodManagerInternal { public abstract void updateImeWindowStatus(boolean disableImeIcon, int displayId); /** + * Updates and reports whether the IME switcher button should be shown, regardless whether + * SystemUI or the IME is responsible for drawing it and the corresponding navigation bar. + * + * @param displayId the display for which to update the IME switcher button visibility. + * @param userId the user for which to update the IME switcher button visibility. + */ + public abstract void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId); + + /** * Finish stylus handwriting by calling {@link InputMethodService#finishStylusHandwriting()} if * there is an ongoing handwriting session. */ @@ -290,7 +319,8 @@ public abstract class InputMethodManagerInternal { } @Override - public boolean switchToInputMethod(String imeId, @UserIdInt int userId) { + public boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + @UserIdInt int userId) { return false; } @@ -335,6 +365,10 @@ public abstract class InputMethodManagerInternal { } @Override + public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) { + } + + @Override public void onSessionForAccessibilityCreated(int accessibilityConnectionId, IAccessibilityInputMethodSession session, @UserIdInt int userId) { } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 85af7ab8a10f..ecbbd46bea76 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -181,6 +181,7 @@ import com.android.server.SystemService; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.input.InputManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener; +import com.android.server.inputmethod.InputMethodMenuControllerNew.MenuItem; import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; @@ -218,6 +219,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. static final String TAG = "InputMethodManagerService"; public static final String PROTO_ARG = "--proto"; + /** + * Timeout in milliseconds in {@link #systemRunning()} to make sure that users are initialized + * in {@link Lifecycle#initializeUsersAsync(int[])}. + */ + @DurationMillisLong + private static final long SYSTEM_READY_USER_INIT_TIMEOUT = 3000; + @Retention(SOURCE) @IntDef({ShellCommandResult.SUCCESS, ShellCommandResult.FAILURE}) private @interface ShellCommandResult { @@ -360,6 +368,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private final UserManagerInternal mUserManagerInternal; @MultiUserUnawareField private final InputMethodMenuController mMenuController; + private final InputMethodMenuControllerNew mMenuControllerNew; @GuardedBy("ImfLock.class") @MultiUserUnawareField @@ -566,7 +575,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } switch (key) { case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: { - mMenuController.updateKeyboardFromSettingsLocked(); + if (!Flags.imeSwitcherRevamp()) { + mMenuController.updateKeyboardFromSettingsLocked(); + } break; } case Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE: { @@ -631,7 +642,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } } - mMenuController.hideInputMethodMenu(); + if (Flags.imeSwitcherRevamp()) { + synchronized (ImfLock.class) { + final var bindingController = getInputMethodBindingController(senderUserId); + mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), + senderUserId); + } + } else { + mMenuController.hideInputMethodMenu(); + } } else { Slog.w(TAG, "Unexpected intent " + intent); } @@ -683,24 +702,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. super(true); } - @GuardedBy("ImfLock.class") - private boolean isChangingPackagesOfCurrentUserLocked() { - final int userId = getChangingUserId(); - final boolean retval = userId == mCurrentUserId; - if (DEBUG) { - if (!retval) { - Slog.d(TAG, "--- ignore this call back from a background user: " + userId); - } - } - return retval; - } - @Override public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) { synchronized (ImfLock.class) { - if (!isChangingPackagesOfCurrentUserLocked()) { - return false; - } final int userId = getChangingUserId(); final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); String curInputMethodId = settings.getSelectedInputMethod(); @@ -1023,7 +1027,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Called directly from UserManagerService. Do not block the calling thread. final int userId = user.id; SecureSettingsWrapper.onUserRemoved(userId); - AdditionalSubtypeMapRepository.remove(userId, mService.mIoHandler); + AdditionalSubtypeMapRepository.remove(userId); InputMethodSettingsRepository.remove(userId); mService.mUserDataRepository.remove(userId); } @@ -1054,39 +1058,31 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @AnyThread private void initializeUsersAsync(@UserIdInt int[] userIds) { + Slog.d(TAG, "Schedule initialization for users=" + Arrays.toString(userIds)); mService.mIoHandler.post(() -> { final var service = mService; final var context = service.mContext; final var userManagerInternal = service.mUserManagerInternal; - // We first create InputMethodMap for each user without loading AdditionalSubtypes. - final int numUsers = userIds.length; - final InputMethodMap[] rawMethodMaps = new InputMethodMap[numUsers]; - for (int i = 0; i < numUsers; ++i) { - final int userId = userIds[i]; - rawMethodMaps[i] = InputMethodManagerService.queryInputMethodServicesInternal( - context, userId, AdditionalSubtypeMap.EMPTY_MAP, + for (int userId : userIds) { + Slog.d(TAG, "Start initialization for user=" + userId); + final var additionalSubtypeMap = + AdditionalSubtypeMapRepository.ensureInitializedAndGet(userId); + final var settings = InputMethodManagerService.queryInputMethodServicesInternal( + context, userId, additionalSubtypeMap, DirectBootAwareness.AUTO).getMethodMap(); + InputMethodSettingsRepository.put(userId, + InputMethodSettings.create(settings, userId)); + final int profileParentId = userManagerInternal.getProfileParentId(userId); final boolean value = InputMethodDrawsNavBarResourceMonitor.evaluate(context, profileParentId); final var userData = mService.getUserData(userId); userData.mImeDrawsNavBar.set(value); - } - // Then create full InputMethodMap for each user. Note that - // AdditionalSubtypeMapRepository#get() and InputMethodSettingsRepository#put() - // need to be called with ImfLock held (b/352387655). - // TODO(b/343601565): Avoid ImfLock after fixing b/352387655. - synchronized (ImfLock.class) { - for (int i = 0; i < numUsers; ++i) { - final int userId = userIds[i]; - final var map = AdditionalSubtypeMapRepository.get(userId); - final var methodMap = rawMethodMaps[i].applyAdditionalSubtypes(map); - final var settings = InputMethodSettings.create(methodMap, userId); - InputMethodSettingsRepository.put(userId, settings); - } + userData.mBackgroundLoadLatch.countDown(); + Slog.d(TAG, "Complete initialization for user=" + userId); } }); } @@ -1103,11 +1099,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO); InputMethodSettingsRepository.put(userId, newSettings); - if (mCurrentUserId == userId) { + if (!mConcurrentMultiUserModeEnabled) { // We need to rebuild IMEs. postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId); updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId); - } else if (mConcurrentMultiUserModeEnabled) { + } else { + // TODO(b/352758479): Stop relying on initializeVisibleBackgroundUserLocked() initializeVisibleBackgroundUserLocked(userId); } } @@ -1171,6 +1168,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. : bindingControllerFactory); mMenuController = new InputMethodMenuController(this); + mMenuControllerNew = Flags.imeSwitcherRevamp() + ? new InputMethodMenuControllerNew() : null; mVisibilityStateComputer = new ImeVisibilityStateComputer(this); mVisibilityApplier = new DefaultImeVisibilityApplier(this); @@ -1331,10 +1330,42 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } + private void waitForUserInitialization() { + final int[] userIds = mUserManagerInternal.getUserIds(); + final long deadlineNanos = SystemClock.elapsedRealtimeNanos() + + TimeUnit.MILLISECONDS.toNanos(SYSTEM_READY_USER_INIT_TIMEOUT); + boolean interrupted = false; + try { + for (int userId : userIds) { + final var latch = getUserData(userId).mBackgroundLoadLatch; + boolean awaitResult; + while (true) { + try { + final long remainingNanos = + Math.max(deadlineNanos - SystemClock.elapsedRealtimeNanos(), 0); + awaitResult = latch.await(remainingNanos, TimeUnit.NANOSECONDS); + break; + } catch (InterruptedException ignored) { + interrupted = true; + } + } + if (!awaitResult) { + Slog.w(TAG, "Timed out for user#" + userId + " to be initialized"); + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + /** * TODO(b/32343335): The entire systemRunning() method needs to be revisited. */ public void systemRunning() { + waitForUserInitialization(); + synchronized (ImfLock.class) { if (DEBUG) { Slog.d(TAG, "--- systemReady"); @@ -1410,9 +1441,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. (windowToken, imeVisible) -> { if (Flags.refactorInsetsController()) { if (imeVisible) { - showSoftInputInternal(windowToken); + showCurrentInputInternal(windowToken); } else { - hideSoftInputInternal(windowToken); + hideCurrentInputInternal(windowToken); } } }); @@ -1782,7 +1813,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. ImeTracker.PHASE_SERVER_WAIT_IME); userData.mCurStatsToken = null; // TODO: Make mMenuController multi-user aware - mMenuController.hideInputMethodMenuLocked(); + if (Flags.imeSwitcherRevamp()) { + mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); + } else { + mMenuController.hideInputMethodMenuLocked(); + } } } @@ -1884,7 +1919,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (Flags.refactorInsetsController()) { if (isShowRequestedForCurrentWindow(userId) && userData.mImeBindingState != null && userData.mImeBindingState.mFocusedWindow != null) { - showSoftInputInternal(userData.mImeBindingState.mFocusedWindow); + showCurrentInputInternal(userData.mImeBindingState.mFocusedWindow); } } else { if (isShowRequestedForCurrentWindow(userId)) { @@ -2599,7 +2634,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!mShowOngoingImeSwitcherForPhones) return false; // When the IME switcher dialog is shown, the IME switcher button should be hidden. // TODO(b/305849394): Make mMenuController multi-user aware. - if (mMenuController.getSwitchingDialogLocked() != null) return false; + final boolean switcherMenuShowing = Flags.imeSwitcherRevamp() + ? mMenuControllerNew.isShowing() + : mMenuController.getSwitchingDialogLocked() != null; + if (switcherMenuShowing) { + return false; + } // When we are switching IMEs, the IME switcher button should be hidden. final var bindingController = getInputMethodBindingController(userId); if (!Objects.equals(bindingController.getCurId(), @@ -2614,7 +2654,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. || (visibility & InputMethodService.IME_INVISIBLE) != 0) { return false; } - if (mWindowManagerInternal.isHardKeyboardAvailable()) { + if (mWindowManagerInternal.isHardKeyboardAvailable() && !Flags.imeSwitcherRevamp()) { // When physical keyboard is attached, we show the ime switcher (or notification if // NavBar is not available) because SHOW_IME_WITH_HARD_KEYBOARD settings currently // exists in the IME switcher dialog. Might be OK to remove this condition once @@ -2625,6 +2665,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + if (Flags.imeSwitcherRevamp()) { + // The IME switcher button should be shown when the current IME specified a + // language settings activity. + final var curImi = settings.getMethodMap().get(settings.getSelectedInputMethod()); + if (curImi != null && curImi.createImeLanguageSettingsActivityIntent() != null) { + return true; + } + } + return hasMultipleSubtypesForSwitcher(false /* nonAuxOnly */, settings); } @@ -2794,7 +2843,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final var curId = bindingController.getCurId(); // TODO(b/305849394): Make mMenuController multi-user aware. - if (mMenuController.getSwitchingDialogLocked() != null + final boolean switcherMenuShowing = Flags.imeSwitcherRevamp() + ? mMenuControllerNew.isShowing() + : mMenuController.getSwitchingDialogLocked() != null; + if (switcherMenuShowing || !Objects.equals(curId, bindingController.getSelectedMethodId())) { // When the IME switcher dialog is shown, or we are switching IMEs, // the back button should be in the default state (as if the IME is not shown). @@ -2813,7 +2865,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) { updateInputMethodsFromSettingsLocked(enabledMayChange, userId); - mMenuController.updateKeyboardFromSettingsLocked(); + if (!Flags.imeSwitcherRevamp()) { + mMenuController.updateKeyboardFromSettingsLocked(); + } } /** @@ -3097,8 +3151,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - boolean showSoftInputInternal(IBinder windowToken) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInputInternal"); + boolean showCurrentInputInternal(IBinder windowToken) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#showSoftInput", mDumper); synchronized (ImfLock.class) { @@ -3117,8 +3171,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - boolean hideSoftInputInternal(IBinder windowToken) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInputInternal"); + boolean hideCurrentInputInternal(IBinder windowToken) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#hideSoftInput", mDumper); synchronized (ImfLock.class) { @@ -3979,8 +4033,68 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShownForTest() { synchronized (ImfLock.class) { - return mMenuController.isisInputMethodPickerShownForTestLocked(); + return Flags.imeSwitcherRevamp() + ? mMenuControllerNew.isShowing() + : mMenuController.isisInputMethodPickerShownForTestLocked(); + } + } + + /** + * Gets the list of Input Method Switcher Menu items and the index of the selected item. + * + * @param items the list of input method and subtype items. + * @param selectedImeId the ID of the selected input method. + * @param selectedSubtypeId the ID of the selected input method subtype, + * or {@link #NOT_A_SUBTYPE_ID} if no subtype is selected. + * @param userId the ID of the user for which to get the menu items. + * @return the list of menu items, and the index of the selected item, + * or {@code -1} if no item is selected. + */ + @GuardedBy("ImfLock.class") + @NonNull + private Pair<List<MenuItem>, Integer> getInputMethodPickerItems( + @NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId, + int selectedSubtypeId, @UserIdInt int userId) { + final var bindingController = getInputMethodBindingController(userId); + final var settings = InputMethodSettingsRepository.get(userId); + + if (selectedSubtypeId == NOT_A_SUBTYPE_ID) { + // TODO(b/351124299): Check if this fallback logic is still necessary. + final var curSubtype = bindingController.getCurrentInputMethodSubtype(); + if (curSubtype != null) { + final var curMethodId = bindingController.getSelectedMethodId(); + final var curImi = settings.getMethodMap().get(curMethodId); + selectedSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode( + curImi, curSubtype.hashCode()); + } + } + + // No item is selected by default. When we have a list of explicitly enabled + // subtypes, the implicit subtype is no longer listed. If the implicit one + // is still selected, no items will be shown as selected. + int selectedIndex = -1; + String prevImeId = null; + final var menuItems = new ArrayList<MenuItem>(); + for (int i = 0; i < items.size(); i++) { + final var item = items.get(i); + final var imeId = item.mImi.getId(); + if (imeId.equals(selectedImeId)) { + final int subtypeId = item.mSubtypeId; + // Check if this is the selected IME-subtype pair. + if ((subtypeId == 0 && selectedSubtypeId == NOT_A_SUBTYPE_ID) + || subtypeId == NOT_A_SUBTYPE_ID + || subtypeId == selectedSubtypeId) { + selectedIndex = i; + } + } + final boolean hasHeader = !imeId.equals(prevImeId); + final boolean hasDivider = hasHeader && prevImeId != null; + prevImeId = imeId; + menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi, item.mSubtypeId, + hasHeader, hasDivider)); } + + return new Pair<>(menuItems, selectedIndex); } @BinderThread @@ -4625,7 +4739,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. proto.write(IS_INTERACTIVE, mIsInteractive); proto.write(BACK_DISPOSITION, bindingController.getBackDisposition()); proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis()); - proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard()); + if (!Flags.imeSwitcherRevamp()) { + proto.write(SHOW_IME_WITH_HARD_KEYBOARD, + mMenuController.getShowImeWithHardKeyboard()); + } proto.write(CONCURRENT_MULTI_USER_MODE_ENABLED, mConcurrentMultiUserModeEnabled); proto.end(token); } @@ -4931,8 +5048,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. synchronized (ImfLock.class) { final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); + final int userId = settings.getUserId(); final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked() - && mWindowManagerInternal.isKeyguardSecure(settings.getUserId()); + && mWindowManagerInternal.isKeyguardSecure(userId); final String lastInputMethodId = settings.getSelectedInputMethod(); int lastInputMethodSubtypeId = settings.getSelectedInputMethodSubtypeId(lastInputMethodId); @@ -4945,12 +5063,35 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Slog.w(TAG, "Show switching menu failed, imList is empty," + " showAuxSubtypes: " + showAuxSubtypes + " isScreenLocked: " + isScreenLocked - + " userId: " + settings.getUserId()); + + " userId: " + userId); return false; } - mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, - lastInputMethodId, lastInputMethodSubtypeId, imList); + if (Flags.imeSwitcherRevamp()) { + if (DEBUG) { + Slog.v(TAG, "Show IME switcher menu," + + " showAuxSubtypes=" + showAuxSubtypes + + " displayId=" + displayId + + " preferredInputMethodId=" + lastInputMethodId + + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId); + } + + final var itemsAndIndex = getInputMethodPickerItems(imList, + lastInputMethodId, lastInputMethodSubtypeId, userId); + final var menuItems = itemsAndIndex.first; + final int selectedIndex = itemsAndIndex.second; + + if (selectedIndex == -1) { + Slog.w(TAG, "Switching menu shown with no item selected" + + ", IME id: " + lastInputMethodId + + ", subtype index: " + lastInputMethodSubtypeId); + } + + mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId); + } else { + mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, + lastInputMethodId, lastInputMethodSubtypeId, imList); + } } return true; @@ -5021,7 +5162,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // -------------------------------------------------------------- case MSG_HARD_KEYBOARD_SWITCH_CHANGED: - mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1); + if (!Flags.imeSwitcherRevamp()) { + mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1); + } synchronized (ImfLock.class) { sendOnNavButtonFlagsChangedToAllImesLocked(); } @@ -5591,7 +5734,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) { + private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId, + @UserIdInt int userId) { final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); if (mConcurrentMultiUserModeEnabled || userId == mCurrentUserId) { if (!settings.getMethodMap().containsKey(imeId) @@ -5599,7 +5743,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. .contains(settings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } - setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID, userId); + setInputMethodLocked(imeId, subtypeId, userId); return true; } if (!settings.getMethodMap().containsKey(imeId) @@ -5608,6 +5752,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return false; // IME is not found or not enabled. } settings.putSelectedInputMethod(imeId); + // For non-current user, only reset subtypeId (instead of setting the given one). settings.putSelectedSubtype(NOT_A_SUBTYPE_ID); return true; } @@ -5753,9 +5898,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @Override - public boolean switchToInputMethod(String imeId, @UserIdInt int userId) { + public boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + @UserIdInt int userId) { synchronized (ImfLock.class) { - return switchToInputMethodLocked(imeId, userId); + return switchToInputMethodLocked(imeId, subtypeId, userId); } } @@ -5852,7 +5998,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // input target changed, in case seeing the dialog dismiss flickering during // the next focused window starting the input connection. if (mLastImeTargetWindow != userData.mImeBindingState.mFocusedWindow) { - mMenuController.hideInputMethodMenuLocked(); + if (Flags.imeSwitcherRevamp()) { + final var bindingController = getInputMethodBindingController(userId); + mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); + } else { + mMenuController.hideInputMethodMenuLocked(); + } } } } @@ -5871,6 +6022,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @Override + public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) { + synchronized (ImfLock.class) { + updateSystemUiLocked(userId); + final var userData = getUserData(userId); + sendOnNavButtonFlagsChangedLocked(userData); + } + } + + @Override public void onSessionForAccessibilityCreated(int accessibilityConnectionId, IAccessibilityInputMethodSession session, @UserIdInt int userId) { synchronized (ImfLock.class) { @@ -6192,6 +6352,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }; mUserDataRepository.forAllUserData(userDataDump); + if (Flags.imeSwitcherRevamp()) { + p.println(" menuControllerNew:"); + mMenuControllerNew.dump(p, " "); + } p.println(" mCurToken=" + bindingController.getCurToken()); p.println(" mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId()); p.println(" mCurHostInputToken=" + bindingController.getCurHostInputToken()); @@ -6638,7 +6802,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. continue; } boolean failedToSelectUnknownIme = !switchToInputMethodLocked(imeId, - userId); + NOT_A_SUBTYPE_ID, userId); if (failedToSelectUnknownIme) { error.print("Unknown input method "); error.print(imeId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java new file mode 100644 index 000000000000..cbb1807a6c2e --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + + +import static com.android.server.inputmethod.InputMethodManagerService.DEBUG; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Printer; +import android.util.Slog; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodInfo; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.widget.RecyclerView; + +import java.util.List; + +/** + * Controller for showing and hiding the Input Method Switcher Menu. + */ +final class InputMethodMenuControllerNew { + + private static final String TAG = InputMethodMenuControllerNew.class.getSimpleName(); + + /** + * The horizontal offset from the menu to the edge of the screen corresponding + * to {@link Gravity#END}. + */ + private static final int HORIZONTAL_OFFSET = 16; + + /** The title of the window, used for debugging. */ + private static final String WINDOW_TITLE = "IME Switcher Menu"; + + private final InputMethodDialogWindowContext mDialogWindowContext = + new InputMethodDialogWindowContext(); + + @Nullable + private AlertDialog mDialog; + + @Nullable + private List<MenuItem> mMenuItems; + + /** + * Shows the Input Method Switcher Menu, with a list of IMEs and their subtypes. + * + * @param items the list of menu items. + * @param selectedIndex the index of the menu item that is selected. + * If no other IMEs are enabled, this index will be out of reach. + * @param displayId the ID of the display where the menu was requested. + * @param userId the ID of the user that requested the menu. + */ + void show(@NonNull List<MenuItem> items, int selectedIndex, int displayId, + @UserIdInt int userId) { + // Hide the menu in case it was already showing. + hide(displayId, userId); + + final Context dialogWindowContext = mDialogWindowContext.get(displayId); + final var builder = new AlertDialog.Builder(dialogWindowContext, + com.android.internal.R.style.Theme_DeviceDefault_InputMethodSwitcherDialog); + final var inflater = LayoutInflater.from(builder.getContext()); + + // Create the content view. + final View contentView = inflater + .inflate(com.android.internal.R.layout.input_method_switch_dialog_new, null); + contentView.setAccessibilityPaneTitle( + dialogWindowContext.getText(com.android.internal.R.string.select_input_method)); + builder.setView(contentView); + + final DialogInterface.OnClickListener onClickListener = (dialog, which) -> { + if (which != selectedIndex) { + final var item = items.get(which); + InputMethodManagerInternal.get() + .switchToInputMethod(item.mImi.getId(), item.mSubtypeId, userId); + } + hide(displayId, userId); + }; + + final var selectedImi = selectedIndex >= 0 ? items.get(selectedIndex).mImi : null; + final var languageSettingsIntent = selectedImi != null + ? selectedImi.createImeLanguageSettingsActivityIntent() : null; + final boolean hasLanguageSettingsButton = languageSettingsIntent != null; + if (hasLanguageSettingsButton) { + final View buttonBar = contentView + .requireViewById(com.android.internal.R.id.button_bar); + buttonBar.setVisibility(View.VISIBLE); + + languageSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Button languageSettingsButton = contentView + .requireViewById(com.android.internal.R.id.button1); + languageSettingsButton.setVisibility(View.VISIBLE); + languageSettingsButton.setOnClickListener(v -> { + v.getContext().startActivity(languageSettingsIntent); + hide(displayId, userId); + }); + } + + // Create the current IME subtypes list. + final RecyclerView recyclerView = contentView + .requireViewById(com.android.internal.R.id.list); + recyclerView.setAdapter(new Adapter(items, selectedIndex, inflater, onClickListener)); + // Scroll to the currently selected IME. + recyclerView.scrollToPosition(selectedIndex); + // Indicate that the list can be scrolled. + recyclerView.setScrollIndicators( + hasLanguageSettingsButton ? View.SCROLL_INDICATOR_BOTTOM : 0); + + builder.setOnCancelListener(dialog -> hide(displayId, userId)); + mMenuItems = items; + mDialog = builder.create(); + mDialog.setCanceledOnTouchOutside(true); + final Window w = mDialog.getWindow(); + w.setHideOverlayWindows(true); + final WindowManager.LayoutParams attrs = w.getAttributes(); + // Use an alternate token for the dialog for that window manager can group the token + // with other IME windows based on type vs. grouping based on whichever token happens + // to get selected by the system later on. + attrs.token = dialogWindowContext.getWindowContextToken(); + attrs.gravity = Gravity.getAbsoluteGravity(Gravity.BOTTOM | Gravity.END, + dialogWindowContext.getResources().getConfiguration().getLayoutDirection()); + attrs.x = HORIZONTAL_OFFSET; + attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + attrs.type = WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG; + // Used for debugging only, not user visible. + attrs.setTitle(WINDOW_TITLE); + w.setAttributes(attrs); + + mDialog.show(); + InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId); + } + + /** + * Hides the Input Method Switcher Menu. + * + * @param displayId the ID of the display from where the menu should be hidden. + * @param userId the ID of the user for which the menu should be hidden. + */ + void hide(int displayId, @UserIdInt int userId) { + if (DEBUG) Slog.v(TAG, "Hide IME switcher menu."); + + mMenuItems = null; + // Cannot use dialog.isShowing() here, as the cancel listener flow already resets mShowing. + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + + InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId); + } + } + + /** + * Returns whether the Input Method Switcher Menu is showing. + */ + boolean isShowing() { + return mDialog != null && mDialog.isShowing(); + } + + void dump(@NonNull Printer pw, @NonNull String prefix) { + final boolean showing = isShowing(); + pw.println(prefix + " isShowing: " + showing); + + if (showing) { + pw.println(prefix + " menuItems: " + mMenuItems); + } + } + + /** + * Item to be shown in the Input Method Switcher Menu, containing an input method and + * optionally an input method subtype. + */ + static class MenuItem { + + /** The name of the input method. */ + @NonNull + private final CharSequence mImeName; + + /** + * The name of the input method subtype, or {@code null} if this item doesn't have a + * subtype. + */ + @Nullable + private final CharSequence mSubtypeName; + + /** The info of the input method. */ + @NonNull + private final InputMethodInfo mImi; + + /** + * The index of the subtype in the input method's array of subtypes, + * or {@link InputMethodUtils#NOT_A_SUBTYPE_ID} if this item doesn't have a subtype. + */ + @IntRange(from = NOT_A_SUBTYPE_ID) + private final int mSubtypeId; + + /** Whether this item has a group header (only the first item of each input method). */ + private final boolean mHasHeader; + + /** + * Whether this item should has a group divider (same as {@link #mHasHeader}, + * excluding the first IME). + */ + private final boolean mHasDivider; + + MenuItem(@NonNull CharSequence imeName, @Nullable CharSequence subtypeName, + @NonNull InputMethodInfo imi, @IntRange(from = NOT_A_SUBTYPE_ID) int subtypeId, + boolean hasHeader, boolean hasDivider) { + mImeName = imeName; + mSubtypeName = subtypeName; + mImi = imi; + mSubtypeId = subtypeId; + mHasHeader = hasHeader; + mHasDivider = hasDivider; + } + + @Override + public String toString() { + return "MenuItem{" + + "mImeName=" + mImeName + + " mSubtypeName=" + mSubtypeName + + " mSubtypeId=" + mSubtypeId + + " mHasHeader=" + mHasHeader + + " mHasDivider=" + mHasDivider + + "}"; + } + } + + private static class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> { + + /** The list of items to show. */ + @NonNull + private final List<MenuItem> mItems; + /** The index of the selected item. */ + private final int mSelectedIndex; + @NonNull + private final LayoutInflater mInflater; + @NonNull + private final DialogInterface.OnClickListener mOnClickListener; + + Adapter(@NonNull List<MenuItem> items, int selectedIndex, + @NonNull LayoutInflater inflater, + @NonNull DialogInterface.OnClickListener onClickListener) { + mItems = items; + mSelectedIndex = selectedIndex; + mInflater = inflater; + mOnClickListener = onClickListener; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final View view = mInflater.inflate( + com.android.internal.R.layout.input_method_switch_item_new, parent, false); + + return new ViewHolder(view, mOnClickListener); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.bind(mItems.get(position), position == mSelectedIndex /* isSelected */); + } + + @Override + public int getItemCount() { + return mItems.size(); + } + + private static class ViewHolder extends RecyclerView.ViewHolder { + + /** The container of the item. */ + @NonNull + private final View mContainer; + /** The name of the item. */ + @NonNull + private final TextView mName; + /** Indicator for the selected status of the item. */ + @NonNull + private final ImageView mCheckmark; + /** The group header optionally drawn above the item. */ + @NonNull + private final TextView mHeader; + /** The group divider optionally drawn above the item. */ + @NonNull + private final View mDivider; + + private ViewHolder(@NonNull View itemView, + @NonNull DialogInterface.OnClickListener onClickListener) { + super(itemView); + + mContainer = itemView.requireViewById(com.android.internal.R.id.list_item); + mName = itemView.requireViewById(com.android.internal.R.id.text); + mCheckmark = itemView.requireViewById(com.android.internal.R.id.image); + mHeader = itemView.requireViewById(com.android.internal.R.id.header_text); + mDivider = itemView.requireViewById(com.android.internal.R.id.divider); + + mContainer.setOnClickListener((v) -> + onClickListener.onClick(null /* dialog */, getAdapterPosition())); + } + + /** + * Binds the given item to the current view. + * + * @param item the item to bind. + * @param isSelected whether this is selected. + */ + private void bind(@NonNull MenuItem item, boolean isSelected) { + // Use the IME name for subtypes with an empty subtype name. + final var name = TextUtils.isEmpty(item.mSubtypeName) + ? item.mImeName : item.mSubtypeName; + mContainer.setActivated(isSelected); + // Activated is the correct state, but we also set selected for accessibility info. + mContainer.setSelected(isSelected); + mName.setSelected(isSelected); + mName.setText(name); + mCheckmark.setVisibility(isSelected ? View.VISIBLE : View.GONE); + mHeader.setText(item.mImeName); + mHeader.setVisibility(item.mHasHeader ? View.VISIBLE : View.GONE); + mDivider.setVisibility(item.mHasDivider ? View.VISIBLE : View.GONE); + } + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java index 50ba36450bda..1b840362a8cf 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java @@ -24,7 +24,8 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; final class InputMethodSettingsRepository { - @GuardedBy("ImfLock.class") + // TODO(b/352594784): Should we user other lock primitives? + @GuardedBy("sPerUserMap") @NonNull private static final SparseArray<InputMethodSettings> sPerUserMap = new SparseArray<>(); @@ -35,23 +36,28 @@ final class InputMethodSettingsRepository { } @NonNull - @GuardedBy("ImfLock.class") + @AnyThread static InputMethodSettings get(@UserIdInt int userId) { - final InputMethodSettings obj = sPerUserMap.get(userId); + final InputMethodSettings obj; + synchronized (sPerUserMap) { + obj = sPerUserMap.get(userId); + } if (obj != null) { return obj; } return InputMethodSettings.createEmptyMap(userId); } - @GuardedBy("ImfLock.class") + @AnyThread static void put(@UserIdInt int userId, @NonNull InputMethodSettings obj) { - sPerUserMap.put(userId, obj); + synchronized (sPerUserMap) { + sPerUserMap.put(userId, obj); + } } @AnyThread static void remove(@UserIdInt int userId) { - synchronized (ImfLock.class) { + synchronized (sPerUserMap) { sPerUserMap.remove(userId); } } diff --git a/services/core/java/com/android/server/inputmethod/UserData.java b/services/core/java/com/android/server/inputmethod/UserData.java index ec5c9e6a3550..be5732174b1d 100644 --- a/services/core/java/com/android/server/inputmethod/UserData.java +++ b/services/core/java/com/android/server/inputmethod/UserData.java @@ -28,6 +28,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; /** Placeholder for all IMMS user specific fields */ @@ -35,6 +36,13 @@ final class UserData { @UserIdInt final int mUserId; + /** + * Tells whether {@link InputMethodManagerService.Lifecycle#initializeUsersAsync(int[])} is + * completed for this user or not. + */ + @NonNull + final CountDownLatch mBackgroundLoadLatch = new CountDownLatch(1); + @NonNull final InputMethodBindingController mBindingController; diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index a821f54520df..c4d601d03652 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -67,19 +67,13 @@ public class SystemImpl implements SystemInterface { private static final String TAG_SIGNATURE = "signature"; private static final String TAG_FALLBACK = "isFallback"; private static final String PIN_GROUP = "webview"; - private final WebViewProviderInfo[] mWebViewProviderPackages; - // Initialization-on-demand holder idiom for getting the WebView provider packages once and - // for all in a thread-safe manner. - private static class LazyHolder { - private static final SystemImpl INSTANCE = new SystemImpl(); - } + private final Context mContext; + private final WebViewProviderInfo[] mWebViewProviderPackages; - public static SystemImpl getInstance() { - return LazyHolder.INSTANCE; - } + SystemImpl(Context context) { + mContext = context; - private SystemImpl() { int numFallbackPackages = 0; int numAvailableByDefaultPackages = 0; XmlResourceParser parser = null; @@ -184,14 +178,14 @@ public class SystemImpl implements SystemInterface { } @Override - public String getUserChosenWebViewProvider(Context context) { - return Settings.Global.getString(context.getContentResolver(), + public String getUserChosenWebViewProvider() { + return Settings.Global.getString(mContext.getContentResolver(), Settings.Global.WEBVIEW_PROVIDER); } @Override - public void updateUserSetting(Context context, String newProviderName) { - Settings.Global.putString(context.getContentResolver(), + public void updateUserSetting(String newProviderName) { + Settings.Global.putString(mContext.getContentResolver(), Settings.Global.WEBVIEW_PROVIDER, newProviderName == null ? "" : newProviderName); } @@ -207,8 +201,8 @@ public class SystemImpl implements SystemInterface { } @Override - public void enablePackageForAllUsers(Context context, String packageName, boolean enable) { - UserManager userManager = (UserManager)context.getSystemService(Context.USER_SERVICE); + public void enablePackageForAllUsers(String packageName, boolean enable) { + UserManager userManager = mContext.getSystemService(UserManager.class); for(UserInfo userInfo : userManager.getUsers()) { enablePackageForUser(packageName, enable, userInfo.id); } @@ -228,16 +222,15 @@ public class SystemImpl implements SystemInterface { } @Override - public void installExistingPackageForAllUsers(Context context, String packageName) { - UserManager userManager = context.getSystemService(UserManager.class); + public void installExistingPackageForAllUsers(String packageName) { + UserManager userManager = mContext.getSystemService(UserManager.class); for (UserInfo userInfo : userManager.getUsers()) { installPackageForUser(packageName, userInfo.id); } } private void installPackageForUser(String packageName, int userId) { - final Context context = AppGlobals.getInitialApplication(); - final Context contextAsUser = context.createContextAsUser(UserHandle.of(userId), 0); + final Context contextAsUser = mContext.createContextAsUser(UserHandle.of(userId), 0); final PackageInstaller installer = contextAsUser.getPackageManager().getPackageInstaller(); installer.installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN, null); } @@ -255,29 +248,28 @@ public class SystemImpl implements SystemInterface { } @Override - public List<UserPackage> getPackageInfoForProviderAllUsers(Context context, - WebViewProviderInfo configInfo) { - return UserPackage.getPackageInfosAllUsers(context, configInfo.packageName, PACKAGE_FLAGS); + public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo) { + return UserPackage.getPackageInfosAllUsers(mContext, configInfo.packageName, PACKAGE_FLAGS); } @Override - public int getMultiProcessSetting(Context context) { + public int getMultiProcessSetting() { if (updateServiceV2()) { throw new IllegalStateException( "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set."); } return Settings.Global.getInt( - context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0); + mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0); } @Override - public void setMultiProcessSetting(Context context, int value) { + public void setMultiProcessSetting(int value) { if (updateServiceV2()) { throw new IllegalStateException( "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set."); } Settings.Global.putInt( - context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value); + mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value); } @Override diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java index ad32f623c80d..3b77d07412ce 100644 --- a/services/core/java/com/android/server/webkit/SystemInterface.java +++ b/services/core/java/com/android/server/webkit/SystemInterface.java @@ -16,7 +16,6 @@ package com.android.server.webkit; -import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -34,19 +33,19 @@ import java.util.List; * @hide */ public interface SystemInterface { - public WebViewProviderInfo[] getWebViewPackages(); - public int onWebViewProviderChanged(PackageInfo packageInfo); - public long getFactoryPackageVersion(String packageName) throws NameNotFoundException; + WebViewProviderInfo[] getWebViewPackages(); + int onWebViewProviderChanged(PackageInfo packageInfo); + long getFactoryPackageVersion(String packageName) throws NameNotFoundException; - public String getUserChosenWebViewProvider(Context context); - public void updateUserSetting(Context context, String newProviderName); - public void killPackageDependents(String packageName); + String getUserChosenWebViewProvider(); + void updateUserSetting(String newProviderName); + void killPackageDependents(String packageName); - public void enablePackageForAllUsers(Context context, String packageName, boolean enable); - public void installExistingPackageForAllUsers(Context context, String packageName); + void enablePackageForAllUsers(String packageName, boolean enable); + void installExistingPackageForAllUsers(String packageName); - public boolean systemIsDebuggable(); - public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo) + boolean systemIsDebuggable(); + PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo) throws NameNotFoundException; /** * Get the PackageInfos of all users for the package represented by {@param configInfo}. @@ -54,15 +53,14 @@ public interface SystemInterface { * certain user. The returned array can contain null PackageInfos if the given package * is uninstalled for some user. */ - public List<UserPackage> getPackageInfoForProviderAllUsers(Context context, - WebViewProviderInfo configInfo); + List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo); - public int getMultiProcessSetting(Context context); - public void setMultiProcessSetting(Context context, int value); - public void notifyZygote(boolean enableMultiProcess); + int getMultiProcessSetting(); + void setMultiProcessSetting(int value); + void notifyZygote(boolean enableMultiProcess); /** Start the zygote if it's not already running. */ - public void ensureZygoteStarted(); - public boolean isMultiProcessDefaultEnabled(); + void ensureZygoteStarted(); + boolean isMultiProcessDefaultEnabled(); - public void pinWebviewIfRequired(ApplicationInfo appInfo); + void pinWebviewIfRequired(ApplicationInfo appInfo); } diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java index 043470f62850..7acb864cbb40 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java @@ -73,9 +73,9 @@ public class WebViewUpdateService extends SystemService { public WebViewUpdateService(Context context) { super(context); if (updateServiceV2()) { - mImpl = new WebViewUpdateServiceImpl2(context, SystemImpl.getInstance()); + mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context)); } else { - mImpl = new WebViewUpdateServiceImpl(context, SystemImpl.getInstance()); + mImpl = new WebViewUpdateServiceImpl(new SystemImpl(context)); } } diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java index dcf20f97ef71..b9be4a2deef2 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java @@ -16,7 +16,6 @@ package com.android.server.webkit; import android.annotation.Nullable; -import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; @@ -92,7 +91,6 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE; private final SystemInterface mSystemInterface; - private final Context mContext; private long mMinimumVersionCode = -1; @@ -110,8 +108,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private final Object mLock = new Object(); - WebViewUpdateServiceImpl(Context context, SystemInterface systemInterface) { - mContext = context; + WebViewUpdateServiceImpl(SystemInterface systemInterface) { mSystemInterface = systemInterface; } @@ -173,7 +170,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { try { synchronized (mLock) { mCurrentWebViewPackage = findPreferredWebViewPackage(); - String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userSetting = mSystemInterface.getUserChosenWebViewProvider(); if (userSetting != null && !userSetting.equals(mCurrentWebViewPackage.packageName)) { // Don't persist the user-chosen setting across boots if the package being @@ -181,8 +178,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { // be surprised by the device switching to using a certain webview package, // that was uninstalled/disabled a long time ago, if it is installed/enabled // again. - mSystemInterface.updateUserSetting(mContext, - mCurrentWebViewPackage.packageName); + mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName); } onWebViewProviderChanged(mCurrentWebViewPackage); } @@ -203,8 +199,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders); if (fallbackProvider != null) { Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName); - mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName, - true); + mSystemInterface.enablePackageForAllUsers(fallbackProvider.packageName, true); } else { Slog.e(TAG, "No valid provider and no fallback available."); } @@ -316,7 +311,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { oldPackage = mCurrentWebViewPackage; if (newProviderName != null) { - mSystemInterface.updateUserSetting(mContext, newProviderName); + mSystemInterface.updateUserSetting(newProviderName); } try { @@ -447,7 +442,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException { ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos(); - String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(); // If the user has chosen provider, use that (if it's installed and enabled for all // users). @@ -455,7 +450,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { if (providerAndPackage.provider.packageName.equals(userChosenProvider)) { // userPackages can contain null objects. List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, + mSystemInterface.getPackageInfoForProviderAllUsers( providerAndPackage.provider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return providerAndPackage.packageInfo; @@ -470,7 +465,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { if (providerAndPackage.provider.availableByDefault) { // userPackages can contain null objects. List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, + mSystemInterface.getPackageInfoForProviderAllUsers( providerAndPackage.provider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return providerAndPackage.packageInfo; @@ -658,7 +653,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { @Override public boolean isMultiProcessEnabled() { - int settingValue = mSystemInterface.getMultiProcessSetting(mContext); + int settingValue = mSystemInterface.getMultiProcessSetting(); if (mSystemInterface.isMultiProcessDefaultEnabled()) { // Multiprocess should be enabled unless the user has turned it off manually. return settingValue > MULTIPROCESS_SETTING_OFF_VALUE; @@ -671,7 +666,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { @Override public void enableMultiProcess(boolean enable) { PackageInfo current = getCurrentWebViewPackage(); - mSystemInterface.setMultiProcessSetting(mContext, + mSystemInterface.setMultiProcessSetting( enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE); mSystemInterface.notifyZygote(enable); if (current != null) { @@ -725,7 +720,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { pw.println(" WebView packages:"); for (WebViewProviderInfo provider : allProviders) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider); + mSystemInterface.getPackageInfoForProviderAllUsers(provider); PackageInfo systemUserPackageInfo = userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo(); if (systemUserPackageInfo == null) { @@ -741,7 +736,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { systemUserPackageInfo.applicationInfo.targetSdkVersion); if (validity == VALIDITY_OK) { boolean installedForAllUsers = isInstalledAndEnabledForAllUsers( - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider)); + mSystemInterface.getPackageInfoForProviderAllUsers(provider)); pw.println(String.format( " Valid package %s (%s) is %s installed/enabled for all users", systemUserPackageInfo.packageName, diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index 993597eedd2c..307c15b72c76 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -16,7 +16,6 @@ package com.android.server.webkit; import android.annotation.Nullable; -import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; @@ -86,7 +85,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { private static final int VALIDITY_NO_LIBRARY_FLAG = 4; private final SystemInterface mSystemInterface; - private final Context mContext; private final WebViewProviderInfo mDefaultProvider; private long mMinimumVersionCode = -1; @@ -108,8 +106,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { private final Object mLock = new Object(); - WebViewUpdateServiceImpl2(Context context, SystemInterface systemInterface) { - mContext = context; + WebViewUpdateServiceImpl2(SystemInterface systemInterface) { mSystemInterface = systemInterface; WebViewProviderInfo[] webviewProviders = getWebViewPackages(); @@ -194,8 +191,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { } if (mCurrentWebViewPackage.packageName.equals(mDefaultProvider.packageName)) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, mDefaultProvider); + mSystemInterface.getPackageInfoForProviderAllUsers(mDefaultProvider); return !isInstalledAndEnabledForAllUsers(userPackages); } else { return false; @@ -216,10 +212,8 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { TAG, "No provider available for all users, trying to install and enable " + mDefaultProvider.packageName); - mSystemInterface.installExistingPackageForAllUsers( - mContext, mDefaultProvider.packageName); - mSystemInterface.enablePackageForAllUsers( - mContext, mDefaultProvider.packageName, true); + mSystemInterface.installExistingPackageForAllUsers(mDefaultProvider.packageName); + mSystemInterface.enablePackageForAllUsers(mDefaultProvider.packageName, true); } @Override @@ -229,7 +223,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { synchronized (mLock) { mCurrentWebViewPackage = findPreferredWebViewPackage(); repairNeeded = shouldTriggerRepairLocked(); - String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userSetting = mSystemInterface.getUserChosenWebViewProvider(); if (userSetting != null && !userSetting.equals(mCurrentWebViewPackage.packageName)) { // Don't persist the user-chosen setting across boots if the package being @@ -237,8 +231,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { // be surprised by the device switching to using a certain webview package, // that was uninstalled/disabled a long time ago, if it is installed/enabled // again. - mSystemInterface.updateUserSetting(mContext, - mCurrentWebViewPackage.packageName); + mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName); } onWebViewProviderChanged(mCurrentWebViewPackage); } @@ -362,7 +355,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { oldPackage = mCurrentWebViewPackage; if (newProviderName != null) { - mSystemInterface.updateUserSetting(mContext, newProviderName); + mSystemInterface.updateUserSetting(newProviderName); } try { @@ -493,7 +486,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { Counter.logIncrement("webview.value_find_preferred_webview_package_counter"); // If the user has chosen provider, use that (if it's installed and enabled for all // users). - String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(); WebViewProviderInfo userChosenProvider = getWebViewProviderForPackage(userChosenPackageName); if (userChosenProvider != null) { @@ -502,8 +495,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { mSystemInterface.getPackageInfoForProvider(userChosenProvider); if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, userChosenProvider); + mSystemInterface.getPackageInfoForProviderAllUsers(userChosenProvider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return packageInfo; } @@ -779,7 +771,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { pw.println(" WebView packages:"); for (WebViewProviderInfo provider : allProviders) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider); + mSystemInterface.getPackageInfoForProviderAllUsers(provider); PackageInfo systemUserPackageInfo = userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo(); if (systemUserPackageInfo == null) { @@ -798,8 +790,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { if (validity == VALIDITY_OK) { boolean installedForAllUsers = isInstalledAndEnabledForAllUsers( - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, provider)); + mSystemInterface.getPackageInfoForProviderAllUsers(provider)); pw.println( TextUtils.formatSimple( " Valid package %s (%s) is %s installed/enabled for all users", diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index fa6ac651a059..400919a88b1f 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2867,7 +2867,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mStartingData != null) { if (mStartingData.mAssociatedTask != null) { // The snapshot type may have called associateStartingDataWithTask(). - attachStartingSurfaceToAssociatedTask(); + // If this activity is rotated, don't attach to task to preserve the transform. + if (!hasFixedRotationTransform()) { + attachStartingSurfaceToAssociatedTask(); + } } else if (isEmbedded()) { associateStartingWindowWithTaskIfNeeded(); } @@ -2898,6 +2901,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A || mStartingData.mAssociatedTask != null) { return; } + if (task.isVisible() && !task.inTransition()) { + // Don't associated with task if the task is visible especially when the activity is + // embedded. We just need to show splash screen on the activity in case the first frame + // is not ready. + return; + } associateStartingDataWithTask(); attachStartingSurfaceToAssociatedTask(); } diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index 1f341147deb1..e0c0c2c60123 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -22,7 +22,7 @@ import android.os.SystemProperties; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; +import com.android.server.wm.utils.DesktopModeFlagsUtil; /** * Constants for desktop mode feature @@ -35,8 +35,8 @@ public final class DesktopModeHelper { "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); /** Whether desktop mode is enabled. */ - static boolean isDesktopModeEnabled() { - return Flags.enableDesktopWindowingMode(); + static boolean isDesktopModeEnabled(@NonNull Context context) { + return DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE.isEnabled(context); } /** @@ -60,7 +60,7 @@ public final class DesktopModeHelper { * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return isDesktopModeEnabled() + return isDesktopModeEnabled(context) && (!shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context)); } } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 348384203fba..dcadb0f31085 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -397,9 +397,11 @@ class InsetsStateController { onRequestedVisibleTypesChanged(newControlTargets.valueAt(i)); } newControlTargets.clear(); - // Check for and try to run the scheduled show IME request (if it exists), as we - // now applied the surface transaction and notified the target of the new control. - getImeSourceProvider().checkAndStartShowImePostLayout(); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + // Check for and try to run the scheduled show IME request (if it exists), as we + // now applied the surface transaction and notified the target of the new control. + getImeSourceProvider().checkAndStartShowImePostLayout(); + } }); } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 60d3e787cac4..12c50739f66b 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.res.Configuration.ASSETS_SEQ_UNDEFINED; import static android.os.Build.VERSION_CODES.Q; @@ -73,6 +74,7 @@ import android.os.LocaleList; import android.os.Message; import android.os.Process; import android.os.RemoteException; +import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Log; @@ -112,6 +114,13 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio private static final String TAG_RELEASE = TAG + POSTFIX_RELEASE; private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION; + /** + * The max number of processes which can be top scheduling group if there are non-top visible + * freeform activities run in the process. + */ + private static final int MAX_NUM_PERCEPTIBLE_FREEFORM = + SystemProperties.getInt("persist.wm.max_num_perceptible_freeform", 1); + private static final int MAX_RAPID_ACTIVITY_LAUNCH_COUNT = 200; private static final long RAPID_ACTIVITY_LAUNCH_MS = 500; private static final long RESET_RAPID_ACTIVITY_LAUNCH_MS = 3 * RAPID_ACTIVITY_LAUNCH_MS; @@ -318,6 +327,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio public static final int ACTIVITY_STATE_FLAG_HAS_RESUMED = 1 << 21; public static final int ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK = 1 << 22; public static final int ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN = 1 << 23; + public static final int ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM = 1 << 24; public static final int ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER = 0x0000ffff; /** @@ -1229,6 +1239,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio ActivityRecord.State bestInvisibleState = DESTROYED; boolean allStoppingFinishing = true; boolean visible = false; + boolean hasResumedFreeform = false; int minTaskLayer = Integer.MAX_VALUE; int stateFlags = 0; final boolean wasResumed = hasResumedActivity(); @@ -1256,6 +1267,8 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio .processPriorityPolicyForMultiWindowMode() && task.getAdjacentTask() != null) { stateFlags |= ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN; + } else if (windowingMode == WINDOWING_MODE_FREEFORM) { + hasResumedFreeform = true; } } if (minTaskLayer > 0) { @@ -1289,6 +1302,12 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } + if (hasResumedFreeform + && com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode() + // Exclude task layer 1 because it is already the top most. + && minTaskLayer > 1 && minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) { + stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM; + } stateFlags |= minTaskLayer & ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER; if (visible) { stateFlags |= ACTIVITY_STATE_FLAG_IS_VISIBLE; @@ -2105,6 +2124,9 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio if ((stateFlags & ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN) != 0) { pw.print("RS|"); } + if ((stateFlags & ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) { + pw.print("PF|"); + } } } else if ((stateFlags & ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED) != 0) { pw.print("P|"); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 9ebb89dfe9b6..a36cff6d7bc5 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -4648,14 +4648,16 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (!isImeLayeringTarget()) { return false; } - // Note that we don't process IME window if the IME input target is not on the screen. - // In case some unexpected IME visibility cases happen like starting the remote - // animation on the keyguard but seeing the IME window that originally on the app - // which behinds the keyguard. - final WindowState imeInputTarget = getImeInputTarget(); - if (imeInputTarget != null - && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) { - return false; + if (!com.android.window.flags.Flags.doNotSkipImeByTargetVisibility()) { + // Note that we don't process IME window if the IME input target is not on the screen. + // In case some unexpected IME visibility cases happen like starting the remote + // animation on the keyguard but seeing the IME window that originally on the app + // which behinds the keyguard. + final WindowState imeInputTarget = getImeInputTarget(); + if (imeInputTarget != null + && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) { + return false; + } } return mDisplayContent.forAllImeWindows(callback, traverseTopToBottom); } @@ -5504,7 +5506,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override public SurfaceControl getAnimationLeashParent() { - if (isStartingWindowAssociatedToTask()) { + if (mActivityRecord != null && !mActivityRecord.hasFixedRotationTransform() + && isStartingWindowAssociatedToTask()) { return mStartingData.mAssociatedTask.mSurfaceControl; } return super.getAnimationLeashParent(); diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java new file mode 100644 index 000000000000..4211764085b1 --- /dev/null +++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.utils; + +import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET; + +import android.annotation.Nullable; +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import com.android.window.flags.Flags; + +import java.util.function.Supplier; + +/** + * Util to check desktop mode flags state. + * + * This utility is used to allow developer option toggles to override flags related to desktop + * windowing. + * + * Computes whether Desktop Windowing related flags should be enabled by using the aconfig flag + * value and the developer option override state (if applicable). + * + * This is a partial copy of {@link com.android.wm.shell.shared.desktopmode.DesktopModeFlags} which + * is to be used in WM core. + */ +public enum DesktopModeFlagsUtil { + // All desktop mode related flags to be overridden by developer option toggle will be added here + DESKTOP_WINDOWING_MODE( + Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true), + WALLPAPER_ACTIVITY( + Flags::enableDesktopWindowingWallpaperActivity, /* shouldOverrideByDevOption= */ true); + + private static final String TAG = "DesktopModeFlagsUtil"; + private static final String SYSTEM_PROPERTY_OVERRIDE_KEY = + "sys.wmshell.desktopmode.dev_toggle_override"; + + // Function called to obtain aconfig flag value. + private final Supplier<Boolean> mFlagFunction; + // Whether the flag state should be affected by developer option. + private final boolean mShouldOverrideByDevOption; + + // Local cache for toggle override, which is initialized once on its first access. It needs to + // be refreshed only on reboots as overridden state takes effect on reboots. + private static ToggleOverride sCachedToggleOverride; + + DesktopModeFlagsUtil(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) { + this.mFlagFunction = flagFunction; + this.mShouldOverrideByDevOption = shouldOverrideByDevOption; + } + + /** + * Determines state of flag based on the actual flag and desktop mode developer option + * overrides. + * + * Note: this method makes sure that a constant developer toggle overrides is read until + * reboot. + */ + public boolean isEnabled(Context context) { + if (!Flags.showDesktopWindowingDevOption() + || !mShouldOverrideByDevOption + || context.getContentResolver() == null) { + return mFlagFunction.get(); + } else { + boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode(); + return switch (getToggleOverride(context)) { + case OVERRIDE_UNSET -> mFlagFunction.get(); + // When toggle override matches its default state, don't override flags. This + // helps users reset their feature overrides. + case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && mFlagFunction.get(); + case OVERRIDE_ON -> shouldToggleBeEnabledByDefault ? mFlagFunction.get() : true; + }; + } + } + + private ToggleOverride getToggleOverride(Context context) { + // If cached, return it + if (sCachedToggleOverride != null) { + return sCachedToggleOverride; + } + + // Otherwise, fetch and cache it + ToggleOverride override = getToggleOverrideFromSystem(context); + sCachedToggleOverride = override; + Log.d(TAG, "Toggle override initialized to: " + override); + return override; + } + + /** + * Returns {@link ToggleOverride} from a non-persistent system property if present. Otherwise + * initializes the system property by reading Settings.Global. + */ + private ToggleOverride getToggleOverrideFromSystem(Context context) { + // A non-persistent System Property is used to store override to ensure it remains + // constant till reboot. + String overrideProperty = System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null); + ToggleOverride overrideFromSystemProperties = convertToToggleOverride(overrideProperty); + + // If valid system property, return it + if (overrideFromSystemProperties != null) { + return overrideFromSystemProperties; + } + + // Fallback when System Property is not present (just after reboot) or not valid (user + // manually changed the value): Read from Settings.Global + int settingValue = Settings.Global.getInt( + context.getContentResolver(), + Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, + OVERRIDE_UNSET.getSetting() + ); + ToggleOverride overrideFromSettingsGlobal = + ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET); + // Initialize System Property + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(settingValue)); + return overrideFromSettingsGlobal; + } + + /** + * Converts {@code intString} into {@link ToggleOverride}. Return {@code null} if + * {@code intString} does not correspond to a {@link ToggleOverride}. + */ + private static @Nullable ToggleOverride convertToToggleOverride( + @Nullable String intString + ) { + if (intString == null) return null; + try { + int intValue = Integer.parseInt(intString); + return ToggleOverride.fromSetting(intValue, null); + } catch (NumberFormatException e) { + Log.w(TAG, "Unknown toggleOverride int " + intString); + return null; + } + } + + /** Override state of desktop mode developer option toggle. */ + enum ToggleOverride { + OVERRIDE_UNSET, + OVERRIDE_OFF, + OVERRIDE_ON; + + int getSetting() { + return switch (this) { + case OVERRIDE_ON -> 1; + case OVERRIDE_OFF -> 0; + case OVERRIDE_UNSET -> -1; + }; + } + + static ToggleOverride fromSetting(int setting, @Nullable ToggleOverride fallback) { + return switch (setting) { + case 1 -> OVERRIDE_ON; + case 0 -> OVERRIDE_OFF; + case -1 -> OVERRIDE_UNSET; + default -> fallback; + }; + } + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index dc8cec91001b..6a0dd5a04f82 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -182,6 +182,7 @@ class ActiveAdmin { private static final String TAG_CREDENTIAL_MANAGER_POLICY = "credential-manager-policy"; private static final String TAG_DIALER_PACKAGE = "dialer_package"; private static final String TAG_SMS_PACKAGE = "sms_package"; + private static final String TAG_PROVISIONING_CONTEXT = "provisioning-context"; // If the ActiveAdmin is a permission-based admin, then info will be null because the // permission-based admin is not mapped to a device administrator component. @@ -359,6 +360,8 @@ class ActiveAdmin { int mWifiMinimumSecurityLevel = DevicePolicyManager.WIFI_SECURITY_OPEN; String mDialerPackage; String mSmsPackage; + private String mProvisioningContext; + private static final int PROVISIONING_CONTEXT_LENGTH_LIMIT = 1000; ActiveAdmin(DeviceAdminInfo info, boolean isParent) { this.userId = -1; @@ -404,6 +407,23 @@ class ActiveAdmin { return UserHandle.of(UserHandle.getUserId(info.getActivityInfo().applicationInfo.uid)); } + /** + * Stores metadata about context of setting an active admin + * @param provisioningContext some metadata, for example test method name + */ + public void setProvisioningContext(@Nullable String provisioningContext) { + if (Flags.provisioningContextParameter() + && !TextUtils.isEmpty(provisioningContext) + && !provisioningContext.isBlank()) { + if (provisioningContext.length() > PROVISIONING_CONTEXT_LENGTH_LIMIT) { + mProvisioningContext = provisioningContext.substring( + 0, PROVISIONING_CONTEXT_LENGTH_LIMIT); + } else { + mProvisioningContext = provisioningContext; + } + } + } + void writeToXml(TypedXmlSerializer out) throws IllegalArgumentException, IllegalStateException, IOException { if (info != null) { @@ -694,6 +714,12 @@ class ActiveAdmin { if (!TextUtils.isEmpty(mSmsPackage)) { writeAttributeValueToXml(out, TAG_SMS_PACKAGE, mSmsPackage); } + + if (Flags.provisioningContextParameter() && !TextUtils.isEmpty(mProvisioningContext)) { + out.startTag(null, TAG_PROVISIONING_CONTEXT); + out.attribute(null, ATTR_VALUE, mProvisioningContext); + out.endTag(null, TAG_PROVISIONING_CONTEXT); + } } private void writePackagePolicy(TypedXmlSerializer out, String tag, @@ -1006,6 +1032,9 @@ class ActiveAdmin { mDialerPackage = parser.getAttributeValue(null, ATTR_VALUE); } else if (TAG_SMS_PACKAGE.equals(tag)) { mSmsPackage = parser.getAttributeValue(null, ATTR_VALUE); + } else if (Flags.provisioningContextParameter() + && TAG_PROVISIONING_CONTEXT.equals(tag)) { + mProvisioningContext = parser.getAttributeValue(null, ATTR_VALUE); } else { Slogf.w(LOG_TAG, "Unknown admin tag: %s", tag); XmlUtils.skipCurrentTag(parser); @@ -1496,5 +1525,10 @@ class ActiveAdmin { pw.println(mDialerPackage); pw.print("mSmsPackage="); pw.println(mSmsPackage); + + if (Flags.provisioningContextParameter()) { + pw.print("mProvisioningContext="); + pw.println(mProvisioningContext); + } } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 032d6b56af1b..8cc7383b9bf6 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3943,10 +3943,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { /** * @param adminReceiver The admin to add * @param refreshing true = update an active admin, no error + * @param userHandle which user this admin will be set on + * @param provisioningContext additional information for debugging */ @Override public void setActiveAdmin( - ComponentName adminReceiver, boolean refreshing, int userHandle) { + ComponentName adminReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { if (!mHasFeature) { return; } @@ -3972,6 +3978,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { newAdmin.testOnlyAdmin = (existingAdmin != null) ? existingAdmin.testOnlyAdmin : isPackageTestOnly(adminReceiver.getPackageName(), userHandle); + newAdmin.setProvisioningContext(provisioningContext); policy.mAdminMap.put(adminReceiver, newAdmin); int replaceIndex = -1; final int N = policy.mAdminList.size(); @@ -12830,7 +12837,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { }); // Set admin. - setActiveAdmin(profileOwner, /* refreshing= */ true, userId); + setActiveAdmin(profileOwner, /* refreshing= */ true, userId, null); setProfileOwner(profileOwner, userId); synchronized (getLockObject()) { @@ -21883,7 +21890,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @UserIdInt int userId, @UserIdInt int callingUserId, ComponentName adminComponent) { final String adminPackage = adminComponent.getPackageName(); enablePackage(adminPackage, callingUserId); - setActiveAdmin(adminComponent, /* refreshing= */ true, userId); + setActiveAdmin(adminComponent, /* refreshing= */ true, userId, null); } private void enablePackage(String packageName, @UserIdInt int userId) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java index eb893fcfee1f..0cd5b47b75c0 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java @@ -17,6 +17,7 @@ package com.android.server.devicepolicy; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.content.ComponentName; import android.os.ShellCommand; import android.os.SystemClock; @@ -46,11 +47,13 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private static final String USER_OPTION = "--user"; private static final String DO_ONLY_OPTION = "--device-owner-only"; + private static final String PROVISIONING_CONTEXT_OPTION = "--provisioning-context"; private final DevicePolicyManagerService mService; private int mUserId = UserHandle.USER_SYSTEM; private ComponentName mComponent; private boolean mSetDoOnly; + private String mProvisioningContext = null; DevicePolicyManagerServiceShellCommand(DevicePolicyManagerService service) { mService = Objects.requireNonNull(service); @@ -127,15 +130,28 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { pw.printf(" Lists the device / profile owners per user \n\n"); pw.printf(" %s\n", CMD_LIST_POLICY_EXEMPT_APPS); pw.printf(" Lists the apps that are exempt from policies\n\n"); - pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", - CMD_SET_ACTIVE_ADMIN, USER_OPTION); - pw.printf(" Sets the given component as active admin for an existing user.\n\n"); - pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" - + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION); - pw.printf(" Sets the given component as active admin, and its package as device owner." - + "\n\n"); - pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", - CMD_SET_PROFILE_OWNER, USER_OPTION); + if (Flags.provisioningContextParameter()) { + pw.printf(" %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_ACTIVE_ADMIN, USER_OPTION, PROVISIONING_CONTEXT_OPTION); + pw.printf(" Sets the given component as active admin for an existing user.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" + + " [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION, PROVISIONING_CONTEXT_OPTION); + pw.printf(" Sets the given component as active admin, and its package as device" + + " owner.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_PROFILE_OWNER, USER_OPTION, PROVISIONING_CONTEXT_OPTION); + } else { + pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", + CMD_SET_ACTIVE_ADMIN, USER_OPTION); + pw.printf(" Sets the given component as active admin for an existing user.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" + + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION); + pw.printf(" Sets the given component as active admin, and its package as device" + + " owner.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", + CMD_SET_PROFILE_OWNER, USER_OPTION); + } pw.printf(" Sets the given component as active admin and profile owner for an existing " + "user.\n\n"); pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", @@ -243,7 +259,7 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private int runSetActiveAdmin(PrintWriter pw) { parseArgs(); - mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId); + mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext); pw.printf("Success: Active admin set to component %s\n", mComponent.flattenToShortString()); return 0; @@ -253,7 +269,12 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { parseArgs(); boolean isAdminAdded = false; try { - mService.setActiveAdmin(mComponent, /* refreshing= */ false, mUserId); + mService.setActiveAdmin( + mComponent, + /* refreshing= */ false, + mUserId, + mProvisioningContext + ); isAdminAdded = true; } catch (IllegalArgumentException e) { pw.printf("%s was already an admin for user %d. No need to set it again.\n", @@ -291,7 +312,7 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private int runSetProfileOwner(PrintWriter pw) { parseArgs(); - mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId); + mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext); try { if (!mService.setProfileOwner(mComponent, mUserId)) { @@ -363,6 +384,8 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { } } else if (DO_ONLY_OPTION.equals(opt)) { mSetDoOnly = true; + } else if (PROVISIONING_CONTEXT_OPTION.equals(opt)) { + mProvisioningContext = getNextArgRequired(); } else { throw new IllegalArgumentException("Unknown option: " + opt); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index c2a069d17446..267ce26515d7 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -161,6 +161,7 @@ public class InputMethodManagerServiceTestBase { .spyStatic(InputMethodUtils.class) .mockStatic(ServiceManager.class) .spyStatic(AdditionalSubtypeMapRepository.class) + .spyStatic(AdditionalSubtypeUtils.class) .startMocking(); mContext = spy(InstrumentationRegistry.getInstrumentation().getContext()); @@ -235,6 +236,7 @@ public class InputMethodManagerServiceTestBase { // The background writer thread in AdditionalSubtypeMapRepository should be stubbed out. doNothing().when(AdditionalSubtypeMapRepository::startWriterThread); + doReturn(AdditionalSubtypeMap.EMPTY_MAP).when(() -> AdditionalSubtypeUtils.load(anyInt())); mServiceThread = new ServiceThread( @@ -267,6 +269,10 @@ public class InputMethodManagerServiceTestBase { LocalServices.removeServiceForTest(InputMethodManagerInternal.class); lifecycle.onStart(); + // Emulate that the user initialization is done. + AdditionalSubtypeMapRepository.ensureInitializedAndGet(mCallingUserId); + mInputMethodManagerService.getUserData(mCallingUserId).mBackgroundLoadLatch.countDown(); + // After this boot phase, services can broadcast Intents. lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 624c8971d36f..c6aea5a290e9 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -2137,6 +2137,14 @@ public final class DisplayPowerControllerTest { private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock, DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock, boolean isEnabled) { + + setUpDisplay(displayId, uniqueId, logicalDisplayMock, displayDeviceMock, + displayDeviceConfigMock, isEnabled, "display_name"); + } + + private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock, + DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock, + boolean isEnabled, String displayName) { DisplayInfo info = new DisplayInfo(); DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo(); deviceInfo.uniqueId = uniqueId; @@ -2148,6 +2156,7 @@ public final class DisplayPowerControllerTest { when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false); when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo); when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId); + when(displayDeviceMock.getNameLocked()).thenReturn(displayName); when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock); when(displayDeviceConfigMock.getProximitySensor()).thenReturn( new SensorData(Sensor.STRING_TYPE_PROXIMITY, null)); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java index 397d77c52f68..26f6e91d29c8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java @@ -43,10 +43,14 @@ public final class BrightnessEventTest { mBrightnessEvent = new BrightnessEvent(1); mBrightnessEvent.setReason( getReason(BrightnessReason.REASON_DOZE, BrightnessReason.MODIFIER_LOW_POWER)); - mBrightnessEvent.setPhysicalDisplayId("test"); + mBrightnessEvent.setPhysicalDisplayId("987654321"); + mBrightnessEvent.setPhysicalDisplayName("display_name"); mBrightnessEvent.setDisplayState(Display.STATE_ON); mBrightnessEvent.setDisplayPolicy(POLICY_BRIGHT); mBrightnessEvent.setLux(100.0f); + mBrightnessEvent.setPercent(46.5f); + mBrightnessEvent.setNits(893.8f); + mBrightnessEvent.setUnclampedBrightness(0.65f); mBrightnessEvent.setPreThresholdLux(150.0f); mBrightnessEvent.setTime(System.currentTimeMillis()); mBrightnessEvent.setInitialBrightness(25.0f); @@ -77,12 +81,13 @@ public final class BrightnessEventTest { public void testToStringWorksAsExpected() { String actualString = mBrightnessEvent.toString(false); String expectedString = - "BrightnessEvent: disp=1, physDisp=test, displayState=ON, displayPolicy=BRIGHT," - + " brt=0.6, initBrt=25.0, rcmdBrt=0.6, preBrt=NaN, lux=100.0, preLux=150.0," - + " hbmMax=0.62, hbmMode=off, rbcStrength=-1, thrmMax=0.65, powerFactor=0.2," - + " wasShortTermModelActive=true, flags=, reason=doze [ low_pwr ]," - + " autoBrightness=true, strategy=" + DISPLAY_BRIGHTNESS_STRATEGY_NAME - + ", autoBrightnessMode=idle"; + "BrightnessEvent: brt=0.6 (46.5%), nits= 893.8, lux=100.0, reason=doze [ " + + "low_pwr ], strat=strategy_name, state=ON, policy=BRIGHT, flags=, " + + "initBrt=25.0, rcmdBrt=0.6, preBrt=NaN, preLux=150.0, " + + "wasShortTermModelActive=true, autoBrightness=true (idle), " + + "unclampedBrt=0.65, hbmMax=0.62, hbmMode=off, thrmMax=0.65, " + + "rbcStrength=-1, powerFactor=0.2, physDisp=display_name(987654321), " + + "logicalId=1"; assertEquals(expectedString, actualString); } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 1dbd5320cac6..8656b991b5fc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -500,6 +500,13 @@ public class MockingOomAdjusterTests { updateOomAdj(app); assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP); assertEquals("resumed-split-screen-activity", app.mState.getAdjType()); + + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE + | WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) + .when(wpc).getActivityStateFlags(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP); + assertEquals("perceptible-freeform-activity", app.mState.getAdjType()); } @SuppressWarnings("GuardedBy") diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java index 758c84a26dcd..ef9580c54de6 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java @@ -101,7 +101,7 @@ public class AbsoluteVolumeBehaviorTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)) { + mock(AudioServerPermissionProvider.class), r -> r.run()) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java index 2cb02bdd2806..464515632997 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java @@ -78,7 +78,7 @@ public class AudioDeviceVolumeManagerTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)) { + mock(AudioServerPermissionProvider.class), r -> r.run()) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java index 037c3c00443c..b7100ea00a40 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java @@ -87,7 +87,7 @@ public class AudioServiceTest { .thenReturn(AppOpsManager.MODE_ALLOWED); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null, - mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider); + mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider, r -> r.run()); } /** diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java index 27b552fa7cdd..746645a8c585 100644 --- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java @@ -78,7 +78,7 @@ public class DeviceVolumeBehaviorTest { mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)); + mock(AudioServerPermissionProvider.class), r -> r.run()); mTestLooper.dispatchAll(); } diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index 8e34ee1b6a42..e45ab319146c 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -160,7 +160,7 @@ public class VolumeHelperTest { @NonNull PermissionEnforcer enforcer, AudioServerPermissionProvider permissionProvider) { super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, appOps, enforcer, permissionProvider); + audioPolicy, looper, appOps, enforcer, permissionProvider, r -> r.run()); } public void setDeviceForStream(int stream, int device) { diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java index 8a9538f2374a..ebdde94237eb 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java @@ -66,7 +66,7 @@ public class NetworkEventTest extends DpmTestBase { any(UserHandle.class)); mDpmTestable = new DevicePolicyManagerServiceTestable(getServices(), mSpiedDpmMockContext); setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_UID); - mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE); + mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE, null); } @Test diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java index 54d11387752c..cbf79356e9c4 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java +++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java @@ -16,7 +16,6 @@ package com.android.server.webkit; -import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -66,10 +65,12 @@ public class TestSystemImpl implements SystemInterface { } @Override - public String getUserChosenWebViewProvider(Context context) { return mUserProvider; } + public String getUserChosenWebViewProvider() { + return mUserProvider; + } @Override - public void updateUserSetting(Context context, String newProviderName) { + public void updateUserSetting(String newProviderName) { mUserProvider = newProviderName; } @@ -77,14 +78,14 @@ public class TestSystemImpl implements SystemInterface { public void killPackageDependents(String packageName) {} @Override - public void enablePackageForAllUsers(Context context, String packageName, boolean enable) { + public void enablePackageForAllUsers(String packageName, boolean enable) { for(int userId : mUsers) { enablePackageForUser(packageName, enable, userId); } } @Override - public void installExistingPackageForAllUsers(Context context, String packageName) { + public void installExistingPackageForAllUsers(String packageName) { for (int userId : mUsers) { installPackageForUser(packageName, userId); } @@ -131,8 +132,7 @@ public class TestSystemImpl implements SystemInterface { } @Override - public List<UserPackage> getPackageInfoForProviderAllUsers( - Context context, WebViewProviderInfo info) { + public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo info) { Map<Integer, PackageInfo> userPackages = mPackages.get(info.packageName); List<UserPackage> ret = new ArrayList(); // Loop over defined users, and find the corresponding package for each user. @@ -185,12 +185,12 @@ public class TestSystemImpl implements SystemInterface { } @Override - public int getMultiProcessSetting(Context context) { + public int getMultiProcessSetting() { return mMultiProcessSetting; } @Override - public void setMultiProcessSetting(Context context, int value) { + public void setMultiProcessSetting(int value) { mMultiProcessSetting = value; } diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java index e181a513b637..06479c84bfc7 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java @@ -104,10 +104,10 @@ public class WebViewUpdateServiceTest { mTestSystemImpl = Mockito.spy(testing); if (updateServiceV2()) { mWebViewUpdateServiceImpl = - new WebViewUpdateServiceImpl2(null /*Context*/, mTestSystemImpl); + new WebViewUpdateServiceImpl2(mTestSystemImpl); } else { mWebViewUpdateServiceImpl = - new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl); + new WebViewUpdateServiceImpl(mTestSystemImpl); } } @@ -140,7 +140,7 @@ public class WebViewUpdateServiceTest { WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) { setupWithPackagesAndRelroCount(webviewPackages, numRelros); if (userSetting != null) { - mTestSystemImpl.updateUserSetting(null, userSetting); + mTestSystemImpl.updateUserSetting(userSetting); } // Add (enabled and valid) package infos for each provider setEnabledAndValidPackageInfos(webviewPackages); @@ -313,7 +313,7 @@ public class WebViewUpdateServiceTest { }; setupWithPackagesNonDebuggable(packages); // Start with the setting pointing to the invalid package - mTestSystemImpl.updateUserSetting(null, invalidPackage); + mTestSystemImpl.updateUserSetting(invalidPackage); mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */, true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature} , 0 /* updateTime */)); @@ -481,7 +481,7 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo(secondPackage, "", true, false, null)}; setupWithPackages(packages); // Start with the setting pointing to the second package - mTestSystemImpl.updateUserSetting(null, secondPackage); + mTestSystemImpl.updateUserSetting(secondPackage); // Have all packages be enabled, so that we can change provider however we want to setEnabledAndValidPackageInfos(packages); @@ -572,7 +572,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic re-enables the fallback package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl).enablePackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true)); + Mockito.eq(testPackage), Mockito.eq(true)); // Fake the message about the enabling having changed the package state, // and check we now use that package. @@ -657,7 +657,7 @@ public class WebViewUpdateServiceTest { null)}; setupWithPackages(packages); // Start with the setting pointing to the secondary package - mTestSystemImpl.updateUserSetting(null, secondaryPackage); + mTestSystemImpl.updateUserSetting(secondaryPackage); int secondaryUserId = 10; int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID; if (multiUser) { @@ -710,7 +710,7 @@ public class WebViewUpdateServiceTest { null)}; setupWithPackages(packages); // Start with the setting pointing to the secondary package - mTestSystemImpl.updateUserSetting(null, secondaryPackage); + mTestSystemImpl.updateUserSetting(secondaryPackage); setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); int newUser = 100; mTestSystemImpl.addUser(newUser); @@ -832,14 +832,13 @@ public class WebViewUpdateServiceTest { true /* installed */)); // Set user-chosen package - mTestSystemImpl.updateUserSetting(null, chosenPackage); + mTestSystemImpl.updateUserSetting(chosenPackage); runWebViewBootPreparationOnMainSync(); // Verify that we switch the setting to point to the current package - Mockito.verify(mTestSystemImpl).updateUserSetting( - Mockito.anyObject(), Mockito.eq(nonChosenPackage)); - assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider(null)); + Mockito.verify(mTestSystemImpl).updateUserSetting(Mockito.eq(nonChosenPackage)); + assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider()); checkPreparationPhasesForPackage(nonChosenPackage, 1); } @@ -976,7 +975,7 @@ public class WebViewUpdateServiceTest { setEnabledAndValidPackageInfos(packages); // Start with the setting pointing to the third package - mTestSystemImpl.updateUserSetting(null, thirdPackage); + mTestSystemImpl.updateUserSetting(thirdPackage); runWebViewBootPreparationOnMainSync(); checkPreparationPhasesForPackage(thirdPackage, 1); @@ -1167,7 +1166,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 5; if (multiUser) { mTestSystemImpl.addUser(secondaryUserId); @@ -1220,7 +1219,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 412; mTestSystemImpl.addUser(secondaryUserId); @@ -1277,7 +1276,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 4; mTestSystemImpl.addUser(secondaryUserId); @@ -1290,7 +1289,7 @@ public class WebViewUpdateServiceTest { 0 /* updateTime */, (testHidden ? true : false) /* hidden */)); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); runWebViewBootPreparationOnMainSync(); @@ -1458,7 +1457,7 @@ public class WebViewUpdateServiceTest { runWebViewBootPreparationOnMainSync(); checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */); - mTestSystemImpl.setMultiProcessSetting(null /* context */, settingValue); + mTestSystemImpl.setMultiProcessSetting(settingValue); assertEquals(expectEnabled, mWebViewUpdateServiceImpl.isMultiProcessEnabled()); } @@ -1492,7 +1491,7 @@ public class WebViewUpdateServiceTest { }; setupWithPackages(packages); // Start with the setting pointing to the invalid package - mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName); + mTestSystemImpl.updateUserSetting(oldSdkPackage.packageName); mTestSystemImpl.setPackageInfo(newSdkPackage); mTestSystemImpl.setPackageInfo(currentSdkPackage); @@ -1545,8 +1544,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic re-enables the default package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl) - .enablePackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true)); + .enablePackageForAllUsers(Mockito.eq(testPackage), Mockito.eq(true)); } @Test @@ -1570,8 +1568,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic tries to install the default package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } @Test @@ -1598,8 +1595,7 @@ public class WebViewUpdateServiceTest { // Check that we try to re-install the default package. Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } /** @@ -1632,8 +1628,7 @@ public class WebViewUpdateServiceTest { // Check that we try to re-install the default package for all users. Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } private void testDefaultPackageChosen(PackageInfo packageInfo) { 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 b4505fad1b20..24fc7ee0c392 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2955,7 +2955,8 @@ public class ActivityRecordTests extends WindowTestsBase { @Test public void testStartingWindowInTaskFragment() { - final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true) + .setVisible(false).build(); final WindowState startingWindow = createWindowState( new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1); activity1.addWindow(startingWindow); @@ -3011,6 +3012,28 @@ public class ActivityRecordTests extends WindowTestsBase { } @Test + public void testStartingWindowInTaskFragmentWithVisibleTask() { + final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Task task = activity1.getTask(); + final Rect taskBounds = task.getBounds(); + final Rect tfBounds = new Rect(taskBounds.left, taskBounds.top, + taskBounds.left + taskBounds.width() / 2, taskBounds.bottom); + final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm).setParentTask(task) + .setBounds(tfBounds).build(); + + final ActivityRecord activity2 = new ActivityBuilder(mAtm).build(); + final WindowState startingWindow = createWindowState( + new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1); + taskFragment.addChild(activity2); + activity2.addWindow(startingWindow); + activity2.mStartingData = mock(StartingData.class); + activity2.attachStartingWindow(startingWindow); + + assertNull(activity2.mStartingData.mAssociatedTask); + assertNull(task.mSharedStartingData); + } + + @Test public void testTransitionAnimationBounds() { removeGlobalMinSizeRestriction(); final Task task = new TaskBuilder(mSupervisor) diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index 0bf27d11493b..f93ffb83178f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -747,6 +747,8 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { } } + @android.platform.test.annotations.RequiresFlagsDisabled( + com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY) @SetupWindows(addWindows = W_INPUT_METHOD) @Test public void testLaunchRemoteAnimationWithoutImeBehind() { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java index 593e983f3d23..22def515a98e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.eq; import android.platform.test.annotations.Presubmit; @@ -60,4 +61,24 @@ public class WindowContainerTraversalTests extends WindowTestsBase { verify(c).accept(eq(mDockedDividerWindow)); verify(c).accept(eq(mImeWindow)); } + + @android.platform.test.annotations.RequiresFlagsEnabled( + com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY) + @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) + @Test + public void testTraverseImeRegardlessOfImeTarget() { + mDisplayContent.setImeLayeringTarget(mAppWindow); + mDisplayContent.setImeInputTarget(mAppWindow); + mAppWindow.mHasSurface = false; + mAppWindow.mActivityRecord.setVisibleRequested(false); + mAppWindow.mActivityRecord.setVisible(false); + + final boolean[] foundIme = { false }; + mDisplayContent.forAllWindows(w -> { + if (w == mImeWindow) { + foundIme[0] = true; + } + }, true /* traverseTopToBottom */); + assertTrue("IME must be found", foundIme[0]); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 41f1ac72c56d..ea2abf7ddcb8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -141,8 +141,8 @@ import java.util.HashMap; import java.util.List; /** Common base class for window manager unit test classes. */ -class WindowTestsBase extends SystemServiceTestsBase { - final Context mContext = getInstrumentation().getTargetContext(); +public class WindowTestsBase extends SystemServiceTestsBase { + protected final Context mContext = getInstrumentation().getTargetContext(); // Default package name static final String DEFAULT_COMPONENT_PACKAGE_NAME = "com.foo"; diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java new file mode 100644 index 000000000000..e5f2f89ccead --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.utils; + +import static com.android.server.wm.utils.DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE; +import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_OFF; +import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_ON; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; +import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.test.filters.SmallTest; + +import com.android.server.wm.WindowTestRunner; +import com.android.server.wm.WindowTestsBase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Field; + +/** + * Test class for [DesktopModeFlagsUtil] + * + * Build/Install/Run: + * atest WmTests:DesktopModeFlagsUtilTest + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DesktopModeFlagsUtilTest extends WindowTestsBase { + + @Rule + public SetFlagsRule setFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + resetCache(); + } + + private static final String SYSTEM_PROPERTY_OVERRIDE_KEY = + "sys.wmshell.desktopmode.dev_toggle_override"; + + @Test + @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_OFF.getSetting()); + // In absence of dev options, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + + @Test + @DisableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_ON.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For overridableFlag, for unset overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For overridableFlag, for unset overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_noOverride_featureFlagOn_returnsTrue() { + setOverride(null); + + // For overridableFlag, in absence of overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noOverride_featureFlagOff_returnsFalse() { + setOverride(null); + + // For overridableFlag, in absence of overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_unrecognizableOverride_featureFlagOn_returnsTrue() { + setOverride(-2); + + // For overridableFlag, for unrecognized overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_unrecognizableOverride_featureFlagOff_returnsFalse() { + setOverride(-2); + + // For overridableFlag, for unrecognizable overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideOff_featureFlagOn_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideOn_featureFlagOff_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + + setOverride(OVERRIDE_ON.getSetting()); + + // Keep overrides constant through the process + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + + setOverride(OVERRIDE_OFF.getSetting()); + + // Keep overrides constant through the process + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noProperty_overrideOn_featureFlagOff_returnsTrueAndPropertyOn() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(OVERRIDE_ON.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_noProperty_overrideUnset_featureFlagOn_returnsTrueAndPropertyUnset() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noProperty_overrideUnset_featureFlagOff_returnsFalseAndPropertyUnset() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyNotInt_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc"); + setOverride(OVERRIDE_OFF.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if currently invalid + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyInvalid_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2"); + setOverride(OVERRIDE_OFF.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if currently invalid + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyOff_overrideOn_featureFlagOn_returnsFalseAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf( + OVERRIDE_OFF.getSetting())); + setOverride(OVERRIDE_ON.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_propertyOn_overrideOff_featureFlagOff_returnsTrueAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(OVERRIDE_ON.getSetting())); + setOverride(OVERRIDE_OFF.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyUnset_overrideOff_featureFlagOn_returnsTrueAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, + String.valueOf(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + setOverride(OVERRIDE_OFF.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY}) + public void isEnabled_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOn_overrideOn_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideOn_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_ON.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOn_overrideOff_featureFlagOn_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideOn_featureFlagOff_returnTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_OFF.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + private void setOverride(Integer setting) { + ContentResolver contentResolver = mContext.getContentResolver(); + String key = Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES; + + if (setting == null) { + Settings.Global.putString(contentResolver, key, null); + } else { + Settings.Global.putInt(contentResolver, key, setting); + } + } + + private void resetCache() throws Exception { + Field cachedToggleOverride = DesktopModeFlagsUtil.class.getDeclaredField( + "sCachedToggleOverride"); + cachedToggleOverride.setAccessible(true); + cachedToggleOverride.set(null, null); + + // Clear override cache stored in System property + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + } +} diff --git a/telephony/OWNERS b/telephony/OWNERS index 7607c64150d8..92af034217a9 100644 --- a/telephony/OWNERS +++ b/telephony/OWNERS @@ -15,4 +15,4 @@ per-file CarrierConfigManager.java=set noparent per-file CarrierConfigManager.java=amruthr@google.com,tgunn@google.com,rgreenwalt@google.com,satk@google.com #Domain Selection is jointly owned, add additional owners for domain selection specific files -per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,forestchoi@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com +per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,jaesikkong@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp index e839fc1ceb0f..7739171b347f 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -137,22 +137,25 @@ bool ParseFeatureFlagsParameter(StringPiece arg, android::IDiagnostics* diag, diag->Error(android::DiagMessage() << "No name given for one or more flags in: " << arg); return false; } + std::vector<std::string> name_parts = util::Split(flag_name, ':'); if (name_parts.size() > 2) { diag->Error(android::DiagMessage() << "Invalid feature flag and optional value '" << flag_and_value - << "'. Must be in the format 'flag_name[:ro][=true|false]"); + << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]"); return false; } flag_name = name_parts[0]; bool read_only = false; if (name_parts.size() == 2) { - if (name_parts[1] == "ro") { + if (name_parts[1] == "ro" || name_parts[1] == "READ_ONLY") { read_only = true; + } else if (name_parts[1] == "READ_WRITE") { + read_only = false; } else { diag->Error(android::DiagMessage() << "Invalid feature flag and optional value '" << flag_and_value - << "'. Must be in the format 'flag_name[:ro][=true|false]"); + << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]"); return false; } } diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp index 35bc63714e58..78183409ad8f 100644 --- a/tools/aapt2/cmd/Util_test.cpp +++ b/tools/aapt2/cmd/Util_test.cpp @@ -383,7 +383,7 @@ TEST(UtilTest, ParseFeatureFlagsParameter_InvalidValue) { TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) { auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics(); FeatureFlagValues feature_flag_values; - ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar=true,foo:ro=false", diagnostics, + ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar:READ_WRITE=true,foo:ro=false", diagnostics, &feature_flag_values)); EXPECT_THAT( feature_flag_values, @@ -394,11 +394,11 @@ TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) { TEST(UtilTest, ParseFeatureFlagsParameter_Valid) { auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics(); FeatureFlagValues feature_flag_values; - ASSERT_TRUE(ParseFeatureFlagsParameter("foo= true, bar:ro =FALSE,baz=, quux", diagnostics, - &feature_flag_values)); + ASSERT_TRUE(ParseFeatureFlagsParameter("foo:READ_ONLY= true, bar:ro =FALSE,baz:READ_WRITE=, quux", + diagnostics, &feature_flag_values)); EXPECT_THAT( feature_flag_values, - UnorderedElementsAre(Pair("foo", FeatureFlagProperties{false, std::optional<bool>(true)}), + UnorderedElementsAre(Pair("foo", FeatureFlagProperties{true, std::optional<bool>(true)}), Pair("bar", FeatureFlagProperties{true, std::optional<bool>(false)}), Pair("baz", FeatureFlagProperties{false, std::nullopt}), Pair("quux", FeatureFlagProperties{false, std::nullopt}))); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java index 277a508ced57..5ecf5cf0b723 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java @@ -115,6 +115,9 @@ public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { /** Creates a {@link AppInfo} from the human-readable DOM element. */ public AppInfo createFromHrElement(Element appInfoEle, long version) throws MalformedXmlException { + if (appInfoEle == null) { + return null; + } XmlUtils.throwIfExtraneousAttributes( appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedHrAttrs, version)); XmlUtils.throwIfExtraneousChildrenHr( @@ -184,6 +187,9 @@ public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ public AppInfo createFromOdElement(Element appInfoEle, long version) throws MalformedXmlException { + if (appInfoEle == null) { + return null; + } XmlUtils.throwIfExtraneousChildrenOd( appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedOdEleNames, version)); var requiredOdEles = XmlUtils.getMostRecentVersion(mRequiredOdEles, version); diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java index 14e65e5e5b2b..e3aa50a4cee2 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java @@ -20,8 +20,11 @@ import com.android.asllib.marshallable.AndroidSafetyLabelTest; import com.android.asllib.marshallable.AppInfoTest; import com.android.asllib.marshallable.DataLabelsTest; import com.android.asllib.marshallable.DataTypeEqualityTest; +import com.android.asllib.marshallable.DeveloperInfoTest; import com.android.asllib.marshallable.SafetyLabelsTest; +import com.android.asllib.marshallable.SecurityLabelsTest; import com.android.asllib.marshallable.SystemAppSafetyLabelTest; +import com.android.asllib.marshallable.ThirdPartyVerificationTest; import com.android.asllib.marshallable.TransparencyInfoTest; import org.junit.runner.RunWith; @@ -36,6 +39,9 @@ import org.junit.runners.Suite; DataTypeEqualityTest.class, SafetyLabelsTest.class, SystemAppSafetyLabelTest.class, - TransparencyInfoTest.class + TransparencyInfoTest.class, + DeveloperInfoTest.class, + SecurityLabelsTest.class, + ThirdPartyVerificationTest.class }) public class AllTests {} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java index 283ccbc44791..6470c060af87 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java @@ -28,6 +28,7 @@ import org.junit.runners.JUnit4; import org.w3c.dom.Element; import java.nio.file.Paths; +import java.util.List; @RunWith(JUnit4.class) public class AndroidSafetyLabelTest { @@ -37,12 +38,16 @@ public class AndroidSafetyLabelTest { "com/android/asllib/androidsafetylabel/od"; private static final String MISSING_VERSION_FILE_NAME = "missing-version.xml"; - private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml"; + private static final String VALID_V2_FILE_NAME = "valid-empty.xml"; + private static final String VALID_V1_FILE_NAME = "valid-v1.xml"; private static final String WITH_SAFETY_LABELS_FILE_NAME = "with-safety-labels.xml"; private static final String WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME = "with-system-app-safety-label.xml"; private static final String WITH_TRANSPARENCY_INFO_FILE_NAME = "with-transparency-info.xml"; + public static final List<String> REQUIRED_FIELD_NAMES_OD_V2 = + List.of("system_app_safety_label", "transparency_info"); + @Before public void setUp() throws Exception { System.out.println("set up."); @@ -56,12 +61,12 @@ public class AndroidSafetyLabelTest { odToHrExpectException(MISSING_VERSION_FILE_NAME); } - /** Test for android safety label valid empty. */ + /** Test for android safety label valid v2. */ @Test - public void testAndroidSafetyLabelValidEmptyFile() throws Exception { - System.out.println("starting testAndroidSafetyLabelValidEmptyFile."); - testHrToOdAndroidSafetyLabel(VALID_EMPTY_FILE_NAME); - testOdToHrAndroidSafetyLabel(VALID_EMPTY_FILE_NAME); + public void testAndroidSafetyLabelValidV2File() throws Exception { + System.out.println("starting testAndroidSafetyLabelValidV2File."); + testHrToOdAndroidSafetyLabel(VALID_V2_FILE_NAME); + testOdToHrAndroidSafetyLabel(VALID_V2_FILE_NAME); } /** Test for android safety label with safety labels. */ @@ -72,6 +77,34 @@ public class AndroidSafetyLabelTest { testOdToHrAndroidSafetyLabel(WITH_SAFETY_LABELS_FILE_NAME); } + /** Tests missing required fields fails, V2. */ + @Test + public void testMissingRequiredFieldsOdV2() throws Exception { + for (String reqField : REQUIRED_FIELD_NAMES_OD_V2) { + System.out.println("testing missing required field od v2: " + reqField); + var ele = + TestUtils.getElementFromResource( + Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, VALID_V2_FILE_NAME)); + TestUtils.removeOdChildEleWithName(ele, reqField); + assertThrows( + MalformedXmlException.class, + () -> new AndroidSafetyLabelFactory().createFromOdElement(ele)); + } + } + + /** Tests missing optional fields succeeds, V1. */ + @Test + public void testMissingOptionalFieldsOdV1() throws Exception { + for (String reqField : REQUIRED_FIELD_NAMES_OD_V2) { + System.out.println("testing missing optional field od v1: " + reqField); + var ele = + TestUtils.getElementFromResource( + Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, VALID_V1_FILE_NAME)); + TestUtils.removeOdChildEleWithName(ele, reqField); + var unused = new AndroidSafetyLabelFactory().createFromOdElement(ele); + } + } + private void hrToOdExpectException(String fileName) { assertThrows( MalformedXmlException.class, diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java index b557fea9572b..cc58a61760f4 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java @@ -35,8 +35,6 @@ import javax.xml.parsers.ParserConfigurationException; @RunWith(JUnit4.class) public class DataLabelsTest { - private static final long DEFAULT_VERSION = 2L; - private static final String DATA_LABELS_HR_PATH = "com/android/asllib/datalabels/hr"; private static final String DATA_LABELS_OD_PATH = "com/android/asllib/datalabels/od"; diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java new file mode 100644 index 000000000000..a4472b1b78e5 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib.marshallable; + +import static org.junit.Assert.assertThrows; + +import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Element; + +import java.nio.file.Paths; +import java.util.List; + +@RunWith(JUnit4.class) +public class DeveloperInfoTest { + private static final String DEVELOPER_INFO_HR_PATH = "com/android/asllib/developerinfo/hr"; + private static final String DEVELOPER_INFO_OD_PATH = "com/android/asllib/developerinfo/od"; + public static final List<String> REQUIRED_FIELD_NAMES = + List.of("address", "countryRegion", "email", "name", "relationship"); + public static final List<String> REQUIRED_FIELD_NAMES_OD = + List.of("address", "country_region", "email", "name", "relationship"); + public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website", "registryId"); + public static final List<String> OPTIONAL_FIELD_NAMES_OD = + List.of("website", "app_developer_registry_id"); + + private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + } + + /** Test for all fields valid. */ + @Test + public void testAllFieldsValid() throws Exception { + System.out.println("starting testAllFieldsValid."); + testHrToOdDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME); + testOdToHrDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME); + } + + /** Tests missing required fields fails. */ + @Test + public void testMissingRequiredFields() throws Exception { + System.out.println("Starting testMissingRequiredFields"); + for (String reqField : REQUIRED_FIELD_NAMES) { + System.out.println("testing missing required field: " + reqField); + var developerInfoEle = + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + developerInfoEle.removeAttribute(reqField); + + assertThrows( + MalformedXmlException.class, + () -> new DeveloperInfoFactory().createFromHrElement(developerInfoEle)); + } + + for (String reqField : REQUIRED_FIELD_NAMES_OD) { + System.out.println("testing missing required field od: " + reqField); + var developerInfoEle = + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(developerInfoEle, reqField); + + assertThrows( + MalformedXmlException.class, + () -> new DeveloperInfoFactory().createFromOdElement(developerInfoEle)); + } + } + + /** Tests missing optional fields passes. */ + @Test + public void testMissingOptionalFields() throws Exception { + for (String optField : OPTIONAL_FIELD_NAMES) { + var developerInfoEle = + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + developerInfoEle.removeAttribute(optField); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromHrElement(developerInfoEle); + developerInfo.toOdDomElement(TestUtils.document()); + } + + for (String optField : OPTIONAL_FIELD_NAMES_OD) { + var developerInfoEle = + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(developerInfoEle, optField); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromOdElement(developerInfoEle); + developerInfo.toHrDomElement(TestUtils.document()); + } + } + + private void testHrToOdDeveloperInfo(String fileName) throws Exception { + var doc = TestUtils.document(); + DeveloperInfo developerInfo = + new DeveloperInfoFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_HR_PATH, fileName))); + Element developerInfoEle = developerInfo.toOdDomElement(doc); + doc.appendChild(developerInfoEle); + TestUtils.testFormatToFormat(doc, Paths.get(DEVELOPER_INFO_OD_PATH, fileName)); + } + + private void testOdToHrDeveloperInfo(String fileName) throws Exception { + var doc = TestUtils.document(); + DeveloperInfo developerInfo = + new DeveloperInfoFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_OD_PATH, fileName))); + Element developerInfoEle = developerInfo.toHrDomElement(doc); + doc.appendChild(developerInfoEle); + TestUtils.testFormatToFormat(doc, Paths.get(DEVELOPER_INFO_HR_PATH, fileName)); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java index 7cd510f0ddfc..fc8ff00794ad 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java @@ -26,13 +26,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.w3c.dom.Element; -import org.xml.sax.SAXException; -import java.io.IOException; import java.nio.file.Paths; -import javax.xml.parsers.ParserConfigurationException; - @RunWith(JUnit4.class) public class SafetyLabelsTest { private static final long DEFAULT_VERSION = 2L; @@ -42,6 +38,8 @@ public class SafetyLabelsTest { private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml"; private static final String WITH_DATA_LABELS_FILE_NAME = "with-data-labels.xml"; + private static final String VALID_V1_FILE_NAME = "valid-v1.xml"; + private static final String UNRECOGNIZED_FIELD_V2_FILE_NAME = "unrecognized-field-v2.xml"; @Before public void setUp() throws Exception { @@ -52,61 +50,59 @@ public class SafetyLabelsTest { @Test public void testSafetyLabelsValidEmptyFile() throws Exception { System.out.println("starting testSafetyLabelsValidEmptyFile."); - testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME); - testOdToHrSafetyLabels(VALID_EMPTY_FILE_NAME); + testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME, DEFAULT_VERSION); + testOdToHrSafetyLabels(VALID_EMPTY_FILE_NAME, DEFAULT_VERSION); } /** Test for safety labels with data labels. */ @Test public void testSafetyLabelsWithDataLabels() throws Exception { System.out.println("starting testSafetyLabelsWithDataLabels."); - testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME); - testOdToHrSafetyLabels(WITH_DATA_LABELS_FILE_NAME); + testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME, DEFAULT_VERSION); + testOdToHrSafetyLabels(WITH_DATA_LABELS_FILE_NAME, DEFAULT_VERSION); } - private void hrToOdExpectException(String fileName) - throws ParserConfigurationException, IOException, SAXException { - var safetyLabelsEle = - TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_HR_PATH, fileName)); - assertThrows( - MalformedXmlException.class, - () -> - new SafetyLabelsFactory() - .createFromHrElement(safetyLabelsEle, DEFAULT_VERSION)); + /** Tests valid fields v1. */ + @Test + public void testValidFieldsV1() throws Exception { + var ele = + TestUtils.getElementFromResource( + Paths.get(SAFETY_LABELS_OD_PATH, VALID_V1_FILE_NAME)); + var unused = new SafetyLabelsFactory().createFromOdElement(ele, 1L); } - private void odToHrExpectException(String fileName) - throws ParserConfigurationException, IOException, SAXException { - var safetyLabelsEle = - TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_OD_PATH, fileName)); + /** Tests unrecognized field v2. */ + @Test + public void testUnrecognizedFieldV2() throws Exception { + var ele = + TestUtils.getElementFromResource( + Paths.get(SAFETY_LABELS_OD_PATH, VALID_V1_FILE_NAME)); assertThrows( MalformedXmlException.class, - () -> - new SafetyLabelsFactory() - .createFromOdElement(safetyLabelsEle, DEFAULT_VERSION)); + () -> new SafetyLabelsFactory().createFromOdElement(ele, 2L)); } - private void testHrToOdSafetyLabels(String fileName) throws Exception { + private void testHrToOdSafetyLabels(String fileName, long version) throws Exception { var doc = TestUtils.document(); SafetyLabels safetyLabels = new SafetyLabelsFactory() .createFromHrElement( TestUtils.getElementFromResource( Paths.get(SAFETY_LABELS_HR_PATH, fileName)), - DEFAULT_VERSION); + version); Element appInfoEle = safetyLabels.toOdDomElement(doc); doc.appendChild(appInfoEle); TestUtils.testFormatToFormat(doc, Paths.get(SAFETY_LABELS_OD_PATH, fileName)); } - private void testOdToHrSafetyLabels(String fileName) throws Exception { + private void testOdToHrSafetyLabels(String fileName, long version) throws Exception { var doc = TestUtils.document(); SafetyLabels safetyLabels = new SafetyLabelsFactory() .createFromOdElement( TestUtils.getElementFromResource( Paths.get(SAFETY_LABELS_OD_PATH, fileName)), - DEFAULT_VERSION); + version); Element appInfoEle = safetyLabels.toHrDomElement(doc); doc.appendChild(appInfoEle); TestUtils.testFormatToFormat(doc, Paths.get(SAFETY_LABELS_HR_PATH, fileName)); diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java new file mode 100644 index 000000000000..9d197a2cf7f5 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib.marshallable; + +import com.android.asllib.testutils.TestUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Element; + +import java.nio.file.Paths; +import java.util.List; + +@RunWith(JUnit4.class) +public class SecurityLabelsTest { + private static final String SECURITY_LABELS_HR_PATH = "com/android/asllib/securitylabels/hr"; + private static final String SECURITY_LABELS_OD_PATH = "com/android/asllib/securitylabels/od"; + + public static final List<String> OPTIONAL_FIELD_NAMES = + List.of("isDataDeletable", "isDataEncrypted"); + public static final List<String> OPTIONAL_FIELD_NAMES_OD = + List.of("is_data_deletable", "is_data_encrypted"); + + private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + } + + /** Test for all fields valid. */ + @Test + public void testAllFieldsValid() throws Exception { + System.out.println("starting testAllFieldsValid."); + testHrToOdSecurityLabels(ALL_FIELDS_VALID_FILE_NAME); + testOdToHrSecurityLabels(ALL_FIELDS_VALID_FILE_NAME); + } + + /** Tests missing optional fields passes. */ + @Test + public void testMissingOptionalFields() throws Exception { + for (String optField : OPTIONAL_FIELD_NAMES) { + var ele = + TestUtils.getElementFromResource( + Paths.get(SECURITY_LABELS_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + ele.removeAttribute(optField); + SecurityLabels securityLabels = new SecurityLabelsFactory().createFromHrElement(ele); + securityLabels.toOdDomElement(TestUtils.document()); + } + for (String optField : OPTIONAL_FIELD_NAMES_OD) { + var ele = + TestUtils.getElementFromResource( + Paths.get(SECURITY_LABELS_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(ele, optField); + SecurityLabels securityLabels = new SecurityLabelsFactory().createFromOdElement(ele); + securityLabels.toHrDomElement(TestUtils.document()); + } + } + + private void testHrToOdSecurityLabels(String fileName) throws Exception { + var doc = TestUtils.document(); + SecurityLabels securityLabels = + new SecurityLabelsFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(SECURITY_LABELS_HR_PATH, fileName))); + Element ele = securityLabels.toOdDomElement(doc); + doc.appendChild(ele); + TestUtils.testFormatToFormat(doc, Paths.get(SECURITY_LABELS_OD_PATH, fileName)); + } + + private void testOdToHrSecurityLabels(String fileName) throws Exception { + var doc = TestUtils.document(); + SecurityLabels securityLabels = + new SecurityLabelsFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(SECURITY_LABELS_OD_PATH, fileName))); + Element ele = securityLabels.toHrDomElement(doc); + doc.appendChild(ele); + TestUtils.testFormatToFormat(doc, Paths.get(SECURITY_LABELS_HR_PATH, fileName)); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java index 9dcc6529969e..04bcd783a1dd 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java @@ -43,6 +43,7 @@ public class SystemAppSafetyLabelTest { "com/android/asllib/systemappsafetylabel/od"; private static final String VALID_FILE_NAME = "valid.xml"; + private static final String VALID_V1_FILE_NAME = "valid-v1.xml"; private static final String MISSING_BOOL_FILE_NAME = "missing-bool.xml"; /** Logic for setting up tests (empty if not yet needed). */ @@ -57,59 +58,81 @@ public class SystemAppSafetyLabelTest { @Test public void testValid() throws Exception { System.out.println("starting testValid."); - testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME); - testOdToHrSystemAppSafetyLabel(VALID_FILE_NAME); + testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME, DEFAULT_VERSION); + testOdToHrSystemAppSafetyLabel(VALID_FILE_NAME, DEFAULT_VERSION); + } + + /** Test for valid v1. */ + @Test + public void testValidV1() throws Exception { + System.out.println("starting testValidV1."); + var doc = TestUtils.document(); + var unused = + new SystemAppSafetyLabelFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get( + SYSTEM_APP_SAFETY_LABEL_OD_PATH, + VALID_V1_FILE_NAME)), + 1L); + } + + /** Test for testV1InvalidAsV2. */ + @Test + public void testV1InvalidAsV2() throws Exception { + System.out.println("starting testV1InvalidAsV2."); + odToHrExpectException(VALID_V1_FILE_NAME, 2L); } /** Tests missing bool. */ @Test public void testMissingBool() throws Exception { System.out.println("starting testMissingBool."); - hrToOdExpectException(MISSING_BOOL_FILE_NAME); - odToHrExpectException(MISSING_BOOL_FILE_NAME); + hrToOdExpectException(MISSING_BOOL_FILE_NAME, DEFAULT_VERSION); + odToHrExpectException(MISSING_BOOL_FILE_NAME, DEFAULT_VERSION); } - private void hrToOdExpectException(String fileName) + private void hrToOdExpectException(String fileName, long version) throws ParserConfigurationException, IOException, SAXException { var ele = TestUtils.getElementFromResource( Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)); assertThrows( MalformedXmlException.class, - () -> new SystemAppSafetyLabelFactory().createFromHrElement(ele, DEFAULT_VERSION)); + () -> new SystemAppSafetyLabelFactory().createFromHrElement(ele, version)); } - private void odToHrExpectException(String fileName) + private void odToHrExpectException(String fileName, long version) throws ParserConfigurationException, IOException, SAXException { var ele = TestUtils.getElementFromResource( Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)); assertThrows( MalformedXmlException.class, - () -> new SystemAppSafetyLabelFactory().createFromOdElement(ele, DEFAULT_VERSION)); + () -> new SystemAppSafetyLabelFactory().createFromOdElement(ele, version)); } - private void testHrToOdSystemAppSafetyLabel(String fileName) throws Exception { + private void testHrToOdSystemAppSafetyLabel(String fileName, long version) throws Exception { var doc = TestUtils.document(); SystemAppSafetyLabel systemAppSafetyLabel = new SystemAppSafetyLabelFactory() .createFromHrElement( TestUtils.getElementFromResource( Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)), - DEFAULT_VERSION); + version); Element resultingEle = systemAppSafetyLabel.toOdDomElement(doc); doc.appendChild(resultingEle); TestUtils.testFormatToFormat(doc, Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)); } - private void testOdToHrSystemAppSafetyLabel(String fileName) throws Exception { + private void testOdToHrSystemAppSafetyLabel(String fileName, long version) throws Exception { var doc = TestUtils.document(); SystemAppSafetyLabel systemAppSafetyLabel = new SystemAppSafetyLabelFactory() .createFromOdElement( TestUtils.getElementFromResource( Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)), - DEFAULT_VERSION); + version); Element resultingEle = systemAppSafetyLabel.toHrDomElement(doc); doc.appendChild(resultingEle); TestUtils.testFormatToFormat(doc, Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)); diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java new file mode 100644 index 000000000000..ebb2e93af920 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib.marshallable; + +import static org.junit.Assert.assertThrows; + +import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Element; + +import java.nio.file.Paths; + +@RunWith(JUnit4.class) +public class ThirdPartyVerificationTest { + private static final String THIRD_PARTY_VERIFICATION_HR_PATH = + "com/android/asllib/thirdpartyverification/hr"; + private static final String THIRD_PARTY_VERIFICATION_OD_PATH = + "com/android/asllib/thirdpartyverification/od"; + + private static final String VALID_FILE_NAME = "valid.xml"; + private static final String MISSING_URL_FILE_NAME = "missing-url.xml"; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + } + + /** Test for valid. */ + @Test + public void testValid() throws Exception { + System.out.println("starting testValid."); + testHrToOdThirdPartyVerification(VALID_FILE_NAME); + testOdToHrThirdPartyVerification(VALID_FILE_NAME); + } + + /** Tests missing url. */ + @Test + public void testMissingUrl() throws Exception { + System.out.println("starting testMissingUrl."); + hrToOdExpectException(MISSING_URL_FILE_NAME); + odToHrExpectException(MISSING_URL_FILE_NAME); + } + + private void hrToOdExpectException(String fileName) { + assertThrows( + MalformedXmlException.class, + () -> { + new ThirdPartyVerificationFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName))); + }); + } + + private void odToHrExpectException(String fileName) { + assertThrows( + MalformedXmlException.class, + () -> { + new ThirdPartyVerificationFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName))); + }); + } + + private void testHrToOdThirdPartyVerification(String fileName) throws Exception { + var doc = TestUtils.document(); + ThirdPartyVerification thirdPartyVerification = + new ThirdPartyVerificationFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName))); + Element ele = thirdPartyVerification.toOdDomElement(doc); + doc.appendChild(ele); + TestUtils.testFormatToFormat(doc, Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName)); + } + + private void testOdToHrThirdPartyVerification(String fileName) throws Exception { + var doc = TestUtils.document(); + ThirdPartyVerification thirdPartyVerification = + new ThirdPartyVerificationFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName))); + Element ele = thirdPartyVerification.toHrDomElement(doc); + doc.appendChild(ele); + TestUtils.testFormatToFormat(doc, Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName)); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java index 6547fb952944..b27d6ddb6243 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java @@ -16,16 +16,23 @@ package com.android.asllib.marshallable; +import static org.junit.Assert.assertThrows; + import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.w3c.dom.Element; +import org.xml.sax.SAXException; +import java.io.IOException; import java.nio.file.Paths; +import javax.xml.parsers.ParserConfigurationException; + @RunWith(JUnit4.class) public class TransparencyInfoTest { private static final long DEFAULT_VERSION = 2L; @@ -35,6 +42,10 @@ public class TransparencyInfoTest { private static final String TRANSPARENCY_INFO_OD_PATH = "com/android/asllib/transparencyinfo/od"; private static final String WITH_APP_INFO_FILE_NAME = "with-app-info.xml"; + private static final String VALID_EMPTY_V1_FILE_NAME = "valid-empty-v1.xml"; + private static final String VALID_DEV_INFO_V1_FILE_NAME = "valid-dev-info-v1.xml"; + private static final String WITH_APP_INFO_AND_DEV_INFO_FILE_NAME = + "with-app-info-v2-and-dev-info-v1.xml"; @Before public void setUp() throws Exception { @@ -45,33 +56,78 @@ public class TransparencyInfoTest { @Test public void testTransparencyInfoWithAppInfo() throws Exception { System.out.println("starting testTransparencyInfoWithAppInfo."); - testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME); - testOdToHrTransparencyInfo(WITH_APP_INFO_FILE_NAME); + testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME, DEFAULT_VERSION); + testOdToHrTransparencyInfo(WITH_APP_INFO_FILE_NAME, DEFAULT_VERSION); + } + + /** Test for testMissingAppInfoFailsInV2. */ + @Test + public void testMissingAppInfoFailsInV2() throws Exception { + System.out.println("starting testMissingAppInfoFailsInV2."); + odToHrExpectException(VALID_EMPTY_V1_FILE_NAME, 2L); + } + + /** Test for testMissingAppInfoPassesInV1. */ + @Test + public void testMissingAppInfoPassesInV1() throws Exception { + System.out.println("starting testMissingAppInfoPassesInV1."); + testParseOdTransparencyInfo(VALID_EMPTY_V1_FILE_NAME, 1L); + } + + /** Test for testDeveloperInfoExistencePassesInV1. */ + @Test + public void testDeveloperInfoExistencePassesInV1() throws Exception { + System.out.println("starting testDeveloperInfoExistencePassesInV1."); + testParseOdTransparencyInfo(VALID_DEV_INFO_V1_FILE_NAME, 1L); } - private void testHrToOdTransparencyInfo(String fileName) throws Exception { + /** Test for testDeveloperInfoExistenceFailsInV2. */ + @Test + public void testDeveloperInfoExistenceFailsInV2() throws Exception { + System.out.println("starting testDeveloperInfoExistenceFailsInV2."); + odToHrExpectException(WITH_APP_INFO_AND_DEV_INFO_FILE_NAME, 2L); + } + + private void testHrToOdTransparencyInfo(String fileName, long version) throws Exception { var doc = TestUtils.document(); TransparencyInfo transparencyInfo = new TransparencyInfoFactory() .createFromHrElement( TestUtils.getElementFromResource( Paths.get(TRANSPARENCY_INFO_HR_PATH, fileName)), - DEFAULT_VERSION); + version); Element resultingEle = transparencyInfo.toOdDomElement(doc); doc.appendChild(resultingEle); TestUtils.testFormatToFormat(doc, Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)); } - private void testOdToHrTransparencyInfo(String fileName) throws Exception { + private void testParseOdTransparencyInfo(String fileName, long version) throws Exception { + var unused = + new TransparencyInfoFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)), + version); + } + + private void testOdToHrTransparencyInfo(String fileName, long version) throws Exception { var doc = TestUtils.document(); TransparencyInfo transparencyInfo = new TransparencyInfoFactory() .createFromOdElement( TestUtils.getElementFromResource( Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)), - DEFAULT_VERSION); + version); Element resultingEle = transparencyInfo.toHrDomElement(doc); doc.appendChild(resultingEle); TestUtils.testFormatToFormat(doc, Paths.get(TRANSPARENCY_INFO_HR_PATH, fileName)); } + + private void odToHrExpectException(String fileName, long version) + throws ParserConfigurationException, IOException, SAXException { + var ele = TestUtils.getElementFromResource(Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)); + assertThrows( + MalformedXmlException.class, + () -> new TransparencyInfoFactory().createFromOdElement(ele, version)); + } } diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml new file mode 100644 index 000000000000..7e984e333ceb --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml @@ -0,0 +1,8 @@ +<bundle> + <long name="version" value="1"/> + <pbundle_as_map name="system_app_safety_label"> + <string name="url" value="www.example.com"/> + </pbundle_as_map> + <pbundle_as_map name="transparency_info"> + </pbundle_as_map> +</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml index 810078e777fb..01fd7180c3a6 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml @@ -21,5 +21,5 @@ <string name="category" value="Food and drink"/> <string name="email" value="max@maxloh.com"/> <string name="website" value="www.example.com"/> - <string name="unrecognized" value="www.example.com"/> + <boolean name="aps_compliant" value="false"/> </pbundle_as_map> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml new file mode 100644 index 000000000000..1384a2f6dd52 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml @@ -0,0 +1,9 @@ +<pbundle_as_map name="safety_labels"> + <pbundle_as_map name="security_labels"> + <boolean name="is_data_deletable" value="true" /> + <boolean name="is_data_encrypted" value="false" /> + </pbundle_as_map> + <pbundle_as_map name="third_party_verification"> + <string name="url" value="www.example.com"/> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml new file mode 100644 index 000000000000..f96535b4b49b --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml @@ -0,0 +1,3 @@ +<pbundle_as_map name="system_app_safety_label"> + <string name="url" value="www.example.com"/> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml new file mode 100644 index 000000000000..d7a4e1a959b7 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml @@ -0,0 +1,12 @@ + +<pbundle_as_map name="transparency_info"> + <pbundle_as_map name="developer_info"> + <string name="name" value="max"/> + <string name="email" value="max@example.com"/> + <string name="address" value="111 blah lane"/> + <string name="country_region" value="US"/> + <long name="relationship" value="5"/> + <string name="website" value="example.com"/> + <string name="app_developer_registry_id" value="registry_id"/> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty-v1.xml index af574cf92b3a..af574cf92b3a 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty-v1.xml diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info-v2-and-dev-info-v1.xml index b5e64b925ca5..b5e64b925ca5 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info-v2-and-dev-info-v1.xml |