diff --git a/core/java/android/app/ILocaleManager.aidl b/core/java/android/app/ILocaleManager.aidl new file mode 100644 index 0000000000000..348cb2d30739a --- /dev/null +++ b/core/java/android/app/ILocaleManager.aidl @@ -0,0 +1,43 @@ + +/* + * Copyright (C) 2021 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.app; + +import android.os.LocaleList; + +/** + * Internal interface used to control app-specific locales. + * + *
Use the {@link android.app.LocaleManager} class rather than going through
+ * this Binder interface directly. See {@link android.app.LocaleManager} for
+ * more complete documentation.
+ *
+ * @hide
+ */
+ interface ILocaleManager {
+
+ /**
+ * Sets a specified app’s app-specific UI locales.
+ */
+ void setApplicationLocales(String packageName, int userId, in LocaleList locales);
+
+ /**
+ * Returns the specified app's app-specific locales.
+ */
+ LocaleList getApplicationLocales(String packageName, int userId);
+
+ }
\ No newline at end of file
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 232e220a6bd4f..6ea6fe037918a 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3801,6 +3801,7 @@ public abstract class Context {
//@hide: TIME_ZONE_DETECTOR_SERVICE,
PERMISSION_SERVICE,
LIGHTS_SERVICE,
+ LOCALE_SERVICE,
//@hide: PEOPLE_SERVICE,
//@hide: DEVICE_STATE_SERVICE,
//@hide: SPEECH_RECOGNITION_SERVICE,
@@ -5783,6 +5784,15 @@ public abstract class Context {
*/
public static final String DISPLAY_HASH_SERVICE = "display_hash";
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.app.LocaleManager}.
+ *
+ * @see #getSystemService(String)
+ * @hide
+ */
+ public static final String LOCALE_SERVICE = "locale";
+
/**
* Determine whether the given permission is allowed for a particular
* process and user ID running in the system.
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index b9eec6e261e1e..7f0c5d443b1cb 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -464,6 +464,10 @@
This service is API entry point for storing app-specific UI locales
+ */
+public class LocaleManagerService extends SystemService {
+ private static final String TAG = "LocaleManagerService";
+ private final Context mContext;
+ private final LocaleManagerService.LocaleManagerBinderService mBinderService;
+ private ActivityTaskManagerInternal mActivityTaskManagerInternal;
+ private ActivityManagerInternal mActivityManagerInternal;
+ private PackageManagerInternal mPackageManagerInternal;
+ public static final boolean DEBUG = false;
+
+ public LocaleManagerService(Context context) {
+ super(context);
+ mContext = context;
+ mBinderService = new LocaleManagerBinderService();
+ mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+ }
+
+ @VisibleForTesting
+ LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal,
+ ActivityManagerInternal activityManagerInternal,
+ PackageManagerInternal packageManagerInternal) {
+ super(context);
+ mContext = context;
+ mBinderService = new LocaleManagerBinderService();
+ mActivityTaskManagerInternal = activityTaskManagerInternal;
+ mActivityManagerInternal = activityManagerInternal;
+ mPackageManagerInternal = packageManagerInternal;
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.LOCALE_SERVICE, mBinderService);
+ }
+
+ private final class LocaleManagerBinderService extends ILocaleManager.Stub {
+ @Override
+ public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
+ @NonNull LocaleList locales) throws RemoteException {
+ LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales);
+ }
+
+ @Override
+ @NonNull
+ public LocaleList getApplicationLocales(@NonNull String appPackageName,
+ @UserIdInt int userId) throws RemoteException {
+ return LocaleManagerService.this.getApplicationLocales(appPackageName, userId);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ LocaleManagerService.this.dump(fd, pw, args);
+ }
+
+ @Override
+ public void onShellCommand(FileDescriptor in, FileDescriptor out,
+ FileDescriptor err, String[] args, ShellCallback callback,
+ ResultReceiver resultReceiver) {
+ (new LocaleManagerShellCommand(mBinderService))
+ .exec(this, in, out, err, args, callback, resultReceiver);
+ }
+ }
+
+ /**
+ * Sets the current UI locales for a specified app.
+ */
+ public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
+ @NonNull LocaleList locales) throws RemoteException, IllegalArgumentException {
+ requireNonNull(appPackageName);
+ requireNonNull(locales);
+
+ //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
+ userId = mActivityManagerInternal.handleIncomingUser(
+ Binder.getCallingPid(), Binder.getCallingUid(), userId,
+ false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+ "setApplicationLocales", appPackageName);
+
+ // This function handles two types of set operations:
+ // 1.) A normal, non-privileged app setting its own locale.
+ // 2.) A privileged system service setting locales of another package.
+ // The least privileged case is a normal app performing a set, so check that first and
+ // set locales if the package name is owned by the app. Next, check if the caller has the
+ // necessary permission and set locales.
+ boolean isCallerOwner = isPackageOwnedByCaller(appPackageName, userId);
+ if (!isCallerOwner) {
+ enforceChangeConfigurationPermission();
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ setApplicationLocalesUnchecked(appPackageName, userId, locales);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+
+ private void setApplicationLocalesUnchecked(@NonNull String appPackageName,
+ @UserIdInt int userId, @NonNull LocaleList locales) {
+ if (DEBUG) {
+ Slog.d(TAG, "setApplicationLocales: setting locales for package " + appPackageName
+ + " and user " + userId);
+ }
+ final ActivityTaskManagerInternal.PackageConfigurationUpdater updater =
+ mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName,
+ userId);
+ updater.setLocales(locales).commit();
+ }
+
+
+ /**
+ * Checks if the package is owned by the calling app or not for the given user id.
+ *
+ * @throws IllegalArgumentException if package not found for given userid
+ */
+ private boolean isPackageOwnedByCaller(String appPackageName, int userId) {
+ final int uid = mPackageManagerInternal
+ .getPackageUid(appPackageName, /* flags */ 0, userId);
+ if (uid < 0) {
+ Slog.w(TAG, "Unknown package " + appPackageName + " for user " + userId);
+ throw new IllegalArgumentException("Unknown package: " + appPackageName
+ + " for user " + userId);
+ }
+ //Once valid package found, ignore the userId part for validating package ownership
+ //as apps with INTERACT_ACROSS_USERS permission could be changing locale for different user.
+ return UserHandle.isSameApp(Binder.getCallingUid(), uid);
+ }
+
+ private void enforceChangeConfigurationPermission() {
+ mContext.enforceCallingPermission(
+ android.Manifest.permission.CHANGE_CONFIGURATION, "setApplicationLocales");
+ }
+
+ /**
+ * Returns the current UI locales for the specified app.
+ */
+ @NonNull
+ public LocaleList getApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId)
+ throws RemoteException, IllegalArgumentException {
+ requireNonNull(appPackageName);
+
+ //Allow apps with INTERACT_ACROSS_USERS permission to query locales for different user.
+ userId = mActivityManagerInternal.handleIncomingUser(
+ Binder.getCallingPid(), Binder.getCallingUid(), userId,
+ false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+ "getApplicationLocales", appPackageName);
+
+ // This function handles two types of query operations:
+ // 1.) A normal, non-privileged app querying its own locale.
+ // 2.) 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)) {
+ enforceReadAppSpecificLocalesPermission();
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return getApplicationLocalesUnchecked(appPackageName, userId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private LocaleList getApplicationLocalesUnchecked(@NonNull String appPackageName,
+ @UserIdInt int userId) {
+ if (DEBUG) {
+ Slog.d(TAG, "getApplicationLocales: fetching locales for package " + appPackageName
+ + " and user " + userId);
+ }
+
+ final ActivityTaskManagerInternal.PackageConfig appConfig =
+ mActivityTaskManagerInternal.getApplicationConfig(appPackageName, userId);
+ if (appConfig == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "getApplicationLocales: application config not found for "
+ + appPackageName + " and user id " + userId);
+ }
+ return LocaleList.getEmptyLocaleList();
+ }
+ LocaleList locales = appConfig.mLocales;
+ return locales != null ? locales : LocaleList.getEmptyLocaleList();
+ }
+
+ private void enforceReadAppSpecificLocalesPermission() {
+ mContext.enforceCallingPermission(
+ android.Manifest.permission.READ_APP_SPECIFIC_LOCALES,
+ "getApplicationLocales");
+ }
+
+ /**
+ * Dumps useful info related to service.
+ */
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ // TODO(b/201766221): Implement when there is state.
+ }
+}
diff --git a/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
new file mode 100644
index 0000000000000..769ea1797fb77
--- /dev/null
+++ b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locales;
+
+import android.app.ActivityManager;
+import android.app.ILocaleManager;
+import android.os.LocaleList;
+import android.os.RemoteException;
+import android.os.ShellCommand;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+
+/**
+ * Shell commands for {@link LocaleManagerService}
+ */
+public class LocaleManagerShellCommand extends ShellCommand {
+ private final ILocaleManager mBinderService;
+
+ LocaleManagerShellCommand(ILocaleManager localeManager) {
+ mBinderService = localeManager;
+ }
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+ switch (cmd) {
+ case "set-app-locales":
+ return runSetAppLocales();
+ case "get-app-locales":
+ return runGetAppLocales();
+ default: {
+ return handleDefaultCommands(cmd);
+ }
+ }
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("Locale manager (locale) shell commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" set-app-locales