diff --git a/core/api/current.txt b/core/api/current.txt index ef19d26674690..84e0b9f66c3f0 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -5713,6 +5713,7 @@ package android.app { public class LocaleManager { method @NonNull public android.os.LocaleList getApplicationLocales(); + method @NonNull @RequiresPermission(value="android.permission.READ_APP_SPECIFIC_LOCALES", conditional=true) public android.os.LocaleList getApplicationLocales(@NonNull String); method public void setApplicationLocales(@NonNull android.os.LocaleList); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 58893baff0214..313fc75ed668e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -799,7 +799,6 @@ package android.app { } public class LocaleManager { - method @NonNull @RequiresPermission(android.Manifest.permission.READ_APP_SPECIFIC_LOCALES) public android.os.LocaleList getApplicationLocales(@NonNull String); method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void setApplicationLocales(@NonNull String, @NonNull android.os.LocaleList); } diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java index 2aa7c8e60236a..332a6212b9e36 100644 --- a/core/java/android/app/LocaleManager.java +++ b/core/java/android/app/LocaleManager.java @@ -95,18 +95,20 @@ public class LocaleManager { } /** - * Returns the current UI locales for a specific app (described by package name). + * Returns the current UI locales for a specified app (described by package name). * *

Returns a {@link LocaleList#getEmptyLocaleList()} if no app-specific locales are set. * - * Note: Non-system apps should read Locale information via their in-process - * LocaleLists. + *

This API can be used by an app's installer + * (per {@link android.content.pm.InstallSourceInfo#getInstallingPackageName}) to retrieve + * the app's locales. + * All other cases require {@code android.Manifest.permission#READ_APP_SPECIFIC_LOCALES}. + * Apps should generally retrieve their own locales via their in-process LocaleLists, + * or by calling {@link #getApplicationLocales()}. * * @param appPackageName the package name of the app for which to retrieve the locales. - * @hide */ - @SystemApi - @RequiresPermission(Manifest.permission.READ_APP_SPECIFIC_LOCALES) + @RequiresPermission(value = Manifest.permission.READ_APP_SPECIFIC_LOCALES, conditional = true) @UserHandleAware @NonNull public LocaleList getApplicationLocales(@NonNull String appPackageName) { diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index 1657b220d32cf..d459f8df99b60 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -239,18 +239,13 @@ public class LocaleManagerService extends SystemService { */ private void notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales) { - try { - String installingPackageName = mContext.getPackageManager() - .getInstallSourceInfo(appPackageName).getInstallingPackageName(); - if (installingPackageName != null) { - Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, - appPackageName, locales); - //Set package name to ensure that only installer of the app receives this intent. - intent.setPackage(installingPackageName); - mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); - } - } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Package not found " + appPackageName); + String installingPackageName = getInstallingPackageName(appPackageName); + if (installingPackageName != null) { + Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, + appPackageName, locales); + //Set package name to ensure that only installer of the app receives this intent. + intent.setPackage(installingPackageName); + mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); } } @@ -291,8 +286,7 @@ public class LocaleManagerService extends SystemService { */ private boolean isPackageOwnedByCaller(String appPackageName, int userId, @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics) { - final int uid = mPackageManagerInternal - .getPackageUid(appPackageName, /* flags */ 0, userId); + final int uid = getPackageUid(appPackageName, userId); if (uid < 0) { Slog.w(TAG, "Unknown package " + appPackageName + " for user " + userId); if (atomRecordForMetrics != null) { @@ -336,13 +330,17 @@ public class LocaleManagerService extends SystemService { false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL, "getApplicationLocales", appPackageName); - // This function handles two types of query operations: + // This function handles three types of query operations: // 1.) A normal, non-privileged app querying its own locale. - // 2.) A privileged system service querying locales of another package. + // 2.) The installer of the given app querying locales of a package installed + // by said installer. + // 3.) A privileged system service querying locales of another package. // The least privileged case is a normal app performing a query, so check that first and - // get locales if the package name is owned by the app. Next, check if the caller has the - // necessary permission and get locales. - if (!isPackageOwnedByCaller(appPackageName, userId)) { + // get locales if the package name is owned by the app. Next check if the calling app + // is the installer of the given app and get locales. If neither conditions matched, + // check if the caller has the necessary permission and fetch locales. + if (!isPackageOwnedByCaller(appPackageName, userId) + && !isCallerInstaller(appPackageName, userId)) { enforceReadAppSpecificLocalesPermission(); } final long token = Binder.clearCallingIdentity(); @@ -374,12 +372,41 @@ public class LocaleManagerService extends SystemService { return locales != null ? locales : LocaleList.getEmptyLocaleList(); } + /** + * Checks if the calling app is the installer of the app whose locale changed. + */ + private boolean isCallerInstaller(String appPackageName, int userId) { + String installingPackageName = getInstallingPackageName(appPackageName); + if (installingPackageName != null) { + // Get the uid of installer-on-record to compare with the calling uid. + int installerUid = getPackageUid(installingPackageName, userId); + return installerUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(), installerUid); + } + return false; + } + private void enforceReadAppSpecificLocalesPermission() { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.READ_APP_SPECIFIC_LOCALES, "getApplicationLocales"); } + private int getPackageUid(String appPackageName, int userId) { + return mPackageManagerInternal + .getPackageUid(appPackageName, /* flags */ 0, userId); + } + + @Nullable + private String getInstallingPackageName(String packageName) { + try { + return mContext.getPackageManager() + .getInstallSourceInfo(packageName).getInstallingPackageName(); + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "Package not found " + packageName); + } + return null; + } + /** * Dumps useful info related to service. */ diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index 658f8d52d74b4..ee2bb0ad429af 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -58,6 +59,7 @@ import org.mockito.Mock; @RunWith(AndroidJUnit4.class) public class LocaleManagerServiceTest { private static final String DEFAULT_PACKAGE_NAME = "com.android.myapp"; + private static final String DEFAULT_INSTALLER_PACKAGE_NAME = "com.android.myapp.installer"; private static final int DEFAULT_USER_ID = 0; private static final int DEFAULT_UID = Binder.getCallingUid() + 100; private static final int INVALID_UID = -1; @@ -66,7 +68,8 @@ public class LocaleManagerServiceTest { LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS); private static final InstallSourceInfo DEFAULT_INSTALL_SOURCE_INFO = new InstallSourceInfo( /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, - /* originatingPackageName = */ null, /* installingPackageName = */ null); + /* originatingPackageName = */ null, + /* installingPackageName = */ DEFAULT_INSTALLER_PACKAGE_NAME); private LocaleManagerService mLocaleManagerService; private LocaleManagerBackupHelper mMockBackupHelper; @@ -89,7 +92,7 @@ public class LocaleManagerServiceTest { mMockActivityManager = mock(ActivityManagerInternal.class); mMockPackageManagerInternal = mock(PackageManagerInternal.class); - // For unit tests, set the default (null) installer info + // For unit tests, set the default installer info PackageManager mockPackageManager = mock(PackageManager.class); doReturn(DEFAULT_INSTALL_SOURCE_INFO).when(mockPackageManager) .getInstallSourceInfo(anyString()); @@ -275,6 +278,23 @@ public class LocaleManagerServiceTest { assertEquals(DEFAULT_LOCALES, locales); } + @Test + public void testGetApplicationLocales_callerIsInstaller_returnsLocales() + throws Exception { + doReturn(DEFAULT_UID).when(mMockPackageManagerInternal) + .getPackageUid(eq(DEFAULT_PACKAGE_NAME), anyLong(), anyInt()); + doReturn(Binder.getCallingUid()).when(mMockPackageManagerInternal) + .getPackageUid(eq(DEFAULT_INSTALLER_PACKAGE_NAME), anyLong(), anyInt()); + doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES)) + .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt()); + + LocaleList locales = + mLocaleManagerService.getApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + + verify(mMockContext, never()).enforceCallingOrSelfPermission(any(), any()); + assertEquals(DEFAULT_LOCALES, locales); + } + private static void assertNoLocalesStored(LocaleList locales) { assertNull(locales); }