Loading core/api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -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); } core/api/system-current.txt +0 −1 Original line number Diff line number Diff line Loading @@ -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); } core/java/android/app/LocaleManager.java +8 −6 Original line number Diff line number Diff line Loading @@ -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). * * <p>Returns a {@link LocaleList#getEmptyLocaleList()} if no app-specific locales are set. * * <b>Note:</b> Non-system apps should read Locale information via their in-process * LocaleLists. * <p>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) { Loading services/core/java/com/android/server/locales/LocaleManagerService.java +46 −19 Original line number Diff line number Diff line Loading @@ -239,9 +239,7 @@ public class LocaleManagerService extends SystemService { */ private void notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales) { try { String installingPackageName = mContext.getPackageManager() .getInstallSourceInfo(appPackageName).getInstallingPackageName(); String installingPackageName = getInstallingPackageName(appPackageName); if (installingPackageName != null) { Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, appPackageName, locales); Loading @@ -249,9 +247,6 @@ public class LocaleManagerService extends SystemService { intent.setPackage(installingPackageName); mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); } } catch (PackageManager.NameNotFoundException e) { Slog.w(TAG, "Package not found " + appPackageName); } } /** Loading Loading @@ -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) { Loading Loading @@ -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(); Loading Loading @@ -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. */ Loading services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +22 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading @@ -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()); Loading Loading @@ -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); } Loading Loading
core/api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -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); }
core/api/system-current.txt +0 −1 Original line number Diff line number Diff line Loading @@ -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); }
core/java/android/app/LocaleManager.java +8 −6 Original line number Diff line number Diff line Loading @@ -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). * * <p>Returns a {@link LocaleList#getEmptyLocaleList()} if no app-specific locales are set. * * <b>Note:</b> Non-system apps should read Locale information via their in-process * LocaleLists. * <p>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) { Loading
services/core/java/com/android/server/locales/LocaleManagerService.java +46 −19 Original line number Diff line number Diff line Loading @@ -239,9 +239,7 @@ public class LocaleManagerService extends SystemService { */ private void notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales) { try { String installingPackageName = mContext.getPackageManager() .getInstallSourceInfo(appPackageName).getInstallingPackageName(); String installingPackageName = getInstallingPackageName(appPackageName); if (installingPackageName != null) { Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, appPackageName, locales); Loading @@ -249,9 +247,6 @@ public class LocaleManagerService extends SystemService { intent.setPackage(installingPackageName); mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); } } catch (PackageManager.NameNotFoundException e) { Slog.w(TAG, "Package not found " + appPackageName); } } /** Loading Loading @@ -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) { Loading Loading @@ -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(); Loading Loading @@ -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. */ Loading
services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +22 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading @@ -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()); Loading Loading @@ -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); } Loading