Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f2d3022e authored by “Ankita's avatar “Ankita
Browse files

Add new LocaleManagerService and its shell commands

Ignore-AOSP-First: permission present in internal branch only

Test: tested manually via adb

Bug: 194094788
Bug: 194484378

Change-Id: I413a74fc1b15d164de7f2098e975e0b792685394
parent 396c9c5f
Loading
Loading
Loading
Loading
+43 −0
Original line number Diff line number Diff line

/*
 * 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
+10 −0
Original line number Diff line number Diff line
@@ -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.
+4 −0
Original line number Diff line number Diff line
@@ -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" />

+246 −0
Original line number Diff line number Diff line
/*
 * 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.
    }
}
+159 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
Loading