diff --git a/api/system-current.txt b/api/system-current.txt index d3785a68c692..a33e927f49bc 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -1346,8 +1346,9 @@ package android.content { public abstract class Context { method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public boolean bindServiceAsUser(@RequiresPermission android.content.Intent, android.content.ServiceConnection, int, android.os.UserHandle); + method @NonNull public android.content.Context createContextAsUser(@NonNull android.os.UserHandle, int); method public abstract android.content.Context createCredentialProtectedStorageContext(); - method public android.content.Context createPackageContextAsUser(String, int, android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; + method @NonNull public android.content.Context createPackageContextAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; method @Nullable public abstract java.io.File getPreloadsFileCache(); method public abstract boolean isCredentialProtectedStorage(); method public abstract void sendBroadcast(android.content.Intent, @Nullable String, @Nullable android.os.Bundle); diff --git a/api/test-current.txt b/api/test-current.txt index d3bea18fb944..29c580da327f 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -638,7 +638,8 @@ package android.content { } public abstract class Context { - method public android.content.Context createPackageContextAsUser(String, int, android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; + method @NonNull public android.content.Context createContextAsUser(@NonNull android.os.UserHandle, int); + method @NonNull public android.content.Context createPackageContextAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; method public abstract android.view.Display getDisplay(); method public abstract int getDisplayId(); method public android.os.UserHandle getUser(); diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 41a4fba0434c..9c46b23d8df8 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2200,6 +2200,15 @@ public Context createPackageContextAsUser(String packageName, int flags, UserHan "Application package " + packageName + " not found"); } + @Override + public Context createContextAsUser(UserHandle user, @CreatePackageOptions int flags) { + try { + return createPackageContextAsUser(getPackageName(), flags, user); + } catch (NameNotFoundException e) { + throw new IllegalStateException("Own package not found: package=" + getPackageName()); + } + } + @Override public Context createContextForSplit(String splitName) throws NameNotFoundException { if (!mPackageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index d8235cf8734a..6e0e41b110d6 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5232,8 +5232,9 @@ public abstract Context createPackageContext(String packageName, */ @SystemApi @TestApi + @NonNull public Context createPackageContextAsUser( - String packageName, @CreatePackageOptions int flags, UserHandle user) + @NonNull String packageName, @CreatePackageOptions int flags, @NonNull UserHandle user) throws PackageManager.NameNotFoundException { if (Build.IS_ENG) { throw new IllegalStateException("createPackageContextAsUser not overridden!"); @@ -5241,6 +5242,23 @@ public Context createPackageContextAsUser( return this; } + /** + * Similar to {@link #createPackageContext(String, int)}, but for the own package with a + * different {@link UserHandle}. For example, {@link #getContentResolver()} + * will open any {@link Uri} as the given user. + * + * @hide + */ + @SystemApi + @TestApi + @NonNull + public Context createContextAsUser(@NonNull UserHandle user, @CreatePackageOptions int flags) { + if (Build.IS_ENG) { + throw new IllegalStateException("createContextAsUser not overridden!"); + } + return this; + } + /** * Creates a context given an {@link android.content.pm.ApplicationInfo}. * diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 0859f97e81a1..7993ea192424 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -883,6 +883,12 @@ public Context createPackageContextAsUser(String packageName, int flags, UserHan return mBase.createPackageContextAsUser(packageName, flags, user); } + /** @hide */ + @Override + public Context createContextAsUser(UserHandle user, @CreatePackageOptions int flags) { + return mBase.createContextAsUser(user, flags); + } + /** @hide */ @Override @UnsupportedAppUsage diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index b44b6d90811e..fce60faf6170 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -1277,6 +1277,13 @@ public static class SessionParams implements Parcelable { /** {@hide} */ public static final int UID_UNKNOWN = -1; + /** + * This value is derived from the maximum file name length. No package above this limit + * can ever be successfully installed on the device. + * @hide + */ + public static final int MAX_PACKAGE_NAME_LENGTH = 255; + /** {@hide} */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public int mode = MODE_INVALID; @@ -1450,6 +1457,8 @@ public void setAppIcon(@Nullable Bitmap appIcon) { /** * Optionally set a label representing the app being installed. + * + * This value will be trimmed to the first 1000 characters. */ public void setAppLabel(@Nullable CharSequence appLabel) { this.appLabel = (appLabel != null) ? appLabel.toString() : null; @@ -1519,7 +1528,8 @@ public void setGrantedRuntimePermissions(String[] permissions) { * *

Initially, all restricted permissions are whitelisted but you can change * which ones are whitelisted by calling this method or the corresponding ones - * on the {@link PackageManager}. + * on the {@link PackageManager}. Only soft or hard restricted permissions on the current + * Android version are supported and any invalid entries will be removed. * * @see PackageManager#addWhitelistedRestrictedPermission(String, String, int) * @see PackageManager#removeWhitelistedRestrictedPermission(String, String, int) diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java index aa8e84262049..7cef05ac8733 100644 --- a/core/java/android/content/pm/PackageItemInfo.java +++ b/core/java/android/content/pm/PackageItemInfo.java @@ -48,8 +48,16 @@ * in the implementation of Parcelable in subclasses. */ public class PackageItemInfo { - /** The maximum length of a safe label, in characters */ - private static final int MAX_SAFE_LABEL_LENGTH = 1000; + + /** + * The maximum length of a safe label, in characters + * + * TODO(b/157997155): It may make sense to expose this publicly so that apps can check for the + * value and truncate the strings/use a different label, without having to hardcode and make + * assumptions about the value. + * @hide + */ + public static final int MAX_SAFE_LABEL_LENGTH = 1000; /** @hide */ public static final float DEFAULT_MAX_LABEL_SIZE_PX = 500f; diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index cd9a7081bd5c..aac2275cfe94 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -1644,7 +1644,7 @@ private static ApkLite parseApkLiteInner(File apkFile, FileDescriptor fd, String } } - private static String validateName(String name, boolean requireSeparator, + public static String validateName(String name, boolean requireSeparator, boolean requireFilename) { final int N = name.length(); boolean hasSep = false; diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java index 3e6312754359..bf584c957aa0 100644 --- a/core/java/android/os/PersistableBundle.java +++ b/core/java/android/os/PersistableBundle.java @@ -268,6 +268,43 @@ public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserExcept XmlUtils.writeMapXml(mMap, out, this); } + /** + * Checks whether all keys and values are within the given character limit. + * Note: Maximum character limit of String that can be saved to XML as part of bundle is 65535. + * Otherwise IOException is thrown. + * @param limit length of String keys and values in the PersistableBundle, including nested + * PersistableBundles to check against. + * + * @hide + */ + public boolean isBundleContentsWithinLengthLimit(int limit) { + unparcel(); + if (mMap == null) { + return true; + } + for (int i = 0; i < mMap.size(); i++) { + if (mMap.keyAt(i) != null && mMap.keyAt(i).length() > limit) { + return false; + } + final Object value = mMap.valueAt(i); + if (value instanceof String && ((String) value).length() > limit) { + return false; + } else if (value instanceof String[]) { + String[] stringArray = (String[]) value; + for (int j = 0; j < stringArray.length; j++) { + if (stringArray[j] != null + && stringArray[j].length() > limit) { + return false; + } + } + } else if (value instanceof PersistableBundle + && !((PersistableBundle) value).isBundleContentsWithinLengthLimit(limit)) { + return false; + } + } + return true; + } + /** @hide */ static class MyReadMapCallback implements XmlUtils.ReadMapCallback { @Override diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index da41478e91a6..fc714923bf41 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -77,6 +77,21 @@ public class UserManager { private Boolean mIsManagedProfileCached; + /** Maximum length of username. + * @hide + */ + public static final int MAX_USER_NAME_LENGTH = 100; + + /** Maximum length of user property String value. + * @hide + */ + public static final int MAX_ACCOUNT_STRING_LENGTH = 500; + + /** Maximum length of account options String values. + * @hide + */ + public static final int MAX_ACCOUNT_OPTIONS_LENGTH = 1000; + /** * @hide * No user restriction. @@ -2199,15 +2214,15 @@ public UserInfo createRestrictedProfile(String name) { * time, the preferred user name and account information are used by the setup process for that * user. * - * @param userName Optional name to assign to the user. + * @param userName Optional name to assign to the user. Character limit is 100. * @param accountName Optional account name that will be used by the setup wizard to initialize - * the user. + * the user. Character limit is 500. * @param accountType Optional account type for the account to be created. This is required - * if the account name is specified. + * if the account name is specified. Character limit is 500. * @param accountOptions Optional bundle of data to be passed in during account creation in the * new user via {@link AccountManager#addAccount(String, String, String[], * Bundle, android.app.Activity, android.accounts.AccountManagerCallback, - * Handler)}. + * Handler)}. Character limit is 1000. * @return An Intent that can be launched from an Activity. * @see #USER_CREATION_FAILED_NOT_PERMITTED * @see #USER_CREATION_FAILED_NO_MORE_USERS diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index c923f8e7b5ff..0fafe2c7e23d 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -411,6 +411,8 @@ private Process.ProcessStartResult zygoteSendArgsAndGetResult( throw new ZygoteStartFailedEx("Embedded newlines not allowed"); } else if (arg.indexOf('\r') >= 0) { throw new ZygoteStartFailedEx("Embedded carriage returns not allowed"); + } else if (arg.indexOf('\u0000') >= 0) { + throw new ZygoteStartFailedEx("Embedded nulls not allowed"); } } @@ -869,6 +871,14 @@ private boolean maybeSetApiBlacklistExemptions(ZygoteState state, boolean sendIf return true; } + for (/* NonNull */ String s : mApiBlacklistExemptions) { + // indexOf() is intrinsified and faster than contains(). + if (s.indexOf('\n') >= 0 || s.indexOf('\r') >= 0 || s.indexOf('\u0000') >= 0) { + Slog.e(LOG_TAG, "Failed to set API denylist exemptions: Bad character"); + mApiBlacklistExemptions = Collections.emptyList(); + return false; + } + } try { state.mZygoteOutputWriter.write(Integer.toString(mApiBlacklistExemptions.size() + 1)); state.mZygoteOutputWriter.newLine(); diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java index 905c7811e457..39395074b916 100644 --- a/core/java/android/service/notification/StatusBarNotification.java +++ b/core/java/android/service/notification/StatusBarNotification.java @@ -273,6 +273,18 @@ public int getUserId() { return this.user.getIdentifier(); } + /** + * Like {@link #getUserId()} but handles special users. + * @hide + */ + public int getNormalizedUserId() { + int userId = getUserId(); + if (userId == UserHandle.USER_ALL) { + userId = UserHandle.USER_SYSTEM; + } + return userId; + } + /** The package that the notification belongs to. */ public String getPackageName() { return pkg; diff --git a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java index 03da9bc939ec..74dedc38a922 100644 --- a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java +++ b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java @@ -110,6 +110,14 @@ private String checkUserCreationRequirements() { if (cantCreateUser) { setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED); return null; + } else if (!(isUserPropertyWithinLimit(mUserName, UserManager.MAX_USER_NAME_LENGTH) + && isUserPropertyWithinLimit(mAccountName, UserManager.MAX_ACCOUNT_STRING_LENGTH) + && isUserPropertyWithinLimit(mAccountType, UserManager.MAX_ACCOUNT_STRING_LENGTH)) + || (mAccountOptions != null && !mAccountOptions.isBundleContentsWithinLengthLimit( + UserManager.MAX_ACCOUNT_OPTIONS_LENGTH))) { + setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED); + Log.i(TAG, "User properties must not exceed their character limits"); + return null; } else if (cantCreateAnyMoreUsers) { setResult(UserManager.USER_CREATION_FAILED_NO_MORE_USERS); return null; @@ -137,4 +145,8 @@ public void onClick(DialogInterface dialog, int which) { } finish(); } + + private boolean isUserPropertyWithinLimit(String property, int limit) { + return property == null || property.length() <= limit; + } } diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 52d0adba0a05..fe2ff54194fb 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -106,6 +106,9 @@ class ZygoteConnection { throw ex; } + if (peer.getUid() != Process.SYSTEM_UID) { + throw new ZygoteSecurityException("Only system UID is allowed to connect to Zygote."); + } isEof = false; } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 43e12b0129e1..459f1db00be9 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -1782,8 +1782,8 @@ public boolean isBiometricAllowedForUser(int userId) { } public boolean isUserInLockdown(int userId) { - return getStrongAuthForUser(userId) - == StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; + return (getStrongAuthForUser(userId) + & StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN) != 0; } private ICheckCredentialProgressCallback wrapCallback( diff --git a/core/tests/PackageInstallerSessions/Android.bp b/core/tests/PackageInstallerSessions/Android.bp new file mode 100644 index 000000000000..a564d30f3008 --- /dev/null +++ b/core/tests/PackageInstallerSessions/Android.bp @@ -0,0 +1,42 @@ +// +// Copyright 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. +// + +android_test { + name: "FrameworksCorePackageInstallerSessionsTests", + + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "androidx.test.rules", + "compatibility-device-util-axt", + "frameworks-base-testutils", + "platform-test-annotations", + "testng", + "truth-prebuilt", + ], + + libs: [ + "android.test.runner", + "android.test.base", + "framework", + "framework-res", + ], + + platform_apis: true, + sdk_version: "test_current", + test_suites: ["device-tests"], +} diff --git a/core/tests/PackageInstallerSessions/AndroidManifest.xml b/core/tests/PackageInstallerSessions/AndroidManifest.xml new file mode 100644 index 000000000000..5b22d2b4f3e3 --- /dev/null +++ b/core/tests/PackageInstallerSessions/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/core/tests/PackageInstallerSessions/src/android/content/pm/PackageSessionTests.kt b/core/tests/PackageInstallerSessions/src/android/content/pm/PackageSessionTests.kt new file mode 100644 index 000000000000..494c92a8aa3f --- /dev/null +++ b/core/tests/PackageInstallerSessions/src/android/content/pm/PackageSessionTests.kt @@ -0,0 +1,188 @@ +/* + * 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 android.content.pm + +import android.content.Context +import android.content.pm.PackageInstaller.SessionParams +import android.platform.test.annotations.Presubmit +import androidx.test.InstrumentationRegistry +import androidx.test.filters.LargeTest +import com.android.compatibility.common.util.ShellIdentityUtils +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.testng.Assert.assertThrows +import kotlin.random.Random + +/** + * For verifying public [PackageInstaller] session APIs. This differs from + * [com.android.server.pm.PackageInstallerSessionTest] in services because that mocks the session, + * whereas this test uses the installer on device. + */ +@Presubmit +class PackageSessionTests { + + companion object { + /** + * Permissions marked "hardRestricted" or "softRestricted" in core/res/AndroidManifest.xml. + */ + private val RESTRICTED_PERMISSIONS = listOf( + "android.permission.SEND_SMS", + "android.permission.RECEIVE_SMS", + "android.permission.READ_SMS", + "android.permission.RECEIVE_WAP_PUSH", + "android.permission.RECEIVE_MMS", + "android.permission.READ_CELL_BROADCASTS", + "android.permission.ACCESS_BACKGROUND_LOCATION", + "android.permission.READ_CALL_LOG", + "android.permission.WRITE_CALL_LOG", + "android.permission.PROCESS_OUTGOING_CALLS" + ) + } + + private val context: Context = InstrumentationRegistry.getContext() + + private val installer = context.packageManager.packageInstaller + + @Before + @After + fun abandonAllSessions() { + installer.mySessions.asSequence() + .map { it.sessionId } + .forEach { + try { + installer.abandonSession(it) + } catch (ignored: Exception) { + // Querying for sessions checks by calling package name, but abandoning + // checks by UID, which won't match if this test failed to clean up + // on a previous install + run + uninstall, so ignore these failures. + } + } + } + + @Test + fun truncateAppLabel() { + val longLabel = invalidAppLabel() + val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { + setAppLabel(longLabel) + } + + createSession(params) { + assertThat(installer.getSessionInfo(it)?.appLabel) + .isEqualTo(longLabel.take(PackageItemInfo.MAX_SAFE_LABEL_LENGTH)) + } + } + + @Test + fun removeInvalidAppPackageName() { + val longName = invalidPackageName() + val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { + setAppPackageName(longName) + } + + createSession(params) { + assertThat(installer.getSessionInfo(it)?.appPackageName) + .isEqualTo(null) + } + } + + @Test + fun removeInvalidInstallerPackageName() { + val longName = invalidPackageName() + val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { + setInstallerPackageName(longName) + } + + createSession(params) { + // If a custom installer name is dropped, it defaults to the caller + assertThat(installer.getSessionInfo(it)?.installerPackageName) + .isEqualTo(context.packageName) + } + } + + @Test + fun truncateWhitelistPermissions() { + val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { + setWhitelistedRestrictedPermissions(invalidPermissions()) + } + + createSession(params) { + assertThat(installer.getSessionInfo(it)?.whitelistedRestrictedPermissions!!) + .containsExactlyElementsIn(RESTRICTED_PERMISSIONS) + } + } + + @LargeTest + @Test + fun allocateMaxSessionsWithPermission() { + ShellIdentityUtils.invokeWithShellPermissions { + repeat(1024) { createDummySession() } + assertThrows(IllegalStateException::class.java) { createDummySession() } + } + } + + @LargeTest + @Test + fun allocateMaxSessionsNoPermission() { + repeat(50) { createDummySession() } + assertThrows(IllegalStateException::class.java) { createDummySession() } + } + + private fun createDummySession() { + installer.createSession(SessionParams(SessionParams.MODE_FULL_INSTALL) + .apply { + setAppPackageName(invalidPackageName()) + setAppLabel(invalidAppLabel()) + setWhitelistedRestrictedPermissions(invalidPermissions()) + }) + } + + private fun invalidPackageName(maxLength: Int = SessionParams.MAX_PACKAGE_NAME_LENGTH): String { + return (0 until (maxLength + 10)) + .asSequence() + .mapIndexed { index, _ -> + // A package name needs at least one separator + if (index == 2) { + '.' + } else { + Random.nextInt('z' - 'a').toChar() + 'a'.toInt() + } + } + .joinToString(separator = "") + } + + private fun invalidAppLabel() = (0 until PackageItemInfo.MAX_SAFE_LABEL_LENGTH + 10) + .asSequence() + .map { Random.nextInt(Char.MAX_VALUE.toInt()).toChar() } + .joinToString(separator = "") + + private fun invalidPermissions() = RESTRICTED_PERMISSIONS.toMutableSet() + .apply { + // Add some invalid permission names + repeat(10) { add(invalidPackageName(300)) } + } + + private fun createSession(params: SessionParams, block: (Int) -> Unit = {}) { + val sessionId = installer.createSession(params) + try { + block(sessionId) + } finally { + installer.abandonSession(sessionId) + } + } +} diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java index 9913531cdf13..433a35bffeb8 100644 --- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java @@ -19,6 +19,9 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; @@ -48,12 +51,15 @@ @SmallTest public class LockPatternUtilsTest { + private ILockSettings mLockSettings; + private static final int USER_ID = 1; private static final int DEMO_USER_ID = 5; private LockPatternUtils mLockPatternUtils; private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoMode) throws Exception { + mLockSettings = Mockito.mock(ILockSettings.class); final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); final MockContentResolver cr = new MockContentResolver(context); @@ -61,13 +67,12 @@ private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoM when(context.getContentResolver()).thenReturn(cr); Settings.Global.putInt(cr, Settings.Global.DEVICE_DEMO_MODE, deviceDemoMode); - final ILockSettings ils = Mockito.mock(ILockSettings.class); - when(ils.havePassword(DEMO_USER_ID)).thenReturn(isSecure); - when(ils.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED, DEMO_USER_ID)) - .thenReturn((long) PASSWORD_QUALITY_MANAGED); + when(mLockSettings.havePassword(DEMO_USER_ID)).thenReturn(isSecure); + when(mLockSettings.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED, + DEMO_USER_ID)).thenReturn((long) PASSWORD_QUALITY_MANAGED); // TODO(b/63758238): stop spying the class under test mLockPatternUtils = spy(new LockPatternUtils(context)); - when(mLockPatternUtils.getLockSettings()).thenReturn(ils); + when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings); doReturn(true).when(mLockPatternUtils).hasSecureLockScreen(); final UserInfo userInfo = Mockito.mock(UserInfo.class); @@ -77,6 +82,31 @@ private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoM when(context.getSystemService(Context.USER_SERVICE)).thenReturn(um); } + @Test + public void isUserInLockDown() throws Exception { + configureTest(true, false, 2); + + // GIVEN strong auth not required + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(STRONG_AUTH_NOT_REQUIRED); + + // THEN user isn't in lockdown + assertFalse(mLockPatternUtils.isUserInLockdown(USER_ID)); + + // GIVEN lockdown + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); + + // THEN user is in lockdown + assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID)); + + // GIVEN lockdown and lockout + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN | STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); + + // THEN user is in lockdown + assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID)); + } + @Test public void isLockScreenDisabled_isDemoUser_true() throws Exception { configureTest(false, true, 2); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 91c3bcb7225b..a781da06aef8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -47,6 +47,7 @@ import android.os.Build; import android.os.Bundle; import android.os.SystemClock; +import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.AttributeSet; @@ -454,6 +455,8 @@ private void setIconRunning(ImageView imageView, boolean running) { public void setEntry(@NonNull NotificationEntry entry) { mEntry = entry; mStatusBarNotification = entry.notification; + mImageResolver = new NotificationInlineImageResolver(userContextForEntry(mContext, entry), + new NotificationInlineImageCache()); if (mStatusBarNotification != null) { updateAlarmOrCall(); AppLockManager appLockManager = (AppLockManager) mContext @@ -1665,8 +1668,6 @@ public ExpandableNotificationRow(Context context, AttributeSet attrs) { mFalsingManager = Dependency.get(FalsingManager.class); // TODO: inject into a controller. mNotificationInflater = new NotificationContentInflater(this); mMenuRow = new NotificationMenuRow(mContext); - mImageResolver = new NotificationInlineImageResolver(context, - new NotificationInlineImageCache()); mMediaManager = Dependency.get(NotificationMediaManager.class); initDimens(); } @@ -1679,6 +1680,14 @@ public void setStatusBarStateController(StatusBarStateController statusBarStateC mStatusbarStateController = statusBarStateController; } + private static Context userContextForEntry(Context base, NotificationEntry entry) { + if (base.getUserId() == entry.notification.getNormalizedUserId()) { + return base; + } + return base.createContextAsUser( + UserHandle.of(entry.notification.getNormalizedUserId()), /* flags= */ 0); + } + private void initDimens() { mNotificationMinHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_min_height_legacy); 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 466be072afdb..885b28aebc89 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 @@ -26,6 +26,7 @@ import android.os.SystemClock; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ImageResolver; import com.android.internal.widget.LocalImageResolver; import com.android.internal.widget.MessagingMessage; @@ -54,7 +55,7 @@ public class NotificationInlineImageResolver implements ImageResolver { * @param imageCache The implementation of internal cache. */ public NotificationInlineImageResolver(Context context, ImageCache imageCache) { - mContext = context.getApplicationContext(); + mContext = context; mImageCache = imageCache; if (mImageCache != null) { @@ -62,6 +63,11 @@ public NotificationInlineImageResolver(Context context, ImageCache imageCache) { } } + @VisibleForTesting + public Context getContext() { + return mContext; + } + /** * Check if this resolver has its internal cache implementation. * @return True if has its internal cache, false otherwise. diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestableContext.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestableContext.java index f792d7d11e15..6324569411ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestableContext.java +++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestableContext.java @@ -14,15 +14,20 @@ package com.android.systemui; +import android.annotation.NonNull; import android.content.Context; import android.testing.LeakCheck; import android.testing.TestableContext; import android.util.ArrayMap; import android.view.Display; +import java.util.HashMap; +import java.util.Map; + public class SysuiTestableContext extends TestableContext implements SysUiServiceProvider { private ArrayMap, Object> mComponents; + private final Map mContextForUser = new HashMap<>(); public SysuiTestableContext(Context base) { super(base); @@ -59,4 +64,22 @@ public Context createDisplayContext(Display display) { new SysuiTestableContext(getBaseContext().createDisplayContext(display)); return context; } + + /** + * Sets a Context object that will be returned as the result of {@link #createContextAsUser} + * for a specific {@code user}. + */ + public void prepareCreateContextAsUser(UserHandle user, Context context) { + mContextForUser.put(user, context); + } + + @Override + @NonNull + public Context createContextAsUser(UserHandle user, int flags) { + Context userContext = mContextForUser.get(user); + if (userContext != null) { + return userContext; + } + return super.createContextAsUser(user, flags); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index d526d104630e..c9b29fe7e490 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -21,6 +21,10 @@ import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_ALL; import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_HEADS_UP; import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_PUBLIC; +import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.PKG; +import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.USER_HANDLE; + +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -38,6 +42,8 @@ import android.app.AppOpsManager; import android.app.NotificationChannel; +import android.content.Context; +import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -48,6 +54,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestableContext; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationTestHelper; @@ -377,4 +384,24 @@ public void testGetIsNonblockable_criticalDeviceFunction() throws Exception { assertTrue(row.getIsNonblockable()); } + + @Test + public void imageResolver_sameNotificationUser_usesContext() throws Exception { + ExpandableNotificationRow row = mNotificationTestHelper.createRow(PKG, + USER_HANDLE.getUid(1234), USER_HANDLE); + + assertThat(row.getImageResolver().getContext()).isSameInstanceAs(mContext); + } + + @Test + public void imageResolver_differentNotificationUser_createsUserContext() throws Exception { + UserHandle user = new UserHandle(33); + Context userContext = new SysuiTestableContext(mContext); + mContext.prepareCreateContextAsUser(user, userContext); + + ExpandableNotificationRow row = mNotificationTestHelper.createRow(PKG, + user.getUid(1234), user); + + assertThat(row.getImageResolver().getContext()).isSameInstanceAs(userContext); + } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 194c90e125f9..dbc9d42a4adc 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1623,10 +1623,13 @@ private void updateServicesLocked(UserState userState) { boolean isUnlockingOrUnlocked = LocalServices.getService(UserManagerInternal.class) .isUserUnlockingOrUnlocked(userState.mUserId); + // Store the list of installed services. + mTempComponentNameSet.clear(); for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) { AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i); ComponentName componentName = ComponentName.unflattenFromString( installedService.getId()); + mTempComponentNameSet.add(componentName); AccessibilityServiceConnection service = componentNameToServiceMap.get(componentName); @@ -1673,6 +1676,25 @@ private void updateServicesLocked(UserState userState) { if (audioManager != null) { audioManager.setAccessibilityServiceUids(mTempIntArray); } + // If any services have been removed, remove them from the enabled list and the touch + // exploration granted list. + boolean anyServiceRemoved = + userState.mEnabledServices.removeIf((comp) -> !mTempComponentNameSet.contains(comp)) + || userState.mTouchExplorationGrantedServices.removeIf( + (comp) -> !mTempComponentNameSet.contains(comp)); + if (anyServiceRemoved) { + // Update the enabled services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mEnabledServices, + userState.mUserId); + // Update the touch exploration granted services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + userState.mTouchExplorationGrantedServices, + userState.mUserId); + } + mTempComponentNameSet.clear(); updateAccessibilityEnabledSetting(userState); } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 1bd5201f5b26..58a1064682d3 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -32,8 +32,10 @@ import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.content.ComponentName; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.graphics.Rect; import android.metrics.LogMaker; @@ -214,6 +216,31 @@ protected boolean updateLocked(boolean disabled) { @Override // from PerUserSystemService protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) throws NameNotFoundException { + final List resolveInfos = + getContext().getPackageManager().queryIntentServicesAsUser( + new Intent(AutofillService.SERVICE_INTERFACE), + // The MATCH_INSTANT flag is added because curret autofill CTS module is + // defined in one apk, which makes the test autofill service installed in a + // instant app when the CTS tests are running in instant app mode. + // TODO: Remove MATCH_INSTANT flag after completing refactoring the CTS module + // to make the test autofill service a separate apk. + PackageManager.GET_META_DATA | PackageManager.MATCH_INSTANT, + mUserId); + boolean serviceHasAutofillIntentFilter = false; + for (ResolveInfo resolveInfo : resolveInfos) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (serviceInfo.getComponentName().equals(serviceComponent)) { + serviceHasAutofillIntentFilter = true; + break; + } + } + if (!serviceHasAutofillIntentFilter) { + Slog.w(TAG, + "Autofill service from '" + serviceComponent.getPackageName() + "' does" + + "not have intent filter " + AutofillService.SERVICE_INTERFACE); + throw new SecurityException("Service does not declare intent filter " + + AutofillService.SERVICE_INTERFACE); + } mInfo = new AutofillServiceInfo(getContext(), serviceComponent, mUserId); return mInfo.getServiceInfo(); } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index a3c7159c2b05..75ee995f7c74 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -182,6 +182,7 @@ public void onStopUser(int userHandle) { final MessageHandler mHandler; + private static final int TIMEOUT_DELAY_MS = 1000 * 60 * 15; // Messages that can be sent on mHandler private static final int MESSAGE_TIMED_OUT = 3; private static final int MESSAGE_COPY_SHARED_ACCOUNT = 4; @@ -3485,6 +3486,11 @@ public void onResult(Bundle result) { // Strip auth token from result. result.remove(AccountManager.KEY_AUTHTOKEN); + if (!checkKeyIntent(Binder.getCallingUid(), result)) { + onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + "invalid intent in bundle returned"); + return; + } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, @@ -4774,6 +4780,7 @@ public Session(UserAccounts accounts, IAccountManagerResponse response, String a synchronized (mSessions) { mSessions.put(toString(), this); } + scheduleTimeout(); if (response != null) { try { response.asBinder().linkToDeath(this, 0 /* flags */); @@ -4940,6 +4947,11 @@ private void unbind() { } } + private void scheduleTimeout() { + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_TIMED_OUT, this), TIMEOUT_DELAY_MS); + } + public void cancelTimeout() { mHandler.removeMessages(MESSAGE_TIMED_OUT, this); } @@ -4976,6 +4988,9 @@ public void onServiceDisconnected(ComponentName name) { public void onTimedOut() { IAccountManagerResponse response = getResponseAndClose(); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onTimedOut"); + } if (response != null) { try { response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, @@ -5060,6 +5075,11 @@ public void onResult(Bundle result) { } else { if (mStripAuthTokenFromResult) { result.remove(AccountManager.KEY_AUTHTOKEN); + if (!checkKeyIntent(Binder.getCallingUid(), result)) { + onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + "invalid intent in bundle returned"); + return; + } } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, getClass().getSimpleName() diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 4828bbfff676..2d9cfcb3ebb5 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -141,7 +141,9 @@ abstract public class ManagedServices { // List of approved packages or components (by user, then by primary/secondary) that are // allowed to be bound as managed services. A package or component appearing in this list does // not mean that we are currently bound to said package/component. - private ArrayMap>> mApproved = new ArrayMap<>(); + @GuardedBy("mApproved") + protected final ArrayMap>> mApproved = + new ArrayMap<>(); // True if approved services are stored in xml, not settings. private boolean mUseXml; @@ -573,6 +575,23 @@ protected boolean isPackageOrComponentAllowed(String pkgOrComponent, int userId) return false; } + protected boolean isPackageOrComponentAllowedWithPermission(ComponentName component, + int userId) { + if (!(isPackageOrComponentAllowed(component.flattenToString(), userId) + || isPackageOrComponentAllowed(component.getPackageName(), userId))) { + return false; + } + return componentHasBindPermission(component, userId); + } + + private boolean componentHasBindPermission(ComponentName component, int userId) { + ServiceInfo info = getServiceInfo(component, userId); + if (info == null) { + return false; + } + return mConfig.bindPermission.equals(info.permission); + } + protected boolean isPackageAllowed(String pkg, int userId) { if (pkg == null) { return false; @@ -623,6 +642,7 @@ public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] u for (int uid : uidList) { if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) { anyServicesInvolved = true; + trimApprovedListsForInvalidServices(pkgName, UserHandle.getUserId(uid)); } } } @@ -749,8 +769,7 @@ protected void setComponentState(ComponentName component, boolean enabled) { for (int i = 0; i < userIds.size(); i++) { final int userId = userIds.get(i); if (enabled) { - if (isPackageOrComponentAllowed(component.flattenToString(), userId) - || isPackageOrComponentAllowed(component.getPackageName(), userId)) { + if (isPackageOrComponentAllowedWithPermission(component, userId)) { registerServiceLocked(component, userId); } else { Slog.d(TAG, component + " no longer has permission to be bound"); @@ -889,6 +908,33 @@ private boolean removeUninstalledItemsFromApprovedLists(int uninstalledUserId, S return removed; } + private void trimApprovedListsForInvalidServices(String packageName, int userId) { + synchronized (mApproved) { + final ArrayMap> approvedByType = mApproved.get(userId); + if (approvedByType == null) { + return; + } + for (int i = 0; i < approvedByType.size(); i++) { + final ArraySet approved = approvedByType.valueAt(i); + for (int j = approved.size() - 1; j >= 0; j--) { + final String approvedPackageOrComponent = approved.valueAt(j); + if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) { + final ComponentName component = ComponentName.unflattenFromString( + approvedPackageOrComponent); + if (component != null && !componentHasBindPermission(component, userId)) { + approved.removeAt(j); + if (DEBUG) { + Slog.v(TAG, "Removing " + approvedPackageOrComponent + + " from approved list; no bind permission found " + + mConfig.bindPermission); + } + } + } + } + } + } + } + protected String getPackageName(String packageOrComponent) { final ComponentName component = ComponentName.unflattenFromString(packageOrComponent); if (component != null) { @@ -1048,26 +1094,20 @@ private void bindToServices(SparseArray> componentsToBind) { final int userId = componentsToBind.keyAt(i); final Set add = componentsToBind.get(userId); for (ComponentName component : add) { - try { - ServiceInfo info = mPm.getServiceInfo(component, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); - if (info == null) { - Slog.w(TAG, "Not binding " + getCaption() + " service " + component - + ": service not found"); - continue; - } - if (!mConfig.bindPermission.equals(info.permission)) { - Slog.w(TAG, "Not binding " + getCaption() + " service " + component - + ": it does not require the permission " + mConfig.bindPermission); - continue; - } - Slog.v(TAG, - "enabling " + getCaption() + " for " + userId + ": " + component); - registerService(component, userId); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); + ServiceInfo info = getServiceInfo(component, userId); + if (info == null) { + Slog.w(TAG, "Not binding " + getCaption() + " service " + component + + ": service not found"); + continue; } + if (!mConfig.bindPermission.equals(info.permission)) { + Slog.w(TAG, "Not binding " + getCaption() + " service " + component + + ": it does not require the permission " + mConfig.bindPermission); + continue; + } + Slog.v(TAG, + "enabling " + getCaption() + " for " + userId + ": " + component); + registerService(component, userId); } } } @@ -1081,6 +1121,15 @@ private void registerService(final ComponentName name, final int userid) { } } + @VisibleForTesting + void reregisterService(final ComponentName cn, final int userId) { + // If rebinding a package that died, ensure it still has permission + // after the rebind delay + if (isPackageOrComponentAllowedWithPermission(cn, userId)) { + registerService(cn, userId); + } + } + /** * Inject a system service into the management list. */ @@ -1181,7 +1230,7 @@ public void onBindingDied(ComponentName name) { mHandler.postDelayed(new Runnable() { @Override public void run() { - registerService(name, userid); + reregisterService(name, userid); } }, ON_BINDING_DIED_REBIND_DELAY_MS); } else { @@ -1313,6 +1362,19 @@ private void unbindService(ServiceConnection connection, ComponentName component } } + private ServiceInfo getServiceInfo(ComponentName component, int userId) { + try { + return mPm.getServiceInfo(component, + PackageManager.GET_META_DATA + | PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + userId); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return null; + } + public class ManagedServiceInfo implements IBinder.DeathRecipient { public IInterface service; public ComponentName component; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 0837adc770c7..777ed41c4079 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -444,6 +444,10 @@ public class NotificationManagerService extends SystemService { private KeyguardManager mKeyguardManager; + // True if the toast that's on top of the queue is being shown at the moment. + @GuardedBy("mToastQueue") + private boolean mIsCurrentToastShown = false; + // The last key in this list owns the hardware. ArrayList mLights = new ArrayList<>(); @@ -751,17 +755,21 @@ private void writePolicyXml(OutputStream stream, boolean forBackup, int userId) private static final class ToastRecord { + final int uid; final int pid; final String pkg; + final boolean isSystemToast; final ITransientNotification callback; int duration; int displayId; Binder token; - ToastRecord(int pid, String pkg, ITransientNotification callback, int duration, + ToastRecord(int uid, int pid, String pkg, boolean isSystemToast, ITransientNotification callback, int duration, Binder token, int displayId) { + this.uid = uid; this.pid = pid; this.pkg = pkg; + this.isSystemToast = isSystemToast; this.callback = callback; this.duration = duration; this.token = token; @@ -2524,10 +2532,21 @@ record = mToastQueue.get(index); Binder token = new Binder(); mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId); - record = new ToastRecord(callingPid, pkg, callback, duration, token, + record = new ToastRecord(callingUid, callingPid, pkg, isSystemToast, callback, duration, token, displayId); - mToastQueue.add(record); - index = mToastQueue.size() - 1; + + // Insert system toasts at the front of the queue + int systemToastInsertIdx = mToastQueue.size(); + if (isSystemToast) { + systemToastInsertIdx = getInsertIndexForSystemToastLocked(); + } + if (systemToastInsertIdx < mToastQueue.size()) { + index = systemToastInsertIdx; + mToastQueue.add(index, record); + } else { + mToastQueue.add(record); + index = mToastQueue.size() - 1; + } keepProcessAliveIfNeededLocked(callingPid); } // If it's at index 0, it's the current toast. It doesn't matter if it's @@ -2543,6 +2562,23 @@ record = new ToastRecord(callingPid, pkg, callback, duration, token, } } + @GuardedBy("mToastQueue") + private int getInsertIndexForSystemToastLocked() { + // If there are other system toasts: insert after the last one + int idx = 0; + for (ToastRecord r : mToastQueue) { + if (idx == 0 && mIsCurrentToastShown) { + idx++; + continue; + } + if (!r.isSystemToast) { + return idx; + } + idx++; + } + return idx; + } + @Override public void cancelToast(String pkg, ITransientNotification callback) { Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback); @@ -4340,6 +4376,10 @@ public void updateNotificationChannelFromPrivilegedListener(INotificationListene Preconditions.checkNotNull(user); verifyPrivilegedListener(token, user, false); + + final NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( + pkg, getUidForPackageAndUser(pkg, user), channel.getId(), true); + verifyPrivilegedListenerUriPermission(Binder.getCallingUid(), channel, originalChannel); updateNotificationChannelInt(pkg, getUidForPackageAndUser(pkg, user), channel, true); } @@ -4419,6 +4459,24 @@ private void verifyPrivilegedListener(INotificationListener token, UserHandle us } } + private void verifyPrivilegedListenerUriPermission(int sourceUid, + @NonNull NotificationChannel updateChannel, + @Nullable NotificationChannel originalChannel) { + // Check that the NLS has the required permissions to access the channel + final Uri soundUri = updateChannel.getSound(); + final Uri originalSoundUri = + (originalChannel != null) ? originalChannel.getSound() : null; + if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) { + Binder.withCleanCallingIdentity(() -> { + mUgmInternal.checkGrantUriPermission(sourceUid, null, + ContentProvider.getUriWithoutUserId(soundUri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(soundUri, + UserHandle.getUserId(sourceUid))); + }); + } + } + private int getUidForPackageAndUser(String pkg, UserHandle user) throws RemoteException { int uid = 0; long identity = Binder.clearCallingIdentity(); @@ -6485,12 +6543,17 @@ public void run() { @GuardedBy("mToastQueue") void showNextToastLocked() { + if (mIsCurrentToastShown) { + return; // Don't show the same toast twice. + } + ToastRecord record = mToastQueue.get(0); while (record != null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); try { record.callback.show(record.token); scheduleDurationReachedLocked(record); + mIsCurrentToastShown = true; return; } catch (RemoteException e) { Slog.w(TAG, "Object died trying to show notification " + record.callback @@ -6522,6 +6585,10 @@ void cancelToastLocked(int index) { // the list anyway } + if (index == 0) { + mIsCurrentToastShown = false; + } + ToastRecord lastToast = mToastQueue.remove(index); mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */, diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index f37362a34659..4d77598b74c3 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -20,6 +20,7 @@ import static org.xmlpull.v1.XmlPullParser.START_TAG; import android.Manifest; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.AppGlobals; import android.app.AppOpsManager; @@ -41,7 +42,9 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageParser; import android.content.pm.ParceledListSlice; import android.content.pm.VersionedPackage; import android.graphics.Bitmap; @@ -125,8 +128,10 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private static final long MAX_AGE_MILLIS = 3 * DateUtils.DAY_IN_MILLIS; /** Automatically destroy staged sessions that have not changed state in this time */ private static final long MAX_TIME_SINCE_UPDATE_MILLIS = 7 * DateUtils.DAY_IN_MILLIS; - /** Upper bound on number of active sessions for a UID */ - private static final long MAX_ACTIVE_SESSIONS = 1024; + /** Upper bound on number of active sessions for a UID that has INSTALL_PACKAGES */ + private static final long MAX_ACTIVE_SESSIONS_WITH_PERMISSION = 1024; + /** Upper bound on number of active sessions for a UID without INSTALL_PACKAGES */ + private static final long MAX_ACTIVE_SESSIONS_NO_PERMISSION = 50; /** Upper bound on number of historical sessions for a UID */ private static final long MAX_HISTORICAL_SESSIONS = 1048576; /** Destroy sessions older than this on storage free request */ @@ -535,6 +540,25 @@ private int createSessionInternal(SessionParams params, String installerPackageN throw new SecurityException("User restriction prevents installing"); } + // App package name and label length is restricted so that really long strings aren't + // written to disk. + if (params.appPackageName != null && !isValidPackageName(params.appPackageName)) { + params.appPackageName = null; + } + + params.appLabel = TextUtils.trimToSize(params.appLabel, + PackageItemInfo.MAX_SAFE_LABEL_LENGTH); + + // Validate installer package name. + if (params.installerPackageName != null && !isValidPackageName( + params.installerPackageName)) { + params.installerPackageName = null; + } + + String requestedInstallerPackageName = + params.installerPackageName != null ? params.installerPackageName + : installerPackageName; + if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) { params.installFlags |= PackageManager.INSTALL_FROM_ADB; @@ -638,12 +662,23 @@ private int createSessionInternal(SessionParams params, String installerPackageN } } + if (params.whitelistedRestrictedPermissions != null) { + mPermissionManager.retainHardAndSoftRestrictedPermissions( + params.whitelistedRestrictedPermissions); + } + final int sessionId; final PackageInstallerSession session; synchronized (mSessions) { // Sanity check that installer isn't going crazy final int activeCount = getSessionCount(mSessions, callingUid); - if (activeCount >= MAX_ACTIVE_SESSIONS) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES) + == PackageManager.PERMISSION_GRANTED) { + if (activeCount >= MAX_ACTIVE_SESSIONS_WITH_PERMISSION) { + throw new IllegalStateException( + "Too many active sessions for UID " + callingUid); + } + } else if (activeCount >= MAX_ACTIVE_SESSIONS_NO_PERMISSION) { throw new IllegalStateException( "Too many active sessions for UID " + callingUid); } @@ -776,6 +811,19 @@ private int allocateSessionIdLocked() { throw new IllegalStateException("Failed to allocate session ID"); } + private static boolean isValidPackageName(@NonNull String packageName) { + if (packageName.length() > SessionParams.MAX_PACKAGE_NAME_LENGTH) { + return false; + } + // "android" is a valid package name + String errorMessage = PackageParser.validateName( + packageName, /* requireSeparator= */ false, /* requireFilename */ true); + if (errorMessage != null) { + return false; + } + return true; + } + private File getTmpSessionDir(String volumeUuid) { return Environment.getDataAppDirectory(volumeUuid); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 948085270856..ab1c7e1a85ce 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -13724,6 +13724,9 @@ int installExistingPackageAsUser(@Nullable String packageName, @UserIdInt int us if (pkgSetting == null) { return PackageManager.INSTALL_FAILED_INVALID_URI; } + if (instantApp && (pkgSetting.isSystem() || isUpdatedSystemApp(pkgSetting))) { + return PackageManager.INSTALL_FAILED_INVALID_URI; + } if (!canViewInstantApps(callingUid, UserHandle.getUserId(callingUid))) { // only allow the existing package to be used if it's installed as a full // application for at least one user diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index c6bc7576147f..da018ad04179 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -19,6 +19,7 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.Person; +import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -28,12 +29,14 @@ import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.os.PersistableBundle; +import android.os.SystemClock; import android.text.format.Formatter; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; @@ -119,6 +122,11 @@ class ShortcutPackage extends ShortcutPackageItem { private static final String KEY_BITMAPS = "bitmaps"; private static final String KEY_BITMAP_BYTES = "bitmapBytes"; + @VisibleForTesting + public static final int REPORT_USAGE_BUFFER_SIZE = 3; + + private final Object mLock = new Object(); + /** * All the shortcuts from the package, keyed on IDs. */ @@ -143,6 +151,9 @@ class ShortcutPackage extends ShortcutPackageItem { private long mLastKnownForegroundElapsedTime; + @GuardedBy("mLock") + private List mLastReportedTime = new ArrayList<>(); + private ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName, ShortcutPackageInfo spi) { super(shortcutUser, packageUserId, packageName, @@ -1352,6 +1363,30 @@ public boolean hasNonManifestShortcuts() { return false; } + void reportShortcutUsed(@NonNull final UsageStatsManagerInternal usageStatsManagerInternal, + @NonNull final String shortcutId) { + synchronized (mLock) { + final long currentTS = SystemClock.elapsedRealtime(); + final ShortcutService s = mShortcutUser.mService; + if (mLastReportedTime.isEmpty() + || mLastReportedTime.size() < REPORT_USAGE_BUFFER_SIZE) { + mLastReportedTime.add(currentTS); + } else if (currentTS - mLastReportedTime.get(0) > s.mSaveDelayMillis) { + mLastReportedTime.remove(0); + mLastReportedTime.add(currentTS); + } else { + return; + } + final long token = s.injectClearCallingIdentity(); + try { + usageStatsManagerInternal.reportShortcutUsage(getPackageName(), shortcutId, + getUser().getUserId()); + } finally { + s.injectRestoreCallingIdentity(token); + } + } + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) { pw.println(); diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 2e2883dcb2a5..f4c812743918 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -315,7 +315,7 @@ public boolean test(PackageInfo pi) { private CompressFormat mIconPersistFormat; private int mIconPersistQuality; - private int mSaveDelayMillis; + int mSaveDelayMillis; private final IPackageManager mIPackageManager; private final PackageManagerInternal mPackageManagerInternal; @@ -1566,6 +1566,19 @@ void injectEnforceCallingPermission( mContext.enforceCallingPermission(permission, message); } + private void verifyCallerUserId(@UserIdInt int userId) { + if (isCallerSystem()) { + return; // no check + } + + final int callingUid = injectBinderCallingUid(); + + // Otherwise, make sure the arguments are valid. + if (UserHandle.getUserId(callingUid) != userId) { + throw new SecurityException("Invalid user-ID"); + } + } + private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) { Preconditions.checkStringNotEmpty(packageName, "packageName"); @@ -2285,10 +2298,11 @@ public void reportShortcutUsed(String packageName, String shortcutId, int userId shortcutId, packageName, userId)); } + final ShortcutPackage ps; synchronized (mLock) { throwIfUserLockedL(userId); - final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId); + ps = getPackageShortcutsForPublisherLocked(packageName, userId); if (ps.findShortcutById(shortcutId) == null) { Log.w(TAG, String.format("reportShortcutUsed: package %s doesn't have shortcut %s", @@ -2297,16 +2311,13 @@ public void reportShortcutUsed(String packageName, String shortcutId, int userId } } - final long token = injectClearCallingIdentity(); - try { - mUsageStatsManagerInternal.reportShortcutUsage(packageName, shortcutId, userId); - } finally { - injectRestoreCallingIdentity(token); - } + ps.reportShortcutUsed(mUsageStatsManagerInternal, shortcutId); } @Override public boolean isRequestPinItemSupported(int callingUserId, int requestType) { + verifyCallerUserId(callingUserId); + final long token = injectClearCallingIdentity(); try { return mShortcutRequestPinProcessor diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 318c11141cfe..645ee1a2f12e 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -225,8 +225,6 @@ public class UserManagerService extends IUserManager.Stub { private static final int USER_VERSION = 7; - private static final int MAX_USER_STRING_LENGTH = 500; - private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms // Maximum number of managed profiles permitted per user is 1. This cannot be increased @@ -2420,16 +2418,18 @@ void writeUserLP(UserData userData, OutputStream os) if (userData.persistSeedData) { if (userData.seedAccountName != null) { serializer.attribute(null, ATTR_SEED_ACCOUNT_NAME, - truncateString(userData.seedAccountName)); + truncateString(userData.seedAccountName, + UserManager.MAX_ACCOUNT_STRING_LENGTH)); } if (userData.seedAccountType != null) { serializer.attribute(null, ATTR_SEED_ACCOUNT_TYPE, - truncateString(userData.seedAccountType)); + truncateString(userData.seedAccountType, + UserManager.MAX_ACCOUNT_STRING_LENGTH)); } } if (userInfo.name != null) { serializer.startTag(null, TAG_NAME); - serializer.text(truncateString(userInfo.name)); + serializer.text(truncateString(userInfo.name, UserManager.MAX_USER_NAME_LENGTH)); serializer.endTag(null, TAG_NAME); } synchronized (mRestrictionsLock) { @@ -2470,11 +2470,11 @@ void writeUserLP(UserData userData, OutputStream os) serializer.endDocument(); } - private String truncateString(String original) { - if (original == null || original.length() <= MAX_USER_STRING_LENGTH) { + private String truncateString(String original, int limit) { + if (original == null || original.length() <= limit) { return original; } - return original.substring(0, MAX_USER_STRING_LENGTH); + return original.substring(0, limit); } /* @@ -2819,7 +2819,7 @@ private UserInfo createUserInternalUnchecked(@Nullable String name, @UserInfoFla private UserInfo createUserInternalUncheckedNoTracing(@Nullable String name, @UserInfoFlag int flags, @UserIdInt int parentId, boolean preCreate, @Nullable String[] disallowedPackages, @NonNull TimingsTraceLog t) { - String truncatedName = truncateString(name); + String truncatedName = truncateString(name, UserManager.MAX_USER_NAME_LENGTH); // First try to use a pre-created user (if available). // NOTE: currently we don't support pre-created managed profiles if (!preCreate && (parentId < 0 && !UserInfo.isManagedProfile(flags))) { @@ -3877,9 +3877,14 @@ public void setSeedAccountData(int userId, String accountName, String accountTyp Slog.e(LOG_TAG, "No such user for settings seed data u=" + userId); return; } - userData.seedAccountName = truncateString(accountName); - userData.seedAccountType = truncateString(accountType); - userData.seedAccountOptions = accountOptions; + userData.seedAccountName = truncateString(accountName, + UserManager.MAX_ACCOUNT_STRING_LENGTH); + userData.seedAccountType = truncateString(accountType, + UserManager.MAX_ACCOUNT_STRING_LENGTH); + if (accountOptions != null && accountOptions.isBundleContentsWithinLengthLimit( + UserManager.MAX_ACCOUNT_OPTIONS_LENGTH)) { + userData.seedAccountOptions = accountOptions; + } userData.persistSeedData = persist; } if (persist) { diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 4ece487bf870..b47994d5aa22 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -1011,10 +1011,11 @@ private void removeDynamicPermission( if (bp == null) { return; } - if (bp.isDynamic()) { + if (!bp.isDynamic()) { // TODO: switch this back to SecurityException Slog.wtf(TAG, "Not allowed to modify non-dynamic permission " + permName); + return; } mSettings.removePermissionLocked(permName); if (callback != null) { @@ -3469,5 +3470,19 @@ public void removeOnRuntimePermissionStateChangedListener( PermissionManagerService.this.removeOnRuntimePermissionStateChangedListener( listener); } + + @Override + public void retainHardAndSoftRestrictedPermissions(@NonNull List permissions) { + synchronized (mLock) { + Iterator iterator = permissions.iterator(); + while (iterator.hasNext()) { + String permission = iterator.next(); + BasePermission basePermission = mSettings.mPermissions.get(permission); + if (basePermission == null || !basePermission.isHardOrSoftRestricted()) { + iterator.remove(); + } + } + } + } } } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java index a2f64eafe151..39ae8f589db2 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java @@ -36,6 +36,7 @@ * TODO: Should be merged into PermissionManagerInternal, but currently uses internal classes. */ public abstract class PermissionManagerServiceInternal extends PermissionManagerInternal { + /** * Callbacks invoked when interesting actions have been taken on a permission. *

@@ -212,4 +213,10 @@ public abstract void enforceCrossUserPermission(int callingUid, int userId, /** Get all permission that have a certain protection level */ public abstract @NonNull ArrayList getAllPermissionWithProtectionLevel( @PermissionInfo.Protection int protectionLevel); + + /** + * Removes invalid permissions which are not {@link PermissionInfo#FLAG_HARD_RESTRICTED} or + * {@link PermissionInfo#FLAG_SOFT_RESTRICTED} from the input. + */ + public abstract void retainHardAndSoftRestrictedPermissions(@NonNull List permissions); } diff --git a/services/print/java/com/android/server/print/PrintManagerService.java b/services/print/java/com/android/server/print/PrintManagerService.java index c9b9f3e6bd48..2bed4b5a81f5 100644 --- a/services/print/java/com/android/server/print/PrintManagerService.java +++ b/services/print/java/com/android/server/print/PrintManagerService.java @@ -252,12 +252,44 @@ public Icon getCustomPrinterIcon(PrinterId printerId, int userId) { } final long identity = Binder.clearCallingIdentity(); try { - return userState.getCustomPrinterIcon(printerId); + Icon icon = userState.getCustomPrinterIcon(printerId); + return validateIconUserBoundary(icon); } finally { Binder.restoreCallingIdentity(identity); } } + /** + * Validates the custom printer icon to see if it's not in the calling user space. + * If the condition is not met, return null. Otherwise, return the original icon. + * + * @param icon + * @return icon (validated) + */ + private Icon validateIconUserBoundary(Icon icon) { + // Refer to Icon#getUriString for context. The URI string is invalid for icons of + // incompatible types. + if (icon != null && (icon.getType() == Icon.TYPE_URI)) { + String encodedUser = icon.getUri().getEncodedUserInfo(); + + // If there is no encoded user, the URI is calling into the calling user space + if (encodedUser != null) { + int userId = Integer.parseInt(encodedUser); + // resolve encoded user + final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId); + + synchronized (mLock) { + // Only the current group members can get the printer icons. + if (resolveCallingProfileParentLocked(resolvedUserId) + != getCurrentUserId()) { + return null; + } + } + } + } + return icon; + } + @Override public void cancelPrintJob(PrintJobId printJobId, int appId, int userId) { if (printJobId == null) { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java index 18970322d854..27cf3502d489 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java @@ -1940,6 +1940,8 @@ public void testThrottling_resetByInternalCall() throws Exception { public void testReportShortcutUsed() { mRunningUsers.put(USER_10, true); + mService.updateConfigurationLocked( + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1"); runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { reset(mMockUsageStatsManagerInternal); diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index e9edba58a3dd..69548f839c1e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -16,6 +16,8 @@ package com.android.server.pm; +import static org.junit.Assert.assertTrue; + import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -24,6 +26,7 @@ import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.os.Bundle; +import android.os.PersistableBundle; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -601,6 +604,106 @@ public void testConcurrentUserCreate() throws Exception { assertEquals(canBeCreatedCount, created.get()); } + @Test + public void testAddUserAccountData_validStringValuesAreSaved_validBundleIsSaved() { + assumeManagedUsersSupported(); + + String userName = "User"; + String accountName = "accountName"; + String accountType = "accountType"; + String arrayKey = "StringArrayKey"; + String stringKey = "StringKey"; + String intKey = "IntKey"; + String nestedBundleKey = "PersistableBundleKey"; + String value1 = "Value 1"; + String value2 = "Value 2"; + String value3 = "Value 3"; + + UserInfo userInfo = mUserManager.createUser(userName, + UserManager.USER_TYPE_FULL_SECONDARY, 0); + + PersistableBundle accountOptions = new PersistableBundle(); + String[] stringArray = {value1, value2}; + accountOptions.putInt(intKey, 1234); + PersistableBundle nested = new PersistableBundle(); + nested.putString(stringKey, value3); + accountOptions.putPersistableBundle(nestedBundleKey, nested); + accountOptions.putStringArray(arrayKey, stringArray); + + mUserManager.clearSeedAccountData(); + mUserManager.setSeedAccountData(mContext.getUserId(), accountName, + accountType, accountOptions); + + //assert userName accountName and accountType were saved correctly + assertTrue(mUserManager.getUserInfo(userInfo.id).name.equals(userName)); + assertTrue(mUserManager.getSeedAccountName().equals(accountName)); + assertTrue(mUserManager.getSeedAccountType().equals(accountType)); + + //assert bundle with correct values was added + assertThat(mUserManager.getSeedAccountOptions().containsKey(arrayKey)).isTrue(); + assertThat(mUserManager.getSeedAccountOptions().getPersistableBundle(nestedBundleKey) + .getString(stringKey)).isEqualTo(value3); + assertThat(mUserManager.getSeedAccountOptions().getStringArray(arrayKey)[0]) + .isEqualTo(value1); + + mUserManager.removeUser(userInfo.id); + } + + @Test + public void testAddUserAccountData_invalidStringValuesAreTruncated_invalidBundleIsDropped() { + assumeManagedUsersSupported(); + + String tooLongString = generateLongString(); + String userName = "User " + tooLongString; + String accountType = "Account Type " + tooLongString; + String accountName = "accountName " + tooLongString; + String arrayKey = "StringArrayKey"; + String stringKey = "StringKey"; + String intKey = "IntKey"; + String nestedBundleKey = "PersistableBundleKey"; + String value1 = "Value 1"; + String value2 = "Value 2"; + + UserInfo userInfo = mUserManager.createUser(userName, + UserManager.USER_TYPE_FULL_SECONDARY, 0); + + PersistableBundle accountOptions = new PersistableBundle(); + String[] stringArray = {value1, value2}; + accountOptions.putInt(intKey, 1234); + PersistableBundle nested = new PersistableBundle(); + nested.putString(stringKey, tooLongString); + accountOptions.putPersistableBundle(nestedBundleKey, nested); + accountOptions.putStringArray(arrayKey, stringArray); + mUserManager.clearSeedAccountData(); + mUserManager.setSeedAccountData(mContext.getUserId(), accountName, + accountType, accountOptions); + + //assert userName was truncated + assertTrue(mUserManager.getUserInfo(userInfo.id).name.length() + == UserManager.MAX_USER_NAME_LENGTH); + + //assert accountName and accountType got truncated + assertTrue(mUserManager.getSeedAccountName().length() + == UserManager.MAX_ACCOUNT_STRING_LENGTH); + assertTrue(mUserManager.getSeedAccountType().length() + == UserManager.MAX_ACCOUNT_STRING_LENGTH); + + //assert bundle with invalid values was dropped + assertThat(mUserManager.getSeedAccountOptions() == null).isTrue(); + + mUserManager.removeUser(userInfo.id); + } + + private String generateLongString() { + String partialString = "Test Name Test Name Test Name Test Name Test Name Test Name Test " + + "Name Test Name Test Name Test Name "; //String of length 100 + StringBuilder resultString = new StringBuilder(); + for (int i = 0; i < 600; i++) { + resultString.append(partialString); + } + return resultString.toString(); + } + private boolean isPackageInstalledForUser(String packageName, int userId) { try { return mPackageManager.getPackageInfoAsUser(packageName, 0, userId) != null; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index 8aaf29a11033..cac620f409f3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -28,8 +28,10 @@ import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -624,6 +626,58 @@ public void testUpgradeAppBindsNewServices() throws Exception { } } + @Test + public void testUpgradeAppNoPermissionNoRebind() throws Exception { + Context context = spy(getContext()); + doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any()); + + ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, + mIpm, + APPROVAL_BY_COMPONENT); + + List packages = new ArrayList<>(); + packages.add("package"); + addExpectedServices(service, packages, 0); + + final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1"); + final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2"); + + // Both components are approved initially + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryPackages.clear(); + + loadXml(service); + + //Component package/C1 loses bind permission + when(mIpm.getServiceInfo(any(), anyInt(), anyInt())).thenAnswer( + (Answer) invocation -> { + ComponentName invocationCn = invocation.getArgument(0); + if (invocationCn != null) { + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = invocationCn.getPackageName(); + serviceInfo.name = invocationCn.getClassName(); + if (invocationCn.equals(unapprovedComponent)) { + serviceInfo.permission = "none"; + } else { + serviceInfo.permission = service.getConfig().bindPermission; + } + serviceInfo.metaData = null; + return serviceInfo; + } + return null; + } + ); + + // Trigger package update + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent)); + assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent)); + } + @Test public void testSetPackageOrComponentEnabled() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { 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 5c2e27080986..b6fd7823b173 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -2037,6 +2037,73 @@ public void testUpdateNotificationChannelFromPrivilegedListener_badUser() throws eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); } + @Test + public void testUpdateNotificationChannelFromPrivilegedListener_noSoundUriPermission() + throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + List associations = new ArrayList<>(); + associations.add("a"); + when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + .thenReturn(associations); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), + eq(mTestNotificationChannel.getId()), anyBoolean())) + .thenReturn(mTestNotificationChannel); + + final Uri soundUri = Uri.parse("content://media/test/sound/uri"); + final NotificationChannel updatedNotificationChannel = new NotificationChannel( + TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + updatedNotificationChannel.setSound(soundUri, + updatedNotificationChannel.getAudioAttributes()); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + assertThrows(SecurityException.class, + () -> mBinderService.updateNotificationChannelFromPrivilegedListener(null, PKG, + Process.myUserHandle(), updatedNotificationChannel)); + + verify(mPreferencesHelper, never()).updateNotificationChannel( + anyString(), anyInt(), any(), anyBoolean()); + + verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), + eq(Process.myUserHandle()), eq(mTestNotificationChannel), + eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); + } + + @Test + public void testUpdateNotificationChannelFromPrivilegedListener_noSoundUriPermission_sameSound() + throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + List associations = new ArrayList<>(); + associations.add("a"); + when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + .thenReturn(associations); + when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), + eq(mTestNotificationChannel.getId()), anyBoolean())) + .thenReturn(mTestNotificationChannel); + + final Uri soundUri = Settings.System.DEFAULT_NOTIFICATION_URI; + final NotificationChannel updatedNotificationChannel = new NotificationChannel( + TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + updatedNotificationChannel.setSound(soundUri, + updatedNotificationChannel.getAudioAttributes()); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + mBinderService.updateNotificationChannelFromPrivilegedListener( + null, PKG, Process.myUserHandle(), updatedNotificationChannel); + + verify(mPreferencesHelper, times(1)).updateNotificationChannel( + anyString(), anyInt(), any(), anyBoolean()); + + verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), + eq(Process.myUserHandle()), eq(mTestNotificationChannel), + eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); + } + @Test public void testGetNotificationChannelFromPrivilegedListener_cdm_success() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); @@ -4415,6 +4482,32 @@ public void testAllowForegroundToasts() throws Exception { } @Test + public void testDontCallShowToastAgainOnTheSameTextToast() throws Exception { + final String testPackage = "testPackageName"; + assertEquals(0, mService.mToastQueue.size()); + mService.isSystemUid = false; + + // package is not suspended + when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + .thenReturn(false); + + setAppInForegroundForToasts(mUid, true); + + Binder token = new Binder(); + INotificationManager nmService = (INotificationManager) mService.mService; + + // first time trying to show the toast, showToast gets called + nmService.enqueueTextToast(testPackage, token, "Text", 2000, 0, null); + verify(mStatusBar, times(1)) + .showToast(anyInt(), any(), any(), any(), any(), anyInt(), any()); + + // second time trying to show the same toast, showToast isn't called again (total number of + // invocations stays at one) + nmService.enqueueTextToast(testPackage, token, "Text", 2000, 0, null); + verify(mStatusBar, times(1)) + .showToast(anyInt(), any(), any(), any(), any(), anyInt(), any()); + } + public void testDisallowToastsFromSuspendedPackages() throws Exception { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); @@ -4480,6 +4573,74 @@ public void testAlwaysAllowSystemToasts() throws Exception { assertEquals(1, mService.mToastQueue.size()); } + @Test + public void testPrioritizeSystemToasts() throws Exception { + // Insert non-system toasts + final String testPackage = "testPackageName"; + assertEquals(0, mService.mToastQueue.size()); + mService.isSystemUid = false; + mService.isSystemAppId = false; + setToastRateIsWithinQuota(true); + setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); + + // package is not suspended + when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + .thenReturn(false); + + INotificationManager nmService = (INotificationManager) mService.mService; + + // Enqueue maximum number of toasts for test package + for (int i = 0; i < NotificationManagerService.MAX_PACKAGE_TOASTS; i++) { + nmService.enqueueTextToast(testPackage, new Binder(), "Text", 2000, 0, null); + } + + // Enqueue system toast + final String testPackageSystem = "testPackageNameSystem"; + mService.isSystemUid = true; + setIfPackageHasPermissionToAvoidToastRateLimiting(testPackageSystem, false); + when(mPackageManager.isPackageSuspendedForUser(testPackageSystem, UserHandle.getUserId(mUid))) + .thenReturn(false); + + nmService.enqueueToast(testPackageSystem, new Binder(), new TestableToastCallback(), 2000, 0); + + // System toast is inserted at the front of the queue, behind current showing toast + assertEquals(testPackageSystem, mService.mToastQueue.get(1).pkg); + } + + @Test + public void testPrioritizeSystemToasts_enqueueAfterExistingSystemToast() throws Exception { + // Insert system toasts + final String testPackageSystem1 = "testPackageNameSystem1"; + assertEquals(0, mService.mToastQueue.size()); + mService.isSystemUid = true; + setToastRateIsWithinQuota(true); + setIfPackageHasPermissionToAvoidToastRateLimiting(testPackageSystem1, false); + + // package is not suspended + when(mPackageManager.isPackageSuspendedForUser(testPackageSystem1, UserHandle.getUserId(mUid))) + .thenReturn(false); + + INotificationManager nmService = (INotificationManager) mService.mService; + + // Enqueue maximum number of toasts for test package + for (int i = 0; i < NotificationManagerService.MAX_PACKAGE_TOASTS; i++) { + nmService.enqueueTextToast(testPackageSystem1, new Binder(), "Text", 2000, 0, null); + } + + // Enqueue another system toast + final String testPackageSystem2 = "testPackageNameSystem2"; + mService.isSystemUid = true; + setIfPackageHasPermissionToAvoidToastRateLimiting(testPackageSystem2, false); + when(mPackageManager.isPackageSuspendedForUser(testPackageSystem2, UserHandle.getUserId(mUid))) + .thenReturn(false); + + nmService.enqueueToast(testPackageSystem2, new Binder(), new TestableToastCallback(), 2000, 0); + + // System toast is inserted at the back of the queue, after the other system toasts + assertEquals(testPackageSystem2, + mService.mToastQueue.get(mService.mToastQueue.size() - 1).pkg); + } + @Test public void testOnNotificationSmartReplySent() { final int replyIndex = 2; diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java index a95b6f11e98a..5053ceedc703 100644 --- a/test-mock/src/android/test/mock/MockContext.java +++ b/test-mock/src/android/test/mock/MockContext.java @@ -756,6 +756,12 @@ public Context createPackageContextAsUser(String packageName, int flags, UserHan throw new UnsupportedOperationException(); } + /** {@hide} */ + @Override + public Context createContextAsUser(UserHandle user, @CreatePackageOptions int flags) { + throw new UnsupportedOperationException(); + } + /** {@hide} */ @Override public int getUserId() {