diff options
15 files changed, 548 insertions, 28 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 6be7052caa49..395cfe1f7b77 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1831,6 +1831,10 @@ public class Notification implements Parcelable } } + private void visitUris(@NonNull Consumer<Uri> visitor) { + visitIconUri(visitor, getIcon()); + } + @Override public Action clone() { return new Action( @@ -2508,7 +2512,7 @@ public class Notification implements Parcelable if (actions != null) { for (Action action : actions) { - visitIconUri(visitor, action.getIcon()); + action.visitUris(visitor); } } @@ -2534,8 +2538,8 @@ public class Notification implements Parcelable visitor.accept(person.getIconUri()); } - final RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) - extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS); + final RemoteInputHistoryItem[] history = getParcelableArrayFromBundle(extras, + Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, RemoteInputHistoryItem.class); if (history != null) { for (int i = 0; i < history.length; i++) { RemoteInputHistoryItem item = history[i]; @@ -2579,6 +2583,11 @@ public class Notification implements Parcelable if (mBubbleMetadata != null) { visitIconUri(visitor, mBubbleMetadata.getIcon()); } + + if (extras != null && extras.containsKey(WearableExtender.EXTRA_WEARABLE_EXTENSIONS)) { + WearableExtender extender = new WearableExtender(this); + extender.visitUris(visitor); + } } /** @@ -10289,6 +10298,12 @@ public class Notification implements Parcelable mFlags &= ~mask; } } + + private void visitUris(@NonNull Consumer<Uri> visitor) { + for (Action action : mActions) { + action.visitUris(visitor); + } + } } /** diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 553cc91fdd5f..f9e19b5fa7de 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -465,6 +465,12 @@ public class NotificationManager { */ public static final int BUBBLE_PREFERENCE_SELECTED = 2; + /** + * Maximum length of the component name of a registered NotificationListenerService. + * @hide + */ + public static int MAX_SERVICE_COMPONENT_NAME_LENGTH = 500; + @UnsupportedAppUsage private static INotificationManager sService; diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp index 9f4e3e516ada..81ada6ad535c 100644 --- a/core/jni/android_view_InputDevice.cpp +++ b/core/jni/android_view_InputDevice.cpp @@ -14,6 +14,7 @@ * limitations under the License. */ +#include <binder/Parcel.h> #include <input/Input.h> #include <android_runtime/AndroidRuntime.h> @@ -48,9 +49,16 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi return NULL; } + sp<KeyCharacterMap> map = deviceInfo.getKeyCharacterMap(); + if (map != nullptr) { + Parcel parcel; + map->writeToParcel(&parcel); + map = map->readFromParcel(&parcel); + } + ScopedLocalRef<jobject> kcmObj(env, - android_view_KeyCharacterMap_create(env, deviceInfo.getId(), - deviceInfo.getKeyCharacterMap())); + android_view_KeyCharacterMap_create(env, deviceInfo.getId(), + map)); if (!kcmObj.get()) { return NULL; } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java new file mode 100644 index 000000000000..367b8e642411 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java @@ -0,0 +1,49 @@ +/* + * 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.content.ComponentName; +import android.content.Context; +import android.media.browse.MediaBrowser; +import android.os.Bundle; + +import javax.inject.Inject; + +/** + * Testable wrapper around {@link MediaBrowser} constructor + */ +public class MediaBrowserFactory { + private final Context mContext; + + @Inject + public MediaBrowserFactory(Context context) { + mContext = context; + } + + /** + * Creates a new MediaBrowser + * + * @param serviceComponent + * @param callback + * @param rootHints + * @return + */ + public MediaBrowser create(ComponentName serviceComponent, + MediaBrowser.ConnectionCallback callback, Bundle rootHints) { + return new MediaBrowser(mContext, serviceComponent, callback, rootHints); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index 4ec746fcb153..3fb7c7339a38 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -28,6 +28,7 @@ import android.os.UserHandle import android.provider.Settings import android.service.media.MediaBrowserService import android.util.Log +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.tuner.TunerService @@ -47,7 +48,8 @@ class MediaResumeListener @Inject constructor( private val context: Context, private val broadcastDispatcher: BroadcastDispatcher, @Background private val backgroundExecutor: Executor, - private val tunerService: TunerService + private val tunerService: TunerService, + private val mediaBrowserFactory: ResumeMediaBrowserFactory ) : MediaDataManager.Listener { private var useMediaResumption: Boolean = Utils.useMediaResumption(context) @@ -58,7 +60,8 @@ class MediaResumeListener @Inject constructor( private var mediaBrowser: ResumeMediaBrowser? = null private var currentUserId: Int = context.userId - private val userChangeReceiver = object : BroadcastReceiver() { + @VisibleForTesting + val userChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_USER_UNLOCKED == intent.action) { loadMediaResumptionControls() @@ -87,9 +90,16 @@ class MediaResumeListener @Inject constructor( Log.e(TAG, "Error getting package information", e) } - Log.d(TAG, "Adding resume controls $desc") - mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token, - appName.toString(), appIntent, component.packageName) + Log.d(TAG, "Adding resume controls for ${browser.userId}: $desc") + mediaDataManager.addResumptionControls( + browser.userId, + desc, + resumeAction, + token, + appName.toString(), + appIntent, + component.packageName + ) } } @@ -132,7 +142,11 @@ class MediaResumeListener @Inject constructor( val component = ComponentName(packageName, className) resumeComponents.add(component) } - Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}") + Log.d( + TAG, + "loaded resume components for $currentUserId: " + + "${resumeComponents.toArray().contentToString()}" + ) } /** @@ -143,9 +157,19 @@ class MediaResumeListener @Inject constructor( return } + val pm = context.packageManager resumeComponents.forEach { - val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it) - browser.findRecentMedia() + // Verify that the service exists for this user + val intent = Intent(MediaBrowserService.SERVICE_INTERFACE) + intent.component = it + val inf = pm.resolveServiceAsUser(intent, 0, currentUserId) + if (inf != null) { + val browser = + mediaBrowserFactory.create(mediaBrowserCallback, it, currentUserId) + browser.findRecentMedia() + } else { + Log.d(TAG, "User $currentUserId does not have component $it") + } } } @@ -159,7 +183,7 @@ class MediaResumeListener @Inject constructor( Log.d(TAG, "Checking for service component for " + data.packageName) val pm = context.packageManager val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) - val resumeInfo = pm.queryIntentServices(serviceIntent, 0) + val resumeInfo = pm.queryIntentServicesAsUser(serviceIntent, 0, currentUserId) val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName @@ -183,7 +207,7 @@ class MediaResumeListener @Inject constructor( private fun tryUpdateResumptionList(key: String, componentName: ComponentName) { Log.d(TAG, "Testing if we can connect to $componentName") mediaBrowser?.disconnect() - mediaBrowser = ResumeMediaBrowser(context, + mediaBrowser = mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { override fun onConnected() { Log.d(TAG, "yes we can resume with $componentName") @@ -200,7 +224,9 @@ class MediaResumeListener @Inject constructor( mediaBrowser = null } }, - componentName) + componentName, + currentUserId + ) mediaBrowser?.testConnection() } @@ -235,7 +261,7 @@ class MediaResumeListener @Inject constructor( private fun getResumeAction(componentName: ComponentName): Runnable { return Runnable { mediaBrowser?.disconnect() - mediaBrowser = ResumeMediaBrowser(context, + mediaBrowser = mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { override fun onConnected() { if (mediaBrowser?.token == null) { @@ -257,7 +283,8 @@ class MediaResumeListener @Inject constructor( mediaBrowser = null } }, - componentName) + componentName, + currentUserId) mediaBrowser?.restart() } } diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java index 68b6785849aa..b0997df6b8af 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java @@ -16,6 +16,7 @@ package com.android.systemui.media; +import android.annotation.UserIdInt; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -46,6 +47,9 @@ public class ResumeMediaBrowser { private static final String TAG = "ResumeMediaBrowser"; private final Context mContext; private final Callback mCallback; + private MediaBrowserFactory mBrowserFactory; + @UserIdInt private final int mUserId; + private MediaBrowser mMediaBrowser; private ComponentName mComponentName; @@ -54,11 +58,15 @@ public class ResumeMediaBrowser { * @param context the context * @param callback used to report media items found * @param componentName Component name of the MediaBrowserService this browser will connect to + * @param userId ID of the current user */ - public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) { + public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName, + MediaBrowserFactory browserFactory, @UserIdInt int userId) { mContext = context; mCallback = callback; mComponentName = componentName; + mBrowserFactory = browserFactory; + mUserId = userId; } /** @@ -74,7 +82,7 @@ public class ResumeMediaBrowser { disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, + mMediaBrowser = mBrowserFactory.create( mComponentName, mConnectionCallback, rootHints); @@ -182,7 +190,7 @@ public class ResumeMediaBrowser { disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, mComponentName, + mMediaBrowser = mBrowserFactory.create(mComponentName, new MediaBrowser.ConnectionCallback() { @Override public void onConnected() { @@ -213,6 +221,14 @@ public class ResumeMediaBrowser { } /** + * Get the ID of the user associated with this broswer + * @return the user ID + */ + public @UserIdInt int getUserId() { + return mUserId; + } + + /** * Get the media session token * @return the token, or null if the MediaBrowser is null or disconnected */ @@ -268,7 +284,7 @@ public class ResumeMediaBrowser { }; Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, + mMediaBrowser = mBrowserFactory.create( mComponentName, connectionCallback, rootHints); diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java new file mode 100644 index 000000000000..5744190cfe63 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java @@ -0,0 +1,51 @@ +/* + * 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.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Context; + +import javax.inject.Inject; + +/** + * Testable wrapper around {@link ResumeMediaBrowser} constructor + */ +public class ResumeMediaBrowserFactory { + private final Context mContext; + private final MediaBrowserFactory mBrowserFactory; + + @Inject + public ResumeMediaBrowserFactory(Context context, MediaBrowserFactory browserFactory) { + mContext = context; + mBrowserFactory = browserFactory; + } + + /** + * Creates a new ResumeMediaBrowser. + * + * @param callback will be called on connection or error, and addTrack when media item found + * @param componentName component to browse + * @param userId ID of the current user + * @return + */ + public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback, + ComponentName componentName, @UserIdInt int userId) { + return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory, + userId); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt new file mode 100644 index 000000000000..0801ce90b23e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt @@ -0,0 +1,232 @@ +/* + * 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.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.graphics.Color +import android.media.MediaDescription +import android.media.session.MediaSession +import android.provider.Settings +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.tuner.TunerService +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +private const val KEY = "TEST_KEY" +private const val OLD_KEY = "RESUME_KEY" +private const val APP = "APP" +private const val BG_COLOR = Color.RED +private const val PACKAGE_NAME = "PKG" +private const val CLASS_NAME = "CLASS" +private const val ARTIST = "ARTIST" +private const val TITLE = "TITLE" +private const val USER_ID = 0 +private const val MEDIA_PREFERENCES = "media_control_prefs" +private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3" + +private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() +private fun <T> eq(value: T): T = Mockito.eq(value) ?: value +private fun <T> any(): T = Mockito.any<T>() + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class MediaResumeListenerTest : SysuiTestCase() { + + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var mediaDataManager: MediaDataManager + @Mock private lateinit var device: MediaDeviceData + @Mock private lateinit var token: MediaSession.Token + @Mock private lateinit var tunerService: TunerService + @Mock private lateinit var resumeBrowserFactory: ResumeMediaBrowserFactory + @Mock private lateinit var resumeBrowser: ResumeMediaBrowser + @Mock private lateinit var sharedPrefs: SharedPreferences + @Mock private lateinit var sharedPrefsEditor: SharedPreferences.Editor + @Mock private lateinit var mockContext: Context + @Mock private lateinit var pendingIntent: PendingIntent + + @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback> + @Captor lateinit var userIdCaptor: ArgumentCaptor<Int> + + private lateinit var executor: FakeExecutor + private lateinit var data: MediaData + private lateinit var resumeListener: MediaResumeListener + + private var originalQsSetting = Settings.Global.getInt(context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) + private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RESUME, 0) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + Settings.Global.putInt(context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) + Settings.Secure.putInt(context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RESUME, 1) + + whenever(resumeBrowserFactory.create(capture(callbackCaptor), any(), capture(userIdCaptor))) + .thenReturn(resumeBrowser) + + // resume components are stored in sharedpreferences + whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt())) + .thenReturn(sharedPrefs) + whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS) + whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor) + whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor) + whenever(mockContext.packageManager).thenReturn(context.packageManager) + whenever(mockContext.contentResolver).thenReturn(context.contentResolver) + whenever(mockContext.userId).thenReturn(context.userId) + + executor = FakeExecutor(FakeSystemClock()) + resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, + tunerService, resumeBrowserFactory) + resumeListener.setManager(mediaDataManager) + mediaDataManager.addListener(resumeListener) + + data = MediaData( + userId = USER_ID, + initialized = true, + backgroundColor = BG_COLOR, + app = APP, + appIcon = null, + artist = ARTIST, + song = TITLE, + artwork = null, + actions = emptyList(), + actionsToShowInCompact = emptyList(), + packageName = PACKAGE_NAME, + token = token, + clickIntent = null, + device = device, + active = true, + notificationKey = KEY, + resumeAction = null) + } + + @After + fun tearDown() { + Settings.Global.putInt(context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting) + Settings.Secure.putInt(context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting) + } + + @Test + fun testUserUnlocked_userChangeWhileQuerying() { + val firstUserId = context.userId + val secondUserId = firstUserId + 1 + val description = MediaDescription.Builder().setTitle(TITLE).build() + val component = ComponentName(PACKAGE_NAME, CLASS_NAME) + + setUpMbsWithValidResolveInfo() + whenever(resumeBrowser.token).thenReturn(token) + whenever(resumeBrowser.appIntent).thenReturn(pendingIntent) + + val unlockIntent = + Intent(Intent.ACTION_USER_UNLOCKED).apply { + putExtra(Intent.EXTRA_USER_HANDLE, firstUserId) + } + + // When the first user unlocks and we query their recent media + resumeListener.userChangeReceiver.onReceive(context, unlockIntent) + whenever(resumeBrowser.userId).thenReturn(userIdCaptor.value) + verify(resumeBrowser, times(3)).findRecentMedia() + + // And the user changes before the MBS response is received + val changeIntent = + Intent(Intent.ACTION_USER_SWITCHED).apply { + putExtra(Intent.EXTRA_USER_HANDLE, secondUserId) + } + resumeListener.userChangeReceiver.onReceive(context, changeIntent) + callbackCaptor.value.addTrack(description, component, resumeBrowser) + + // Then the loaded media is correctly associated with the first user + verify(mediaDataManager) + .addResumptionControls( + eq(firstUserId), + eq(description), + any(), + eq(token), + eq(PACKAGE_NAME), + eq(pendingIntent), + eq(PACKAGE_NAME) + ) + } + + @Test + fun testUserUnlocked_noComponent_doesNotQuery() { + // Set up a valid MBS, but user does not have the service available + setUpMbsWithValidResolveInfo() + val pm = mock(PackageManager::class.java) + whenever(mockContext.packageManager).thenReturn(pm) + whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(null) + + val unlockIntent = + Intent(Intent.ACTION_USER_UNLOCKED).apply { + putExtra(Intent.EXTRA_USER_HANDLE, context.userId) + } + + // When the user is unlocked, but does not have the component installed + resumeListener.userChangeReceiver.onReceive(context, unlockIntent) + + // Then we never attempt to connect to it + verify(resumeBrowser, never()).findRecentMedia() + } + + /** Sets up mocks to successfully find a MBS that returns valid media. */ + private fun setUpMbsWithValidResolveInfo() { + val pm = mock(PackageManager::class.java) + whenever(mockContext.packageManager).thenReturn(pm) + val resolveInfo = ResolveInfo() + val serviceInfo = ServiceInfo() + serviceInfo.packageName = PACKAGE_NAME + resolveInfo.serviceInfo = serviceInfo + resolveInfo.serviceInfo.name = CLASS_NAME + val resumeInfo = listOf(resolveInfo) + whenever(pm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).thenReturn(resumeInfo) + whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(resolveInfo) + whenever(pm.getApplicationLabel(any())).thenReturn(PACKAGE_NAME) + } +}
\ No newline at end of file diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java index 80b0375a229d..8954a0c39091 100644 --- a/services/autofill/java/com/android/server/autofill/Helper.java +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -18,6 +18,8 @@ package com.android.server.autofill; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.ActivityManager; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.ViewNode; import android.app.assist.AssistStructure.WindowNode; @@ -31,13 +33,18 @@ import android.view.View; import android.view.WindowManager; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; +import android.widget.RemoteViews; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.ArrayUtils; import java.io.PrintWriter; + +import java.util.Arrays; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + public final class Helper { @@ -71,6 +78,44 @@ public final class Helper { throw new UnsupportedOperationException("contains static members only"); } + private static boolean checkRemoteViewUriPermissions( + @UserIdInt int userId, @NonNull RemoteViews rView) { + final AtomicBoolean permissionsOk = new AtomicBoolean(true); + + rView.visitUris(uri -> { + int uriOwnerId = android.content.ContentProvider.getUserIdFromUri(uri); + boolean allowed = uriOwnerId == userId; + permissionsOk.set(allowed && permissionsOk.get()); + }); + + return permissionsOk.get(); + } + + /** + * Checks the URI permissions of the remote view, + * to see if the current userId is able to access it. + * + * Returns the RemoteView that is passed if user is able, null otherwise. + * + * TODO: instead of returning a null remoteview when + * the current userId cannot access an URI, + * return a new RemoteView with the URI removed. + */ + public static @Nullable RemoteViews sanitizeRemoteView(RemoteViews rView) { + if (rView == null) return null; + + int userId = ActivityManager.getCurrentUser(); + + boolean ok = checkRemoteViewUriPermissions(userId, rView); + if (!ok) { + Slog.w(TAG, + "sanitizeRemoteView() user: " + userId + + " tried accessing resource that does not belong to them"); + } + return (ok ? rView : null); + } + + @Nullable static AutofillId[] toArray(@Nullable ArraySet<AutofillId> set) { if (set == null) return null; diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java index 890208720f97..b35fd92e366d 100644 --- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java @@ -141,8 +141,9 @@ final class FillUi { final LayoutInflater inflater = LayoutInflater.from(mContext); - final RemoteViews headerPresentation = response.getHeader(); - final RemoteViews footerPresentation = response.getFooter(); + final RemoteViews headerPresentation = Helper.sanitizeRemoteView(response.getHeader()); + final RemoteViews footerPresentation = Helper.sanitizeRemoteView(response.getFooter()); + final ViewGroup decor; if (mFullScreen) { decor = (ViewGroup) inflater.inflate(R.layout.autofill_dataset_picker_fullscreen, null); @@ -220,6 +221,9 @@ final class FillUi { ViewGroup container = decor.findViewById(R.id.autofill_dataset_picker); final View content; try { + if (Helper.sanitizeRemoteView(response.getPresentation()) == null) { + throw new RuntimeException("Permission error accessing RemoteView"); + } content = response.getPresentation().applyWithTheme( mContext, decor, interceptionHandler, mThemeId); container.addView(content); @@ -298,7 +302,8 @@ final class FillUi { final Dataset dataset = response.getDatasets().get(i); final int index = dataset.getFieldIds().indexOf(focusedViewId); if (index >= 0) { - final RemoteViews presentation = dataset.getFieldPresentation(index); + final RemoteViews presentation = Helper.sanitizeRemoteView( + dataset.getFieldPresentation(index)); if (presentation == null) { Slog.w(TAG, "not displaying UI on field " + focusedViewId + " because " + "service didn't provide a presentation for it on " + dataset); diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 1c3116699b2d..7b45ef881e94 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -361,8 +361,7 @@ final class SaveUi { return false; } writeLog(MetricsEvent.AUTOFILL_SAVE_CUSTOM_DESCRIPTION); - - final RemoteViews template = customDescription.getPresentation(); + final RemoteViews template = Helper.sanitizeRemoteView(customDescription.getPresentation()); if (template == null) { Slog.w(TAG, "No remote view on custom description"); return false; diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index ef8670209c44..584031ce880c 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4839,6 +4839,9 @@ public class AccountManagerService Bundle simulateBundle = p.readBundle(); p.recycle(); Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT); + if (intent != null && intent.getClass() != Intent.class) { + return false; + } Intent simulateIntent = simulateBundle.getParcelable(AccountManager.KEY_INTENT); if (intent == null) { return (simulateIntent == null); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6822a4d05745..1c4d69136ece 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -4932,6 +4932,11 @@ public class NotificationManagerService extends SystemService { boolean granted) { Objects.requireNonNull(listener); checkCallerIsSystemOrShell(); + if (granted && listener.flattenToString().length() + > NotificationManager.MAX_SERVICE_COMPONENT_NAME_LENGTH) { + throw new IllegalArgumentException( + "Component name too long: " + listener.flattenToString()); + } final long identity = Binder.clearCallingIdentity(); try { if (mAllowedManagedServicePackages.test( diff --git a/services/core/java/com/android/server/vr/VrManagerService.java b/services/core/java/com/android/server/vr/VrManagerService.java index 45689ce73c9f..7eeba02542e4 100644 --- a/services/core/java/com/android/server/vr/VrManagerService.java +++ b/services/core/java/com/android/server/vr/VrManagerService.java @@ -1045,7 +1045,11 @@ public class VrManagerService extends SystemService for (ComponentName c : possibleServices) { if (Objects.equals(c.getPackageName(), pkg)) { - nm.setNotificationListenerAccessGrantedForUser(c, userId, true); + try { + nm.setNotificationListenerAccessGrantedForUser(c, userId, true); + } catch (Exception e) { + Slog.w(TAG, "Could not grant NLS access to package " + pkg, e); + } } } } 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 af39e56cc137..65844b1bfe13 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -3144,6 +3144,34 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testSetListenerAccessForUser_grantWithNameTooLong_throws() throws Exception { + UserHandle user = UserHandle.of(mContext.getUserId() + 10); + ComponentName c = new ComponentName("com.example.package", + com.google.common.base.Strings.repeat("Blah", 150)); + + try { + mBinderService.setNotificationListenerAccessGrantedForUser(c, user.getIdentifier(), + /* enabled= */ true); + fail("Should've thrown IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Good! + } + } + + @Test + public void testSetListenerAccessForUser_revokeWithNameTooLong_okay() throws Exception { + UserHandle user = UserHandle.of(mContext.getUserId() + 10); + ComponentName c = new ComponentName("com.example.package", + com.google.common.base.Strings.repeat("Blah", 150)); + + mBinderService.setNotificationListenerAccessGrantedForUser( + c, user.getIdentifier(), /* enabled= */ false); + + verify(mListeners).setPackageOrComponentEnabled( + c.flattenToString(), user.getIdentifier(), true, /* enabled= */ false); + } + + @Test public void testSetAssistantAccessForUser() throws Exception { UserHandle user = UserHandle.of(10); List<UserInfo> uis = new ArrayList<>(); @@ -4285,6 +4313,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .addExtras(extras) .build(); + // Serialize and deserialize the notification to make sure nothing breaks in the process, + // since that's what will usually happen before we get to call visitUris. + Parcel parcel = Parcel.obtain(); + n.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + n = new Notification(parcel); + Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class); n.visitUris(visitor); verify(visitor, times(1)).accept(eq(audioContents)); @@ -4363,6 +4398,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testVisitUris_wearableExtender() { + Icon actionIcon = Icon.createWithContentUri("content://media/action"); + Icon wearActionIcon = Icon.createWithContentUri("content://media/wearAction"); + PendingIntent intent = PendingIntent.getActivity(mContext, 0, new Intent(), + PendingIntent.FLAG_IMMUTABLE); + Notification n = new Notification.Builder(mContext, "a") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .addAction(new Notification.Action.Builder(actionIcon, "Hey!", intent).build()) + .extend(new Notification.WearableExtender().addAction( + new Notification.Action.Builder(wearActionIcon, "Wear!", intent).build())) + .build(); + + Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class); + n.visitUris(visitor); + + verify(visitor).accept(eq(actionIcon.getUri())); + verify(visitor).accept(eq(wearActionIcon.getUri())); + } + + @Test public void testSetNotificationPolicy_preP_setOldFields() { ZenModeHelper mZenModeHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = mZenModeHelper; |