diff options
127 files changed, 1951 insertions, 679 deletions
diff --git a/Android.bp b/Android.bp index ec7740437c2d..14a2bff8ad13 100644 --- a/Android.bp +++ b/Android.bp @@ -534,6 +534,8 @@ java_library { static_libs: [ "exoplayer2-extractor", "android.hardware.wifi-V1.0-java-constants", + // Additional dependencies needed to build the ike API classes. + "ike-internals", ], apex_available: ["//apex_available:platform"], visibility: [ diff --git a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java index ecc78ce7cf34..113f8fe9e248 100644 --- a/apex/blobstore/framework/java/android/app/blob/BlobHandle.java +++ b/apex/blobstore/framework/java/android/app/blob/BlobHandle.java @@ -51,6 +51,7 @@ public final class BlobHandle implements Parcelable { }; private static final int LIMIT_BLOB_TAG_LENGTH = 128; // characters + private static final int LIMIT_BLOB_LABEL_LENGTH = 100; // characters /** * Cyrptographically secure hash algorithm used to generate hash of the blob this handle is @@ -128,6 +129,9 @@ public final class BlobHandle implements Parcelable { * * @param digest the SHA-256 hash of the blob this is representing. * @param label a label indicating what the blob is, that can be surfaced to the user. + * The length of the label cannot be more than 100 characters. It is recommended + * to keep this brief. This may be truncated and ellipsized if it is too long + * to be displayed to the user. * @param expiryTimeMillis the time in secs after which the blob should be invalidated and not * allowed to be accessed by any other app, * in {@link System#currentTimeMillis()} timebase or {@code 0} to @@ -205,9 +209,9 @@ public final class BlobHandle implements Parcelable { final BlobHandle other = (BlobHandle) obj; return this.algorithm.equals(other.algorithm) && Arrays.equals(this.digest, other.digest) - && this.label.equals(other.label) + && this.label.toString().equals(other.label.toString()) && this.expiryTimeMillis == other.expiryTimeMillis - && this.tag.equals(tag); + && this.tag.equals(other.tag); } @Override @@ -233,6 +237,7 @@ public final class BlobHandle implements Parcelable { Preconditions.checkArgumentIsSupported(SUPPORTED_ALGOS, algorithm); Preconditions.checkByteArrayNotEmpty(digest, "digest"); Preconditions.checkStringNotEmpty(label, "label must not be null"); + Preconditions.checkArgument(label.length() <= LIMIT_BLOB_LABEL_LENGTH, "label too long"); Preconditions.checkArgumentNonnegative(expiryTimeMillis, "expiryTimeMillis must not be negative"); Preconditions.checkStringNotEmpty(tag, "tag must not be null"); diff --git a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java index 9c1acafa800d..39f7526560a9 100644 --- a/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java +++ b/apex/blobstore/framework/java/android/app/blob/BlobStoreManager.java @@ -347,7 +347,9 @@ public class BlobStoreManager { * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to * acquire a lease for. * @param description a short description string that can be surfaced - * to the user explaining what the blob is used for. + * to the user explaining what the blob is used for. It is recommended to + * keep this description brief. This may be truncated and ellipsized + * if it is too long to be displayed to the user. * @param leaseExpiryTimeMillis the time in milliseconds after which the lease can be * automatically released, in {@link System#currentTimeMillis()} * timebase. If its value is {@code 0}, then the behavior of this @@ -458,7 +460,9 @@ public class BlobStoreManager { * @param blobHandle the {@link BlobHandle} representing the blob that the caller wants to * acquire a lease for. * @param description a short description string that can be surfaced - * to the user explaining what the blob is used for. + * to the user explaining what the blob is used for. It is recommended to + * keep this description brief. This may be truncated and + * ellipsized if it is too long to be displayed to the user. * * @throws IOException when there is an I/O error while acquiring a lease to the blob. * @throws SecurityException when the blob represented by the {@code blobHandle} does not diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java index 79cd1b17a5b5..bb9f13f1712c 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java @@ -25,6 +25,7 @@ import android.content.Context; import android.os.Environment; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; +import android.text.TextUtils; import android.util.DataUnit; import android.util.Log; import android.util.Slog; @@ -171,6 +172,13 @@ class BlobStoreConfig { public static int MAX_BLOB_ACCESS_PERMITTED_PACKAGES = DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES; + /** + * Denotes the maximum number of characters that a lease description can have. + */ + public static final String KEY_LEASE_DESC_CHAR_LIMIT = "lease_desc_char_limit"; + public static int DEFAULT_LEASE_DESC_CHAR_LIMIT = 300; + public static int LEASE_DESC_CHAR_LIMIT = DEFAULT_LEASE_DESC_CHAR_LIMIT; + static void refresh(Properties properties) { if (!NAMESPACE_BLOBSTORE.equals(properties.getNamespace())) { return; @@ -221,6 +229,10 @@ class BlobStoreConfig { MAX_BLOB_ACCESS_PERMITTED_PACKAGES = properties.getInt(key, DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES); break; + case KEY_LEASE_DESC_CHAR_LIMIT: + LEASE_DESC_CHAR_LIMIT = properties.getInt(key, + DEFAULT_LEASE_DESC_CHAR_LIMIT); + break; default: Slog.wtf(TAG, "Unknown key in device config properties: " + key); } @@ -262,6 +274,8 @@ class BlobStoreConfig { fout.println(String.format(dumpFormat, KEY_MAX_BLOB_ACCESS_PERMITTED_PACKAGES, MAX_BLOB_ACCESS_PERMITTED_PACKAGES, DEFAULT_MAX_BLOB_ACCESS_PERMITTED_PACKAGES)); + fout.println(String.format(dumpFormat, KEY_LEASE_DESC_CHAR_LIMIT, + LEASE_DESC_CHAR_LIMIT, DEFAULT_LEASE_DESC_CHAR_LIMIT)); } } @@ -368,6 +382,18 @@ class BlobStoreConfig { return DeviceConfigProperties.MAX_BLOB_ACCESS_PERMITTED_PACKAGES; } + /** + * Returns the lease description truncated to + * {@link DeviceConfigProperties#LEASE_DESC_CHAR_LIMIT} characters. + */ + public static CharSequence getTruncatedLeaseDescription(CharSequence description) { + if (TextUtils.isEmpty(description)) { + return description; + } + return TextUtils.trimToLengthWithEllipsis(description, + DeviceConfigProperties.LEASE_DESC_CHAR_LIMIT); + } + @Nullable public static File prepareBlobFile(long sessionId) { final File blobsDir = prepareBlobsDir(); diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java index 520e8bbf9f93..d37dfdeaa583 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java @@ -1500,6 +1500,8 @@ public class BlobStoreManagerService extends SystemService { "leaseExpiryTimeMillis must not be negative"); Objects.requireNonNull(packageName, "packageName must not be null"); + description = BlobStoreConfig.getTruncatedLeaseDescription(description); + final int callingUid = Binder.getCallingUid(); verifyCallingPackage(callingUid, packageName); diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index 12e428ce5674..7a016522d597 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -587,6 +587,8 @@ message Atom { BytesTransferByTagAndMetered bytes_transfer_by_tag_and_metered = 10083 [(module) = "framework"]; DNDModeProto dnd_mode_rule = 10084 [(module) = "framework"]; + GeneralExternalStorageAccessStats general_external_storage_access_stats = + 10085 [(module) = "mediaprovider"]; } // DO NOT USE field numbers above 100,000 in AOSP. @@ -4553,6 +4555,31 @@ message VmsClientConnectionStateChanged { optional State state = 2; } +message MimeTypes { + repeated string mime_types = 1; +} + +/** + * Logs statistics regarding accesses to external storage. + * All stats are normalized for one day period. + * + * Logged from: + * packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java + */ +message GeneralExternalStorageAccessStats { + optional int32 uid = 1 [(is_uid) = true]; + // Total number of accesses like creation, open, delete and rename/update. + // Includes file path and ContentResolver accesses + optional uint32 total_accesses = 2; + // Number of file path accesses, as opposed to file path and ContentResolver. + optional uint32 file_path_accesses = 3; + // Number of accesses on secondary volumes like SD cards. + // Includes file path and ContentResolver accesses + optional uint32 secondary_storage_accesses = 4; + // Comma-separated list of mime types that were accessed. + optional MimeTypes mime_types_accessed = 5; +} + /** * Logs when MediaProvider has successfully finished scanning a storage volume. * diff --git a/cmds/statsd/tools/localtools/src/com/android/statsd/shelltools/testdrive/TestDrive.java b/cmds/statsd/tools/localtools/src/com/android/statsd/shelltools/testdrive/TestDrive.java index 6384fb12ca68..51bcad115cc5 100644 --- a/cmds/statsd/tools/localtools/src/com/android/statsd/shelltools/testdrive/TestDrive.java +++ b/cmds/statsd/tools/localtools/src/com/android/statsd/shelltools/testdrive/TestDrive.java @@ -342,6 +342,9 @@ public class TestDrive { .addPullAtomPackages(PullAtomPackages.newBuilder() .setAtomId(Atom.TRAIN_INFO_FIELD_NUMBER) .addPackages("AID_STATSD")) + .addPullAtomPackages(PullAtomPackages.newBuilder() + .setAtomId(Atom.GENERAL_EXTERNAL_STORAGE_ACCESS_STATS_FIELD_NUMBER) + .addPackages("com.google.android.providers.media.module")) .setHashStringsInMetricReport(false); } } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 86a3579effe1..a828aac78ded 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1900,11 +1900,13 @@ class ContextImpl extends Context { @Override public Object getSystemService(String name) { + // We may override this API from outer context. + final boolean isUiContext = isUiContext() || getOuterContext().isUiContext(); // Check incorrect Context usage. - if (isUiComponent(name) && !isUiContext() && vmIncorrectContextUseEnabled()) { + if (isUiComponent(name) && !isUiContext && vmIncorrectContextUseEnabled()) { final String errorMessage = "Tried to access visual service " + SystemServiceRegistry.getSystemServiceClassName(name) - + " from a non-visual Context. "; + + " from a non-visual Context:" + getOuterContext(); final String message = "Visual services, such as WindowManager, WallpaperService or " + "LayoutInflater should be accessed from Activity or other visual Context. " + "Use an Activity or a Context created with " @@ -2369,6 +2371,7 @@ class ContextImpl extends Context { context.setResources(createResources(mToken, mPackageInfo, mSplitName, displayId, overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo(), mResources.getLoaders())); + context.mIsUiContext = isUiContext() || getOuterContext().isUiContext(); return context; } diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 4bd7b059dfaa..591a714bfb93 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -65,6 +65,13 @@ public final class CompanionDeviceManager { /** * A device, returned in the activity result of the {@link IntentSender} received in * {@link Callback#onDeviceFound} + * + * Type is: + * <ul> + * <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li> + * <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li> + * <li>for WiFi - {@link android.net.wifi.ScanResult}</li> + * </ul> */ public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE"; diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index 08aa534be152..2d99c413cc89 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -312,7 +312,12 @@ public final class Dataset implements Parcelable { * setting it to the {@link * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. If you * provide a dataset in the result, it will replace the authenticated dataset and - * will be immediately filled in. If you provide a response, it will replace the + * will be immediately filled in. An exception to this behavior is if the original + * dataset represents a pinned inline suggestion (i.e. any of the field in the dataset + * has a pinned inline presentation, see {@link InlinePresentation#isPinned()}), then + * the original dataset will not be replaced, + * so that it can be triggered as a pending intent again. + * If you provide a response, it will replace the * current response and the UI will be refreshed. For example, if you provided * credit card information without the CVV for the data set in the {@link FillResponse * response} then the returned data set should contain the CVV entry. diff --git a/core/java/android/service/autofill/InlinePresentation.java b/core/java/android/service/autofill/InlinePresentation.java index 9cf1b87f7eab..914169485979 100644 --- a/core/java/android/service/autofill/InlinePresentation.java +++ b/core/java/android/service/autofill/InlinePresentation.java @@ -50,7 +50,11 @@ public final class InlinePresentation implements Parcelable { /** * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the - * host. + * host. However, it's eventually up to the host whether the UI is pinned or not. + * + * <p> Also a {@link Dataset} with a pinned inline presentation will not be replaced by the + * new data set returned from authentication intent. See + * {@link Dataset.Builder#setAuthentication(android.content.IntentSender)} for more information. */ private final boolean mPinned; @@ -90,7 +94,11 @@ public final class InlinePresentation implements Parcelable { * Specifies the UI specification for the inline suggestion. * @param pinned * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the - * host. + * host. However, it's eventually up to the host whether the UI is pinned or not. + * + * <p> Also a {@link Dataset} with a pinned inline presentation will not be replaced by the + * new data set returned from authentication intent. See + * {@link Dataset.Builder#setAuthentication(android.content.IntentSender)} for more information. */ @DataClass.Generated.Member public InlinePresentation( @@ -126,7 +134,11 @@ public final class InlinePresentation implements Parcelable { /** * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the - * host. + * host. However, it's eventually up to the host whether the UI is pinned or not. + * + * <p> Also a {@link Dataset} with a pinned inline presentation will not be replaced by the + * new data set returned from authentication intent. See + * {@link Dataset.Builder#setAuthentication(android.content.IntentSender)} for more information. */ @DataClass.Generated.Member public boolean isPinned() { @@ -232,7 +244,7 @@ public final class InlinePresentation implements Parcelable { }; @DataClass.Generated( - time = 1586992400667L, + time = 1593131904745L, codegenVersion = "1.0.15", sourceFile = "frameworks/base/core/java/android/service/autofill/InlinePresentation.java", inputSignatures = "private final @android.annotation.NonNull android.app.slice.Slice mSlice\nprivate final @android.annotation.NonNull android.widget.inline.InlinePresentationSpec mInlinePresentationSpec\nprivate final boolean mPinned\npublic @android.annotation.NonNull @android.annotation.Size(min=0L) java.lang.String[] getAutofillHints()\nclass InlinePresentation extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstDefs=true, genEqualsHashCode=true)") diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index bb0de120dedc..f937bc9e84a9 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -471,6 +471,13 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall } private void performDrawFinished() { + if (mDeferredDestroySurfaceControl != null) { + synchronized (mSurfaceControlLock) { + mTmpTransaction.remove(mDeferredDestroySurfaceControl).apply(); + mDeferredDestroySurfaceControl = null; + } + } + if (mPendingReportDraws > 0) { mDrawFinished = true; if (mAttachedToWindow) { @@ -1194,13 +1201,6 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall + "finishedDrawing"); } - if (mDeferredDestroySurfaceControl != null) { - synchronized (mSurfaceControlLock) { - mTmpTransaction.remove(mDeferredDestroySurfaceControl).apply(); - mDeferredDestroySurfaceControl = null; - } - } - runOnUiThread(this::performDrawFinished); } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 0d2d4d13eb38..ffeeb806ba54 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -500,12 +500,13 @@ public class ViewConfiguration { */ public static ViewConfiguration get(Context context) { if (!context.isUiContext() && vmIncorrectContextUseEnabled()) { - final String errorMessage = "Tried to access UI constants from a non-visual Context."; + final String errorMessage = "Tried to access UI constants from a non-visual Context:" + + context; final String message = "UI constants, such as display metrics or window metrics, " + "must be accessed from Activity or other visual Context. " + "Use an Activity or a Context created with " + "Context#createWindowContext(int, Bundle), which are adjusted to the " - + "configuration and visual bounds of an area on screen."; + + "configuration and visual bounds of an area on screen"; final Exception exception = new IllegalArgumentException(errorMessage); StrictMode.onIncorrectContextUsed(message, exception); Log.e(TAG, errorMessage + message, exception); diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 8c2358d253be..14cf258f18ab 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2040,6 +2040,9 @@ public class ChooserActivity extends ResolverActivity implements if (!isUserRunning(userHandle)) { return false; } + if (!isUserUnlocked(userHandle)) { + return false; + } if (isQuietModeEnabled(userHandle)) { return false; } @@ -2892,6 +2895,12 @@ public class ChooserActivity extends ResolverActivity implements } @VisibleForTesting + protected boolean isUserUnlocked(UserHandle userHandle) { + UserManager userManager = getSystemService(UserManager.class); + return userManager.isUserUnlocked(userHandle); + } + + @VisibleForTesting protected boolean isQuietModeEnabled(UserHandle userHandle) { UserManager userManager = getSystemService(UserManager.class); return userManager.isQuietModeEnabled(userHandle); diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index fba4675a8c9f..233231cfcfdf 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -1279,13 +1279,17 @@ public class ResolverActivity extends Activity implements } private void safelyStartActivityInternal(TargetInfo cti) { - if (mPersonalPackageMonitor != null) { - mPersonalPackageMonitor.unregister(); - } - if (mWorkPackageMonitor != null) { - mWorkPackageMonitor.unregister(); + // If the target is suspended, the activity will not be successfully launched. + // Do not unregister from package manager updates in this case + if (!cti.isSuspended()) { + if (mPersonalPackageMonitor != null) { + mPersonalPackageMonitor.unregister(); + } + if (mWorkPackageMonitor != null) { + mWorkPackageMonitor.unregister(); + } + mRegistered = false; } - mRegistered = false; // If needed, show that intent is forwarded // from managed profile to owner or other way around. if (mProfileSwitchMessageId != -1) { diff --git a/core/res/res/anim/screen_rotate_180_enter.xml b/core/res/res/anim/screen_rotate_180_enter.xml index 889a615e07f4..3b6b4072dbcd 100644 --- a/core/res/res/anim/screen_rotate_180_enter.xml +++ b/core/res/res/anim/screen_rotate_180_enter.xml @@ -25,4 +25,10 @@ android:fillBefore="true" android:fillAfter="true" android:interpolator="@interpolator/fast_out_slow_in" android:duration="@android:integer/config_screen_rotation_total_180" /> + <alpha android:fromAlpha="0.0" android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" android:fillAfter="true" + android:interpolator="@interpolator/screen_rotation_alpha_in" + android:startOffset="@android:integer/config_screen_rotation_fade_in_delay" + android:duration="@android:integer/config_screen_rotation_fade_in" /> </set> diff --git a/core/res/res/anim/screen_rotate_180_exit.xml b/core/res/res/anim/screen_rotate_180_exit.xml index 766fcfae1f91..26fb6d8df506 100644 --- a/core/res/res/anim/screen_rotate_180_exit.xml +++ b/core/res/res/anim/screen_rotate_180_exit.xml @@ -25,4 +25,9 @@ android:fillBefore="true" android:fillAfter="true" android:interpolator="@interpolator/fast_out_slow_in" android:duration="@android:integer/config_screen_rotation_total_180" /> -</set>
\ No newline at end of file + <alpha android:fromAlpha="1.0" android:toAlpha="0.0" + android:fillEnabled="true" + android:fillBefore="true" android:fillAfter="true" + android:interpolator="@interpolator/screen_rotation_alpha_out" + android:duration="@android:integer/config_screen_rotation_fade_out" /> +</set> diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index 0bf10cb710cb..090645f6f7a8 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -1996,7 +1996,7 @@ public class ChooserActivityTest { } @Test - public void testWorkTab_selectingWorkTabWithLockedWorkUser_directShareTargetsNotQueried() { + public void testWorkTab_selectingWorkTabWithNotRunningWorkUser_directShareTargetsNotQueried() { // enable the work tab feature flag ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); @@ -2035,7 +2035,7 @@ public class ChooserActivityTest { } @Test - public void testWorkTab_workUserLocked_workTargetsShown() { + public void testWorkTab_workUserNotRunning_workTargetsShown() { // enable the work tab feature flag ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); @@ -2059,6 +2059,70 @@ public class ChooserActivityTest { assertEquals(3, activity.getWorkListAdapter().getCount()); } + @Test + public void testWorkTab_selectingWorkTabWithLockedWorkUser_directShareTargetsNotQueried() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + sOverrides.isWorkProfileUserUnlocked = false; + boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; + sOverrides.onQueryDirectShareTargets = chooserListAdapter -> { + isQueryDirectShareCalledOnWorkProfile[0] = + (chooserListAdapter.getUserHandle().getIdentifier() == 10); + return null; + }; + boolean[] isQueryTargetServicesCalledOnWorkProfile = new boolean[] { false }; + sOverrides.onQueryTargetServices = chooserListAdapter -> { + isQueryTargetServicesCalledOnWorkProfile[0] = + (chooserListAdapter.getUserHandle().getIdentifier() == 10); + return null; + }; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + assertFalse("Direct share targets were queried on a locked work profile user", + isQueryDirectShareCalledOnWorkProfile[0]); + assertFalse("Target services were queried on a locked work profile user", + isQueryTargetServicesCalledOnWorkProfile[0]); + } + + @Test + public void testWorkTab_workUserLocked_workTargetsShown() { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + sOverrides.isWorkProfileUserUnlocked = false; + + final ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + assertEquals(3, activity.getWorkListAdapter().getCount()); + } + private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { Intent chooserIntent = new Intent(); chooserIntent.setAction(Intent.ACTION_CHOOSER); diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java index b7d6c6196495..d3d5caf3f7e2 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java @@ -229,9 +229,20 @@ public class ChooserWrapperActivity extends ChooserActivity { @Override protected boolean isUserRunning(UserHandle userHandle) { + if (userHandle.equals(UserHandle.SYSTEM)) { + return super.isUserRunning(userHandle); + } return sOverrides.isWorkProfileUserRunning; } + @Override + protected boolean isUserUnlocked(UserHandle userHandle) { + if (userHandle.equals(UserHandle.SYSTEM)) { + return super.isUserUnlocked(userHandle); + } + return sOverrides.isWorkProfileUserUnlocked; + } + /** * We cannot directly mock the activity created since instrumentation creates it. * <p> @@ -258,6 +269,7 @@ public class ChooserWrapperActivity extends ChooserActivity { public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; public boolean isWorkProfileUserRunning; + public boolean isWorkProfileUserUnlocked; public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector; public PackageManager packageManager; @@ -281,6 +293,7 @@ public class ChooserWrapperActivity extends ChooserActivity { hasCrossProfileIntents = true; isQuietModeEnabled = false; isWorkProfileUserRunning = true; + isWorkProfileUserUnlocked = true; packageManager = null; multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() { @Override diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 8520fffb3444..c710bed29361 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -424,6 +424,8 @@ applications that come with the platform <permission name="android.permission.LOCATION_HARDWARE" /> <!-- Permissions required for GTS test - GtsDialerAudioTestCases --> <permission name="android.permission.CAPTURE_AUDIO_OUTPUT" /> + <!-- Permissions required for CTS test - AdbManagerTest --> + <permission name="android.permission.MANAGE_DEBUGGING" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index bfc623faeaef..67a64ac592f2 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -757,6 +757,12 @@ "group": "WM_DEBUG_BOOT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-547111355": { + "message": "hideIme Control target: %s ", + "level": "DEBUG", + "group": "WM_DEBUG_IME", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "-545190927": { "message": "<<< CLOSE TRANSACTION animate", "level": "INFO", @@ -1087,6 +1093,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "95216706": { + "message": "hideIme target: %s ", + "level": "DEBUG", + "group": "WM_DEBUG_IME", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "95281111": { "message": "Attempted to get IME flag of a display that does not exist: %d", "level": "WARN", @@ -1663,12 +1675,6 @@ "group": "WM_SHOW_SURFACE_ALLOC", "at": "com\/android\/server\/wm\/ScreenRotationAnimation.java" }, - "1108406230": { - "message": "stopFreezingDisplayLocked: Returning mWaitingForConfig=%b, mAppsFreezingScreen=%d, mWindowsFreezingScreen=%d, mClientFreezingScreen=%b, mOpeningApps.size()=%d", - "level": "DEBUG", - "group": "WM_DEBUG_ORIENTATION", - "at": "com\/android\/server\/wm\/WindowManagerService.java" - }, "1112047265": { "message": "finishDrawingWindow: %s mDrawState=%s", "level": "DEBUG", @@ -1729,6 +1735,12 @@ "group": "WM_DEBUG_BOOT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1246035185": { + "message": "stopFreezingDisplayLocked: Returning waitingForConfig=%b, waitingForRemoteRotation=%b, mAppsFreezingScreen=%d, mWindowsFreezingScreen=%d, mClientFreezingScreen=%b, mOpeningApps.size()=%d", + "level": "DEBUG", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "1254403969": { "message": "Surface returned was null: %s", "level": "VERBOSE", @@ -1951,12 +1963,6 @@ "group": "WM_DEBUG_APP_TRANSITIONS_ANIM", "at": "com\/android\/server\/wm\/AppTransition.java" }, - "1591969812": { - "message": "updateImeControlTarget %s", - "level": "INFO", - "group": "WM_DEBUG_IME", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "1628345525": { "message": "Now opening app %s", "level": "VERBOSE", diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 197786f42490..c2168f12a351 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -654,6 +654,9 @@ public final class MediaCodecInfo { * <p> * * The following table summarizes the format keys considered by this method. + * This is especially important to consider when targeting a higher SDK version than the + * minimum SDK version, as this method will disregard some keys on devices below the target + * SDK version. * * <table style="width: 0%"> * <thead> @@ -668,7 +671,7 @@ public final class MediaCodecInfo { * </thead> * <tbody> * <tr> - * <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP}</th> + * <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP}</td> * <td rowspan=3>{@link MediaFormat#KEY_MIME}<sup>*</sup>,<br> * {@link MediaFormat#KEY_SAMPLE_RATE},<br> * {@link MediaFormat#KEY_CHANNEL_COUNT},</td> @@ -679,30 +682,51 @@ public final class MediaCodecInfo { * {@link MediaFormat#KEY_WIDTH},<br> * {@link MediaFormat#KEY_HEIGHT},<br> * <strong>no</strong> {@code KEY_FRAME_RATE}</td> - * <td rowspan=4>{@link MediaFormat#KEY_BITRATE_MODE},<br> + * <td rowspan=10>as to the left, plus<br> + * {@link MediaFormat#KEY_BITRATE_MODE},<br> * {@link MediaFormat#KEY_PROFILE} * (and/or {@link MediaFormat#KEY_AAC_PROFILE}<sup>~</sup>),<br> * <!-- {link MediaFormat#KEY_QUALITY},<br> --> * {@link MediaFormat#KEY_COMPLEXITY} * (and/or {@link MediaFormat#KEY_FLAC_COMPRESSION_LEVEL}<sup>~</sup>)</td> * </tr><tr> - * <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}</th> + * <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}</td> * <td rowspan=2>as above, plus<br> * {@link MediaFormat#KEY_FRAME_RATE}</td> * </tr><tr> - * <td>{@link android.os.Build.VERSION_CODES#M}</th> + * <td>{@link android.os.Build.VERSION_CODES#M}</td> * </tr><tr> - * <td>{@link android.os.Build.VERSION_CODES#N}</th> - * <td>as above, plus<br> + * <td>{@link android.os.Build.VERSION_CODES#N}</td> + * <td rowspan=2>as above, plus<br> * {@link MediaFormat#KEY_PROFILE},<br> * <!-- {link MediaFormat#KEY_MAX_BIT_RATE},<br> --> * {@link MediaFormat#KEY_BIT_RATE}</td> - * <td>as above, plus<br> + * <td rowspan=2>as above, plus<br> * {@link MediaFormat#KEY_PROFILE},<br> * {@link MediaFormat#KEY_LEVEL}<sup>+</sup>,<br> * <!-- {link MediaFormat#KEY_MAX_BIT_RATE},<br> --> * {@link MediaFormat#KEY_BIT_RATE},<br> * {@link CodecCapabilities#FEATURE_IntraRefresh}<sup>E</sup></td> + * </tr><tr> + * <td>{@link android.os.Build.VERSION_CODES#N_MR1}</td> + * </tr><tr> + * <td>{@link android.os.Build.VERSION_CODES#O}</td> + * <td rowspan=3 colspan=2>as above, plus<br> + * {@link CodecCapabilities#FEATURE_PartialFrame}<sup>D</sup></td> + * </tr><tr> + * <td>{@link android.os.Build.VERSION_CODES#O_MR1}</td> + * </tr><tr> + * <td>{@link android.os.Build.VERSION_CODES#P}</td> + * </tr><tr> + * <td>{@link android.os.Build.VERSION_CODES#Q}</td> + * <td colspan=2>as above, plus<br> + * {@link CodecCapabilities#FEATURE_FrameParsing}<sup>D</sup>,<br> + * {@link CodecCapabilities#FEATURE_MultipleFrames},<br> + * {@link CodecCapabilities#FEATURE_DynamicTimestamp}</td> + * </tr><tr> + * <td>{@link android.os.Build.VERSION_CODES#R}</td> + * <td colspan=2>as above, plus<br> + * {@link CodecCapabilities#FEATURE_LowLatency}<sup>D</sup></td> * </tr> * <tr> * <td colspan=4> diff --git a/packages/SettingsLib/RestrictedLockUtils/res/values-pt-rPT/strings.xml b/packages/SettingsLib/RestrictedLockUtils/res/values-pt-rPT/strings.xml index e57d1cc11a20..908e2fbbff5b 100644 --- a/packages/SettingsLib/RestrictedLockUtils/res/values-pt-rPT/strings.xml +++ b/packages/SettingsLib/RestrictedLockUtils/res/values-pt-rPT/strings.xml @@ -18,5 +18,5 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="enabled_by_admin" msgid="6630472777476410137">"Ativado pelo administrador"</string> - <string name="disabled_by_admin" msgid="4023569940620832713">"Desativada pelo administrador"</string> + <string name="disabled_by_admin" msgid="4023569940620832713">"Desativado pelo administrador"</string> </resources> diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index b64a9e7a25bf..570c2ab3efd1 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -314,6 +314,9 @@ <!-- Permissions required for GTS test - GtsDialerAudioTestCases --> <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" /> + <!-- Permissions required for CTS test - AdbManagerTest --> + <uses-permission android:name="android.permission.MANAGE_DEBUGGING" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" android:defaultToDeviceProtectedStorage="true" diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml index 8441282bd46d..3c641afea0d6 100644 --- a/packages/SystemUI/res/layout/media_view.xml +++ b/packages/SystemUI/res/layout/media_view.xml @@ -65,25 +65,16 @@ <!-- Actions must be ordered left-to-right even in RTL layout. However, they appear in a chain with the album art and the title, and must as a group appear at the end of that chain. This is - accomplished by having the guidebox (an invisible view that is positioned around all 5 actions) - in the chain with the album art and the title. The actions are in a LTR chain bounded by that - guidebox, and the ambiguity of how wide the guidebox should be is resolved by using a barrier - which forces it's starting edge to be as far to the end as possible while fitting the actions. - --> + accomplished by having all actions appear in a LTR chain within the parent, and then biasing it + to the right side, then this barrier is used to bound the text views. --> <androidx.constraintlayout.widget.Barrier android:id="@+id/media_action_barrier" android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" + app:layout_constraintTop_toTopOf="parent" app:barrierDirection="start" - /> - - <View - android:id="@+id/media_action_guidebox" - android:layout_width="0dp" - android:layout_height="48dp" - android:layout_marginTop="16dp" - android:visibility="invisible" + app:constraint_referenced_ids="action0,action1,action2,action3,action4" /> <ImageButton @@ -201,7 +192,6 @@ android:fontFamily="@*android:string/config_headlineFontFamilyMedium" android:singleLine="true" android:textColor="@color/media_primary_text" - android:textDirection="locale" android:textSize="16sp" /> <!-- Artist name --> @@ -212,14 +202,13 @@ android:fontFamily="@*android:string/config_headlineFontFamily" android:singleLine="true" android:textColor="@color/media_secondary_text" - android:textDirection="locale" android:textSize="14sp" /> <com.android.internal.widget.CachingIconView android:id="@+id/icon" android:tint="@color/media_primary_text" - android:layout_width="20dp" - android:layout_height="20dp" /> + android:layout_width="@dimen/qs_media_icon_size" + android:layout_height="@dimen/qs_media_icon_size" /> <!-- Buttons to remove this view when no longer needed --> <include diff --git a/packages/SystemUI/res/values-ar/strings.xml b/packages/SystemUI/res/values-ar/strings.xml index df2169624b37..ec1e076f8f67 100644 --- a/packages/SystemUI/res/values-ar/strings.xml +++ b/packages/SystemUI/res/values-ar/strings.xml @@ -721,7 +721,7 @@ <string name="inline_turn_off_notifications" msgid="8543989584403106071">"إيقاف الإشعارات"</string> <string name="inline_keep_showing_app" msgid="4393429060390649757">"هل تريد الاستمرار في تلقي إشعارات من هذا التطبيق؟"</string> <string name="notification_silence_title" msgid="8608090968400832335">"صامتة"</string> - <string name="notification_alert_title" msgid="3656229781017543655">"تلقائي"</string> + <string name="notification_alert_title" msgid="3656229781017543655">"تلقائية"</string> <string name="notification_bubble_title" msgid="8330481035191903164">"فقاعة"</string> <string name="notification_channel_summary_low" msgid="4860617986908931158">"بدون صوت أو اهتزاز"</string> <string name="notification_conversation_summary_low" msgid="1734433426085468009">"بدون صوت أو اهتزاز وتظهر في موضع أسفل في قسم المحادثات"</string> @@ -1045,7 +1045,7 @@ <string name="magnification_overlay_title" msgid="6584179429612427958">"نافذة تراكب التكبير"</string> <string name="magnification_window_title" msgid="4863914360847258333">"نافذة التكبير"</string> <string name="magnification_controls_title" msgid="8421106606708891519">"عناصر التحكم في نافذة التكبير"</string> - <string name="quick_controls_title" msgid="6839108006171302273">"أدوات التحكم بالجهاز"</string> + <string name="quick_controls_title" msgid="6839108006171302273">"أدوات التحكم بالأجهزة"</string> <string name="quick_controls_subtitle" msgid="1667408093326318053">"إضافة عناصر تحكّم لأجهزتك المتصلة"</string> <string name="quick_controls_setup_title" msgid="8901436655997849822">"إعداد أدوات التحكم بالجهاز"</string> <string name="quick_controls_setup_subtitle" msgid="1681506617879773824">"اضغط مع الاستمرار على زر التشغيل للوصول إلى عناصر التحكّم"</string> diff --git a/packages/SystemUI/res/values-as/strings.xml b/packages/SystemUI/res/values-as/strings.xml index 949852802b2c..289efd5c7f52 100644 --- a/packages/SystemUI/res/values-as/strings.xml +++ b/packages/SystemUI/res/values-as/strings.xml @@ -1046,8 +1046,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"নিয়ন্ত্ৰণসমূহ পুনৰ সজাবলৈ ধৰি ৰাখক আৰু টানি আনি এৰক"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"সকলো নিয়ন্ত্ৰণ আঁতৰোৱা হৈছে"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"সালসলনিসমূহ ছেভ নহ’ল"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"অন্য এপ্সমূহ চাওক"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"নিয়ন্ত্ৰণসমূহ ল’ড কৰিবপৰা নগ’ল। এপ্টোৰ ছেটিংসমূহ সলনি কৰা হোৱা নাই বুলি নিশ্চিত কৰিবলৈ <xliff:g id="APP">%s</xliff:g> এপ্টো পৰীক্ষা কৰক।"</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"সমিল নিয়ন্ত্ৰণসমূহ উপলব্ধ নহয়"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"অন্য"</string> diff --git a/packages/SystemUI/res/values-bs/strings.xml b/packages/SystemUI/res/values-bs/strings.xml index 9dbb82cbfa86..34f4b9fbc3e2 100644 --- a/packages/SystemUI/res/values-bs/strings.xml +++ b/packages/SystemUI/res/values-bs/strings.xml @@ -628,9 +628,7 @@ <string name="volume_ringer_status_silent" msgid="3691324657849880883">"Isključi zvuk"</string> <string name="qs_status_phone_vibrate" msgid="7055409506885541979">"Na telefonu je uključena vibracija"</string> <string name="qs_status_phone_muted" msgid="3763664791309544103">"Zvuk na telefonu je isključen"</string> - <!-- String.format failed for translation --> - <!-- no translation found for volume_stream_content_description_unmute (7729576371406792977) --> - <skip /> + <string name="volume_stream_content_description_unmute" msgid="7729576371406792977">"%1$s. Dodirnite da uključite zvukove."</string> <string name="volume_stream_content_description_vibrate" msgid="4858111994183089761">"%1$s. Dodirnite za postavljanje vibracije. Zvukovi usluga pristupačnosti mogu biti isključeni."</string> <string name="volume_stream_content_description_mute" msgid="4079046784917920984">"%1$s. Dodirnite da isključite zvuk. Zvukovi usluga pristupačnosti mogu biti isključeni."</string> <string name="volume_stream_content_description_vibrate_a11y" msgid="2742330052979397471">"%1$s. Dodirnite da postavite vibraciju."</string> diff --git a/packages/SystemUI/res/values-gu/strings.xml b/packages/SystemUI/res/values-gu/strings.xml index ee286266ef28..54f45ea652e9 100644 --- a/packages/SystemUI/res/values-gu/strings.xml +++ b/packages/SystemUI/res/values-gu/strings.xml @@ -1046,8 +1046,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"નિયંત્રણોને ફરીથી ગોઠવવા માટે તેમને હોલ્ડ કરીને ખેંચો"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"બધા નિયંત્રણો કાઢી નાખ્યા"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"ફેરફારો સાચવ્યા નથી"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"અન્ય બધી ઍપ જુઓ"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"નિયંત્રણો લોડ કરી શકાયા નથી. ઍપના સેટિંગ બદલાયા નથી તેની ખાતરી કરવા માટે <xliff:g id="APP">%s</xliff:g> ઍપ ચેક કરો."</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"સુસંગત નિયંત્રણો ઉપલબ્ધ નથી"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"અન્ય"</string> diff --git a/packages/SystemUI/res/values-hi/strings.xml b/packages/SystemUI/res/values-hi/strings.xml index 96d837fe77cb..1f6000b4f49d 100644 --- a/packages/SystemUI/res/values-hi/strings.xml +++ b/packages/SystemUI/res/values-hi/strings.xml @@ -1048,7 +1048,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"कंट्रोल का क्रम फिर से बदलने के लिए उन्हें दबाकर रखें और खींचें"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"सभी कंट्रोल हटा दिए गए"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"बदलाव सेव नहीं किए गए"</string> - <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"अन्य ऐप्लिकेशन देखें"</string> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"दूसरे ऐप्लिकेशन देखें"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"कंट्रोल लोड नहीं किए जा सके. <xliff:g id="APP">%s</xliff:g> ऐप्लिकेशन देखें, ताकि यह पक्का किया जा सके कि ऐप्लिकेशन की सेटिंग में कोई बदलाव नहीं हुआ है."</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"इस सेटिंग के साथ काम करने वाले कंट्रोल उपलब्ध नहीं हैं"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"अन्य"</string> diff --git a/packages/SystemUI/res/values-iw/strings.xml b/packages/SystemUI/res/values-iw/strings.xml index 7a5b446a6b52..cb4fade417f2 100644 --- a/packages/SystemUI/res/values-iw/strings.xml +++ b/packages/SystemUI/res/values-iw/strings.xml @@ -1058,8 +1058,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"יש ללחוץ לחיצה ארוכה ולגרור כדי לארגן מחדש את הפקדים"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"כל הפקדים הוסרו"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"השינויים לא נשמרו"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"הצגת אפליקציות אחרות"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"לא ניתן היה לטעון את הפקדים. יש לבדוק את האפליקציה <xliff:g id="APP">%s</xliff:g> כדי לוודא שהגדרות האפליקציה לא השתנו."</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"פקדים תואמים לא זמינים"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"אחר"</string> diff --git a/packages/SystemUI/res/values-kn/strings.xml b/packages/SystemUI/res/values-kn/strings.xml index 58633e67656a..d545e3123f21 100644 --- a/packages/SystemUI/res/values-kn/strings.xml +++ b/packages/SystemUI/res/values-kn/strings.xml @@ -1046,8 +1046,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"ನಿಯಂತ್ರಣಗಳನ್ನು ಮರುಹೊಂದಿಸಲು ಹೋಲ್ಡ್ ಮಾಡಿ ಮತ್ತು ಡ್ರ್ಯಾಗ್ ಮಾಡಿ"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"ಎಲ್ಲಾ ನಿಯಂತ್ರಣಗಳನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"ಬದಲಾವಣೆಗಳನ್ನು ಉಳಿಸಲಾಗಿಲ್ಲ"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"ಇತರ ಆ್ಯಪ್ಗಳನ್ನು ವೀಕ್ಷಿಸಿ"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"ನಿಯಂತ್ರಣಗಳನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ. ಆ್ಯಪ್ ಸೆಟ್ಟಿಂಗ್ಗಳು ಬದಲಾಗಿಲ್ಲ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಲು <xliff:g id="APP">%s</xliff:g> ಆ್ಯಪ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ."</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"ಹೊಂದಾಣಿಕೆಯ ನಿಯಂತ್ರಣಗಳು ಲಭ್ಯವಿಲ್ಲ"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"ಇತರ"</string> diff --git a/packages/SystemUI/res/values-ml/strings.xml b/packages/SystemUI/res/values-ml/strings.xml index 280c0c43ac6a..2a3ce7460f20 100644 --- a/packages/SystemUI/res/values-ml/strings.xml +++ b/packages/SystemUI/res/values-ml/strings.xml @@ -1046,8 +1046,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"നിയന്ത്രണങ്ങൾ പുനഃക്രമീകരിക്കാൻ അമർത്തിപ്പിടിച്ച് വലിച്ചിടുക"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"എല്ലാ നിയന്ത്രണങ്ങളും നീക്കം ചെയ്തു"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"മാറ്റങ്ങൾ സംരക്ഷിച്ചിട്ടില്ല"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"മറ്റ് ആപ്പുകൾ കാണുക"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"നിയന്ത്രണങ്ങൾ ലോഡ് ചെയ്യാനായില്ല. ആപ്പ് ക്രമീകരണം മാറ്റിയിട്ടില്ലെന്ന് ഉറപ്പാക്കാൻ <xliff:g id="APP">%s</xliff:g> ആപ്പ് പരിശോധിക്കുക."</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"അനുയോജ്യമായ നിയന്ത്രണങ്ങൾ ലഭ്യമല്ല"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"മറ്റുള്ളവ"</string> diff --git a/packages/SystemUI/res/values-mr/strings.xml b/packages/SystemUI/res/values-mr/strings.xml index 1c4ff5aabe84..850ef28a021f 100644 --- a/packages/SystemUI/res/values-mr/strings.xml +++ b/packages/SystemUI/res/values-mr/strings.xml @@ -1046,8 +1046,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"नियंत्रणांची पुनर्रचना करण्यासाठी धरून ठेवा आणि ड्रॅग करा"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"सर्व नियंत्रणे काढून टाकली आहेत"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"बदल सेव्ह केले गेले नाहीत"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"इतर अॅप्स पहा"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"नियंत्रणे लोड करता अली नाहीत. ॲपची सेटिंग्ज बदलली नसल्याची खात्री करण्यासाठी <xliff:g id="APP">%s</xliff:g> ॲप तपासा."</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"कंपॅटिबल नियंत्रणे उपलब्ध नाहीत"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"इतर"</string> diff --git a/packages/SystemUI/res/values-ne/strings.xml b/packages/SystemUI/res/values-ne/strings.xml index 811d9d272cce..daa5f365aadb 100644 --- a/packages/SystemUI/res/values-ne/strings.xml +++ b/packages/SystemUI/res/values-ne/strings.xml @@ -1046,8 +1046,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"नियन्त्रणहरूको क्रम मिलाउन तिनलाई थिचेर ड्र्याग गर्नुहोस्"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"सबै नियन्त्रणहरू हटाइए"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"परिवर्तनहरू सुरक्षित गरिएका छैनन्"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"अन्य एपहरू हेर्नुहोस्"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"नियन्त्रण सुविधाहरू लोड गर्न सकिएन। <xliff:g id="APP">%s</xliff:g> एपका सेटिङ परिवर्तन गरिएका छैनन् भन्ने कुरा सुनिश्चित गर्न उक्त एप जाँच्नुहोस्।"</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"मिल्दा नियन्त्रण सुविधाहरू उपलब्ध छैनन्"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"अन्य"</string> diff --git a/packages/SystemUI/res/values-pa/strings.xml b/packages/SystemUI/res/values-pa/strings.xml index 25cfb9c14cf9..e1d4a634eeb6 100644 --- a/packages/SystemUI/res/values-pa/strings.xml +++ b/packages/SystemUI/res/values-pa/strings.xml @@ -1046,8 +1046,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"ਕੰਟਰੋਲਾਂ ਨੂੰ ਮੁੜ-ਵਿਵਸਥਿਤ ਕਰਨ ਲਈ ਫੜ੍ਹ ਕੇ ਘਸੀਟੋ"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"ਸਾਰੇ ਕੰਟਰੋਲ ਹਟਾਏ ਗਏ"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"ਤਬਦੀਲੀਆਂ ਨੂੰ ਰੱਖਿਅਤ ਨਹੀਂ ਕੀਤਾ ਗਿਆ"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"ਹੋਰ ਐਪਾਂ ਦੇਖੋ"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"ਕੰਟਰੋਲਾਂ ਨੂੰ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ। ਇਹ ਪੱਕਾ ਕਰਨ ਲਈ <xliff:g id="APP">%s</xliff:g> ਐਪ ਦੀ ਜਾਂਚ ਕਰੋ ਕਿ ਐਪ ਸੈਟਿੰਗਾਂ ਨਹੀਂ ਬਦਲੀਆਂ ਹਨ।"</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"ਕੋਈ ਅਨੁਰੂਪ ਕੰਟਰੋਲ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"ਹੋਰ"</string> diff --git a/packages/SystemUI/res/values-te/strings.xml b/packages/SystemUI/res/values-te/strings.xml index 368e4e57787b..addc1b432e86 100644 --- a/packages/SystemUI/res/values-te/strings.xml +++ b/packages/SystemUI/res/values-te/strings.xml @@ -1046,8 +1046,7 @@ <string name="controls_favorite_rearrange" msgid="5616952398043063519">"నియంత్రణల క్రమం మార్చడానికి దేనినైనా పట్టుకుని, లాగి వదిలేయండి"</string> <string name="controls_favorite_removed" msgid="5276978408529217272">"అన్ని నియంత్రణలు తీసివేయబడ్డాయి"</string> <string name="controls_favorite_toast_no_changes" msgid="7094494210840877931">"మార్పులు సేవ్ చేయబడలేదు"</string> - <!-- no translation found for controls_favorite_see_other_apps (7709087332255283460) --> - <skip /> + <string name="controls_favorite_see_other_apps" msgid="7709087332255283460">"ఇతర యాప్లను చూడండి"</string> <string name="controls_favorite_load_error" msgid="5126216176144877419">"కంట్రోల్లను లోడ్ చేయడం సాధ్యపడలేదు. యాప్ సెట్టింగ్లు మారలేదని నిర్ధారించడానికి <xliff:g id="APP">%s</xliff:g> యాప్ను చెక్ చేయండి."</string> <string name="controls_favorite_load_none" msgid="7687593026725357775">"అనుకూల కంట్రోల్లు అందుబాటులో లేవు"</string> <string name="controls_favorite_other_zone_header" msgid="9089613266575525252">"ఇతరం"</string> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 87e1499713bd..eb8758c0d921 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1269,10 +1269,10 @@ <dimen name="qs_media_padding">16dp</dimen> <dimen name="qs_media_panel_outer_padding">16dp</dimen> <dimen name="qs_media_album_size">52dp</dimen> + <dimen name="qs_media_icon_size">16dp</dimen> <dimen name="qs_center_guideline_padding">10dp</dimen> - <dimen name="qs_seamless_icon_size">20dp</dimen> - <dimen name="qs_seamless_fallback_icon_size">20dp</dimen> - <dimen name="qs_seamless_fallback_top_margin">18dp</dimen> + <dimen name="qs_seamless_icon_size">@dimen/qs_media_icon_size</dimen> + <dimen name="qs_seamless_fallback_icon_size">@dimen/qs_seamless_icon_size</dimen> <dimen name="qs_seamless_fallback_end_margin">16dp</dimen> <dimen name="qqs_media_spacing">16dp</dimen> <dimen name="qs_footer_horizontal_margin">22dp</dimen> diff --git a/packages/SystemUI/res/xml/media_collapsed.xml b/packages/SystemUI/res/xml/media_collapsed.xml index b17aa42f1e9f..ee958f28c51b 100644 --- a/packages/SystemUI/res/xml/media_collapsed.xml +++ b/packages/SystemUI/res/xml/media_collapsed.xml @@ -19,11 +19,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> <Constraint android:id="@+id/icon" - android:layout_width="16dp" - android:layout_height="16dp" + android:layout_width="@dimen/qs_media_icon_size" + android:layout_height="@dimen/qs_media_icon_size" android:layout_marginStart="18dp" - android:layout_marginTop="22dp" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toTopOf="@id/app_name" + app:layout_constraintBottom_toBottomOf="@id/app_name" app:layout_constraintStart_toStartOf="parent" /> @@ -59,14 +59,14 @@ android:id="@+id/media_seamless_fallback" android:layout_width="@dimen/qs_seamless_fallback_icon_size" android:layout_height="@dimen/qs_seamless_fallback_icon_size" - android:layout_marginTop="@dimen/qs_seamless_fallback_top_margin" android:layout_marginEnd="@dimen/qs_seamless_fallback_end_margin" android:layout_marginStart="@dimen/qs_center_guideline_padding" android:alpha="0.5" android:visibility="gone" app:layout_constraintHorizontal_bias="1" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toTopOf="@id/app_name" + app:layout_constraintBottom_toBottomOf="@id/app_name" app:layout_constraintStart_toEndOf="@id/center_vertical_guideline" /> @@ -85,10 +85,11 @@ <!-- Song name --> <Constraint android:id="@+id/header_title" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="17dp" android:layout_marginStart="16dp" + app:layout_constrainedWidth="true" app:layout_constraintTop_toBottomOf="@id/app_name" app:layout_constraintBottom_toTopOf="@id/header_artist" app:layout_constraintStart_toEndOf="@id/album_art" @@ -98,10 +99,11 @@ <!-- Artist name --> <Constraint android:id="@+id/header_artist" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="3dp" android:layout_marginBottom="24dp" + app:layout_constrainedWidth="true" app:layout_constraintTop_toBottomOf="@id/header_title" app:layout_constraintStart_toStartOf="@id/header_title" app:layout_constraintEnd_toStartOf="@id/media_action_barrier" @@ -135,27 +137,6 @@ /> <Constraint - android:id="@+id/media_action_barrier" - android:layout_width="0dp" - android:layout_height="0dp" - android:orientation="vertical" - app:layout_constraintTop_toTopOf="parent" - app:barrierDirection="start" - app:constraint_referenced_ids="media_action_guidebox,action0,action1,action2,action3,action4" - /> - - <Constraint - android:id="@+id/media_action_guidebox" - android:layout_width="0dp" - android:layout_height="48dp" - android:layout_marginTop="18dp" - android:visibility="invisible" - app:layout_constraintTop_toBottomOf="@id/app_name" - app:layout_constraintStart_toEndOf="@id/header_title" - app:layout_constraintEnd_toEndOf="parent" - /> - - <Constraint android:id="@+id/action0" android:layout_width="48dp" android:layout_height="48dp" @@ -165,8 +146,9 @@ android:visibility="gone" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintTop_toBottomOf="@id/app_name" - app:layout_constraintLeft_toLeftOf="@id/media_action_guidebox" + app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@id/action1" + app:layout_constraintHorizontal_bias="1" > </Constraint> @@ -220,7 +202,8 @@ app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintTop_toBottomOf="@id/app_name" app:layout_constraintLeft_toRightOf="@id/action3" - app:layout_constraintRight_toRightOf="@id/media_action_guidebox" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintHorizontal_bias="0" > </Constraint> </ConstraintSet> diff --git a/packages/SystemUI/res/xml/media_expanded.xml b/packages/SystemUI/res/xml/media_expanded.xml index 8010f97bf560..d5a02c2d39d5 100644 --- a/packages/SystemUI/res/xml/media_expanded.xml +++ b/packages/SystemUI/res/xml/media_expanded.xml @@ -19,11 +19,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> <Constraint android:id="@+id/icon" - android:layout_width="16dp" - android:layout_height="16dp" + android:layout_width="@dimen/qs_media_icon_size" + android:layout_height="@dimen/qs_media_icon_size" android:layout_marginStart="18dp" - android:layout_marginTop="22dp" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toTopOf="@id/app_name" + app:layout_constraintBottom_toBottomOf="@id/app_name" app:layout_constraintStart_toStartOf="parent" /> @@ -59,14 +59,14 @@ android:id="@+id/media_seamless_fallback" android:layout_width="@dimen/qs_seamless_fallback_icon_size" android:layout_height="@dimen/qs_seamless_fallback_icon_size" - android:layout_marginTop="@dimen/qs_seamless_fallback_top_margin" android:layout_marginEnd="@dimen/qs_seamless_fallback_end_margin" android:layout_marginStart="@dimen/qs_center_guideline_padding" android:alpha="0.5" android:visibility="gone" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toTopOf="@id/app_name" + app:layout_constraintBottom_toBottomOf="@id/app_name" app:layout_constraintStart_toEndOf="@id/center_vertical_guideline" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" /> @@ -83,11 +83,12 @@ <!-- Song name --> <Constraint android:id="@+id/header_title" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" android:layout_marginTop="17dp" android:layout_marginStart="16dp" + app:layout_constrainedWidth="true" app:layout_constraintTop_toBottomOf="@+id/app_name" app:layout_constraintStart_toEndOf="@id/album_art" app:layout_constraintEnd_toEndOf="parent" @@ -96,10 +97,11 @@ <!-- Artist name --> <Constraint android:id="@+id/header_artist" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/qs_media_panel_outer_padding" android:layout_marginTop="3dp" + app:layout_constrainedWidth="true" app:layout_constraintTop_toBottomOf="@id/header_title" app:layout_constraintStart_toStartOf="@id/header_title" app:layout_constraintEnd_toEndOf="parent" diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index 6dc8322a5cf3..c6d128631930 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -124,8 +124,26 @@ class Bubble implements BubbleViewProvider { private int mNotificationId; private int mAppUid = -1; + /** + * A bubble is created and can be updated. This intent is updated until the user first + * expands the bubble. Once the user has expanded the contents, we ignore the intent updates + * to prevent restarting the intent & possibly altering UI state in the activity in front of + * the user. + * + * Once the bubble is overflowed, the activity is finished and updates to the + * notification are respected. Typically an update to an overflowed bubble would result in + * that bubble being added back to the stack anyways. + */ @Nullable private PendingIntent mIntent; + private boolean mIntentActive; + @Nullable + private PendingIntent.CancelListener mIntentCancelListener; + + /** + * Sent when the bubble & notification are no longer visible to the user (i.e. no + * notification in the shade, no bubble in the stack or overflow). + */ @Nullable private PendingIntent mDeleteIntent; @@ -150,13 +168,19 @@ class Bubble implements BubbleViewProvider { mShowBubbleUpdateDot = false; } - /** Used in tests when no UI is required. */ @VisibleForTesting(visibility = PRIVATE) Bubble(@NonNull final NotificationEntry e, - @Nullable final BubbleController.NotificationSuppressionChangedListener listener) { + @Nullable final BubbleController.NotificationSuppressionChangedListener listener, + final BubbleController.PendingIntentCanceledListener intentCancelListener) { Objects.requireNonNull(e); mKey = e.getKey(); mSuppressionListener = listener; + mIntentCancelListener = intent -> { + if (mIntent != null) { + mIntent.unregisterCancelListener(mIntentCancelListener); + } + intentCancelListener.onPendingIntentCanceled(this); + }; setEntry(e); } @@ -238,6 +262,10 @@ class Bubble implements BubbleViewProvider { mExpandedView = null; } mIconView = null; + if (mIntent != null) { + mIntent.unregisterCancelListener(mIntentCancelListener); + } + mIntentActive = false; } void setPendingIntentCanceled() { @@ -371,11 +399,24 @@ class Bubble implements BubbleViewProvider { mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight(); mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); mIcon = entry.getBubbleMetadata().getIcon(); - mIntent = entry.getBubbleMetadata().getIntent(); + + if (!mIntentActive || mIntent == null) { + if (mIntent != null) { + mIntent.unregisterCancelListener(mIntentCancelListener); + } + mIntent = entry.getBubbleMetadata().getIntent(); + if (mIntent != null) { + mIntent.registerCancelListener(mIntentCancelListener); + } + } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { + // Was an intent bubble now it's a shortcut bubble... still unregister the listener + mIntent.unregisterCancelListener(mIntentCancelListener); + mIntent = null; + } mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); } mIsImportantConversation = - entry.getChannel() == null ? false : entry.getChannel().isImportantConversation(); + entry.getChannel() != null && entry.getChannel().isImportantConversation(); } @Nullable @@ -395,10 +436,15 @@ class Bubble implements BubbleViewProvider { } /** - * @return if the bubble was ever expanded + * Sets if the intent used for this bubble is currently active (i.e. populating an + * expanded view, expanded or not). */ - boolean getWasAccessed() { - return mLastAccessed != 0L; + void setIntentActive() { + mIntentActive = true; + } + + boolean isIntentActive() { + return mIntentActive; } /** diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 6ea0cde44282..6f103b020814 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -263,6 +263,16 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** + * Listener to be notified when a pending intent has been canceled for a bubble. + */ + public interface PendingIntentCanceledListener { + /** + * Called when the pending intent for a bubble has been canceled. + */ + void onPendingIntentCanceled(Bubble bubble); + } + + /** * Callback for when the BubbleController wants to interact with the notification pipeline to: * - Remove a previously bubbled notification * - Update the notification shade since bubbled notification should/shouldn't be showing @@ -390,6 +400,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } }); + mBubbleData.setPendingIntentCancelledListener(bubble -> { + if (bubble.getBubbleIntent() == null) { + return; + } + if (bubble.isIntentActive()) { + bubble.setPendingIntentCanceled(); + return; + } + mHandler.post( + () -> removeBubble(bubble.getKey(), + BubbleController.DISMISS_INVALID_INTENT)); + }); mNotificationEntryManager = entryManager; mNotificationGroupManager = groupManager; @@ -1101,23 +1123,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // Lazy init stack view when a bubble is created ensureStackViewCreated(); bubble.setInflateSynchronously(mInflateSynchronously); - bubble.inflate( - b -> { - mBubbleData.notificationEntryUpdated(b, suppressFlyout, - showInShade); - if (bubble.getBubbleIntent() == null) { - return; - } - bubble.getBubbleIntent().registerCancelListener(pendingIntent -> { - if (bubble.getWasAccessed()) { - bubble.setPendingIntentCanceled(); - return; - } - mHandler.post( - () -> removeBubble(bubble.getKey(), - BubbleController.DISMISS_INVALID_INTENT)); - }); - }, + bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), mContext, mStackView, mBubbleIconFactory, false /* skipInflation */); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index c170ee271e1d..d2dc506c8e5c 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -59,7 +59,7 @@ import javax.inject.Singleton; @Singleton public class BubbleData { - BubbleLogger mLogger = new BubbleLoggerImpl(); + private BubbleLogger mLogger = new BubbleLoggerImpl(); private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES; @@ -137,6 +137,7 @@ public class BubbleData { @Nullable private BubbleController.NotificationSuppressionChangedListener mSuppressionListener; + private BubbleController.PendingIntentCanceledListener mCancelledListener; /** * We track groups with summaries that aren't visibly displayed but still kept around because @@ -167,6 +168,11 @@ public class BubbleData { mSuppressionListener = listener; } + public void setPendingIntentCancelledListener( + BubbleController.PendingIntentCanceledListener listener) { + mCancelledListener = listener; + } + public boolean hasBubbles() { return !mBubbles.isEmpty(); } @@ -236,7 +242,7 @@ public class BubbleData { bubbleToReturn = mPendingBubbles.get(key); } else if (entry != null) { // New bubble - bubbleToReturn = new Bubble(entry, mSuppressionListener); + bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener); } else { // Persisted bubble being promoted bubbleToReturn = persistedBubble; @@ -476,6 +482,9 @@ public class BubbleData { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "Cancel overflow bubble: " + b); } + if (b != null) { + b.stopInflation(); + } mLogger.logOverflowRemove(b, reason); mStateChange.bubbleRemoved(b, reason); mOverflowBubbles.remove(b); @@ -483,6 +492,7 @@ public class BubbleData { return; } Bubble bubbleToRemove = mBubbles.get(indexToRemove); + bubbleToRemove.stopInflation(); if (mBubbles.size() == 1) { // Going to become empty, handle specially. setExpandedInternal(false); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index 1211fb491ced..3d3171208b15 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -183,6 +183,9 @@ public class BubbleExpandedView extends LinearLayout { // Apply flags to make behaviour match documentLaunchMode=always. fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + if (mBubble != null) { + mBubble.setIntentActive(); + } mActivityView.startActivity(mPendingIntent, fillInIntent, options); } } catch (RuntimeException e) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index cd27fdf9c947..749b537ea364 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -290,6 +290,12 @@ public class BubbleStackView extends FrameLayout /** Whether we're in the middle of dragging the stack around by touch. */ private boolean mIsDraggingStack = false; + /** + * The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore + * touches from other pointer indices. + */ + private int mPointerIndexDown = -1; + /** Description of current animation controller state. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Stack view state:"); @@ -2220,6 +2226,18 @@ public class BubbleStackView extends FrameLayout @Override public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) { + // Ignore touches from additional pointer indices. + return false; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mPointerIndexDown = ev.getActionIndex(); + } else if (ev.getAction() == MotionEvent.ACTION_UP + || ev.getAction() == MotionEvent.ACTION_CANCEL) { + mPointerIndexDown = -1; + } + boolean dispatched = super.dispatchTouchEvent(ev); // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 6b5c8807fbd4..75f4809d752f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -589,7 +589,7 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable { if (targetUserId != ActivityManager.getCurrentUser()) { return; } - + if (DEBUG) Log.d(TAG, "keyguardDone"); tryKeyguardDone(); } @@ -608,6 +608,7 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable { @Override public void keyguardDonePending(boolean strongAuth, int targetUserId) { Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardDonePending"); + if (DEBUG) Log.d(TAG, "keyguardDonePending"); if (targetUserId != ActivityManager.getCurrentUser()) { Trace.endSection(); return; @@ -626,6 +627,7 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable { @Override public void keyguardGone() { Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardGone"); + if (DEBUG) Log.d(TAG, "keyguardGone"); mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false); mKeyguardDisplayManager.hide(); Trace.endSection(); @@ -1690,9 +1692,14 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable { }; private void tryKeyguardDone() { + if (DEBUG) { + Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - " + + mHideAnimationRun + " animRunning - " + mHideAnimationRunning); + } if (!mKeyguardDonePending && mHideAnimationRun && !mHideAnimationRunning) { handleKeyguardDone(); } else if (!mHideAnimationRun) { + if (DEBUG) Log.d(TAG, "tryKeyguardDone: starting pre-hide animation"); mHideAnimationRun = true; mHideAnimationRunning = true; mKeyguardViewControllerLazy.get() @@ -1919,6 +1926,7 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable { }; private final Runnable mHideAnimationFinishedRunnable = () -> { + Log.e(TAG, "mHideAnimationFinishedRunnable#run"); mHideAnimationRunning = false; tryKeyguardDone(); }; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 7c09accae649..127c5dd54d72 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -39,9 +39,8 @@ class MediaCarouselController @Inject constructor( private val mediaHostStatesManager: MediaHostStatesManager, private val activityStarter: ActivityStarter, @Main executor: DelayableExecutor, - mediaManager: MediaDataCombineLatest, + mediaManager: MediaDataFilter, configurationController: ConfigurationController, - mediaDataManager: MediaDataManager, falsingManager: FalsingManager ) { /** @@ -148,7 +147,7 @@ class MediaCarouselController @Inject constructor( mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, - executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation, + executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation, falsingManager) isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL inflateSettingsButton() @@ -249,6 +248,7 @@ class MediaCarouselController @Inject constructor( val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) existingPlayer.view?.player?.setLayoutParams(lp) + existingPlayer.bind(data) existingPlayer.setListening(currentlyExpanded) updatePlayerToState(existingPlayer, noAnimation = true) if (existingPlayer.isPlaying) { @@ -256,16 +256,18 @@ class MediaCarouselController @Inject constructor( } else { mediaContent.addView(existingPlayer.view?.player) } - } else if (existingPlayer.isPlaying && - mediaContent.indexOfChild(existingPlayer.view?.player) != 0) { - if (visualStabilityManager.isReorderingAllowed) { - mediaContent.removeView(existingPlayer.view?.player) - mediaContent.addView(existingPlayer.view?.player, 0) - } else { - needsReordering = true + } else { + existingPlayer.bind(data) + if (existingPlayer.isPlaying && + mediaContent.indexOfChild(existingPlayer.view?.player) != 0) { + if (visualStabilityManager.isReorderingAllowed) { + mediaContent.removeView(existingPlayer.view?.player) + mediaContent.addView(existingPlayer.view?.player, 0) + } else { + needsReordering = true + } } } - existingPlayer?.bind(data) updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() mediaCarousel.requiresRemeasuring = true @@ -299,6 +301,7 @@ class MediaCarouselController @Inject constructor( if (numPages == 1) { pageIndicator.setLocation(0f) } + updatePageIndicatorAlpha() } /** @@ -464,7 +467,7 @@ class MediaCarouselController @Inject constructor( val width = desiredHostState?.measurementInput?.width ?: 0 val height = desiredHostState?.measurementInput?.height ?: 0 if (width != carouselMeasureWidth && width != 0 || - height != carouselMeasureWidth && height != 0) { + height != carouselMeasureHeight && height != 0) { carouselMeasureWidth = width carouselMeasureHeight = height val playerWidthPlusPadding = carouselMeasureWidth + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index ef2f71100e1a..3096908aca21 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -243,7 +243,7 @@ class MediaCarouselScrollHandler( } val rotation = (1.0f - settingsOffset) * 50 settingsButton.rotation = rotation * -Math.signum(contentTranslation) - val alpha = MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset) + val alpha = MathUtils.saturate(MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset)) settingsButton.alpha = alpha settingsButton.visibility = if (alpha != 0.0f) View.VISIBLE else View.INVISIBLE settingsButton.translationX = newTranslationX diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 8c9cb1b240bf..dafc52ad8025 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -23,6 +23,7 @@ import android.media.session.MediaSession /** State of a media view. */ data class MediaData( + val userId: Int, val initialized: Boolean = false, val backgroundColor: Int, /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt index 11cbc482459a..e8f0e069c98d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt @@ -58,6 +58,17 @@ class MediaDataCombineLatest @Inject constructor( } /** + * Get a map of all non-null data entries + */ + fun getData(): Map<String, MediaData> { + return entries.filter { + (key, pair) -> pair.first != null && pair.second != null + }.mapValues { + (key, pair) -> pair.first!!.copy(device = pair.second) + } + } + + /** * Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData]. */ fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt new file mode 100644 index 000000000000..662831e4a445 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media + +import android.util.Log +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +private const val TAG = "MediaDataFilter" + +/** + * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user + * switches (removing entries for the previous user, adding back entries for the current user) + * + * This is added downstream of [MediaDataManager] since we may still need to handle callbacks from + * background users (e.g. timeouts) that UI classes should ignore. + * Instead, UI classes should listen to this so they can stay in sync with the current user. + */ +@Singleton +class MediaDataFilter @Inject constructor( + private val dataSource: MediaDataCombineLatest, + private val broadcastDispatcher: BroadcastDispatcher, + private val mediaResumeListener: MediaResumeListener, + private val mediaDataManager: MediaDataManager, + private val lockscreenUserManager: NotificationLockscreenUserManager, + @Main private val executor: Executor +) : MediaDataManager.Listener { + private val userTracker: CurrentUserTracker + private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() + + // The filtered mediaEntries, which will be a subset of all mediaEntries in MediaDataManager + private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + + init { + userTracker = object : CurrentUserTracker(broadcastDispatcher) { + override fun onUserSwitched(newUserId: Int) { + // Post this so we can be sure lockscreenUserManager already got the broadcast + executor.execute { handleUserSwitched(newUserId) } + } + } + userTracker.startTracking() + dataSource.addListener(this) + } + + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + if (!lockscreenUserManager.isCurrentProfile(data.userId)) { + return + } + + if (oldKey != null) { + mediaEntries.remove(oldKey) + } + mediaEntries.put(key, data) + + // Notify listeners + val listenersCopy = listeners.toSet() + listenersCopy.forEach { + it.onMediaDataLoaded(key, oldKey, data) + } + } + + override fun onMediaDataRemoved(key: String) { + mediaEntries.remove(key)?.let { + // Only notify listeners if something actually changed + val listenersCopy = listeners.toSet() + listenersCopy.forEach { + it.onMediaDataRemoved(key) + } + } + } + + @VisibleForTesting + internal fun handleUserSwitched(id: Int) { + // If the user changes, remove all current MediaData objects and inform listeners + val listenersCopy = listeners.toSet() + val keyCopy = mediaEntries.keys.toMutableList() + // Clear the list first, to make sure callbacks from listeners if we have any entries + // are up to date + mediaEntries.clear() + keyCopy.forEach { + Log.d(TAG, "Removing $it after user change") + listenersCopy.forEach { listener -> + listener.onMediaDataRemoved(it) + } + } + + dataSource.getData().forEach { (key, data) -> + if (lockscreenUserManager.isCurrentProfile(data.userId)) { + Log.d(TAG, "Re-adding $key after user change") + mediaEntries.put(key, data) + listenersCopy.forEach { listener -> + listener.onMediaDataLoaded(key, null, data) + } + } + } + } + + /** + * Invoked when the user has dismissed the media carousel + */ + fun onSwipeToDismiss() { + val mediaKeys = mediaEntries.keys.toSet() + mediaKeys.forEach { + mediaDataManager.setTimedOut(it, timedOut = true) + } + } + + /** + * Are there any media notifications active? + */ + fun hasActiveMedia() = mediaEntries.any { it.value.active } + + /** + * Are there any media entries we should display? + * If resumption is enabled, this will include inactive players + * If resumption is disabled, we only want to show active players + */ + fun hasAnyMedia() = if (mediaResumeListener.isResumptionEnabled()) { + mediaEntries.isNotEmpty() + } else { + hasActiveMedia() + } + + /** + * Add a listener for filtered [MediaData] changes + */ + fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) + + /** + * Remove a listener that was registered with addListener + */ + fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index d6b6660b778c..8cb93bfc6d4d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -67,7 +67,7 @@ private const val DEFAULT_LUMINOSITY = 0.25f private const val LUMINOSITY_THRESHOLD = 0.05f private const val SATURATION_MULTIPLIER = 0.8f -private val LOADING = MediaData(false, 0, null, null, null, null, null, +private val LOADING = MediaData(-1, false, 0, null, null, null, null, null, emptyList(), emptyList(), "INVALID", null, null, null, true, null) fun isMediaNotification(sbn: StatusBarNotification): Boolean { @@ -116,15 +116,6 @@ class MediaDataManager( broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context)) - private val userChangeReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (Intent.ACTION_USER_SWITCHED == intent.action) { - // Remove all controls, regardless of state - clearData() - } - } - } - private val appChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { @@ -152,9 +143,6 @@ class MediaDataManager( mediaResumeListener.setManager(this) addListener(mediaResumeListener) - val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED) - broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL) - val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) @@ -169,7 +157,6 @@ class MediaDataManager( fun destroy() { context.unregisterReceiver(appChangeReceiver) - broadcastDispatcher.unregisterReceiver(userChangeReceiver) } fun onNotificationAdded(key: String, sbn: StatusBarNotification) { @@ -190,20 +177,6 @@ class MediaDataManager( } } - private fun clearData() { - // Called on user change. Remove all current MediaData objects and inform listeners - val listenersCopy = listeners.toSet() - val keyCopy = mediaEntries.keys.toMutableList() - // Clear the list first, to make sure callbacks from listeners if we have any entries - // are up to date - mediaEntries.clear() - keyCopy.forEach { - listenersCopy.forEach { listener -> - listener.onMediaDataRemoved(it) - } - } - } - private fun removeAllForPackage(packageName: String) { Assert.isMainThread() val listenersCopy = listeners.toSet() @@ -224,6 +197,7 @@ class MediaDataManager( } fun addResumptionControls( + userId: Int, desc: MediaDescription, action: Runnable, token: MediaSession.Token, @@ -238,7 +212,8 @@ class MediaDataManager( mediaEntries.put(packageName, resumeData) } backgroundExecutor.execute { - loadMediaDataInBgForResumption(desc, action, token, appName, appIntent, packageName) + loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent, + packageName) } } @@ -282,7 +257,7 @@ class MediaDataManager( * This will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active */ - private fun setTimedOut(token: String, timedOut: Boolean) { + internal fun setTimedOut(token: String, timedOut: Boolean) { mediaEntries[token]?.let { if (it.active == !timedOut) { return @@ -293,6 +268,7 @@ class MediaDataManager( } private fun loadMediaDataInBgForResumption( + userId: Int, desc: MediaDescription, resumeAction: Runnable, token: MediaSession.Token, @@ -307,7 +283,7 @@ class MediaDataManager( return } - Log.d(TAG, "adding track from browser: $desc") + Log.d(TAG, "adding track for $userId from browser: $desc") // Album art var artworkBitmap = desc.iconBitmap @@ -323,7 +299,7 @@ class MediaDataManager( val mediaAction = getResumeMediaAction(resumeAction) foregroundExecutor.execute { - onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName, + onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName, null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), packageName, token, appIntent, device = null, active = false, resumeAction = resumeAction, resumption = true, notificationKey = packageName, @@ -439,10 +415,11 @@ class MediaDataManager( val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true val active = mediaEntries[key]?.active ?: true - onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist, - song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, - notif.contentIntent, null, active, resumeAction = resumeAction, - notificationKey = key, hasCheckedForResume = hasCheckedForResume)) + onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app, + smallIconDrawable, artist, song, artWorkIcon, actionIcons, + actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, + active, resumeAction = resumeAction, notificationKey = key, + hasCheckedForResume = hasCheckedForResume)) } } @@ -564,18 +541,6 @@ class MediaDataManager( } } - /** - * Are there any media notifications active? - */ - fun hasActiveMedia() = mediaEntries.any { it.value.active } - - /** - * Are there any media entries we should display? - * If resumption is enabled, this will include inactive players - * If resumption is disabled, we only want to show active players - */ - fun hasAnyMedia() = if (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia() - fun setMediaResumptionEnabled(isEnabled: Boolean) { if (useMediaResumption == isEnabled) { return @@ -596,16 +561,6 @@ class MediaDataManager( } } - /** - * Invoked when the user has dismissed the media carousel - */ - fun onSwipeToDismiss() { - val mediaKeys = mediaEntries.keys.toSet() - mediaKeys.forEach { - setTimedOut(it, timedOut = true) - } - } - interface Listener { /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 3d2b72d8fd83..fc33391a9ad1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -40,6 +40,28 @@ import javax.inject.Inject import javax.inject.Singleton /** + * Similarly to isShown but also excludes views that have 0 alpha + */ +val View.isShownNotFaded: Boolean + get() { + var current: View = this + while (true) { + if (current.visibility != View.VISIBLE) { + return false + } + if (current.alpha == 0.0f) { + return false + } + val parent = current.parent ?: return false // We are not attached to the view root + if (parent !is View) { + // we reached the viewroot, hurray + return true + } + current = parent + } + } + +/** * This manager is responsible for placement of the unique media view between the different hosts * and animate the positions of the views to achieve seamless transitions. */ @@ -368,7 +390,7 @@ class MediaHierarchyManager @Inject constructor( // non-trivial reattaching logic happening that will make the view not-shown earlier return true } - return mediaFrame.isShown || animator.isRunning || animationPending + return mediaFrame.isShownNotFaded || animator.isRunning || animationPending } private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index 07a7e618b301..3598719fcb3a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -1,6 +1,5 @@ package com.android.systemui.media -import android.graphics.PointF import android.graphics.Rect import android.util.ArraySet import android.view.View @@ -15,8 +14,7 @@ import javax.inject.Inject class MediaHost @Inject constructor( private val state: MediaHostStateHolder, private val mediaHierarchyManager: MediaHierarchyManager, - private val mediaDataManager: MediaDataManager, - private val mediaDataManagerCombineLatest: MediaDataCombineLatest, + private val mediaDataFilter: MediaDataFilter, private val mediaHostStatesManager: MediaHostStatesManager ) : MediaHostState by state { lateinit var hostView: UniqueObjectHostView @@ -81,12 +79,12 @@ class MediaHost @Inject constructor( // be a delay until the views and the controllers are initialized, leaving us // with either a blank view or the controllers not yet initialized and the // measuring wrong - mediaDataManagerCombineLatest.addListener(listener) + mediaDataFilter.addListener(listener) updateViewVisibility() } override fun onViewDetachedFromWindow(v: View?) { - mediaDataManagerCombineLatest.removeListener(listener) + mediaDataFilter.removeListener(listener) } }) @@ -101,7 +99,7 @@ class MediaHost @Inject constructor( } // This will trigger a state change that ensures that we now have a state available state.measurementInput = input - return mediaHostStatesManager.getPlayerDimensions(state) + return mediaHostStatesManager.updateCarouselDimensions(location, state) } } @@ -115,9 +113,9 @@ class MediaHost @Inject constructor( private fun updateViewVisibility() { visible = if (showsOnlyActiveMedia) { - mediaDataManager.hasActiveMedia() + mediaDataFilter.hasActiveMedia() } else { - mediaDataManager.hasAnyMedia() + mediaDataFilter.hasAnyMedia() } val newVisibility = if (visible) View.VISIBLE else View.GONE if (newVisibility != hostView.visibility) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt index f90af2a01de0..d3954b70ca71 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt @@ -32,6 +32,12 @@ class MediaHostStatesManager @Inject constructor() { private val controllers: MutableSet<MediaViewController> = mutableSetOf() /** + * The overall sizes of the carousel. This is needed to make sure all players in the carousel + * have equal size. + */ + val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf() + + /** * A map with all media states of all locations. */ val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf() @@ -45,6 +51,7 @@ class MediaHostStatesManager @Inject constructor() { if (!hostState.equals(currentState)) { val newState = hostState.copy() mediaHostStates.put(location, newState) + updateCarouselDimensions(location, hostState) // First update all the controllers to ensure they get the chance to measure for (controller in controllers) { controller.stateCallback.onHostStateChanged(location, newState) @@ -61,7 +68,10 @@ class MediaHostStatesManager @Inject constructor() { * Get the dimensions of all players combined, which determines the overall height of the * media carousel and the media hosts. */ - fun getPlayerDimensions(hostState: MediaHostState): MeasurementOutput { + fun updateCarouselDimensions( + @MediaLocation location: Int, + hostState: MediaHostState + ): MeasurementOutput { val result = MeasurementOutput(0, 0) for (controller in controllers) { val measurement = controller.getMeasurementsForState(hostState) @@ -74,6 +84,7 @@ class MediaHostStatesManager @Inject constructor() { } } } + carouselSizes[location] = result return result } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index 0cc1e7bb1b56..4ec746fcb153 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -56,7 +56,7 @@ class MediaResumeListener @Inject constructor( private lateinit var mediaDataManager: MediaDataManager private var mediaBrowser: ResumeMediaBrowser? = null - private var currentUserId: Int + private var currentUserId: Int = context.userId private val userChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -65,7 +65,6 @@ class MediaResumeListener @Inject constructor( } else if (Intent.ACTION_USER_SWITCHED == intent.action) { currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) loadSavedComponents() - loadMediaResumptionControls() } } } @@ -89,13 +88,12 @@ class MediaResumeListener @Inject constructor( } Log.d(TAG, "Adding resume controls $desc") - mediaDataManager.addResumptionControls(desc, resumeAction, token, appName.toString(), - appIntent, component.packageName) + mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token, + appName.toString(), appIntent, component.packageName) } } init { - currentUserId = context.userId if (useMediaResumption) { val unlockFilter = IntentFilter() unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED) @@ -118,6 +116,8 @@ class MediaResumeListener @Inject constructor( }, Settings.Secure.MEDIA_CONTROLS_RESUME) } + fun isResumptionEnabled() = useMediaResumption + private fun loadSavedComponents() { // Make sure list is empty (if we switched users) resumeComponents.clear() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index 9a134dbe0264..8662aacfdab2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -54,7 +54,32 @@ class MediaTimeoutListener @Inject constructor( if (mediaListeners.containsKey(key)) { return } + // Having an old key means that we're migrating from/to resumption. We should invalidate + // the old listener and create a new one. + val migrating = oldKey != null && key != oldKey + var wasPlaying = false + if (migrating) { + if (mediaListeners.containsKey(oldKey)) { + val oldListener = mediaListeners.remove(oldKey) + wasPlaying = oldListener?.playing ?: false + oldListener?.destroy() + if (DEBUG) Log.d(TAG, "migrating key $oldKey to $key, for resumption") + } else { + Log.w(TAG, "Old key $oldKey for player $key doesn't exist. Continuing...") + } + } mediaListeners[key] = PlaybackStateListener(key, data) + + // If a player becomes active because of a migration, we'll need to broadcast its state. + // Doing it now would lead to reentrant callbacks, so let's wait until we're done. + if (migrating && mediaListeners[key]?.playing != wasPlaying) { + mainExecutor.execute { + if (mediaListeners[key]?.playing == true) { + if (DEBUG) Log.d(TAG, "deliver delayed playback state for $key") + timeoutCallback.invoke(key, false /* timedOut */) + } + } + } } override fun onMediaDataRemoved(key: String) { @@ -71,7 +96,7 @@ class MediaTimeoutListener @Inject constructor( ) : MediaController.Callback() { var timedOut = false - private var playing: Boolean? = null + var playing: Boolean? = null // Resume controls may have null token private val mediaController = if (data.token != null) { @@ -83,7 +108,9 @@ class MediaTimeoutListener @Inject constructor( init { mediaController?.registerCallback(this) - onPlaybackStateChanged(mediaController?.playbackState) + // Let's register the cancellations, but not dispatch events now. + // Timeouts didn't happen yet and reentrant events are troublesome. + processState(mediaController?.playbackState, dispatchEvents = false) } fun destroy() { @@ -91,8 +118,12 @@ class MediaTimeoutListener @Inject constructor( } override fun onPlaybackStateChanged(state: PlaybackState?) { + processState(state, dispatchEvents = true) + } + + private fun processState(state: PlaybackState?, dispatchEvents: Boolean) { if (DEBUG) { - Log.v(TAG, "onPlaybackStateChanged: $state") + Log.v(TAG, "processState: $state") } val isPlaying = state != null && isPlayingState(state.state) @@ -116,12 +147,16 @@ class MediaTimeoutListener @Inject constructor( Log.v(TAG, "Execute timeout for $key") } timedOut = true - timeoutCallback(key, timedOut) + if (dispatchEvents) { + timeoutCallback(key, timedOut) + } }, PAUSED_MEDIA_TIMEOUT) } else { expireMediaTimeout(key, "playback started - $state, $key") timedOut = false - timeoutCallback(key, timedOut) + if (dispatchEvents) { + timeoutCallback(key, timedOut) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt index 033a42a03240..38817d7b579e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt @@ -79,6 +79,16 @@ class MediaViewController @Inject constructor( private val tmpState = TransitionViewState() /** + * A temporary state used to store intermediate measurements. + */ + private val tmpState2 = TransitionViewState() + + /** + * A temporary state used to store intermediate measurements. + */ + private val tmpState3 = TransitionViewState() + + /** * A temporary cache key to be used to look up cache entries */ private val tmpKey = CacheKey() @@ -258,7 +268,6 @@ class MediaViewController @Inject constructor( fun attach(transitionLayout: TransitionLayout) { this.transitionLayout = transitionLayout layoutController.attach(transitionLayout) - ensureAllMeasurements() if (currentEndLocation == -1) { return } @@ -304,8 +313,8 @@ class MediaViewController @Inject constructor( // Obtain the view state that we'd want to be at the end // The view might not be bound yet or has never been measured and in that case will be // reset once the state is fully available - val endViewState = obtainViewState(endHostState) ?: return - + var endViewState = obtainViewState(endHostState) ?: return + endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!! layoutController.setMeasureState(endViewState) // If the view isn't bound, we can drop the animation, otherwise we'll execute it @@ -315,7 +324,8 @@ class MediaViewController @Inject constructor( } val result: TransitionViewState - val startViewState = obtainViewState(startHostState) + var startViewState = obtainViewState(startHostState) + startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3) if (!endHostState.visible) { // Let's handle the case where the end is gone first. In this case we take the @@ -350,6 +360,22 @@ class MediaViewController @Inject constructor( animationDelay) } + private fun updateViewStateToCarouselSize( + viewState: TransitionViewState?, + location: Int, + outState: TransitionViewState + ) : TransitionViewState? { + val result = viewState?.copy(outState) ?: return null + val overrideSize = mediaHostStatesManager.carouselSizes[location] + overrideSize?.let { + // To be safe we're using a maximum here. The override size should always be set + // properly though. + result.height = Math.max(it.measuredHeight, result.height) + result.width = Math.max(it.measuredWidth, result.width) + } + return result + } + /** * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. * In the event of [location] not being visible, [locationWhenHidden] will be used instead. @@ -387,13 +413,16 @@ class MediaViewController @Inject constructor( * Clear all existing measurements and refresh the state to match the view. */ fun refreshState() { - if (!firstRefresh) { - // Let's clear all of our measurements and recreate them! - viewStates.clear() - setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress, - applyImmediately = true) + // Let's clear all of our measurements and recreate them! + viewStates.clear() + if (firstRefresh) { + // This is the first bind, let's ensure we pre-cache all measurements. Otherwise + // We'll just load these on demand. + ensureAllMeasurements() + firstRefresh = false } - firstRefresh = false + setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress, + applyImmediately = true) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java index 1842564a4574..68b6785849aa 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java @@ -94,7 +94,7 @@ public class ResumeMediaBrowser { // a request with EXTRA_RECENT; if they don't, no resume controls MediaBrowser.MediaItem child = children.get(0); MediaDescription desc = child.getDescription(); - if (child.isPlayable()) { + if (child.isPlayable() && mMediaBrowser != null) { mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), ResumeMediaBrowser.this); } else { @@ -129,7 +129,7 @@ public class ResumeMediaBrowser { @Override public void onConnected() { Log.d(TAG, "Service connected for " + mComponentName); - if (mMediaBrowser.isConnected()) { + if (mMediaBrowser != null && mMediaBrowser.isConnected()) { String root = mMediaBrowser.getRoot(); if (!TextUtils.isEmpty(root)) { mCallback.onConnected(); @@ -187,7 +187,7 @@ public class ResumeMediaBrowser { @Override public void onConnected() { Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected()); - if (!mMediaBrowser.isConnected()) { + if (mMediaBrowser == null || !mMediaBrowser.isConnected()) { mCallback.onError(); return; } @@ -246,7 +246,7 @@ public class ResumeMediaBrowser { @Override public void onConnected() { Log.d(TAG, "connected"); - if (!mMediaBrowser.isConnected() + if (mMediaBrowser == null || !mMediaBrowser.isConnected() || TextUtils.isEmpty(mMediaBrowser.getRoot())) { mCallback.onError(); } else { diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java index 2980f11b3cbc..ead17867844a 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java @@ -18,16 +18,16 @@ package com.android.systemui.pip; import android.animation.AnimationHandler; import android.animation.Animator; +import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.content.Context; import android.graphics.Rect; import android.view.SurfaceControl; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Interpolators; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -76,7 +76,6 @@ public class PipAnimationController { || direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN; } - private final Interpolator mFastOutSlowInInterpolator; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private PipTransitionAnimator mCurrentAnimator; @@ -90,8 +89,6 @@ public class PipAnimationController { @Inject PipAnimationController(Context context, PipSurfaceTransactionHelper helper) { - mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, - com.android.internal.R.interpolator.fast_out_slow_in); mSurfaceTransactionHelper = helper; } @@ -113,10 +110,11 @@ public class PipAnimationController { } @SuppressWarnings("unchecked") - PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds) { + PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds, + Rect sourceHintRect) { if (mCurrentAnimator == null) { mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, endBounds)); + PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect)); } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA && mCurrentAnimator.isRunning()) { // If we are still animating the fade into pip, then just move the surface and ensure @@ -131,7 +129,7 @@ public class PipAnimationController { } else { mCurrentAnimator.cancel(); mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, endBounds)); + PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect)); } return mCurrentAnimator; } @@ -142,7 +140,7 @@ public class PipAnimationController { private PipTransitionAnimator setupPipTransitionAnimator(PipTransitionAnimator animator) { animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper); - animator.setInterpolator(mFastOutSlowInInterpolator); + animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); animator.setFloatValues(FRACTION_START, FRACTION_END); animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get()); return animator; @@ -341,6 +339,7 @@ public class PipAnimationController { @Override void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { getSurfaceTransactionHelper() + .resetScale(tx, leash, getDestinationBounds()) .crop(tx, leash, getDestinationBounds()) .round(tx, leash, shouldApplyCornerRadius()); tx.show(leash); @@ -356,35 +355,46 @@ public class PipAnimationController { } static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash, - Rect startValue, Rect endValue) { + Rect startValue, Rect endValue, Rect sourceHintRect) { + // Just for simplicity we'll interpolate between the source rect hint insets and empty + // insets to calculate the window crop + final Rect initialStartValue = new Rect(startValue); + final Rect sourceHintRectInsets = sourceHintRect != null + ? new Rect(sourceHintRect.left - startValue.left, + sourceHintRect.top - startValue.top, + startValue.right - sourceHintRect.right, + startValue.bottom - sourceHintRect.bottom) + : null; + final Rect sourceInsets = new Rect(0, 0, 0, 0); + // construct new Rect instances in case they are recycled return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS, endValue, new Rect(startValue), new Rect(endValue)) { - private final Rect mTmpRect = new Rect(); - - private int getCastedFractionValue(float start, float end, float fraction) { - return (int) (start * (1 - fraction) + end * fraction + .5f); - } + private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); + private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); @Override void applySurfaceControlTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, float fraction) { final Rect start = getStartValue(); final Rect end = getEndValue(); - mTmpRect.set( - getCastedFractionValue(start.left, end.left, fraction), - getCastedFractionValue(start.top, end.top, fraction), - getCastedFractionValue(start.right, end.right, fraction), - getCastedFractionValue(start.bottom, end.bottom, fraction)); - setCurrentValue(mTmpRect); + Rect bounds = mRectEvaluator.evaluate(fraction, start, end); + setCurrentValue(bounds); if (inScaleTransition()) { if (isOutPipDirection(getTransitionDirection())) { - getSurfaceTransactionHelper().scale(tx, leash, end, mTmpRect); + getSurfaceTransactionHelper().scale(tx, leash, end, bounds); } else { - getSurfaceTransactionHelper().scale(tx, leash, start, mTmpRect); + getSurfaceTransactionHelper().scale(tx, leash, start, bounds); } } else { - getSurfaceTransactionHelper().crop(tx, leash, mTmpRect); + if (sourceHintRectInsets != null) { + Rect insets = mInsetsEvaluator.evaluate(fraction, sourceInsets, + sourceHintRectInsets); + getSurfaceTransactionHelper().scaleAndCrop(tx, leash, initialStartValue, + bounds, insets); + } else { + getSurfaceTransactionHelper().scale(tx, leash, start, bounds); + } } tx.apply(); } @@ -400,11 +410,11 @@ public class PipAnimationController { @Override void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { - if (!inScaleTransition()) return; // NOTE: intentionally does not apply the transaction here. // this end transaction should get executed synchronously with the final // WindowContainerTransaction in task organizer - getSurfaceTransactionHelper().resetScale(tx, leash, getDestinationBounds()) + getSurfaceTransactionHelper() + .resetScale(tx, leash, getDestinationBounds()) .crop(tx, leash, getDestinationBounds()); } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java index 0d3a16ec1028..8bbd15babf19 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java @@ -289,6 +289,24 @@ public class PipBoundsHandler { } /** + * Updatest the display info and display layout on rotation change. This is needed even when we + * aren't in PIP because the rotation layout is used to calculate the proper insets for the + * next enter animation into PIP. + */ + public void onDisplayRotationChangedNotInPip(int toRotation) { + // Update the display layout, note that we have to do this on every rotation even if we + // aren't in PIP since we need to update the display layout to get the right resources + mDisplayLayout.rotateTo(mContext.getResources(), toRotation); + + // Populate the new {@link #mDisplayInfo}. + // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation, + // therefore, the width/height may require a swap first. + // Moving forward, we should get the new dimensions after rotation from DisplayLayout. + mDisplayInfo.rotation = toRotation; + updateDisplayInfoIfNeeded(); + } + + /** * Updates the display info, calculating and returning the new stack and movement bounds in the * new orientation of the device if necessary. * diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java index fc41d2ea8862..65ea887259be 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java @@ -44,6 +44,7 @@ public class PipSurfaceTransactionHelper implements ConfigurationController.Conf private final float[] mTmpFloat9 = new float[9]; private final RectF mTmpSourceRectF = new RectF(); private final RectF mTmpDestinationRectF = new RectF(); + private final Rect mTmpDestinationRect = new Rect(); @Inject public PipSurfaceTransactionHelper(Context context, ConfigurationController configController) { @@ -90,7 +91,30 @@ public class PipSurfaceTransactionHelper implements ConfigurationController.Conf mTmpDestinationRectF.set(destinationBounds); mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); tx.setMatrix(leash, mTmpTransform, mTmpFloat9) - .setPosition(leash, destinationBounds.left, destinationBounds.top); + .setPosition(leash, mTmpDestinationRectF.left, mTmpDestinationRectF.top); + return this; + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds, Rect insets) { + mTmpSourceRectF.set(sourceBounds); + mTmpDestinationRect.set(sourceBounds); + mTmpDestinationRect.inset(insets); + // Scale by the shortest edge and offset such that the top/left of the scaled inset source + // rect aligns with the top/left of the destination bounds + final float scale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + final float left = destinationBounds.left - insets.left * scale; + final float top = destinationBounds.top - insets.top * scale; + mTmpTransform.setScale(scale, scale); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9) + .setWindowCrop(leash, mTmpDestinationRect) + .setPosition(leash, left, top); return this; } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java index c8a1ca02fdfb..0141dee04086 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java @@ -143,8 +143,10 @@ public class PipTaskOrganizer extends TaskOrganizer implements case MSG_RESIZE_ANIMATE: { Rect currentBounds = (Rect) args.arg2; Rect toBounds = (Rect) args.arg3; + Rect sourceHintRect = (Rect) args.arg4; int duration = args.argi2; - animateResizePip(currentBounds, toBounds, args.argi1 /* direction */, duration); + animateResizePip(currentBounds, toBounds, sourceHintRect, + args.argi1 /* direction */, duration); if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } @@ -307,7 +309,8 @@ public class PipTaskOrganizer extends TaskOrganizer implements public void onTransactionReady(int id, SurfaceControl.Transaction t) { t.apply(); scheduleAnimateResizePip(mLastReportedBounds, destinationBounds, - direction, animationDurationMs, null /* updateBoundsCallback */); + null /* sourceHintRect */, direction, animationDurationMs, + null /* updateBoundsCallback */); mInPip = false; } }); @@ -380,7 +383,8 @@ public class PipTaskOrganizer extends TaskOrganizer implements final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { - scheduleAnimateResizePip(currentBounds, destinationBounds, + final Rect sourceHintRect = getValidSourceHintRect(info, currentBounds); + scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration, null /* updateBoundsCallback */); } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { @@ -391,6 +395,21 @@ public class PipTaskOrganizer extends TaskOrganizer implements } } + /** + * Returns the source hint rect if it is valid (if provided and is contained by the current + * task bounds). + */ + private Rect getValidSourceHintRect(ActivityManager.RunningTaskInfo info, Rect sourceBounds) { + final Rect sourceHintRect = info.pictureInPictureParams != null + && info.pictureInPictureParams.hasSourceBoundsHint() + ? info.pictureInPictureParams.getSourceRectHint() + : null; + if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) { + return sourceHintRect; + } + return null; + } + private void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { // If we are fading the PIP in, then we should move the pip to the final location as // soon as possible, but set the alpha immediately since the transaction can take a @@ -611,13 +630,13 @@ public class PipTaskOrganizer extends TaskOrganizer implements Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); return; } - scheduleAnimateResizePip(mLastReportedBounds, toBounds, + scheduleAnimateResizePip(mLastReportedBounds, toBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback); } private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, - @PipAnimationController.TransitionDirection int direction, int durationMs, - Consumer<Rect> updateBoundsCallback) { + Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, + int durationMs, Consumer<Rect> updateBoundsCallback) { if (!mInPip) { // TODO: tend to use shouldBlockResizeRequest here as well but need to consider // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window @@ -629,6 +648,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements args.arg1 = updateBoundsCallback; args.arg2 = currentBounds; args.arg3 = destinationBounds; + args.arg4 = sourceHintRect; args.argi1 = direction; args.argi2 = durationMs; mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args)); @@ -732,7 +752,8 @@ public class PipTaskOrganizer extends TaskOrganizer implements } final Rect destinationBounds = new Rect(originalBounds); destinationBounds.offset(xOffset, yOffset); - animateResizePip(originalBounds, destinationBounds, TRANSITION_DIRECTION_SAME, durationMs); + animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, + TRANSITION_DIRECTION_SAME, durationMs); } private void resizePip(Rect destinationBounds) { @@ -838,7 +859,8 @@ public class PipTaskOrganizer extends TaskOrganizer implements return WINDOWING_MODE_UNDEFINED; } - private void animateResizePip(Rect currentBounds, Rect destinationBounds, + + private void animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs) { if (Looper.myLooper() != mUpdateHandler.getLooper()) { throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of " @@ -850,7 +872,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements return; } mPipAnimationController - .getAnimator(mLeash, currentBounds, destinationBounds) + .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect) .setTransitionDirection(direction) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(durationMs) diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java index 40a86b78d3ad..7d35416a8d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java @@ -36,7 +36,6 @@ import android.util.Log; import android.util.Pair; import android.view.DisplayInfo; import android.view.IPinnedStackController; -import android.view.SurfaceControl; import android.window.WindowContainerTransaction; import com.android.systemui.Dependency; @@ -96,7 +95,9 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio private final DisplayChangeController.OnDisplayChangingListener mRotationController = ( int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> { if (!mPipTaskOrganizer.isInPip() || mPipTaskOrganizer.isDeferringEnterPipAnimation()) { - // Skip if we aren't in PIP or haven't actually entered PIP yet + // Skip if we aren't in PIP or haven't actually entered PIP yet. We still need to update + // the display layout in the bounds handler in this case. + mPipBoundsHandler.onDisplayRotationChangedNotInPip(toRotation); return; } // If there is an animation running (ie. from a shelf offset), then ensure that we calculate @@ -174,7 +175,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { - if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode() + if (task.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_PINNED) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java index a4edacecfd91..1ca53f907994 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java @@ -55,7 +55,9 @@ import com.android.systemui.pip.PipBoundsHandler; import com.android.systemui.pip.PipTaskOrganizer; import com.android.systemui.util.DeviceConfigProxy; +import java.io.PrintWriter; import java.util.concurrent.Executor; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -94,7 +96,7 @@ public class PipResizeGestureHandler { private final Rect mTmpBottomLeftCorner = new Rect(); private final Rect mTmpBottomRightCorner = new Rect(); private final Rect mDisplayBounds = new Rect(); - private final Supplier<Rect> mMovementBoundsSupplier; + private final Function<Rect, Rect> mMovementBoundsSupplier; private final Runnable mUpdateMovementBoundsRunnable; private int mDelta; @@ -113,7 +115,7 @@ public class PipResizeGestureHandler { public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig, - PipTaskOrganizer pipTaskOrganizer, Supplier<Rect> movementBoundsSupplier, + PipTaskOrganizer pipTaskOrganizer, Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, SysUiState sysUiState) { mContext = context; mDisplayId = context.getDisplayId(); @@ -244,10 +246,15 @@ public class PipResizeGestureHandler { return mTmpRegion.contains(x, y); } + public boolean willStartResizeGesture(MotionEvent ev) { + return mEnableUserResize && isInValidSysUiState() + && isWithinTouchRegion((int) ev.getRawX(), (int) ev.getRawY()); + } + private void setCtrlType(int x, int y) { final Rect currentPipBounds = mMotionHelper.getBounds(); - Rect movementBounds = mMovementBoundsSupplier.get(); + Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); mDisplayBounds.set(movementBounds.left, movementBounds.top, movementBounds.right + currentPipBounds.width(), @@ -353,6 +360,16 @@ public class PipResizeGestureHandler { mMinSize.set(minX, minY); } + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture); + pw.println(innerPrefix + "mIsAttached=" + mIsAttached); + pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); + pw.println(innerPrefix + "mEnableUserResize=" + mEnableUserResize); + pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); + } + class SysUiInputEventReceiver extends BatchedInputEventReceiver { SysUiInputEventReceiver(InputChannel channel, Looper looper) { super(channel, looper, Choreographer.getSfInstance()); diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index 79f99f459ace..b6e4e1628c20 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -303,8 +303,11 @@ public class PipTouchHandler { hideDismissTarget(); }); - MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext, - PipUtils.getTopPipActivity(mContext, mActivityManager)); + Pair<ComponentName, Integer> topPipActivity = PipUtils.getTopPipActivity(mContext, + mActivityManager); + if (topPipActivity.first != null) { + MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext, topPipActivity); + } } }); @@ -641,12 +644,12 @@ public class PipTouchHandler { } MotionEvent ev = (MotionEvent) inputEvent; - if (!mTouchState.isDragging() - && !mMagnetizedPip.getObjectStuckToTarget() - && !mMotionHelper.isAnimating() - && mPipResizeGestureHandler.isWithinTouchRegion( - (int) ev.getRawX(), (int) ev.getRawY())) { + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN + && mPipResizeGestureHandler.willStartResizeGesture(ev)) { + // Initialize the touch state for the gesture, but immediately reset to invalidate the + // gesture mTouchState.onTouchEvent(ev); + mTouchState.reset(); return true; } @@ -1029,8 +1032,11 @@ public class PipTouchHandler { isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize : 0); } - private Rect getMovementBounds() { - return mMovementBounds; + private Rect getMovementBounds(Rect curBounds) { + Rect movementBounds = new Rect(); + mSnapAlgorithm.getMovementBounds(curBounds, mInsetBounds, + movementBounds, mIsImeShowing ? mImeHeight : 0); + return movementBounds; } /** @@ -1062,6 +1068,9 @@ public class PipTouchHandler { pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets); mTouchState.dump(pw, innerPrefix); mMotionHelper.dump(pw, innerPrefix); + if (mPipResizeGestureHandler != null) { + mPipResizeGestureHandler.dump(pw, innerPrefix); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java index 628223630af7..4b2c27321035 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java @@ -716,7 +716,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { - if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode() + if (task.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_PINNED) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index b5afe771926c..b07b1a9561ff 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -44,6 +44,7 @@ import android.view.DisplayCutout; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; @@ -146,6 +147,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); private boolean mHasTopCutout = false; + private int mStatusBarPaddingTop = 0; private int mRoundedCornerPadding = 0; private int mContentMarginStart; private int mContentMarginEnd; @@ -339,6 +341,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements mRoundedCornerPadding = resources.getDimensionPixelSize( R.dimen.rounded_corner_content_padding); + mStatusBarPaddingTop = resources.getDimensionPixelSize(R.dimen.status_bar_padding_top); // Update height for a few views, especially due to landscape mode restricting space. mHeaderTextContainerView.getLayoutParams().height = @@ -460,6 +463,11 @@ public class QuickStatusBarHeader extends RelativeLayout implements private void updateClockPadding() { int clockPaddingLeft = 0; int clockPaddingRight = 0; + + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); + int leftMargin = lp.leftMargin; + int rightMargin = lp.rightMargin; + // The clock might collide with cutouts, let's shift it out of the way. // We only do that if the inset is bigger than our own padding, since it's nicer to // align with @@ -467,16 +475,19 @@ public class QuickStatusBarHeader extends RelativeLayout implements // if there's a cutout, let's use at least the rounded corner inset int cutoutPadding = Math.max(mCutOutPaddingLeft, mRoundedCornerPadding); int contentMarginLeft = isLayoutRtl() ? mContentMarginEnd : mContentMarginStart; - clockPaddingLeft = Math.max(cutoutPadding - contentMarginLeft, 0); + clockPaddingLeft = Math.max(cutoutPadding - contentMarginLeft - leftMargin, 0); } if (mCutOutPaddingRight > 0) { // if there's a cutout, let's use at least the rounded corner inset int cutoutPadding = Math.max(mCutOutPaddingRight, mRoundedCornerPadding); int contentMarginRight = isLayoutRtl() ? mContentMarginStart : mContentMarginEnd; - clockPaddingRight = Math.max(cutoutPadding - contentMarginRight, 0); + clockPaddingRight = Math.max(cutoutPadding - contentMarginRight - rightMargin, 0); } - mSystemIconsView.setPadding(clockPaddingLeft, mWaterfallTopInset, clockPaddingRight, 0); + mSystemIconsView.setPadding(clockPaddingLeft, + mWaterfallTopInset + mStatusBarPaddingTop, + clockPaddingRight, + 0); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 8e878ddc6da1..d6e1a16bc69e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -18,6 +18,7 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT; @@ -72,6 +73,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; +import android.view.WindowInsets; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import android.view.animation.AccelerateInterpolator; @@ -87,6 +89,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.phone.StatusBar; import java.util.ArrayList; @@ -220,6 +223,10 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset private MediaActionSound mCameraSound; + private int mNavMode; + private int mLeftInset; + private int mRightInset; + // standard material ease private final Interpolator mFastOutSlowIn; @@ -301,6 +308,15 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mDismissButton.getBoundsOnScreen(dismissRect); touchRegion.op(dismissRect, Region.Op.UNION); + if (QuickStepContract.isGesturalMode(mNavMode)) { + // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE + Rect inset = new Rect(0, 0, mLeftInset, mDisplayMetrics.heightPixels); + touchRegion.op(inset, Region.Op.UNION); + inset.set(mDisplayMetrics.widthPixels - mRightInset, 0, mDisplayMetrics.widthPixels, + mDisplayMetrics.heightPixels); + touchRegion.op(inset, Region.Op.UNION); + } + inoutInfo.touchableRegion.set(touchRegion); } @@ -356,6 +372,9 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset if (needsUpdate) { reloadAssets(); } + + mNavMode = mContext.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode); } /** @@ -370,6 +389,25 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset // Inflate the screenshot layout mScreenshotLayout = LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null); + // TODO(159460485): Remove this when focus is handled properly in the system + mScreenshotLayout.setOnTouchListener((v, event) -> { + if (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { + // Once the user touches outside, stop listening for input + setWindowFocusable(false); + } + return false; + }); + mScreenshotLayout.setOnApplyWindowInsetsListener((v, insets) -> { + if (QuickStepContract.isGesturalMode(mNavMode)) { + Insets gestureInsets = insets.getInsets( + WindowInsets.Type.systemGestures()); + mLeftInset = gestureInsets.left; + mRightInset = gestureInsets.right; + } else { + mLeftInset = mRightInset = 0; + } + return mScreenshotLayout.onApplyWindowInsets(insets); + }); mScreenshotLayout.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { @@ -432,6 +470,21 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } } + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + if (focusable) { + mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE; + } + if (mScreenshotLayout.isAttachedToWindow()) { + mWindowManager.updateViewLayout(mScreenshotLayout, mWindowLayoutParams); + } + } /** * Creates a new worker thread and saves the screenshot to the media store. @@ -500,6 +553,10 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset if (mDismissAnimation != null && mDismissAnimation.isRunning()) { mDismissAnimation.cancel(); } + + // The window is focusable by default + setWindowFocusable(true); + // Start the post-screenshot animation startAnimation(finisher, screenRect, screenInsets, showFlash); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java index b846aa08c33b..eca4c8082dfe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java @@ -316,7 +316,7 @@ public abstract class AlertingNotificationManager implements NotificationLifetim * of the timer and should be removed externally. * @return true if the notification is sticky */ - protected boolean isSticky() { + public boolean isSticky() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 5628a24f40ef..739d30c2a707 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -100,12 +100,7 @@ public class NotificationMediaManager implements Dumpable { PAUSED_MEDIA_STATES.add(PlaybackState.STATE_STOPPED); PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED); PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR); - } - private static final HashSet<Integer> INACTIVE_MEDIA_STATES = new HashSet<>(); - static { - INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_NONE); - INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_STOPPED); - INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_ERROR); + PAUSED_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING); } private final NotificationEntryManager mEntryManager; @@ -262,15 +257,6 @@ public class NotificationMediaManager implements Dumpable { return !PAUSED_MEDIA_STATES.contains(state); } - /** - * Check if a state should be considered active (playing or paused) - * @param state a PlaybackState - * @return true if active - */ - public static boolean isActiveState(int state) { - return !INACTIVE_MEDIA_STATES.contains(state); - } - public void setUpWithPresenter(NotificationPresenter presenter) { mPresenter = presenter; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index c1acfbadef45..285cf7abce20 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -382,9 +382,8 @@ public class NotifCollection implements Dumpable { final NotificationEntry entry = mNotificationSet.get(sbn.getKey()); if (entry == null) { - crashIfNotInitializing( - new IllegalStateException("No notification to remove with key " - + sbn.getKey())); + // TODO (b/160008901): Throw an exception here + mLogger.logNoNotificationToRemoveWithKey(sbn.getKey()); return; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index 76751eaaecb1..f8a778d6b1d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -121,6 +121,14 @@ class NotifCollectionLogger @Inject constructor( }) } + fun logNoNotificationToRemoveWithKey(key: String) { + buffer.log(TAG, ERROR, { + str1 = key + }, { + "No notification to remove with key $str1" + }) + } + fun logRankingMissing(key: String, rankingMap: RankingMap) { buffer.log(TAG, WARNING, { str1 = key }, { "Ranking update is missing ranking for $str1" }) buffer.log(TAG, DEBUG, {}, { "Ranking map contents:" }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java index 52f7c2cfee96..7bd192d850c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java @@ -124,6 +124,9 @@ public class NotificationInlineImageResolver implements ImageResolver { */ Drawable resolveImage(Uri uri) throws IOException { BitmapDrawable image = resolveImageInternal(uri); + if (image == null || image.getBitmap() == null) { + throw new IOException("resolveImageInternal returned null for uri: " + uri); + } Bitmap bitmap = image.getBitmap(); image.setBitmap(Icon.scaleDownIfNecessary(bitmap, mMaxImageWidth, mMaxImageHeight)); return image; 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 3dcf7ed674c7..e05ba12781c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -428,7 +428,7 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, @Override - protected boolean isSticky() { + public boolean isSticky() { return super.isSticky() || mMenuShownPinned; } @@ -568,6 +568,17 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, } mKeysToRemoveWhenLeavingKeyguard.clear(); } + if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) { + ArrayList<String> keysToRemove = new ArrayList<>(); + for (AlertEntry entry : mAlertEntries.values()) { + if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) { + keysToRemove.add(entry.mEntry.getKey()); + } + } + for (String key : keysToRemove) { + removeAlertEntry(key); + } + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index 39949c82661f..b6a284c5e3c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -26,7 +26,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.util.MathUtils; -import android.util.Slog; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -169,7 +168,7 @@ public class KeyguardBouncer { // This condition may indicate an error on Android, so log it. if (!allowDismissKeyguard) { - Slog.w(TAG, "User can't dismiss keyguard: " + activeUserId + " != " + keyguardUserId); + Log.w(TAG, "User can't dismiss keyguard: " + activeUserId + " != " + keyguardUserId); } mShowingSoon = true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index c32133119386..375af6b099c2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -435,6 +435,11 @@ public class NotificationPanelViewController extends PanelViewController { private Runnable mExpandAfterLayoutRunnable; /** + * Is this a collapse that started on the panel where we should allow the panel to intercept + */ + private boolean mIsPanelCollapseOnQQS; + + /** * If face auth with bypass is running for the first time after you turn on the screen. * (From aod or screen off) */ @@ -1064,7 +1069,11 @@ public class NotificationPanelViewController extends PanelViewController { mInitialTouchX = x; initVelocityTracker(); trackMovement(event); - if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { + if (mKeyguardShowing + && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { + // Dragging down on the lockscreen statusbar should prohibit other interactions + // immediately, otherwise we'll wait on the touchslop. This is to allow + // dragging down to expanded quick settings directly on the lockscreen. mView.getParent().requestDisallowInterceptTouchEvent(true); } if (mQsExpansionAnimator != null) { @@ -1097,9 +1106,10 @@ public class NotificationPanelViewController extends PanelViewController { trackMovement(event); return true; } - if (Math.abs(h) > getTouchSlop(event) + if ((h > getTouchSlop(event) || (h < -getTouchSlop(event) && mQsExpanded)) && Math.abs(h) > Math.abs(x - mInitialTouchX) && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { + mView.getParent().requestDisallowInterceptTouchEvent(true); mQsTracking = true; onQsExpansionStarted(); notifyExpandingFinished(); @@ -1139,6 +1149,7 @@ public class NotificationPanelViewController extends PanelViewController { mDownX = event.getX(); mDownY = event.getY(); mCollapsedOnDown = isFullyCollapsed(); + mIsPanelCollapseOnQQS = canPanelCollapseOnQQS(mDownX, mDownY); mListenForHeadsUp = mCollapsedOnDown && mHeadsUpManager.hasPinnedHeadsUp(); mAllowExpandForSmallExpansion = mExpectingSynthesizedDown; mTouchSlopExceededBeforeDown = mExpectingSynthesizedDown; @@ -1154,6 +1165,24 @@ public class NotificationPanelViewController extends PanelViewController { } } + /** + * Can the panel collapse in this motion because it was started on QQS? + * + * @param downX the x location where the touch started + * @param downY the y location where the touch started + * + * @return true if the panel could be collapsed because it stared on QQS + */ + private boolean canPanelCollapseOnQQS(float downX, float downY) { + if (mCollapsedOnDown || mKeyguardShowing || mQsExpanded) { + return false; + } + View header = mQs == null ? mKeyguardStatusBar : mQs.getHeader(); + return downX >= mQsFrame.getX() && downX <= mQsFrame.getX() + mQsFrame.getWidth() + && downY <= header.getBottom(); + + } + private void flingQsWithCurrentVelocity(float y, boolean isCancelMotionEvent) { float vel = getCurrentQSVelocity(); final boolean expandsQs = flingExpandsQs(vel); @@ -1903,10 +1932,11 @@ public class NotificationPanelViewController extends PanelViewController { } @Override - protected boolean isScrolledToBottom() { + protected boolean canCollapsePanelOnTouch() { if (!isInSettings()) { return mBarState == StatusBarState.KEYGUARD - || mNotificationStackScroller.isScrolledToBottom(); + || mNotificationStackScroller.isScrolledToBottom() + || mIsPanelCollapseOnQQS; } else { return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java index caddc4a874f4..732f25f90eb5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java @@ -460,7 +460,7 @@ public abstract class PanelViewController { } } - protected boolean isScrolledToBottom() { + protected boolean canCollapsePanelOnTouch() { return true; } @@ -1081,7 +1081,7 @@ public abstract class PanelViewController { * upwards. This allows closing the shade from anywhere inside the panel. * * We only do this if the current content is scrolled to the bottom, - * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling + * i.e canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling * gesture * possible. */ @@ -1092,7 +1092,7 @@ public abstract class PanelViewController { } final float x = event.getX(pointerIndex); final float y = event.getY(pointerIndex); - boolean scrolledToBottom = isScrolledToBottom(); + boolean canCollapsePanel = canCollapsePanelOnTouch(); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: @@ -1139,7 +1139,7 @@ public abstract class PanelViewController { case MotionEvent.ACTION_MOVE: final float h = y - mInitialTouchY; addMovement(event); - if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) { + if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown) { float hAbs = Math.abs(h); float touchSlop = getTouchSlop(event); if ((h < -touchSlop || (mAnimatingOnDown && hAbs > touchSlop)) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index 0d5a14960850..07de388598b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -368,7 +368,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { protected boolean expanded; @Override - protected boolean isSticky() { + public boolean isSticky() { return (mEntry.isRowPinned() && expanded) || remoteInputActive || hasFullScreenIntent(mEntry); } diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt index 19c6b806bb59..3347cf6ca2a4 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt @@ -20,9 +20,11 @@ import android.content.Context import android.graphics.Canvas import android.graphics.PointF import android.graphics.Rect +import android.text.Layout import android.util.AttributeSet import android.view.View import android.view.ViewTreeObserver +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.statusbar.CrossFadeHelper @@ -45,12 +47,29 @@ class TransitionLayout @JvmOverloads constructor( private var currentState: TransitionViewState = TransitionViewState() private var updateScheduled = false + private var desiredMeasureWidth = 0 + private var desiredMeasureHeight = 0 /** * The measured state of this view which is the one we will lay ourselves out with. This * may differ from the currentState if there is an external animation or transition running. * This state will not be used to measure the widgets, where the current state is preferred. */ var measureState: TransitionViewState = TransitionViewState() + set(value) { + val newWidth = value.width + val newHeight = value.height + if (newWidth != desiredMeasureWidth || newHeight != desiredMeasureHeight) { + desiredMeasureWidth = newWidth + desiredMeasureHeight = newHeight + // We need to make sure next time we're measured that our onMeasure will be called. + // Otherwise our parent thinks we still have the same height + if (isInLayout()) { + forceLayout() + } else { + requestLayout() + } + } + } private val preDrawApplicator = object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { updateScheduled = false @@ -85,6 +104,23 @@ class TransitionLayout @JvmOverloads constructor( for (i in 0 until childCount) { val child = getChildAt(i) val widgetState = currentState.widgetStates.get(child.id) ?: continue + + // TextViews which are measured and sized differently should be handled with a + // "clip mode", which means we clip explicitly rather than implicitly by passing + // different sizes to measure/layout than setLeftTopRightBottom. + // Then to accommodate RTL text, we need a "clip shift" which allows us to have the + // clipBounds be attached to the right side of the view instead of the left. + val clipModeShift = + if (child is TextView && widgetState.width < widgetState.measureWidth) { + if (child.layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT) { + widgetState.measureWidth - widgetState.width + } else { + 0 + } + } else { + null + } + if (child.measuredWidth != widgetState.measureWidth || child.measuredHeight != widgetState.measureHeight) { val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth, @@ -94,14 +130,17 @@ class TransitionLayout @JvmOverloads constructor( child.measure(measureWidthSpec, measureHeightSpec) child.layout(0, 0, child.measuredWidth, child.measuredHeight) } - val left = widgetState.x.toInt() + contentTranslationX + val clipShift = clipModeShift ?: 0 + val left = widgetState.x.toInt() + contentTranslationX - clipShift val top = widgetState.y.toInt() + contentTranslationY - child.setLeftTopRightBottom(left, top, left + widgetState.width, - top + widgetState.height) + val clipMode = clipModeShift != null + val boundsWidth = if (clipMode) widgetState.measureWidth else widgetState.width + val boundsHeight = if (clipMode) widgetState.measureHeight else widgetState.height + child.setLeftTopRightBottom(left, top, left + boundsWidth, top + boundsHeight) child.scaleX = widgetState.scale child.scaleY = widgetState.scale val clipBounds = child.clipBounds ?: Rect() - clipBounds.set(0, 0, widgetState.width, widgetState.height) + clipBounds.set(clipShift, 0, widgetState.width + clipShift, widgetState.height) child.clipBounds = clipBounds CrossFadeHelper.fadeIn(child, widgetState.alpha) child.visibility = if (widgetState.gone || widgetState.alpha == 0.0f) { @@ -136,7 +175,7 @@ class TransitionLayout @JvmOverloads constructor( MeasureSpec.EXACTLY) child.measure(measureWidthSpec, measureHeightSpec) } - setMeasuredDimension(measureState.width, measureState.height) + setMeasuredDimension(desiredMeasureWidth, desiredMeasureHeight) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java index ed4e6865e508..315caeebe0e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java @@ -107,6 +107,9 @@ public class BubbleDataTest extends SysuiTestCase { @Mock private BubbleController.NotificationSuppressionChangedListener mSuppressionListener; + @Mock + private BubbleController.PendingIntentCanceledListener mPendingIntentCanceledListener; + @Before public void setUp() throws Exception { mNotificationTestHelper = new NotificationTestHelper( @@ -127,20 +130,20 @@ public class BubbleDataTest extends SysuiTestCase { modifyRanking(mEntryInterruptive) .setVisuallyInterruptive(true) .build(); - mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener); + mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null); ExpandableNotificationRow row = mNotificationTestHelper.createBubble(); mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d"); mEntryDismissed.setRow(row); - mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener); + mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null); - mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener); - mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener); - mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener); - mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener); - mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener); - mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener); - mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener); + mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener); + mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener); + mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener); + mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener); + mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener); + mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener); + mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener); mBubbleData = new BubbleData(getContext()); @@ -847,14 +850,6 @@ public class BubbleDataTest extends SysuiTestCase { when(entry.getSbn().getPostTime()).thenReturn(postTime); } - private void setOngoing(NotificationEntry entry, boolean ongoing) { - if (ongoing) { - entry.getSbn().getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; - } else { - entry.getSbn().getNotification().flags &= ~Notification.FLAG_FOREGROUND_SERVICE; - } - } - /** * No ExpandableNotificationRow is required to test BubbleData. This setup is all that is * required for BubbleData functionality and verification. NotificationTestHelper is used only diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java index be03923e7264..2bcc22c4b99e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java @@ -71,7 +71,7 @@ public class BubbleTest extends SysuiTestCase { .setNotification(mNotif) .build(); - mBubble = new Bubble(mEntry, mSuppressionListener); + mBubble = new Bubble(mEntry, mSuppressionListener, null); Intent target = new Intent(mContext, BubblesTestActivity.class); Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt index b7f317b38743..c63781cb110a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -64,6 +64,7 @@ private const val DEVICE_NAME = "DEVICE_NAME" private const val SESSION_KEY = "SESSION_KEY" private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_TITLE = "SESSION_TITLE" +private const val USER_ID = 0 @SmallTest @RunWith(AndroidTestingRunner::class) @@ -180,7 +181,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindWhenUnattached() { - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, null, null, device, true, null) player.bind(state) assertThat(player.isPlaying()).isFalse() @@ -189,7 +190,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindText() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null) player.bind(state) assertThat(appName.getText()).isEqualTo(APP) @@ -200,7 +201,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindBackgroundColor() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null) player.bind(state) val list = ArgumentCaptor.forClass(ColorStateList::class.java) @@ -211,7 +212,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindDevice() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null) player.bind(state) assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME) @@ -223,7 +224,7 @@ public class MediaControlPanelTest : SysuiTestCase() { seamless.id = 1 seamlessFallback.id = 2 player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, true, null) player.bind(state) verify(expandedSet).setVisibility(seamless.id, View.GONE) @@ -235,7 +236,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindNullDevice() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null) player.bind(state) assertThat(seamless.isEnabled()).isTrue() @@ -246,7 +247,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindDeviceResumptionPlayer() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null, resumption = true) player.bind(state) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java index 9fdd9ad744ff..dbc5596d9f4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java @@ -39,6 +39,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import java.util.ArrayList; +import java.util.Map; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -52,6 +53,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { private static final String ARTIST = "ARTIST"; private static final String TITLE = "TITLE"; private static final String DEVICE_NAME = "DEVICE_NAME"; + private static final int USER_ID = 0; private MediaDataCombineLatest mManager; @@ -78,7 +80,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mManager.addListener(mListener); - mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, + mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, false, KEY, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); @@ -158,6 +160,18 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { verify(mListener).onMediaDataLoaded(eq("NEW_KEY"), any(), captor.capture()); } + @Test + public void getDataIncludesDevice() { + // GIVEN that device and media events have been received + mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData); + mDataListener.onMediaDataLoaded(KEY, null, mMediaData); + + // THEN the result of getData includes device info + Map<String, MediaData> results = mManager.getData(); + assertThat(results.get(KEY)).isNotNull(); + assertThat(results.get(KEY).getDevice()).isEqualTo(mDeviceData); + } + private MediaDataManager.Listener captureDataListener() { ArgumentCaptor<MediaDataManager.Listener> captor = ArgumentCaptor.forClass( MediaDataManager.Listener.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt new file mode 100644 index 000000000000..afb64a7649b4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media + +import android.graphics.Color +import androidx.test.filters.SmallTest +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import java.util.concurrent.Executor + +private const val KEY = "TEST_KEY" +private const val KEY_ALT = "TEST_KEY_2" +private const val USER_MAIN = 0 +private const val USER_GUEST = 10 +private const val APP = "APP" +private const val BG_COLOR = Color.RED +private const val PACKAGE = "PKG" +private const val ARTIST = "ARTIST" +private const val TITLE = "TITLE" +private const val DEVICE_NAME = "DEVICE_NAME" + +private fun <T> eq(value: T): T = Mockito.eq(value) ?: value +private fun <T> any(): T = Mockito.any() + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class MediaDataFilterTest : SysuiTestCase() { + + @Mock + private lateinit var combineLatest: MediaDataCombineLatest + @Mock + private lateinit var listener: MediaDataManager.Listener + @Mock + private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock + private lateinit var mediaResumeListener: MediaResumeListener + @Mock + private lateinit var mediaDataManager: MediaDataManager + @Mock + private lateinit var lockscreenUserManager: NotificationLockscreenUserManager + @Mock + private lateinit var executor: Executor + + private lateinit var mediaDataFilter: MediaDataFilter + private lateinit var dataMain: MediaData + private lateinit var dataGuest: MediaData + private val device = MediaDeviceData(true, null, DEVICE_NAME) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mediaDataFilter = MediaDataFilter(combineLatest, broadcastDispatcher, mediaResumeListener, + mediaDataManager, lockscreenUserManager, executor) + mediaDataFilter.addListener(listener) + + // Start all tests as main user + setUser(USER_MAIN) + + // Set up test media data + dataMain = MediaData(USER_MAIN, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + emptyList(), PACKAGE, null, null, device, true, null) + + dataGuest = MediaData(USER_GUEST, true, BG_COLOR, APP, null, ARTIST, TITLE, null, + emptyList(), emptyList(), PACKAGE, null, null, device, true, null) + } + + private fun setUser(id: Int) { + `when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false) + `when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true) + mediaDataFilter.handleUserSwitched(id) + } + + @Test + fun testOnDataLoadedForCurrentUser_callsListener() { + // GIVEN a media for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + // THEN we should tell the listener + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain)) + } + + @Test + fun testOnDataLoadedForGuest_doesNotCallListener() { + // GIVEN a media for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + + // THEN we should NOT tell the listener + verify(listener, never()).onMediaDataLoaded(any(), any(), any()) + } + + @Test + fun testOnRemovedForCurrent_callsListener() { + // GIVEN a media was removed for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataRemoved(KEY) + + // THEN we should tell the listener + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnRemovedForGuest_doesNotCallListener() { + // GIVEN a media was removed for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + mediaDataFilter.onMediaDataRemoved(KEY) + + // THEN we should NOT tell the listener + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnUserSwitched_removesOldUserControls() { + // GIVEN that we have a media loaded for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should remove the main user's media + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnUserSwitched_addsNewUserControls() { + // GIVEN that we had some media for both users + val dataMap = mapOf(KEY to dataMain, KEY_ALT to dataGuest) + `when`(combineLatest.getData()).thenReturn(dataMap) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should add back the guest user media + verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest)) + + // but not the main user's + verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain)) + } + + @Test + fun testHasAnyMedia() { + assertThat(mediaDataFilter.hasAnyMedia()).isFalse() + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + assertThat(mediaDataFilter.hasAnyMedia()).isTrue() + } + + @Test + fun testHasActiveMedia() { + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() + val data = dataMain.copy(active = true) + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + assertThat(mediaDataFilter.hasActiveMedia()).isTrue() + } + + @Test + fun testHasAnyMedia_onlyCurrentUser() { + assertThat(mediaDataFilter.hasAnyMedia()).isFalse() + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest) + assertThat(mediaDataFilter.hasAnyMedia()).isFalse() + } + + @Test + fun testHasActiveMedia_onlyCurrentUser() { + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() + val data = dataGuest.copy(active = true) + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() + } + + @Test + fun testOnNotificationRemoved_doesntHaveMedia() { + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + mediaDataFilter.onMediaDataRemoved(KEY) + assertThat(mediaDataFilter.hasAnyMedia()).isFalse() + } + + @Test + fun testOnSwipeToDismiss_setsTimedOut() { + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onSwipeToDismiss() + + verify(mediaDataManager).setTimedOut(eq(KEY), eq(true)) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index e56bbabfdc0b..6761b282b26a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -35,6 +35,7 @@ private const val PACKAGE_NAME = "com.android.systemui" private const val APP_NAME = "SystemUI" private const val SESSION_ARTIST = "artist" private const val SESSION_TITLE = "title" +private const val USER_ID = 0 private fun <T> anyObject(): T { return Mockito.anyObject<T>() @@ -91,28 +92,15 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test - fun testHasActiveMedia() { - assertThat(mediaDataManager.hasActiveMedia()).isFalse() - val data = mock(MediaData::class.java) - - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) - assertThat(mediaDataManager.hasActiveMedia()).isFalse() - - whenever(data.active).thenReturn(true) - assertThat(mediaDataManager.hasActiveMedia()).isTrue() - } - - @Test - fun testOnSwipeToDismiss_deactivatesMedia() { - val data = MediaData(initialized = true, backgroundColor = 0, app = null, appIcon = null, - artist = null, song = null, artwork = null, actions = emptyList(), + fun testSetTimedOut_deactivatesMedia() { + val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null, + appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(), actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null, clickIntent = null, device = null, active = true, resumeAction = null) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) - mediaDataManager.onSwipeToDismiss() + mediaDataManager.setTimedOut(KEY, timedOut = true) assertThat(data.active).isFalse() } @@ -141,37 +129,6 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) assertThat(listener.data!!.active).isTrue() - - // Swiping away makes the notification not active - mediaDataManager.onSwipeToDismiss() - assertThat(mediaDataManager.hasActiveMedia()).isFalse() - - // And when a notification is updated - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - - // MediaData should still be inactive - assertThat(mediaDataManager.hasActiveMedia()).isFalse() - } - - @Test - fun testHasAnyMedia_whenAddingMedia() { - assertThat(mediaDataManager.hasAnyMedia()).isFalse() - val data = mock(MediaData::class.java) - - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) - assertThat(mediaDataManager.hasAnyMedia()).isTrue() - } - - @Test - fun testOnNotificationRemoved_doesntHaveMedia() { - val data = mock(MediaData::class.java) - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) - mediaDataManager.onNotificationRemoved(KEY) - assertThat(mediaDataManager.hasAnyMedia()).isFalse() } @Test @@ -212,8 +169,8 @@ class MediaDataManagerTest : SysuiTestCase() { setTitle(SESSION_TITLE) build() } - mediaDataManager.addResumptionControls(desc, Runnable {}, session.sessionToken, APP_NAME, - pendingIntent, PACKAGE_NAME) + mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, + APP_NAME, pendingIntent, PACKAGE_NAME) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN the media data indicates that it is for resumption diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt index 6c7f2e8d7925..fc22eeb3ea68 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt @@ -58,6 +58,7 @@ private const val SESSION_KEY = "SESSION_KEY" private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_TITLE = "SESSION_TITLE" private const val DEVICE_NAME = "DEVICE_NAME" +private const val USER_ID = 0 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value @@ -118,7 +119,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setSmallIcon(android.R.drawable.ic_media_pause) setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken())) } - mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null, + mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null, device = null, active = true, resumeAction = null) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt index 916fd0fe11b7..7a8e4f7e9b85 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt @@ -32,7 +32,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito @@ -48,6 +50,7 @@ private const val PACKAGE = "PKG" private const val SESSION_KEY = "SESSION_KEY" private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_TITLE = "SESSION_TITLE" +private const val USER_ID = 0 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> anyObject(): T { @@ -93,7 +96,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { setPlaybackState(playbackBuilder.build()) } session.setActive(true) - mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null, + mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null, device = null, active = true, resumeAction = null) } @@ -118,6 +121,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) verify(executor).executeDelayed(capture(timeoutCaptor), anyLong()) + verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) } @Test @@ -133,6 +137,24 @@ class MediaTimeoutListenerTest : SysuiTestCase() { } @Test + fun testOnMediaDataLoaded_migratesKeys() { + // From not playing + mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + clearInvocations(mediaController) + + // To playing + val playingState = mock(android.media.session.PlaybackState::class.java) + `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING) + `when`(mediaController.playbackState).thenReturn(playingState) + mediaTimeoutListener.onMediaDataLoaded("NEWKEY", KEY, mediaData) + verify(mediaController).unregisterCallback(anyObject()) + verify(mediaController).registerCallback(anyObject()) + + // Enqueues callback + verify(executor).execute(anyObject()) + } + + @Test fun testOnPlaybackStateChanged_schedulesTimeout_whenPaused() { // Assuming we're registered testOnMediaDataLoaded_registersPlaybackListener() diff --git a/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java index b7a2633d0d36..536cae4380c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java @@ -82,7 +82,7 @@ public class PipAnimationControllerTest extends SysuiTestCase { @Test public void getAnimator_withBounds_returnBoundsAnimator() { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, new Rect(), new Rect()); + .getAnimator(mLeash, new Rect(), new Rect(), null); assertEquals("Expect ANIM_TYPE_BOUNDS animation", animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS); @@ -94,12 +94,12 @@ public class PipAnimationControllerTest extends SysuiTestCase { final Rect endValue1 = new Rect(100, 100, 200, 200); final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue1); + .getAnimator(mLeash, startValue, endValue1, null); oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); oldAnimator.start(); final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue2); + .getAnimator(mLeash, startValue, endValue2, null); assertEquals("getAnimator with same type returns same animator", oldAnimator, newAnimator); @@ -129,7 +129,7 @@ public class PipAnimationControllerTest extends SysuiTestCase { final Rect endValue1 = new Rect(100, 100, 200, 200); final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue1); + .getAnimator(mLeash, startValue, endValue1, null); animator.updateEndValue(endValue2); @@ -141,7 +141,7 @@ public class PipAnimationControllerTest extends SysuiTestCase { final Rect startValue = new Rect(0, 0, 100, 100); final Rect endValue = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue); + .getAnimator(mLeash, startValue, endValue, null); animator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); animator.setPipAnimationCallback(mPipAnimationCallback); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 363fe95aae18..359faba48f08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -1273,8 +1273,8 @@ public class NotifCollectionTest extends SysuiTestCase { verify(mInterceptor3, never()).shouldInterceptDismissal(clearable); } - @Test(expected = IllegalStateException.class) - public void testClearNotificationThrowsIfMissing() { + @Test + public void testClearNotificationDoesntThrowIfMissing() { // GIVEN that enough time has passed that we're beyond the forgiveness window mClock.advanceTime(5001); @@ -1287,7 +1287,8 @@ public class NotifCollectionTest extends SysuiTestCase { container.getSbn(), new RankingMap(new Ranking[]{ container.getRanking() })); - // THEN an exception is thrown + // THEN the event is ignored + verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt()); } @Test diff --git a/packages/Tethering/jarjar-rules.txt b/packages/Tethering/jarjar-rules.txt index 2d3108a0bff1..591861f5b837 100644 --- a/packages/Tethering/jarjar-rules.txt +++ b/packages/Tethering/jarjar-rules.txt @@ -3,7 +3,7 @@ # If there are files in that filegroup that are not covered below, the classes in the # module will be overwritten by the ones in the framework. rule com.android.internal.util.** com.android.networkstack.tethering.util.@1 -rule android.net.LocalLog* com.android.networkstack.tethering.LocalLog@1 +rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1 rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1 diff --git a/packages/Tethering/tests/unit/jarjar-rules.txt b/packages/Tethering/tests/unit/jarjar-rules.txt index 1ea56cdf1a3d..ec2d2b02004e 100644 --- a/packages/Tethering/tests/unit/jarjar-rules.txt +++ b/packages/Tethering/tests/unit/jarjar-rules.txt @@ -8,4 +8,4 @@ rule com.android.internal.util.State* com.android.networkstack.tethering.util.St rule com.android.internal.util.StateMachine* com.android.networkstack.tethering.util.StateMachine@1 rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1 -rule android.net.LocalLog* com.android.networkstack.tethering.LocalLog@1 +rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1 diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index d2b1bd1a6008..499a2711d8e6 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -251,7 +251,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub //TODO: Remove this hack private boolean mInitialized; - private Point mTempPoint; + private Point mTempPoint = new Point(); private boolean mIsAccessibilityButtonShown; private AccessibilityUserState getCurrentUserStateLocked() { diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index 373d47ed366b..6f2e6263b937 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -309,14 +309,9 @@ public class TouchExplorer extends BaseEventStreamTransformation @Override public void onDoubleTapAndHold(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - // Try to use the standard accessibility API to long click - if (!mAms.performActionOnAccessibilityFocusedItem( - AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK)) { - Slog.e(LOG_TAG, "ACTION_LONG_CLICK failed."); - if (mDispatcher.longPressWithTouchEvents(event, policyFlags)) { - sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); - mState.startDelegating(); - } + if (mDispatcher.longPressWithTouchEvents(event, policyFlags)) { + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + mState.startDelegating(); } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 9b3d075e3f2c..7ab4369b338a 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -73,6 +73,7 @@ import android.service.autofill.FieldClassificationUserData; import android.service.autofill.FillContext; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; +import android.service.autofill.InlinePresentation; import android.service.autofill.InternalSanitizer; import android.service.autofill.InternalValidator; import android.service.autofill.SaveInfo; @@ -1437,7 +1438,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mClientState = newClientState; } final Dataset dataset = (Dataset) result; - authenticatedResponse.getDatasets().set(datasetIdx, dataset); + final Dataset oldDataset = authenticatedResponse.getDatasets().get(datasetIdx); + if (!isPinnedDataset(oldDataset)) { + authenticatedResponse.getDatasets().set(datasetIdx, dataset); + } autoFill(requestId, datasetIdx, dataset, false); } else { Slog.w(TAG, "invalid index (" + datasetIdx + ") for authentication id " @@ -1455,6 +1459,27 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + /** + * A dataset can potentially have multiple fields, and it's possible that some of the fields' + * has inline presentation and some don't. It's also possible that some of the fields' + * inline presentation is pinned and some isn't. So the concept of whether a dataset is + * pinned or not is ill-defined. Here we say a dataset is pinned if any of the field has a + * pinned inline presentation in the dataset. It's not ideal but hopefully it is sufficient + * for most of the cases. + */ + private static boolean isPinnedDataset(@Nullable Dataset dataset) { + if (dataset != null && dataset.getFieldIds() != null) { + final int numOfFields = dataset.getFieldIds().size(); + for (int i = 0; i < numOfFields; i++) { + final InlinePresentation inlinePresentation = dataset.getFieldInlinePresentation(i); + if (inlinePresentation != null && inlinePresentation.isPinned()) { + return true; + } + } + } + return false; + } + @GuardedBy("mLock") void setAuthenticationResultForAugmentedAutofillLocked(Bundle data, int authId) { final Dataset dataset = (data == null) ? null : diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java index ef81d7159c42..29bb5428dd84 100644 --- a/services/core/java/com/android/server/adb/AdbService.java +++ b/services/core/java/com/android/server/adb/AdbService.java @@ -34,6 +34,7 @@ import android.hardware.usb.UsbManager; import android.net.Uri; import android.os.Binder; import android.os.IBinder; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserHandle; @@ -509,6 +510,14 @@ public class AdbService extends IAdbManager.Stub { } @Override + public int handleShellCommand(ParcelFileDescriptor in, ParcelFileDescriptor out, + ParcelFileDescriptor err, String[] args) { + return new AdbShellCommand(this).exec( + this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), + args); + } + + @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return; diff --git a/services/core/java/com/android/server/adb/AdbShellCommand.java b/services/core/java/com/android/server/adb/AdbShellCommand.java new file mode 100644 index 000000000000..76918529d071 --- /dev/null +++ b/services/core/java/com/android/server/adb/AdbShellCommand.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.adb; + +import android.os.BasicShellCommandHandler; + +import java.io.PrintWriter; +import java.util.Objects; + +/** + * Interprets and executes 'adb shell cmd adb [args]'. + */ +class AdbShellCommand extends BasicShellCommandHandler { + + private final AdbService mService; + + AdbShellCommand(AdbService service) { + mService = Objects.requireNonNull(service); + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(null); + } + + final PrintWriter pw = getOutPrintWriter(); + switch (cmd) { + case "is-wifi-supported": { + pw.println(Boolean.toString(mService.isAdbWifiSupported())); + return 0; + } + case "is-wifi-qr-supported": { + pw.println(Boolean.toString(mService.isAdbWifiQrSupported())); + return 0; + } + default: + return handleDefaultCommands(cmd); + } + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("Adb service commands:"); + pw.println(" help or -h"); + pw.println(" Print this help text."); + pw.println(" is-wifi-supported"); + pw.println(" Returns \"true\" if adb over wifi is supported."); + pw.println(" is-wifi-qr-supported"); + pw.println(" Returns \"true\" if adb over wifi + QR pairing is supported."); + pw.println(); + } +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index c9dbacda368c..a38d42b92600 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -18284,11 +18284,15 @@ public class ActivityManagerService extends IActivityManager.Stub } } - // Now safely dispatch changes to device idle controller. - for (int i = 0; i < N; i++) { - PendingTempWhitelist ptw = list[i]; - mLocalDeviceIdleController.addPowerSaveTempWhitelistAppDirect(ptw.targetUid, - ptw.duration, true, ptw.tag); + // Now safely dispatch changes to device idle controller. Skip this if we're early + // in boot and the controller hasn't yet been brought online: we do not apply + // device idle policy anyway at this phase. + if (mLocalDeviceIdleController != null) { + for (int i = 0; i < N; i++) { + PendingTempWhitelist ptw = list[i]; + mLocalDeviceIdleController.addPowerSaveTempWhitelistAppDirect(ptw.targetUid, + ptw.duration, true, ptw.tag); + } } // And now we can safely remove them from the map. diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index 1cc41b22838e..5124c4a4797e 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -904,6 +904,10 @@ public final class BroadcastQueue { } else if (r.intent.getData() != null) { b.append(r.intent.getData()); } + if (DEBUG_BROADCAST) { + Slog.v(TAG, "Broadcast temp whitelist uid=" + uid + " duration=" + duration + + " : " + b.toString()); + } mService.tempWhitelistUidLocked(uid, duration, b.toString()); } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index b5c173c91a53..6eab0221b7ab 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -1597,7 +1597,8 @@ public class AppOpsService extends IAppOpsService.Stub { packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); packageUpdateFilter.addDataScheme("package"); - mContext.registerReceiver(mOnPackageUpdatedReceiver, packageUpdateFilter); + mContext.registerReceiverAsUser(mOnPackageUpdatedReceiver, UserHandle.ALL, + packageUpdateFilter, null, null); synchronized (this) { for (int uidNum = mUidStates.size() - 1; uidNum >= 0; uidNum--) { @@ -1640,7 +1641,7 @@ public class AppOpsService extends IAppOpsService.Stub { final IntentFilter packageSuspendFilter = new IntentFilter(); packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED); packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED); - mContext.registerReceiver(new BroadcastReceiver() { + mContext.registerReceiverAsUser(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final int[] changedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST); @@ -1664,7 +1665,7 @@ public class AppOpsService extends IAppOpsService.Stub { } } } - }, packageSuspendFilter); + }, UserHandle.ALL, packageSuspendFilter, null, null); final IntentFilter packageAddedFilter = new IntentFilter(); packageAddedFilter.addAction(Intent.ACTION_PACKAGE_ADDED); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 9acb47538043..15e8a92ba0e4 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -4047,7 +4047,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub // Send it to window manager to hide IME from IME target window. // TODO(b/139861270): send to mCurClient.client once IMMS is aware of // actual IME target. - mWindowManagerInternal.hideIme(mHideRequestWindowMap.get(windowToken)); + mWindowManagerInternal.hideIme( + mHideRequestWindowMap.get(windowToken), + mCurClient.selfReportedDisplayId); } } else { // Send to window manager to show IME after IME layout finishes. diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 1a8de9771451..90370ddd21dd 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -1616,6 +1616,7 @@ public class LockSettingsService extends ILockSettings.Stub { synchronized (mSeparateChallengeLock) { if (!setLockCredentialInternal(credential, savedCredential, userId, /* isLockTiedToParent= */ false)) { + scheduleGc(); return false; } setSeparateProfileChallengeEnabledLocked(userId, true, /* unused */ null); @@ -1626,6 +1627,7 @@ public class LockSettingsService extends ILockSettings.Stub { setDeviceUnlockedForUser(userId); } notifySeparateProfileChallengeChanged(userId); + scheduleGc(); return true; } @@ -1965,7 +1967,11 @@ public class LockSettingsService extends ILockSettings.Stub { public VerifyCredentialResponse checkCredential(LockscreenCredential credential, int userId, ICheckCredentialProgressCallback progressCallback) { checkPasswordReadPermission(userId); - return doVerifyCredential(credential, CHALLENGE_NONE, 0, userId, progressCallback); + try { + return doVerifyCredential(credential, CHALLENGE_NONE, 0, userId, progressCallback); + } finally { + scheduleGc(); + } } @Override @@ -1978,8 +1984,12 @@ public class LockSettingsService extends ILockSettings.Stub { challengeType = CHALLENGE_NONE; } - return doVerifyCredential(credential, challengeType, challenge, userId, - null /* progressCallback */); + try { + return doVerifyCredential(credential, challengeType, challenge, userId, + null /* progressCallback */); + } finally { + scheduleGc(); + } } private VerifyCredentialResponse doVerifyCredential(LockscreenCredential credential, @@ -2070,6 +2080,8 @@ public class LockSettingsService extends ILockSettings.Stub { | BadPaddingException | CertificateException | IOException e) { Slog.e(TAG, "Failed to decrypt child profile key", e); throw new IllegalStateException("Unable to get tied profile token"); + } finally { + scheduleGc(); } } @@ -2983,27 +2995,31 @@ public class LockSettingsService extends ILockSettings.Stub { @Override public byte[] getHashFactor(LockscreenCredential currentCredential, int userId) { checkPasswordReadPermission(userId); - if (isManagedProfileWithUnifiedLock(userId)) { - try { - currentCredential = getDecryptedPasswordForTiedProfile(userId); - } catch (Exception e) { - Slog.e(TAG, "Failed to get work profile credential", e); - return null; - } - } - synchronized (mSpManager) { - if (!isSyntheticPasswordBasedCredentialLocked(userId)) { - Slog.w(TAG, "Synthetic password not enabled"); - return null; + try { + if (isManagedProfileWithUnifiedLock(userId)) { + try { + currentCredential = getDecryptedPasswordForTiedProfile(userId); + } catch (Exception e) { + Slog.e(TAG, "Failed to get work profile credential", e); + return null; + } } - long handle = getSyntheticPasswordHandleLocked(userId); - AuthenticationResult auth = mSpManager.unwrapPasswordBasedSyntheticPassword( - getGateKeeperService(), handle, currentCredential, userId, null); - if (auth.authToken == null) { - Slog.w(TAG, "Current credential is incorrect"); - return null; + synchronized (mSpManager) { + if (!isSyntheticPasswordBasedCredentialLocked(userId)) { + Slog.w(TAG, "Synthetic password not enabled"); + return null; + } + long handle = getSyntheticPasswordHandleLocked(userId); + AuthenticationResult auth = mSpManager.unwrapPasswordBasedSyntheticPassword( + getGateKeeperService(), handle, currentCredential, userId, null); + if (auth.authToken == null) { + Slog.w(TAG, "Current credential is incorrect"); + return null; + } + return auth.authToken.derivePasswordHashFactor(); } - return auth.authToken.derivePasswordHashFactor(); + } finally { + scheduleGc(); } } @@ -3287,6 +3303,22 @@ public class LockSettingsService extends ILockSettings.Stub { } } + /** + * Schedules garbage collection to sanitize lockscreen credential remnants in memory. + * + * One source of leftover lockscreen credentials is the unmarshalled binder method arguments. + * Since this method will be called within the binder implementation method, a small delay is + * added before the GC operation to allow the enclosing binder proxy code to complete and + * release references to the argument. + */ + private void scheduleGc() { + mHandler.postDelayed(() -> { + System.gc(); + System.runFinalization(); + System.gc(); + }, 2000); + } + private class DeviceProvisionedObserver extends ContentObserver { private final Uri mDeviceProvisionedUri = Settings.Global.getUriFor( Settings.Global.DEVICE_PROVISIONED); diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java index 25bbfa02fa05..3a4dfaf9bfcd 100644 --- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java +++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java @@ -45,6 +45,7 @@ import com.android.internal.R; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -318,16 +319,6 @@ class BluetoothRouteProvider { btRoute.route = builder.build(); } - private void clearActiveRoutes() { - if (DEBUG) { - Log.d(TAG, "Clearing active routes"); - } - for (BluetoothRouteInfo btRoute : mActiveRoutes) { - setRouteConnectionState(btRoute, STATE_DISCONNECTED); - } - mActiveRoutes.clear(); - } - private void addActiveRoute(BluetoothRouteInfo btRoute) { if (DEBUG) { Log.d(TAG, "Adding active route: " + btRoute.route); @@ -348,18 +339,34 @@ class BluetoothRouteProvider { } } - private void findAndSetActiveHearingAidDevices() { + private void clearActiveRoutesWithType(int type) { if (DEBUG) { - Log.d(TAG, "Setting active hearing aid devices"); + Log.d(TAG, "Clearing active routes with type. type=" + type); + } + Iterator<BluetoothRouteInfo> iter = mActiveRoutes.iterator(); + while (iter.hasNext()) { + BluetoothRouteInfo btRoute = iter.next(); + if (btRoute.route.getType() == type) { + iter.remove(); + setRouteConnectionState(btRoute, STATE_DISCONNECTED); + } } + } - BluetoothHearingAid hearingAidProfile = mHearingAidProfile; - if (hearingAidProfile == null) { - return; + private void addActiveHearingAidDevices(BluetoothDevice device) { + if (DEBUG) { + Log.d(TAG, "Setting active hearing aid devices. device=" + device); } - List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices(); + + // Let the given device be the first active device + BluetoothRouteInfo activeBtRoute = mBluetoothRoutes.get(device.getAddress()); + addActiveRoute(activeBtRoute); + + // A bluetooth route with the same route ID should be added. for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) { - if (activeDevices.contains(btRoute.btDevice)) { + if (TextUtils.equals(btRoute.route.getId(), activeBtRoute.route.getId()) + && !TextUtils.equals(btRoute.btDevice.getAddress(), + activeBtRoute.btDevice.getAddress())) { addActiveRoute(btRoute); } } @@ -465,16 +472,16 @@ class BluetoothRouteProvider { public void onReceive(Context context, Intent intent, BluetoothDevice device) { switch (intent.getAction()) { case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED: - clearActiveRoutes(); + clearActiveRoutesWithType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); if (device != null) { addActiveRoute(mBluetoothRoutes.get(device.getAddress())); } notifyBluetoothRoutesUpdated(); break; case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED: - clearActiveDevices(); + clearActiveRoutesWithType(MediaRoute2Info.TYPE_HEARING_AID); if (device != null) { - findAndSetActiveHearingAidDevices(); + addActiveHearingAidDevices(device); } notifyBluetoothRoutesUpdated(); break; diff --git a/services/core/java/com/android/server/notification/ShortcutHelper.java b/services/core/java/com/android/server/notification/ShortcutHelper.java index 94b690a4dfce..9c3d6d352c89 100644 --- a/services/core/java/com/android/server/notification/ShortcutHelper.java +++ b/services/core/java/com/android/server/notification/ShortcutHelper.java @@ -102,9 +102,13 @@ public class ShortcutHelper { HashMap<String, String> shortcutBubbles = mActiveShortcutBubbles.get(packageName); ArrayList<String> bubbleKeysToRemove = new ArrayList<>(); if (shortcutBubbles != null) { + // Copy to avoid a concurrent modification exception when we remove bubbles from + // shortcutBubbles. + final Set<String> shortcutIds = new HashSet<>(shortcutBubbles.keySet()); + // If we can't find one of our bubbles in the shortcut list, that bubble needs // to be removed. - for (String shortcutId : shortcutBubbles.keySet()) { + for (String shortcutId : shortcutIds) { boolean foundShortcut = false; for (int i = 0; i < shortcuts.size(); i++) { if (shortcuts.get(i).getId().equals(shortcutId)) { diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index f827721be3b7..91b2ea1853fa 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -522,7 +522,9 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) { params.installFlags |= PackageManager.INSTALL_FROM_ADB; - + // adb installs can override the installingPackageName, but not the + // initiatingPackageName + installerPackageName = null; } else { if (callingUid != Process.SYSTEM_UID) { // The supplied installerPackageName must always belong to the calling app. diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 088c5daf30a4..f533eb2cf2be 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -11083,6 +11083,21 @@ public class PackageManagerService extends IPackageManager.Stub pkgSetting.forceQueryableOverride = true; } + // If this is part of a standard install, set the initiating package name, else rely on + // previous device state. + if (reconciledPkg.installArgs != null) { + InstallSource installSource = reconciledPkg.installArgs.installSource; + if (installSource.initiatingPackageName != null) { + final PackageSetting ips = mSettings.mPackages.get( + installSource.initiatingPackageName); + if (ips != null) { + installSource = installSource.setInitiatingPackageSignatures( + ips.signatures); + } + } + pkgSetting.setInstallSource(installSource); + } + // TODO(toddke): Consider a method specifically for modifying the Package object // post scan; or, moving this stuff out of the Package object since it has nothing // to do with the package on disk. @@ -15173,8 +15188,13 @@ public class PackageManagerService extends IPackageManager.Stub idleController.addPowerSaveTempWhitelistAppDirect(Process.myUid(), idleDuration, false, "integrity component"); + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setTemporaryAppWhitelistDuration(idleDuration); + mContext.sendOrderedBroadcastAsUser(integrityVerification, UserHandle.SYSTEM, /* receiverPermission= */ null, + /* appOp= */ AppOpsManager.OP_NONE, + /* options= */ options.toBundle(), new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -15274,6 +15294,8 @@ public class PackageManagerService extends IPackageManager.Stub DeviceIdleInternal idleController = mInjector.getLocalDeviceIdleController(); final long idleDuration = getVerificationTimeout(); + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setTemporaryAppWhitelistDuration(idleDuration); /* * If any sufficient verifiers were listed in the package @@ -15293,7 +15315,9 @@ public class PackageManagerService extends IPackageManager.Stub final Intent sufficientIntent = new Intent(verification); sufficientIntent.setComponent(verifierComponent); - mContext.sendBroadcastAsUser(sufficientIntent, verifierUser); + mContext.sendBroadcastAsUser(sufficientIntent, verifierUser, + /* receiverPermission= */ null, + options.toBundle()); } } } @@ -15312,6 +15336,8 @@ public class PackageManagerService extends IPackageManager.Stub verifierUser.getIdentifier(), false, "package verifier"); mContext.sendOrderedBroadcastAsUser(verification, verifierUser, android.Manifest.permission.PACKAGE_VERIFICATION_AGENT, + /* appOp= */ AppOpsManager.OP_NONE, + /* options= */ options.toBundle(), new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -16078,16 +16104,7 @@ public class PackageManagerService extends IPackageManager.Stub ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT, userId, installerPackageName); } - if (installSource.initiatingPackageName != null) { - final PackageSetting ips = mSettings.mPackages.get( - installSource.initiatingPackageName); - if (ips != null) { - installSource = installSource.setInitiatingPackageSignatures( - ips.signatures); - } - } - ps.setInstallSource(installSource); - mSettings.addInstallerPackageNames(installSource); + mSettings.addInstallerPackageNames(ps.installSource); // When replacing an existing package, preserve the original install reason for all // users that had the package installed before. Similarly for uninstall reasons. @@ -19148,9 +19165,7 @@ public class PackageManagerService extends IPackageManager.Stub final boolean systemApp = isSystemApp(ps); final int userId = user == null ? UserHandle.USER_ALL : user.getIdentifier(); - if (ps.getPermissionsState().hasPermission(Manifest.permission.SUSPEND_APPS, userId)) { - unsuspendForSuspendingPackage(packageName, userId); - } + if ((!systemApp || (flags & PackageManager.DELETE_SYSTEM_APP) != 0) && userId != UserHandle.USER_ALL) { // The caller is asking that the package only be deleted for a single @@ -19208,6 +19223,20 @@ public class PackageManagerService extends IPackageManager.Stub outInfo, writeSettings); } + // If the package removed had SUSPEND_APPS, unset any restrictions that might have been in + // place for all affected users. + int[] affectedUserIds = (outInfo != null) ? outInfo.removedUsers : null; + if (affectedUserIds == null) { + affectedUserIds = resolveUserIds(userId); + } + for (final int affectedUserId : affectedUserIds) { + if (ps.getPermissionsState().hasPermission(Manifest.permission.SUSPEND_APPS, + affectedUserId)) { + unsuspendForSuspendingPackage(packageName, affectedUserId); + removeAllDistractingPackageRestrictions(affectedUserId); + } + } + // Take a note whether we deleted the package for all users if (outInfo != null) { outInfo.removedForAllUsers = mPackages.get(ps.name) == null; diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 1a7490e8b327..8bbe9cc01ada 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -125,7 +125,6 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -291,7 +290,8 @@ class PackageManagerShellCommand extends ShellCommand { case "get-stagedsessions": return runListStagedSessions(); case "uninstall-system-updates": - return uninstallSystemUpdates(); + String packageName = getNextArg(); + return uninstallSystemUpdates(packageName); case "rollback-app": return runRollbackApp(); case "get-moduleinfo": @@ -409,15 +409,22 @@ class PackageManagerShellCommand extends ShellCommand { } } - private int uninstallSystemUpdates() { + private int uninstallSystemUpdates(String packageName) { final PrintWriter pw = getOutPrintWriter(); - List<String> failedUninstalls = new LinkedList<>(); + boolean failedUninstalls = false; try { - final ParceledListSlice<ApplicationInfo> packages = - mInterface.getInstalledApplications( - PackageManager.MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM); final IPackageInstaller installer = mInterface.getPackageInstaller(); - List<ApplicationInfo> list = packages.getList(); + final List<ApplicationInfo> list; + if (packageName == null) { + final ParceledListSlice<ApplicationInfo> packages = + mInterface.getInstalledApplications( + PackageManager.MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM); + list = packages.getList(); + } else { + list = new ArrayList<>(1); + list.add(mInterface.getApplicationInfo(packageName, + PackageManager.MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM)); + } for (ApplicationInfo info : list) { if (info.isUpdatedSystemApp()) { pw.println("Uninstalling updates to " + info.packageName + "..."); @@ -430,7 +437,8 @@ class PackageManagerShellCommand extends ShellCommand { final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); if (status != PackageInstaller.STATUS_SUCCESS) { - failedUninstalls.add(info.packageName); + failedUninstalls = true; + pw.println("Couldn't uninstall package: " + info.packageName); } } } @@ -440,10 +448,7 @@ class PackageManagerShellCommand extends ShellCommand { + e.getMessage() + "]"); return 0; } - if (!failedUninstalls.isEmpty()) { - pw.println("Failure [Couldn't uninstall packages: " - + TextUtils.join(", ", failedUninstalls) - + "]"); + if (failedUninstalls) { return 0; } pw.println("Success"); @@ -3824,9 +3829,10 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" get-harmful-app-warning [--user <USER_ID>] <PACKAGE>"); pw.println(" Return the harmful app warning message for the given app, if present"); pw.println(); - pw.println(" uninstall-system-updates"); - pw.println(" Remove updates to all system applications and fall back to their /system " + - "version."); + pw.println(" uninstall-system-updates [<PACKAGE>]"); + pw.println(" Removes updates to the given system application and falls back to its"); + pw.println(" /system version. Does nothing if the given package is not a system app."); + pw.println(" If no package is specified, removes updates to all system applications."); pw.println(""); pw.println(" get-moduleinfo [--all | --installed] [module-name]"); pw.println(" Displays module info. If module-name is specified only that info is shown"); diff --git a/services/core/java/com/android/server/textclassifier/IconsContentProvider.java b/services/core/java/com/android/server/textclassifier/IconsContentProvider.java index 9b3176d9df67..183e920e4620 100644 --- a/services/core/java/com/android/server/textclassifier/IconsContentProvider.java +++ b/services/core/java/com/android/server/textclassifier/IconsContentProvider.java @@ -27,6 +27,7 @@ import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.AutoCloseOutputStream; import android.os.UserHandle; import android.util.Log; +import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.server.textclassifier.IconsUriHelper.ResourceInfo; @@ -34,6 +35,7 @@ import com.android.server.textclassifier.IconsUriHelper.ResourceInfo; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.Arrays; /** * A content provider that is used to access icons returned from the TextClassifier service. @@ -46,32 +48,40 @@ import java.io.OutputStream; public final class IconsContentProvider extends ContentProvider { private static final String TAG = "IconsContentProvider"; + private static final String MIME_TYPE = "image/png"; + + private final PipeDataWriter<Pair<ResourceInfo, Integer>> mWriter = + (writeSide, uri, mimeType, bundle, args) -> { + try (OutputStream out = new AutoCloseOutputStream(writeSide)) { + final ResourceInfo res = args.first; + final int userId = args.second; + final Drawable drawable = Icon.createWithResource(res.packageName, res.id) + .loadDrawableAsUser(getContext(), userId); + getBitmap(drawable).compress(Bitmap.CompressFormat.PNG, 100, out); + } catch (Exception e) { + Log.e(TAG, "Error retrieving icon for uri: " + uri, e); + } + }; @Override public ParcelFileDescriptor openFile(Uri uri, String mode) { + final ResourceInfo res = IconsUriHelper.getInstance().getResourceInfo(uri); + if (res == null) { + Log.e(TAG, "No icon found for uri: " + uri); + return null; + } + try { - final ResourceInfo res = IconsUriHelper.getInstance().getResourceInfo(uri); - final Drawable drawable = Icon.createWithResource(res.packageName, res.id) - .loadDrawableAsUser(getContext(), UserHandle.getCallingUserId()); - final byte[] data = getBitmapData(drawable); - final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); - final ParcelFileDescriptor readSide = pipe[0]; - final ParcelFileDescriptor writeSide = pipe[1]; - try (OutputStream out = new AutoCloseOutputStream(writeSide)) { - out.write(data); - return readSide; - } - } catch (IOException | RuntimeException e) { - Log.e(TAG, "Error retrieving icon for uri: " + uri, e); + final Pair<ResourceInfo, Integer> args = new Pair(res, UserHandle.getCallingUserId()); + return openPipeHelper(uri, MIME_TYPE, /* bundle= */ null, args, mWriter); + } catch (IOException e) { + Log.e(TAG, "Error opening pipe helper for icon at uri: " + uri, e); } + return null; } - /** - * Returns the bitmap data for the specified drawable. - */ - @VisibleForTesting - public static byte[] getBitmapData(Drawable drawable) { + private static Bitmap getBitmap(Drawable drawable) { if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { throw new IllegalStateException("The icon is zero-sized"); } @@ -85,16 +95,24 @@ public final class IconsContentProvider extends ContentProvider { drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - final byte[] byteArray = stream.toByteArray(); - bitmap.recycle(); - return byteArray; + return bitmap; + } + + /** + * Returns true if the drawables are considered the same. + */ + @VisibleForTesting + public static boolean sameIcon(Drawable one, Drawable two) { + final ByteArrayOutputStream stream1 = new ByteArrayOutputStream(); + getBitmap(one).compress(Bitmap.CompressFormat.PNG, 100, stream1); + final ByteArrayOutputStream stream2 = new ByteArrayOutputStream(); + getBitmap(two).compress(Bitmap.CompressFormat.PNG, 100, stream2); + return Arrays.equals(stream1.toByteArray(), stream2.toByteArray()); } @Override public String getType(Uri uri) { - return "image/png"; + return MIME_TYPE; } @Override diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index c4663ca19f87..2e9f70448488 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3146,11 +3146,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } @Override - boolean checkCompleteDeferredRemoval() { + boolean handleCompleteDeferredRemoval() { if (mIsExiting) { removeIfPossible(); } - return super.checkCompleteDeferredRemoval(); + return super.handleCompleteDeferredRemoval(); } void onRemovedFromDisplay() { diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index b4bc0f5b3a32..8f0de7312ee5 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -86,7 +86,6 @@ import static com.android.server.wm.TaskProto.ACTIVITY_TYPE; import static com.android.server.wm.TaskProto.ANIMATING_BOUNDS; import static com.android.server.wm.TaskProto.BOUNDS; import static com.android.server.wm.TaskProto.CREATED_BY_ORGANIZER; -import static com.android.server.wm.TaskProto.DEFER_REMOVAL; import static com.android.server.wm.TaskProto.DISPLAY_ID; import static com.android.server.wm.TaskProto.FILLS_PARENT; import static com.android.server.wm.TaskProto.LAST_NON_FULLSCREEN_BOUNDS; @@ -248,11 +247,6 @@ class ActivityStack extends Task { private Rect mTmpRect = new Rect(); private Rect mTmpRect2 = new Rect(); - /** Detach this stack from its display when animation completes. */ - // TODO: maybe tie this to WindowContainer#removeChild some how... - // TODO: This is no longer set. Okay to remove or was the set removed by accident? - private boolean mDeferRemoval; - // If this is true, we are in the bounds animating mode. The task will be down or upscaled to // perfectly fit the region it would have been cropped to. We may also avoid certain logic we // would otherwise apply while resizing, while resizing in the bounds animating mode. @@ -3232,9 +3226,6 @@ class ActivityStack extends Task { @Override void dump(PrintWriter pw, String prefix, boolean dumpAll) { - if (mDeferRemoval) { - pw.println(prefix + "mDeferRemoval=true"); - } super.dump(pw, prefix, dumpAll); if (!mExitingActivities.isEmpty()) { pw.println(); @@ -3304,15 +3295,12 @@ class ActivityStack extends Task { } /** Returns true if a removal action is still being deferred. */ - boolean checkCompleteDeferredRemoval() { + boolean handleCompleteDeferredRemoval() { if (isAnimating(TRANSITION | CHILDREN)) { return true; } - if (mDeferRemoval) { - removeImmediately(); - } - return super.checkCompleteDeferredRemoval(); + return super.handleCompleteDeferredRemoval(); } public DisplayInfo getDisplayInfo() { @@ -3380,7 +3368,6 @@ class ActivityStack extends Task { mLastNonFullscreenBounds.dumpDebug(proto, LAST_NON_FULLSCREEN_BOUNDS); } - proto.write(DEFER_REMOVAL, mDeferRemoval); proto.write(ANIMATING_BOUNDS, mBoundsAnimating); if (mSurfaceControl != null) { diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index 407b9fcbca74..6bfcf0c75b83 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -1504,12 +1504,21 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { } void removeTask(Task task, boolean killProcess, boolean removeFromRecents, String reason) { - task.removeTaskActivitiesLocked(reason); - cleanUpRemovedTaskLocked(task, killProcess, removeFromRecents); - mService.getLockTaskController().clearLockedTask(task); - mService.getTaskChangeNotificationController().notifyTaskStackChanged(); - if (task.isPersistable) { - mService.notifyTaskPersisterLocked(null, true); + if (task.mInRemoveTask) { + // Prevent recursion. + return; + } + task.mInRemoveTask = true; + try { + task.performClearTask(reason); + cleanUpRemovedTaskLocked(task, killProcess, removeFromRecents); + mService.getLockTaskController().clearLockedTask(task); + mService.getTaskChangeNotificationController().notifyTaskStackChanged(); + if (task.isPersistable) { + mService.notifyTaskPersisterLocked(null, true); + } + } finally { + task.mInRemoveTask = false; } } @@ -2177,7 +2186,7 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { // split-screen in split-screen. mService.getTaskChangeNotificationController() .notifyActivityDismissingDockedStack(); - taskDisplayArea.onSplitScreenModeDismissed(task.getStack()); + taskDisplayArea.onSplitScreenModeDismissed((ActivityStack) task); taskDisplayArea.mDisplayContent.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS, true /* notifyClients */); } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 241de2e0e068..c56440785bba 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2739,6 +2739,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo @Override void removeImmediately() { mRemovingDisplay = true; + mDeferredRemoval = false; try { if (mParentWindow != null) { mParentWindow.removeEmbeddedDisplayContent(this); @@ -2771,31 +2772,14 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo /** Returns true if a removal action is still being deferred. */ @Override - boolean checkCompleteDeferredRemoval() { - boolean stillDeferringRemoval = false; - - for (int i = getChildCount() - 1; i >= 0; --i) { - final DisplayChildWindowContainer child = getChildAt(i); - stillDeferringRemoval |= child.checkCompleteDeferredRemoval(); - if (getChildCount() == 0) { - // If this display is pending to be removed because it contains an activity with - // {@link ActivityRecord#mIsExiting} is true, this display may be removed when - // completing the removal of the last activity from - // {@link ActivityRecord#checkCompleteDeferredRemoval}. - return false; - } - } + boolean handleCompleteDeferredRemoval() { + final boolean stillDeferringRemoval = super.handleCompleteDeferredRemoval(); if (!stillDeferringRemoval && mDeferredRemoval) { removeImmediately(); return false; } - return true; - } - - /** @return 'true' if removal of this display content is deferred due to active animation. */ - boolean isRemovalDeferred() { - return mDeferredRemoval; + return stillDeferringRemoval; } void adjustForImeIfNeeded() { diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index 8734b5efa45d..3e88566449fe 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -19,10 +19,13 @@ package com.android.server.wm; import static android.os.Process.myPid; import static android.os.Process.myUid; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; +import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.INPUT_CONSUMER_NAVIGATION; import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION; import static android.view.WindowManager.INPUT_CONSUMER_WALLPAPER; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS; @@ -477,12 +480,11 @@ final class InputMonitor { mService.getRecentsAnimationController(); final boolean shouldApplyRecentsInputConsumer = recentsAnimationController != null && recentsAnimationController.shouldApplyInputConsumer(w.mActivityRecord); - if (inputChannel == null || inputWindowHandle == null || w.mRemoved - || (w.cantReceiveTouchInput() && !shouldApplyRecentsInputConsumer)) { + if (inputWindowHandle == null || w.mRemoved) { if (w.mWinAnimator.hasSurface()) { mInputTransaction.setInputWindowInfo( - w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), - mInvalidInputWindow); + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + mInvalidInputWindow); } // Skip this window because it cannot possibly receive input. return; @@ -491,9 +493,23 @@ final class InputMonitor { final int flags = w.mAttrs.flags; final int privateFlags = w.mAttrs.privateFlags; final int type = w.mAttrs.type; - final boolean hasFocus = w.isFocused(); final boolean isVisible = w.isVisibleLw(); + // Assign an InputInfo with type to the overlay window which can't receive input event. + // This is used to omit Surfaces from occlusion detection. + if (inputChannel == null + || (w.cantReceiveTouchInput() && !shouldApplyRecentsInputConsumer)) { + if (!w.mWinAnimator.hasSurface()) { + return; + } + populateOverlayInputInfo(inputWindowHandle, w.getName(), type, isVisible); + mInputTransaction.setInputWindowInfo( + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + inputWindowHandle); + return; + } + + final boolean hasFocus = w.isFocused(); if (mAddRecentsAnimationInputConsumerHandle && shouldApplyRecentsInputConsumer) { if (recentsAnimationController.updateInputConsumerForApp( mRecentsAnimationInputConsumer.mWindowHandle, hasFocus)) { @@ -555,6 +571,28 @@ final class InputMonitor { } } + // This would reset InputWindowHandle fields to prevent it could be found by input event. + // We need to check if any new field of InputWindowHandle could impact the result. + private static void populateOverlayInputInfo(final InputWindowHandle inputWindowHandle, + final String name, final int type, final boolean isVisible) { + inputWindowHandle.name = name; + inputWindowHandle.layoutParamsType = type; + inputWindowHandle.dispatchingTimeoutNanos = + WindowManagerService.DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS; + inputWindowHandle.visible = isVisible; + inputWindowHandle.canReceiveKeys = false; + inputWindowHandle.hasFocus = false; + inputWindowHandle.ownerPid = myPid(); + inputWindowHandle.ownerUid = myUid(); + inputWindowHandle.inputFeatures = INPUT_FEATURE_NO_INPUT_CHANNEL; + inputWindowHandle.scaleFactor = 1; + inputWindowHandle.layoutParamsFlags = + FLAG_NOT_TOUCH_MODAL | FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE; + inputWindowHandle.portalToDisplayId = INVALID_DISPLAY; + inputWindowHandle.touchableRegion.setEmpty(); + inputWindowHandle.setTouchableRegionCrop(null); + } + /** * Helper function to generate an InputInfo with type SECURE_SYSTEM_OVERLAY. This input * info will not have an input channel or be touchable, but is used to omit Surfaces @@ -564,16 +602,7 @@ final class InputMonitor { static void setTrustedOverlayInputInfo(SurfaceControl sc, SurfaceControl.Transaction t, int displayId, String name) { InputWindowHandle inputWindowHandle = new InputWindowHandle(null, displayId); - inputWindowHandle.name = name; - inputWindowHandle.layoutParamsType = TYPE_SECURE_SYSTEM_OVERLAY; - inputWindowHandle.dispatchingTimeoutNanos = -1; - inputWindowHandle.visible = true; - inputWindowHandle.canReceiveKeys = false; - inputWindowHandle.hasFocus = false; - inputWindowHandle.ownerPid = myPid(); - inputWindowHandle.ownerUid = myUid(); - inputWindowHandle.inputFeatures = INPUT_FEATURE_NO_INPUT_CHANNEL; - inputWindowHandle.scaleFactor = 1; + populateOverlayInputInfo(inputWindowHandle, name, TYPE_SECURE_SYSTEM_OVERLAY, true); t.setInputWindowInfo(sc, inputWindowHandle); } } diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 1b58fc1d2d3e..851b533a550d 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -1426,8 +1426,8 @@ class RecentTasks { private void removeUnreachableHiddenTasks(int windowingMode) { for (int i = mHiddenTasks.size() - 1; i >= 0; i--) { final Task hiddenTask = mHiddenTasks.get(i); - if (!hiddenTask.hasChild()) { - // The task was removed by other path. + if (!hiddenTask.hasChild() || hiddenTask.inRecents) { + // The task was removed by other path or it became reachable (added to recents). mHiddenTasks.remove(i); continue; } @@ -1449,6 +1449,9 @@ class RecentTasks { * of task as the given one. */ private void removeForAddTask(Task task) { + // The adding task will be in recents so it is not hidden. + mHiddenTasks.remove(task); + final int removeIndex = findRemoveIndexForAddTask(task); if (removeIndex == -1) { // Nothing to trim @@ -1460,8 +1463,6 @@ class RecentTasks { // callbacks here. final Task removedTask = mTasks.remove(removeIndex); if (removedTask != task) { - // The added task is in recents so it is not hidden. - mHiddenTasks.remove(task); if (removedTask.hasChild()) { // A non-empty task is replaced by a new task. Because the removed task is no longer // managed by the recent tasks list, add it to the hidden list to prevent the task diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 2d30b737b274..119b188dfa00 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -990,9 +990,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } // Remove all deferred displays stacks, tasks, and activities. - for (int displayNdx = mChildren.size() - 1; displayNdx >= 0; --displayNdx) { - mChildren.get(displayNdx).checkCompleteDeferredRemoval(); - } + handleCompleteDeferredRemoval(); forAllDisplays(dc -> { dc.getInputMonitor().updateInputWindowsLw(true /*force*/); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 748244e1a5c2..c664a841fc1f 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -435,6 +435,13 @@ class Task extends WindowContainer<WindowContainer> { static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1; private int mForceHiddenFlags = 0; + // TODO(b/160201781): Revisit double invocation issue in Task#removeChild. + /** + * Skip {@link ActivityStackSupervisor#removeTask(Task, boolean, boolean, String)} execution if + * {@code true} to prevent double traversal of {@link #mChildren} in a loop. + */ + boolean mInRemoveTask; + // When non-null, this is a transaction that will get applied on the next frame returned after // a relayout is requested from the client. While this is only valid on a leaf task; since the // transaction can effect an ancestor task, this also needs to keep track of the ancestor task @@ -1496,11 +1503,8 @@ class Task extends WindowContainer<WindowContainer> { return autoRemoveRecents || (!hasChild() && !getHasBeenVisible()); } - /** - * Completely remove all activities associated with an existing - * task starting at a specified index. - */ - private void performClearTaskAtIndexLocked(String reason) { + /** Completely remove all activities associated with an existing task. */ + void performClearTask(String reason) { // Broken down into to cases to avoid object create due to capturing mStack. if (getStack() == null) { forAllActivities((r) -> { @@ -1524,7 +1528,7 @@ class Task extends WindowContainer<WindowContainer> { */ void performClearTaskLocked() { mReuseTask = true; - performClearTaskAtIndexLocked("clear-task-all"); + performClearTask("clear-task-all"); mReuseTask = false; } @@ -1585,11 +1589,6 @@ class Task extends WindowContainer<WindowContainer> { return false; } - void removeTaskActivitiesLocked(String reason) { - // Just remove the entire task. - performClearTaskAtIndexLocked(reason); - } - String lockTaskAuthToString() { switch (mLockTaskAuth) { case LOCK_TASK_AUTH_DONT_LOCK: return "LOCK_TASK_AUTH_DONT_LOCK"; diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 3360951a8f3f..205a8d2aeeac 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -1746,10 +1746,15 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { void ensureActivitiesVisible(ActivityRecord starting, int configChanges, boolean preserveWindows, boolean notifyClients) { - for (int stackNdx = getStackCount() - 1; stackNdx >= 0; --stackNdx) { - final ActivityStack stack = getStackAt(stackNdx); - stack.ensureActivitiesVisible(starting, configChanges, preserveWindows, - notifyClients); + mAtmService.mStackSupervisor.beginActivityVisibilityUpdate(); + try { + for (int stackNdx = getStackCount() - 1; stackNdx >= 0; --stackNdx) { + final ActivityStack stack = getStackAt(stackNdx); + stack.ensureActivitiesVisible(starting, configChanges, preserveWindows, + notifyClients); + } + } finally { + mAtmService.mStackSupervisor.endActivityVisibilityUpdate(); } } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 2977968f5754..5a73fabaf9d0 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1044,13 +1044,25 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return mChildren.peekLast(); } - /** Returns true if there is still a removal being deferred */ - boolean checkCompleteDeferredRemoval() { + /** + * Removes the containers which were deferred. + * + * @return {@code true} if there is still a removal being deferred. + */ + boolean handleCompleteDeferredRemoval() { boolean stillDeferringRemoval = false; for (int i = mChildren.size() - 1; i >= 0; --i) { final WindowContainer wc = mChildren.get(i); - stillDeferringRemoval |= wc.checkCompleteDeferredRemoval(); + stillDeferringRemoval |= wc.handleCompleteDeferredRemoval(); + if (!hasChild()) { + // All child containers of current level could be removed from a removal of + // descendant. E.g. if a display is pending to be removed because it contains an + // activity with {@link ActivityRecord#mIsExiting} is true, the display may be + // removed when completing the removal of the last activity from + // {@link ActivityRecord#checkCompleteDeferredRemoval}. + return false; + } } return stillDeferringRemoval; diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index c605e3e1ea60..315014c1b248 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -528,8 +528,9 @@ public abstract class WindowManagerInternal { * Hide IME using imeTargetWindow when requested. * * @param imeTargetWindowToken token of the (IME target) window on which IME should be hidden. + * @param displayId the id of the display the IME is on. */ - public abstract void hideIme(IBinder imeTargetWindowToken); + public abstract void hideIme(IBinder imeTargetWindowToken, int displayId); /** * Tell window manager about a package that should not be running with high refresh rate diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index b1756b07ad83..0b50c1cd496a 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -90,6 +90,7 @@ import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_ADD_REMOVE; import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_BOOT; import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_FOCUS; import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT; +import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_IME; import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_KEEP_SCREEN_ON; import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_ORIENTATION; import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_SCREEN_ON; @@ -5606,17 +5607,28 @@ public class WindowManagerService extends IWindowManager.Stub } final DisplayContent displayContent = mRoot.getDisplayContent(mFrozenDisplayId); - final boolean waitingForConfig = displayContent != null && displayContent.mWaitingForConfig; - final int numOpeningApps = displayContent != null ? displayContent.mOpeningApps.size() : 0; - if (waitingForConfig || mAppsFreezingScreen > 0 + final int numOpeningApps; + final boolean waitingForConfig; + final boolean waitingForRemoteRotation; + if (displayContent != null) { + numOpeningApps = displayContent.mOpeningApps.size(); + waitingForConfig = displayContent.mWaitingForConfig; + waitingForRemoteRotation = + displayContent.getDisplayRotation().isWaitingForRemoteRotation(); + } else { + waitingForConfig = waitingForRemoteRotation = false; + numOpeningApps = 0; + } + if (waitingForConfig || waitingForRemoteRotation || mAppsFreezingScreen > 0 || mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_ACTIVE || mClientFreezingScreen || numOpeningApps > 0) { - ProtoLog.d(WM_DEBUG_ORIENTATION, - "stopFreezingDisplayLocked: Returning mWaitingForConfig=%b, " - + "mAppsFreezingScreen=%d, mWindowsFreezingScreen=%d, " - + "mClientFreezingScreen=%b, mOpeningApps.size()=%d", - waitingForConfig, mAppsFreezingScreen, mWindowsFreezingScreen, - mClientFreezingScreen, numOpeningApps); + ProtoLog.d(WM_DEBUG_ORIENTATION, "stopFreezingDisplayLocked: Returning " + + "waitingForConfig=%b, waitingForRemoteRotation=%b, " + + "mAppsFreezingScreen=%d, mWindowsFreezingScreen=%d, " + + "mClientFreezingScreen=%b, mOpeningApps.size()=%d", + waitingForConfig, waitingForRemoteRotation, + mAppsFreezingScreen, mWindowsFreezingScreen, + mClientFreezingScreen, numOpeningApps); return; } @@ -5627,7 +5639,6 @@ public class WindowManagerService extends IWindowManager.Stub // We must make a local copy of the displayId as it can be potentially overwritten later on // in this method. For example, {@link startFreezingDisplayLocked} may be called as a result // of update rotation, but we reference the frozen display after that call in this method. - final int displayId = mFrozenDisplayId; mFrozenDisplayId = INVALID_DISPLAY; mDisplayFrozen = false; mInputManagerCallback.thawInputDispatchingLw(); @@ -7606,24 +7617,26 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public void hideIme(IBinder imeTargetWindowToken) { + public void hideIme(IBinder imeTargetWindowToken, int displayId) { synchronized (mGlobalLock) { WindowState imeTarget = mWindowMap.get(imeTargetWindowToken); - if (imeTarget == null) { - // The target window no longer exists. - return; + ProtoLog.d(WM_DEBUG_IME, "hideIme target: %s ", imeTarget); + DisplayContent dc = mRoot.getDisplayContent(displayId); + if (imeTarget != null) { + imeTarget = imeTarget.getImeControlTarget().getWindow(); + if (imeTarget != null) { + dc = imeTarget.getDisplayContent(); + } + // If there was a pending IME show(), reset it as IME has been + // requested to be hidden. + dc.getInsetsStateController().getImeSourceProvider().abortShowImePostLayout(); } - imeTarget = imeTarget.getImeControlTarget().getWindow(); - final DisplayContent dc = imeTarget != null - ? imeTarget.getDisplayContent() : getDefaultDisplayContentLocked(); - // If there was a pending IME show(), reset it as IME has been - // requested to be hidden. - dc.getInsetsStateController().getImeSourceProvider().abortShowImePostLayout(); - if (dc.mInputMethodControlTarget == null) { - return; + if (dc != null && dc.mInputMethodControlTarget != null) { + ProtoLog.d(WM_DEBUG_IME, "hideIme Control target: %s ", + dc.mInputMethodControlTarget); + dc.mInputMethodControlTarget.hideInsets( + WindowInsets.Type.ime(), true /* fromIme */); } - dc.mInputMethodControlTarget.hideInsets( - WindowInsets.Type.ime(), true /* fromIme */); } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index f1acee5031d8..d4d2f4d7a492 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -81,6 +81,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_POINTER; import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION; import static android.view.WindowManager.LayoutParams.TYPE_PRIORITY_PHONE; import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION; +import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static android.view.WindowManager.LayoutParams.TYPE_SEARCH_BAR; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; @@ -2372,6 +2373,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return false; } + if (mAttrs.type == TYPE_SCREENSHOT) { + // Disallow screenshot windows from being IME targets + return false; + } + final boolean windowsAreFocusable = mActivityRecord == null || mActivityRecord.windowsAreFocusable(); if (!windowsAreFocusable) { // This window can't be an IME target if the app's windows should not be focusable. diff --git a/services/tests/servicestests/src/com/android/server/textclassifier/IconsContentProviderTest.java b/services/tests/servicestests/src/com/android/server/textclassifier/IconsContentProviderTest.java index 72580a3b98c2..a787c321fc66 100644 --- a/services/tests/servicestests/src/com/android/server/textclassifier/IconsContentProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/textclassifier/IconsContentProviderTest.java @@ -50,8 +50,7 @@ public final class IconsContentProviderTest { final Drawable actual = Icon.createWithContentUri(uri).loadDrawable(context); assertThat(actual).isNotNull(); - assertThat(IconsContentProvider.getBitmapData(actual)) - .isEqualTo(IconsContentProvider.getBitmapData(expected)); + assertThat(IconsContentProvider.sameIcon(actual, expected)).isTrue(); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 30af1d34f558..ddb186a1d2da 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1328,9 +1328,10 @@ public class DisplayContentTests extends WindowTestsBase { final DisplayRotation dr = dc.getDisplayRotation(); doCallRealMethod().when(dr).updateRotationUnchecked(anyBoolean()); - Mockito.doReturn(ROTATION_90).when(dr).rotationForOrientation(anyInt(), anyInt()); + // Rotate 180 degree so the display doesn't have configuration change. This condition is + // used for the later verification of stop-freezing (without setting mWaitingForConfig). + doReturn((dr.getRotation() + 2) % 4).when(dr).rotationForOrientation(anyInt(), anyInt()); final boolean[] continued = new boolean[1]; - // TODO(display-merge): Remove cast doAnswer( invocation -> { continued[0] = true; @@ -1356,9 +1357,16 @@ public class DisplayContentTests extends WindowTestsBase { dc.setRotationAnimation(null); mWm.updateRotation(true /* alwaysSendConfiguration */, false /* forceRelayout */); + // If remote rotation is not finished, the display should not be able to unfreeze. + mWm.stopFreezingDisplayLocked(); + assertTrue(mWm.mDisplayFrozen); + assertTrue(called[0]); waitUntilHandlersIdle(); assertTrue(continued[0]); + + mWm.stopFreezingDisplayLocked(); + assertFalse(mWm.mDisplayFrozen); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index 5005c07832ab..fd169018782b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -464,15 +464,19 @@ public class RecentTasksTest extends ActivityTestsBase { mRecentTasks.add(task1); final Task task2 = taskBuilder.apply(true /* visible */); mRecentTasks.add(task2); - // Only the last task is kept in recents and the previous 2 tasks will becomes untracked + final Task task3 = createTaskBuilder(className).build(); + mRecentTasks.add(task3); + // Only the last added task is kept in recents and the previous 2 tasks will become hidden // tasks because their intents are identical. - mRecentTasks.add(createTaskBuilder(className).build()); + mRecentTasks.add(task1); // Go home to trigger the removal of untracked tasks. mRecentTasks.add(createTaskBuilder(".Home").setStack(mTaskContainer.getRootHomeTask()) .build()); + // The task was added into recents again so it is not hidden and shouldn't be removed. + assertNotNull(task1.getTopNonFinishingActivity()); // All activities in the invisible task should be finishing or removed. - assertNull(task1.getTopNonFinishingActivity()); + assertNull(task3.getTopNonFinishingActivity()); // The visible task should not be affected. assertNotNull(task2.getTopNonFinishingActivity()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 1415c506a1c9..9d88ada5a90c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -28,6 +28,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.clearInvocations; import android.graphics.Point; @@ -158,4 +160,30 @@ public class TaskTests extends WindowTestsBase { assertEquals(activity1, task1.isInTask(activity1)); assertNull(task1.isInTask(activity2)); } + + @Test + public void testRemoveChildForOverlayTask() { + final Task task = createTaskStackOnDisplay(mDisplayContent); + final int taskId = task.mTaskId; + final ActivityRecord activity1 = + WindowTestUtils.createActivityRecordInTask(mDisplayContent, task); + final ActivityRecord activity2 = + WindowTestUtils.createActivityRecordInTask(mDisplayContent, task); + final ActivityRecord activity3 = + WindowTestUtils.createActivityRecordInTask(mDisplayContent, task); + activity1.setTaskOverlay(true); + activity2.setTaskOverlay(true); + activity3.setTaskOverlay(true); + + assertEquals(3, task.getChildCount()); + assertTrue(task.onlyHasTaskOverlayActivities(true)); + + task.removeChild(activity1); + + verify(task.mStackSupervisor).removeTask(any(), anyBoolean(), anyBoolean(), anyString()); + assertEquals(2, task.getChildCount()); + task.forAllActivities((r) -> { + assertTrue(r.finishing); + }); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 87485eac3412..efc03df877b7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -27,6 +27,7 @@ import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyFloat; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; @@ -825,6 +826,31 @@ public class WindowContainerTests extends WindowTestsBase { } @Test + public void testHandleCompleteDeferredRemoval() { + final DisplayContent displayContent = createNewDisplay(); + // Do not reparent activity to default display when removing the display. + doReturn(true).when(displayContent).shouldDestroyContentOnRemove(); + final ActivityRecord r = new ActivityTestsBase.StackBuilder(mWm.mRoot) + .setDisplay(displayContent).build().getTopMostActivity(); + // Add a window and make the activity animating so the removal of activity is deferred. + createWindow(null, TYPE_BASE_APPLICATION, r, "win"); + doReturn(true).when(r).isAnimating(anyInt(), anyInt()); + + displayContent.remove(); + // Ensure that ActivityRecord#onRemovedFromDisplay is called. + r.destroyed("test"); + // The removal is deferred, so the activity is still in the display. + assertEquals(r, displayContent.getTopMostActivity()); + + // Assume the animation is done so the deferred removal can continue. + doReturn(false).when(r).isAnimating(anyInt(), anyInt()); + + assertFalse(displayContent.handleCompleteDeferredRemoval()); + assertFalse(displayContent.hasChild()); + assertFalse(r.hasChild()); + } + + @Test public void testTaskCanApplyAnimation() { final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); final Task task = createTaskInStack(stack, 0 /* userId */); diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java index 42b0c608822e..314e95229d29 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java @@ -61,11 +61,12 @@ public class NetworkStagedRollbackTest { private static final TestApp NETWORK_STACK = new TestApp("NetworkStack", getNetworkStackPackageName(), -1, false, findNetworkStackApk()); - private static File findNetworkStackApk() { + private static File[] findNetworkStackApk() { for (String name : NETWORK_STACK_APK_NAMES) { final File apk = new File("/system/priv-app/" + name + "/" + name + ".apk"); if (apk.isFile()) { - return apk; + final File dir = new File("/system/priv-app/" + name); + return dir.listFiles((d, f) -> f.startsWith(name)); } } throw new RuntimeException("Can't find NetworkStackApk"); |