diff options
16 files changed, 403 insertions, 142 deletions
diff --git a/core/java/android/os/WorkSource.java b/core/java/android/os/WorkSource.java index e899f7729efa..65528e306943 100644 --- a/core/java/android/os/WorkSource.java +++ b/core/java/android/os/WorkSource.java @@ -128,7 +128,7 @@ public class WorkSource implements Parcelable { mNames = in.createStringArray(); int numChains = in.readInt(); - if (numChains > 0) { + if (numChains >= 0) { mChains = new ArrayList<>(numChains); in.readParcelableList(mChains, WorkChain.class.getClassLoader(), android.os.WorkSource.WorkChain.class); } else { diff --git a/media/java/android/media/session/ISession.aidl b/media/java/android/media/session/ISession.aidl index 9bf126b7a875..31fb8d03c4a0 100644 --- a/media/java/android/media/session/ISession.aidl +++ b/media/java/android/media/session/ISession.aidl @@ -35,7 +35,7 @@ interface ISession { ISessionController getController(); void setFlags(int flags); void setActive(boolean active); - void setMediaButtonReceiver(in PendingIntent mbr, String sessionPackageName); + void setMediaButtonReceiver(in PendingIntent mbr); void setMediaButtonBroadcastReceiver(in ComponentName broadcastReceiver); void setLaunchPendingIntent(in PendingIntent pi); void destroySession(); diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index bc00c404b11e..84ecc06d172f 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -286,7 +286,7 @@ public final class MediaSession { @Deprecated public void setMediaButtonReceiver(@Nullable PendingIntent mbr) { try { - mBinder.setMediaButtonReceiver(mbr, mContext.getPackageName()); + mBinder.setMediaButtonReceiver(mbr); } catch (RemoteException e) { Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e); } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index f526277a0a37..da602caac0dc 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -16,15 +16,21 @@ package com.android.systemui.clipboardoverlay; +import static android.content.ClipDescription.CLASSIFICATION_COMPLETE; + import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN; + +import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.SystemProperties; import android.provider.DeviceConfig; +import android.provider.Settings; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -51,6 +57,7 @@ public class ClipboardListener extends CoreStartable private final DeviceConfigProxy mDeviceConfig; private final ClipboardOverlayControllerFactory mOverlayFactory; + private final ClipboardToast mClipboardToast; private final ClipboardManager mClipboardManager; private final UiEventLogger mUiEventLogger; private ClipboardOverlayController mClipboardOverlayController; @@ -58,10 +65,11 @@ public class ClipboardListener extends CoreStartable @Inject public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy, ClipboardOverlayControllerFactory overlayFactory, ClipboardManager clipboardManager, - UiEventLogger uiEventLogger) { + ClipboardToast clipboardToast,UiEventLogger uiEventLogger) { super(context); mDeviceConfig = deviceConfigProxy; mOverlayFactory = overlayFactory; + mClipboardToast = clipboardToast; mClipboardManager = clipboardManager; mUiEventLogger = uiEventLogger; } @@ -88,6 +96,15 @@ public class ClipboardListener extends CoreStartable return; } + if (!isUserSetupComplete()) { + // just show a toast, user should not access intents from this state + if (shouldShowToast(clipData)) { + mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource); + mClipboardToast.showCopiedToast(); + } + return; + } + if (mClipboardOverlayController == null) { mClipboardOverlayController = mOverlayFactory.create(mContext); mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource); @@ -116,7 +133,23 @@ public class ClipboardListener extends CoreStartable return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false); } + boolean shouldShowToast(ClipData clipData) { + if (clipData == null) { + return false; + } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) { + // only show for classification complete if we aren't already showing a toast, to ignore + // the duplicate ClipData with classification + return !mClipboardToast.isShowing(); + } + return true; + } + private static boolean isEmulator() { return SystemProperties.getBoolean("ro.boot.qemu", false); } + + private boolean isUserSetupComplete() { + return Settings.Secure.getInt(mContext.getContentResolver(), + SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java index a0b2ab99e240..1e7dbe69b3e2 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java @@ -41,7 +41,9 @@ public enum ClipboardOverlayEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "clipboard overlay tapped outside") CLIPBOARD_OVERLAY_TAP_OUTSIDE(1077), @UiEvent(doc = "clipboard overlay dismissed, miscellaneous reason") - CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078); + CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078), + @UiEvent(doc = "clipboard toast shown") + CLIPBOARD_TOAST_SHOWN(1270); private final int mId; diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java new file mode 100644 index 000000000000..0ed7d2711c62 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 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.clipboardoverlay; + +import android.content.Context; +import android.widget.Toast; + +import com.android.systemui.R; + +import javax.inject.Inject; + +/** + * Utility class for showing a simple clipboard toast on copy. + */ +class ClipboardToast extends Toast.Callback { + private final Context mContext; + private Toast mCopiedToast; + + @Inject + ClipboardToast(Context context) { + mContext = context; + } + + void showCopiedToast() { + if (mCopiedToast != null) { + mCopiedToast.cancel(); + } + mCopiedToast = Toast.makeText(mContext, + R.string.clipboard_overlay_text_copied, Toast.LENGTH_SHORT); + mCopiedToast.show(); + } + + boolean isShowing() { + return mCopiedToast != null; + } + + @Override // Toast.Callback + public void onToastHidden() { + super.onToastHidden(); + mCopiedToast = null; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java index 91214a85ddd5..86402b3202d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java @@ -18,6 +18,8 @@ package com.android.systemui.clipboardoverlay; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; +import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -32,6 +34,7 @@ import android.content.ClipDescription; import android.content.ClipboardManager; import android.os.PersistableBundle; import android.provider.DeviceConfig; +import android.provider.Settings; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -59,6 +62,8 @@ public class ClipboardListenerTest extends SysuiTestCase { @Mock private ClipboardOverlayController mOverlayController; @Mock + private ClipboardToast mClipboardToast; + @Mock private UiEventLogger mUiEventLogger; private DeviceConfigProxyFake mDeviceConfigProxy; @@ -72,6 +77,7 @@ public class ClipboardListenerTest extends SysuiTestCase { @Captor private ArgumentCaptor<String> mStringCaptor; + private ClipboardListener mClipboardListener; @Before public void setup() { @@ -79,7 +85,8 @@ public class ClipboardListenerTest extends SysuiTestCase { when(mClipboardOverlayControllerFactory.create(any())).thenReturn( mOverlayController); when(mClipboardManager.hasPrimaryClip()).thenReturn(true); - + Settings.Secure.putInt( + mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 1); mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, new ClipData.Item("Test Item")); @@ -87,15 +94,17 @@ public class ClipboardListenerTest extends SysuiTestCase { when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource); mDeviceConfigProxy = new DeviceConfigProxyFake(); + + mClipboardListener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mClipboardOverlayControllerFactory, mClipboardManager,mClipboardToast, + mUiEventLogger); } @Test public void test_disabled() { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "false", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); - listener.start(); + mClipboardListener.start(); verifyZeroInteractions(mClipboardManager); verifyZeroInteractions(mUiEventLogger); } @@ -104,9 +113,7 @@ public class ClipboardListenerTest extends SysuiTestCase { public void test_enabled() { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); - listener.start(); + mClipboardListener.start(); verify(mClipboardManager).addPrimaryClipChangedListener(any()); verifyZeroInteractions(mUiEventLogger); } @@ -115,10 +122,8 @@ public class ClipboardListenerTest extends SysuiTestCase { public void test_consecutiveCopies() { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); - listener.start(); - listener.onPrimaryClipChanged(); + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerFactory).create(any()); @@ -132,14 +137,14 @@ public class ClipboardListenerTest extends SysuiTestCase { // Should clear the overlay controller mRunnableCaptor.getValue().run(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerFactory, times(2)).create(any()); // Not calling the runnable here, just change the clip again and verify that the overlay is // NOT recreated. - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerFactory, times(2)).create(any()); } @@ -169,4 +174,31 @@ public class ClipboardListenerTest extends SysuiTestCase { assertTrue(ClipboardListener.shouldSuppressOverlay(suppressableClipData, ClipboardListener.SHELL_PACKAGE, false)); } + + @Test + public void test_logging_enterAndReenter() { + mClipboardListener.start(); + + mClipboardListener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); + } + + @Test + public void test_userSetupIncomplete_showsToast() { + Settings.Secure.putInt( + mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0); + + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource); + verify(mClipboardToast, times(1)).showCopiedToast(); + verifyZeroInteractions(mClipboardOverlayControllerFactory); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java index c7c2cd8d7b4b..5e2f6a0f7a70 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java @@ -51,6 +51,8 @@ public class ClipboardOverlayEventTest extends SysuiTestCase { @Mock private ClipboardOverlayController mOverlayController; @Mock + private ClipboardToast mClipboardToast; + @Mock private UiEventLogger mUiEventLogger; private final String mSampleSource = "Example source"; @@ -75,7 +77,8 @@ public class ClipboardOverlayEventTest extends SysuiTestCase { "true", false); mClipboardListener = new ClipboardListener(getContext(), deviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mClipboardOverlayControllerFactory, mClipboardManager, mClipboardToast, + mUiEventLogger); } @Test diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 9ccf83996782..4044cceb606b 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -377,16 +377,11 @@ public final class PendingIntentRecord extends IIntentSender.Stub { resolvedType = key.requestResolvedType; } - // Apply any launch flags from the ActivityOptions. This is used only by SystemUI - // to ensure that we can launch the pending intent with a consistent launch mode even - // if the provided PendingIntent is immutable (ie. to force an activity to launch into - // a new task, or to launch multiple instances if supported by the app) + // Apply any launch flags from the ActivityOptions. This is to ensure that the caller + // can specify a consistent launch mode even if the PendingIntent is immutable final ActivityOptions opts = ActivityOptions.fromBundle(options); if (opts != null) { - // TODO(b/254490217): Move this check into SafeActivityOptions - if (controller.mAtmInternal.isCallerRecents(Binder.getCallingUid())) { - finalIntent.addFlags(opts.getPendingIntentLaunchFlags()); - } + finalIntent.addFlags(opts.getPendingIntentLaunchFlags()); } // Extract options before clearing calling identity diff --git a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java index 9a190316f4eb..c5fb5270f841 100644 --- a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java +++ b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java @@ -18,6 +18,7 @@ package com.android.server.media; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.BroadcastOptions; import android.app.PendingIntent; import android.content.ComponentName; @@ -37,6 +38,7 @@ import android.view.KeyEvent; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; import java.util.List; /** @@ -102,15 +104,19 @@ final class MediaButtonReceiverHolder { } /** - * Creates a new instance. + * Creates a new instance from a {@link PendingIntent}. + * + * <p>This method assumes the session package name has been validated and effectively belongs to + * the media session's owner. * - * @param context context * @param userId userId - * @param pendingIntent pending intent - * @return Can be {@code null} if pending intent was null. + * @param pendingIntent pending intent that will receive media button events + * @param sessionPackageName package name of media session owner + * @return {@link MediaButtonReceiverHolder} instance or {@code null} if pending intent was + * null. */ - public static MediaButtonReceiverHolder create(Context context, int userId, - PendingIntent pendingIntent, String sessionPackageName) { + public static MediaButtonReceiverHolder create( + int userId, @Nullable PendingIntent pendingIntent, String sessionPackageName) { if (pendingIntent == null) { return null; } @@ -312,7 +318,7 @@ final class MediaButtonReceiverHolder { } private static ComponentName getComponentName(PendingIntent pendingIntent, int componentType) { - List<ResolveInfo> resolveInfos = null; + List<ResolveInfo> resolveInfos = Collections.emptyList(); switch (componentType) { case COMPONENT_TYPE_ACTIVITY: resolveInfos = pendingIntent.queryIntentComponents( @@ -330,32 +336,37 @@ final class MediaButtonReceiverHolder { PACKAGE_MANAGER_COMMON_FLAGS | PackageManager.GET_RECEIVERS); break; } - if (resolveInfos != null && !resolveInfos.isEmpty()) { - return createComponentName(resolveInfos.get(0)); + + for (ResolveInfo resolveInfo : resolveInfos) { + ComponentInfo componentInfo = getComponentInfo(resolveInfo); + if (componentInfo != null && TextUtils.equals(componentInfo.packageName, + pendingIntent.getCreatorPackage()) + && componentInfo.packageName != null && componentInfo.name != null) { + return new ComponentName(componentInfo.packageName, componentInfo.name); + } } + return null; } - private static ComponentName createComponentName(ResolveInfo resolveInfo) { - if (resolveInfo == null) { - return null; - } - ComponentInfo componentInfo; + /** + * Retrieves the {@link ComponentInfo} from a {@link ResolveInfo} instance. Similar to {@link + * ResolveInfo#getComponentInfo()}, but returns {@code null} if this {@link ResolveInfo} points + * to a content provider. + * + * @param resolveInfo Where to extract the {@link ComponentInfo} from. + * @return Either a non-null {@link ResolveInfo#activityInfo} or {@link + * ResolveInfo#serviceInfo}. Otherwise {@code null} if {@link ResolveInfo#providerInfo} is + * not {@code null}. + */ + private static ComponentInfo getComponentInfo(@NonNull ResolveInfo resolveInfo) { // Code borrowed from ResolveInfo#getComponentInfo(). if (resolveInfo.activityInfo != null) { - componentInfo = resolveInfo.activityInfo; + return resolveInfo.activityInfo; } else if (resolveInfo.serviceInfo != null) { - componentInfo = resolveInfo.serviceInfo; + return resolveInfo.serviceInfo; } else { - // We're not interested in content provider. - return null; - } - // Code borrowed from ComponentInfo#getComponentName(). - try { - return new ComponentName(componentInfo.packageName, componentInfo.name); - } catch (IllegalArgumentException | NullPointerException e) { - // This may be happen if resolveActivity() end up with matching multiple activities. - // see PackageManager#resolveActivity(). + // We're not interested in content providers. return null; } } diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 604e8f3949f4..1bd50632ccbf 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -52,6 +52,8 @@ import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SystemClock; +import android.text.TextUtils; +import android.util.EventLog; import android.util.Log; import android.view.KeyEvent; @@ -936,8 +938,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR } @Override - public void setMediaButtonReceiver(PendingIntent pi, String sessionPackageName) - throws RemoteException { + public void setMediaButtonReceiver(PendingIntent pi) throws RemoteException { final long token = Binder.clearCallingIdentity(); try { if ((mPolicies & MediaSessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER) @@ -945,7 +946,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR return; } mMediaButtonReceiverHolder = - MediaButtonReceiverHolder.create(mContext, mUserId, pi, sessionPackageName); + MediaButtonReceiverHolder.create(mUserId, pi, mPackageName); mService.onMediaButtonReceiverChanged(MediaSessionRecord.this); } finally { Binder.restoreCallingIdentity(token); @@ -956,6 +957,14 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR public void setMediaButtonBroadcastReceiver(ComponentName receiver) throws RemoteException { final long token = Binder.clearCallingIdentity(); try { + //mPackageName has been verified in MediaSessionService.enforcePackageName(). + if (receiver != null && !TextUtils.equals( + mPackageName, receiver.getPackageName())) { + EventLog.writeEvent(0x534e4554, "238177121", -1, ""); // SafetyNet logging. + throw new IllegalArgumentException("receiver does not belong to " + + "package name provided to MediaSessionRecord. Pkg = " + mPackageName + + ", Receiver Pkg = " + receiver.getPackageName()); + } if ((mPolicies & MediaSessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER) != 0) { return; diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index b89147ea181a..d08150cf0a56 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -2285,9 +2285,9 @@ public class MediaSessionService extends SystemService implements Monitor { PendingIntent pi = mCustomMediaKeyDispatcher.getMediaButtonReceiver(keyEvent, uid, asSystemService); if (pi != null) { - mediaButtonReceiverHolder = MediaButtonReceiverHolder.create(mContext, - mCurrentFullUserRecord.mFullUserId, pi, - /* sessionPackageName= */ ""); + mediaButtonReceiverHolder = + MediaButtonReceiverHolder.create( + mCurrentFullUserRecord.mFullUserId, pi, ""); } } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index ea6a83e7d4cd..e355e2076514 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1952,34 +1952,39 @@ public class NotificationManagerService extends SystemService { return (haystack & needle) != 0; } - public boolean isInLockDownMode() { - return mIsInLockDownMode; + // Return whether the user is in lockdown mode. + // If the flag is not set, we assume the user is not in lockdown. + public boolean isInLockDownMode(int userId) { + return mUserInLockDownMode.get(userId, false); } @Override public synchronized void onStrongAuthRequiredChanged(int userId) { boolean userInLockDownModeNext = containsFlag(getStrongAuthForUser(userId), STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - mUserInLockDownMode.put(userId, userInLockDownModeNext); - boolean isInLockDownModeNext = mUserInLockDownMode.indexOfValue(true) != -1; - if (mIsInLockDownMode == isInLockDownModeNext) { + // Nothing happens if the lockdown mode of userId keeps the same. + if (userInLockDownModeNext == isInLockDownMode(userId)) { return; } - if (isInLockDownModeNext) { - cancelNotificationsWhenEnterLockDownMode(); + // When the lockdown mode is changed, we perform the following steps. + // If the userInLockDownModeNext is true, all the function calls to + // notifyPostedLocked and notifyRemovedLocked will not be executed. + // The cancelNotificationsWhenEnterLockDownMode calls notifyRemovedLocked + // and postNotificationsWhenExitLockDownMode calls notifyPostedLocked. + // So we shall call cancelNotificationsWhenEnterLockDownMode before + // we set mUserInLockDownMode as true. + // On the other hand, if the userInLockDownModeNext is false, we shall call + // postNotificationsWhenExitLockDownMode after we put false into mUserInLockDownMode + if (userInLockDownModeNext) { + cancelNotificationsWhenEnterLockDownMode(userId); } - // When the mIsInLockDownMode is true, both notifyPostedLocked and - // notifyRemovedLocked will be dismissed. So we shall call - // cancelNotificationsWhenEnterLockDownMode before we set mIsInLockDownMode - // as true and call postNotificationsWhenExitLockDownMode after we set - // mIsInLockDownMode as false. - mIsInLockDownMode = isInLockDownModeNext; + mUserInLockDownMode.put(userId, userInLockDownModeNext); - if (!isInLockDownModeNext) { - postNotificationsWhenExitLockDownMode(); + if (!userInLockDownModeNext) { + postNotificationsWhenExitLockDownMode(userId); } } } @@ -9625,11 +9630,14 @@ public class NotificationManagerService extends SystemService { } } - private void cancelNotificationsWhenEnterLockDownMode() { + private void cancelNotificationsWhenEnterLockDownMode(int userId) { synchronized (mNotificationLock) { int numNotifications = mNotificationList.size(); for (int i = 0; i < numNotifications; i++) { NotificationRecord rec = mNotificationList.get(i); + if (rec.getUser().getIdentifier() != userId) { + continue; + } mListeners.notifyRemovedLocked(rec, REASON_CANCEL_ALL, rec.getStats()); } @@ -9637,14 +9645,23 @@ public class NotificationManagerService extends SystemService { } } - private void postNotificationsWhenExitLockDownMode() { + private void postNotificationsWhenExitLockDownMode(int userId) { synchronized (mNotificationLock) { int numNotifications = mNotificationList.size(); + // Set the delay to spread out the burst of notifications. + long delay = 0; for (int i = 0; i < numNotifications; i++) { NotificationRecord rec = mNotificationList.get(i); - mListeners.notifyPostedLocked(rec, rec); + if (rec.getUser().getIdentifier() != userId) { + continue; + } + mHandler.postDelayed(() -> { + synchronized (mNotificationLock) { + mListeners.notifyPostedLocked(rec, rec); + } + }, delay); + delay += 20; } - } } @@ -9823,12 +9840,15 @@ public class NotificationManagerService extends SystemService { * notifications visible to the given listener. */ @GuardedBy("mNotificationLock") - private NotificationRankingUpdate makeRankingUpdateLocked(ManagedServiceInfo info) { + NotificationRankingUpdate makeRankingUpdateLocked(ManagedServiceInfo info) { final int N = mNotificationList.size(); final ArrayList<NotificationListenerService.Ranking> rankings = new ArrayList<>(); for (int i = 0; i < N; i++) { NotificationRecord record = mNotificationList.get(i); + if (isInLockDownMode(record.getUser().getIdentifier())) { + continue; + } if (!isVisibleToListener(record.getSbn(), record.getNotificationType(), info)) { continue; } @@ -9870,8 +9890,8 @@ public class NotificationManagerService extends SystemService { rankings.toArray(new NotificationListenerService.Ranking[0])); } - boolean isInLockDownMode() { - return mStrongAuthTracker.isInLockDownMode(); + boolean isInLockDownMode(int userId) { + return mStrongAuthTracker.isInLockDownMode(userId); } boolean hasCompanionDevice(ManagedServiceInfo info) { @@ -10927,7 +10947,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") void notifyPostedLocked(NotificationRecord r, NotificationRecord old, boolean notifyAllListeners) { - if (isInLockDownMode()) { + if (isInLockDownMode(r.getUser().getIdentifier())) { return; } @@ -11028,7 +11048,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") public void notifyRemovedLocked(NotificationRecord r, int reason, NotificationStats notificationStats) { - if (isInLockDownMode()) { + if (isInLockDownMode(r.getUser().getIdentifier())) { return; } @@ -11077,10 +11097,6 @@ public class NotificationManagerService extends SystemService { */ @GuardedBy("mNotificationLock") public void notifyRankingUpdateLocked(List<NotificationRecord> changedHiddenNotifications) { - if (isInLockDownMode()) { - return; - } - boolean isHiddenRankingUpdate = changedHiddenNotifications != null && changedHiddenNotifications.size() > 0; // TODO (b/73052211): if the ranking update changed the notification type, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java index 76d4059eb436..0f6606f74ae9 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java @@ -398,63 +398,112 @@ public class NotificationListenersTest extends UiServiceTestCase { @Test public void testNotifyPostedLockedInLockdownMode() { - NotificationRecord r = mock(NotificationRecord.class); - NotificationRecord old = mock(NotificationRecord.class); - - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - mListeners.notifyPostedLocked(r, old, true); - mListeners.notifyPostedLocked(r, old, false); - verify(r, atLeast(2)).getSbn(); - - // in the lockdown mode - reset(r); - reset(old); - when(mNm.isInLockDownMode()).thenReturn(true); - mListeners.notifyPostedLocked(r, old, true); - mListeners.notifyPostedLocked(r, old, false); - verify(r, never()).getSbn(); - } - - @Test - public void testnotifyRankingUpdateLockedInLockdownMode() { - List chn = mock(List.class); - - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - mListeners.notifyRankingUpdateLocked(chn); - verify(chn, atLeast(1)).size(); - - // in the lockdown mode - reset(chn); - when(mNm.isInLockDownMode()).thenReturn(true); - mListeners.notifyRankingUpdateLocked(chn); - verify(chn, never()).size(); + NotificationRecord r0 = mock(NotificationRecord.class); + NotificationRecord old0 = mock(NotificationRecord.class); + UserHandle uh0 = mock(UserHandle.class); + + NotificationRecord r1 = mock(NotificationRecord.class); + NotificationRecord old1 = mock(NotificationRecord.class); + UserHandle uh1 = mock(UserHandle.class); + + // Neither user0 and user1 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(false); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + + mListeners.notifyPostedLocked(r0, old0, true); + mListeners.notifyPostedLocked(r0, old0, false); + verify(r0, atLeast(2)).getSbn(); + + mListeners.notifyPostedLocked(r1, old1, true); + mListeners.notifyPostedLocked(r1, old1, false); + verify(r1, atLeast(2)).getSbn(); + + // Reset + reset(r0); + reset(old0); + reset(r1); + reset(old1); + + // Only user 0 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(true); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + + mListeners.notifyPostedLocked(r0, old0, true); + mListeners.notifyPostedLocked(r0, old0, false); + verify(r0, never()).getSbn(); + + mListeners.notifyPostedLocked(r1, old1, true); + mListeners.notifyPostedLocked(r1, old1, false); + verify(r1, atLeast(2)).getSbn(); } @Test public void testNotifyRemovedLockedInLockdownMode() throws NoSuchFieldException { - NotificationRecord r = mock(NotificationRecord.class); - NotificationStats rs = mock(NotificationStats.class); + NotificationRecord r0 = mock(NotificationRecord.class); + NotificationStats rs0 = mock(NotificationStats.class); + UserHandle uh0 = mock(UserHandle.class); + + NotificationRecord r1 = mock(NotificationRecord.class); + NotificationStats rs1 = mock(NotificationStats.class); + UserHandle uh1 = mock(UserHandle.class); + StatusBarNotification sbn = mock(StatusBarNotification.class); FieldSetter.setField(mNm, NotificationManagerService.class.getDeclaredField("mHandler"), mock(NotificationManagerService.WorkerHandler.class)); - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - when(r.getSbn()).thenReturn(sbn); - mListeners.notifyRemovedLocked(r, 0, rs); - mListeners.notifyRemovedLocked(r, 0, rs); - verify(r, atLeast(2)).getSbn(); - - // in the lockdown mode - reset(r); - reset(rs); - when(mNm.isInLockDownMode()).thenReturn(true); - when(r.getSbn()).thenReturn(sbn); - mListeners.notifyRemovedLocked(r, 0, rs); - mListeners.notifyRemovedLocked(r, 0, rs); - verify(r, never()).getSbn(); + // Neither user0 and user1 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(false); + when(r0.getSbn()).thenReturn(sbn); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + when(r1.getSbn()).thenReturn(sbn); + + mListeners.notifyRemovedLocked(r0, 0, rs0); + mListeners.notifyRemovedLocked(r0, 0, rs0); + verify(r0, atLeast(2)).getSbn(); + + mListeners.notifyRemovedLocked(r1, 0, rs1); + mListeners.notifyRemovedLocked(r1, 0, rs1); + verify(r1, atLeast(2)).getSbn(); + + // Reset + reset(r0); + reset(rs0); + reset(r1); + reset(rs1); + + // Only user 0 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(true); + when(r0.getSbn()).thenReturn(sbn); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + when(r1.getSbn()).thenReturn(sbn); + + mListeners.notifyRemovedLocked(r0, 0, rs0); + mListeners.notifyRemovedLocked(r0, 0, rs0); + verify(r0, never()).getSbn(); + + mListeners.notifyRemovedLocked(r1, 0, rs1); + mListeners.notifyRemovedLocked(r1, 0, rs1); + verify(r1, atLeast(2)).getSbn(); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 60f94f3ec32c..0c190b8556c0 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -170,6 +170,7 @@ import android.service.notification.Adjustment; import android.service.notification.ConversationChannelWrapper; import android.service.notification.NotificationListenerFilter; import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationRankingUpdate; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.service.notification.ZenPolicy; @@ -9680,10 +9681,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mStrongAuthTracker.setGetStrongAuthForUserReturnValue( STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertTrue(mStrongAuthTracker.isInLockDownMode()); - mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(mContext.getUserId())); + mStrongAuthTracker.setGetStrongAuthForUserReturnValue(mContext.getUserId()); mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertFalse(mStrongAuthTracker.isInLockDownMode()); + assertFalse(mStrongAuthTracker.isInLockDownMode(mContext.getUserId())); } @Test @@ -9699,8 +9700,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // when entering the lockdown mode, cancel the 2 notifications. mStrongAuthTracker.setGetStrongAuthForUserReturnValue( STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertTrue(mStrongAuthTracker.isInLockDownMode()); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(0)); // the notifyRemovedLocked function is called twice due to REASON_CANCEL_ALL. ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class); @@ -9709,10 +9710,46 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // exit lockdown mode. mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); - mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertFalse(mStrongAuthTracker.isInLockDownMode(0)); // the notifyPostedLocked function is called twice. - verify(mListeners, times(2)).notifyPostedLocked(any(), any()); + verify(mWorkerHandler, times(2)).postDelayed(any(Runnable.class), anyLong()); + //verify(mListeners, times(2)).notifyPostedLocked(any(), any()); + } + + @Test + public void testMakeRankingUpdateLockedInLockDownMode() { + // post 2 notifications from a same package + NotificationRecord pkgA = new NotificationRecord(mContext, + generateSbn("a", 1000, 9, 0), mTestNotificationChannel); + mService.addNotification(pkgA); + NotificationRecord pkgB = new NotificationRecord(mContext, + generateSbn("a", 1000, 9, 1), mTestNotificationChannel); + mService.addNotification(pkgB); + + mService.setIsVisibleToListenerReturnValue(true); + NotificationRankingUpdate nru = mService.makeRankingUpdateLocked(null); + assertEquals(2, nru.getRankingMap().getOrderedKeys().length); + + // when only user 0 entering the lockdown mode, its notification will be suppressed. + mStrongAuthTracker.setGetStrongAuthForUserReturnValue( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(0)); + assertFalse(mStrongAuthTracker.isInLockDownMode(1)); + + nru = mService.makeRankingUpdateLocked(null); + assertEquals(1, nru.getRankingMap().getOrderedKeys().length); + + // User 0 exits lockdown mode. Its notification will be resumed. + mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertFalse(mStrongAuthTracker.isInLockDownMode(0)); + assertFalse(mStrongAuthTracker.isInLockDownMode(1)); + + nru = mService.makeRankingUpdateLocked(null); + assertEquals(2, nru.getRankingMap().getOrderedKeys().length); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java index 21ca37f4d238..61a6985d473e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java @@ -19,10 +19,12 @@ package com.android.server.notification; import android.companion.ICompanionDeviceManager; import android.content.ComponentName; import android.content.Context; +import android.service.notification.StatusBarNotification; import androidx.annotation.Nullable; import com.android.internal.logging.InstanceIdSequence; +import com.android.server.notification.ManagedServices.ManagedServiceInfo; import java.util.HashSet; import java.util.Set; @@ -38,6 +40,9 @@ public class TestableNotificationManagerService extends NotificationManagerServi @Nullable NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback; + @Nullable + Boolean mIsVisibleToListenerReturnValue = null; + TestableNotificationManagerService(Context context, NotificationRecordLogger logger, InstanceIdSequence notificationInstanceIdSequence) { super(context, logger, notificationInstanceIdSequence); @@ -126,6 +131,19 @@ public class TestableNotificationManagerService extends NotificationManagerServi mShowReviewPermissionsNotification = setting; } + protected void setIsVisibleToListenerReturnValue(boolean value) { + mIsVisibleToListenerReturnValue = value; + } + + @Override + boolean isVisibleToListener(StatusBarNotification sbn, int notificationType, + ManagedServiceInfo listener) { + if (mIsVisibleToListenerReturnValue != null) { + return mIsVisibleToListenerReturnValue; + } + return super.isVisibleToListener(sbn, notificationType, listener); + } + public class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker { private int mGetStrongAuthForUserReturnValue = 0; StrongAuthTrackerFake(Context context) { |