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

Commit 655e90e0 authored by Ankita Vyas's avatar Ankita Vyas Committed by Android (Google) Code Review
Browse files

Merge "Add new LocaleManagerService and its shell commands"

parents c0ca4da6 f2d3022e
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