diff options
8 files changed, 478 insertions, 0 deletions
diff --git a/core/java/android/app/ILocaleManager.aidl b/core/java/android/app/ILocaleManager.aidl new file mode 100644 index 000000000000..348cb2d30739 --- /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. + * + * <p>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 3ab070f2395c..20ee695d1986 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, @@ -5784,6 +5785,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 b9eec6e261e1..7f0c5d443b1c 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -464,6 +464,10 @@ <uses-permission android:name="android.permission.MANAGE_TIME_AND_ZONE_DETECTION" /> <uses-permission android:name="android.permission.SUGGEST_EXTERNAL_TIME" /> + <!-- Permissions needed for testing locale manager service --> + <!-- todo(b/201957547): Add CTS test name when available--> + <uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" /> + <!-- Permission required for CTS test - android.server.biometrics --> <uses-permission android:name="android.permission.USE_BIOMETRIC" /> diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java new file mode 100644 index 000000000000..0045499ec8aa --- /dev/null +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -0,0 +1,246 @@ +/* + * 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 static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.ActivityManagerInternal; +import android.app.ILocaleManager; +import android.content.Context; +import android.content.pm.PackageManagerInternal; +import android.os.Binder; +import android.os.LocaleList; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.os.UserHandle; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; +import com.android.server.SystemService; +import com.android.server.wm.ActivityTaskManagerInternal; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * The implementation of ILocaleManager.aidl. + * + * <p>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 000000000000..769ea1797fb7 --- /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 <PACKAGE_NAME> [--user <USER_ID>] [--locales <LOCALE_INFO>]"); + pw.println(" Set the locales for the specified app."); + pw.println(" --user <USER_ID>: apply for the given user, " + + "the current user is used when unspecified."); + pw.println(" --locales <LOCALE_INFO>: The language tags of locale to be included " + + "as a single String separated by commas"); + pw.println(" Empty locale list is used when unspecified."); + pw.println(" eg. en,en-US,hi "); + pw.println(" get-app-locales <PACKAGE_NAME> [--user <USER_ID>]"); + pw.println(" Get the locales for the specified app."); + pw.println(" --user <USER_ID>: get for the given user, " + + "the current user is used when unspecified."); + } + + private int runSetAppLocales() { + final PrintWriter err = getErrPrintWriter(); + String packageName = getNextArg(); + + if (packageName != null) { + int userId = ActivityManager.getCurrentUser(); + LocaleList locales = LocaleList.getEmptyLocaleList(); + do { + String option = getNextOption(); + if (option == null) { + break; + } + switch (option) { + case "--user": { + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + } + case "--locales": { + locales = parseLocales(); + break; + } + default: { + throw new IllegalArgumentException("Unknown option: " + option); + } + } + } while (true); + + try { + mBinderService.setApplicationLocales(packageName, userId, locales); + } catch (RemoteException e) { + getOutPrintWriter().println("Remote Exception: " + e); + } catch (IllegalArgumentException e) { + getOutPrintWriter().println("Unknown package " + packageName + + " for userId " + userId); + } + } else { + err.println("Error: no package specified"); + return -1; + } + return 0; + } + + private int runGetAppLocales() { + final PrintWriter err = getErrPrintWriter(); + String packageName = getNextArg(); + + if (packageName != null) { + int userId = ActivityManager.getCurrentUser(); + do { + String option = getNextOption(); + if (option == null) { + break; + } + if ("--user".equals(option)) { + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + } else { + throw new IllegalArgumentException("Unknown option: " + option); + } + } while (true); + try { + LocaleList locales = mBinderService.getApplicationLocales(packageName, userId); + getOutPrintWriter().println("Locales for " + packageName + + " for user " + userId + " are " + locales); + } catch (RemoteException e) { + getOutPrintWriter().println("Remote Exception: " + e); + } catch (IllegalArgumentException e) { + getOutPrintWriter().println("Unknown package " + packageName + + " for userId " + userId); + } + } else { + err.println("Error: no package specified"); + return -1; + } + return 0; + } + + private LocaleList parseLocales() { + if (getRemainingArgsCount() <= 0) { + return LocaleList.getEmptyLocaleList(); + } + String[] args = peekRemainingArgs(); + String inputLocales = args[0]; + LocaleList locales = LocaleList.forLanguageTags(inputLocales); + return locales; + } +} diff --git a/services/core/java/com/android/server/locales/OWNERS b/services/core/java/com/android/server/locales/OWNERS new file mode 100644 index 000000000000..be284a766c50 --- /dev/null +++ b/services/core/java/com/android/server/locales/OWNERS @@ -0,0 +1,3 @@ +roosa@google.com +pratyushmore@google.com +goldmanj@google.com diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index dae12754904f..fc60bab116db 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -133,6 +133,7 @@ import com.android.server.input.InputManagerService; import com.android.server.inputmethod.InputMethodManagerService; import com.android.server.integrity.AppIntegrityManagerService; import com.android.server.lights.LightsService; +import com.android.server.locales.LocaleManagerService; import com.android.server.location.LocationManagerService; import com.android.server.media.MediaRouterService; import com.android.server.media.metrics.MediaMetricsManagerService; @@ -1684,6 +1685,15 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(UiModeManagerService.class); t.traceEnd(); + t.traceBegin("StartLocaleManagerService"); + try { + mSystemServiceManager.startService(LocaleManagerService.class); + } catch (Throwable e) { + reportWtf("starting LocaleManagerService service", e); + } + t.traceEnd(); + + if (!mOnlyCore) { t.traceBegin("UpdatePackagesIfNeeded"); try { diff --git a/services/tests/servicestests/src/com/android/server/locales/OWNERS b/services/tests/servicestests/src/com/android/server/locales/OWNERS new file mode 100644 index 000000000000..be284a766c50 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locales/OWNERS @@ -0,0 +1,3 @@ +roosa@google.com +pratyushmore@google.com +goldmanj@google.com |