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

Commit 55fef411 authored by Josh Hou's avatar Josh Hou
Browse files

[Panlingual] Backup and restore the package name of per-app locales set from a delegate selector

Backup and restore the package name of per-app locales set from a delegate selector.

Bug: 234131462
Test: 1.Run some CTS and unit test cases.
      2.Manual test.
Change-Id: Ie2dd536d4df939b521cf00fa88beacb8308523c4
parent d589ad6a
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ import android.os.LocaleList;
     /**
      * Sets a specified app’s app-specific UI locales.
      */
     void setApplicationLocales(String packageName, int userId, in LocaleList locales);
     void setApplicationLocales(String packageName, int userId, in LocaleList locales, boolean fromDelegate);

     /**
      * Returns the specified app's app-specific locales.
+7 −2
Original line number Diff line number Diff line
@@ -71,7 +71,7 @@ public class LocaleManager {
     */
    @UserHandleAware
    public void setApplicationLocales(@NonNull LocaleList locales) {
        setApplicationLocales(mContext.getPackageName(), locales);
        setApplicationLocales(mContext.getPackageName(), locales, false);
    }

    /**
@@ -100,9 +100,14 @@ public class LocaleManager {
    @RequiresPermission(Manifest.permission.CHANGE_CONFIGURATION)
    @UserHandleAware
    public void setApplicationLocales(@NonNull String appPackageName, @NonNull LocaleList locales) {
        setApplicationLocales(appPackageName, locales, true);
    }

    private void setApplicationLocales(@NonNull String appPackageName, @NonNull LocaleList locales,
            boolean fromDelegate) {
        try {
            mService.setApplicationLocales(appPackageName, mContext.getUser().getIdentifier(),
                    locales);
                    locales, fromDelegate);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
+172 −27
Original line number Diff line number Diff line
@@ -27,14 +27,17 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.os.HandlerThread;
import android.os.LocaleList;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;
@@ -44,18 +47,20 @@ import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;

/**
 * Helper class for managing backup and restore of app-specific locales.
@@ -68,9 +73,14 @@ class LocaleManagerBackupHelper {
    private static final String PACKAGE_XML_TAG = "package";
    private static final String ATTR_PACKAGE_NAME = "name";
    private static final String ATTR_LOCALES = "locales";
    private static final String ATTR_CREATION_TIME_MILLIS = "creationTimeMillis";
    private static final String ATTR_DELEGATE_SELECTOR = "delegate_selector";

    private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android";
    /**
     * The name of the xml file used to persist the target package name that sets per-app locales
     * from the delegate selector.
     */
    private static final String LOCALES_FROM_DELEGATE_PREFS = "LocalesFromDelegatePrefs.xml";
    // Stage data would be deleted on reboot since it's stored in memory. So it's retained until
    // retention period OR next reboot, whichever happens earlier.
    private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
@@ -85,23 +95,28 @@ class LocaleManagerBackupHelper {
    // SparseArray because it is more memory-efficient than a HashMap.
    private final SparseArray<StagedData> mStagedData;

    // SharedPreferences to store packages whose app-locale was set by a delegate, as opposed to
    // the application setting the app-locale itself.
    private final SharedPreferences mDelegateAppLocalePackages;
    private final BroadcastReceiver mUserMonitor;

    LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
            PackageManager packageManager, HandlerThread broadcastHandlerThread) {
        this(localeManagerService.mContext, localeManagerService, packageManager, Clock.systemUTC(),
                new SparseArray<>(), broadcastHandlerThread);
                new SparseArray<>(), broadcastHandlerThread, null);
    }

    @VisibleForTesting LocaleManagerBackupHelper(Context context,
            LocaleManagerService localeManagerService,
            PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData,
            HandlerThread broadcastHandlerThread) {
            HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages) {
        mContext = context;
        mLocaleManagerService = localeManagerService;
        mPackageManager = packageManager;
        mClock = clock;
        mStagedData = stagedData;
        mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages
                : createPersistedInfo();

        mUserMonitor = new UserMonitor();
        IntentFilter filter = new IntentFilter();
@@ -127,20 +142,29 @@ class LocaleManagerBackupHelper {
            cleanStagedDataForOldEntriesLocked();
        }

        HashMap<String, String> pkgStates = new HashMap<>();
        HashMap<String, LocalesInfo> pkgStates = new HashMap<>();
        for (ApplicationInfo appInfo : mPackageManager.getInstalledApplicationsAsUser(
                PackageManager.ApplicationInfoFlags.of(0), userId)) {
            try {
                LocaleList appLocales = mLocaleManagerService.getApplicationLocales(
                        appInfo.packageName,
                        userId);
                // Backup locales only for apps which do have app-specific overrides.
                // Backup locales and package names for per-app locales set from a delegate
                // selector only for apps which do have app-specific overrides.
                if (!appLocales.isEmpty()) {
                    if (DEBUG) {
                        Slog.d(TAG, "Add package=" + appInfo.packageName + " locales="
                                + appLocales.toLanguageTags() + " to backup payload");
                    }
                    pkgStates.put(appInfo.packageName, appLocales.toLanguageTags());
                    boolean localeSetFromDelegate = false;
                    if (mDelegateAppLocalePackages != null) {
                        localeSetFromDelegate = mDelegateAppLocalePackages.getStringSet(
                                Integer.toString(userId), Collections.<String>emptySet()).contains(
                                appInfo.packageName);
                    }
                    LocalesInfo localesInfo = new LocalesInfo(appLocales.toLanguageTags(),
                            localeSetFromDelegate);
                    pkgStates.put(appInfo.packageName, localesInfo);
                }
            } catch (RemoteException | IllegalArgumentException e) {
                Slog.e(TAG, "Exception when getting locales for package: " + appInfo.packageName,
@@ -200,7 +224,7 @@ class LocaleManagerBackupHelper {

        final ByteArrayInputStream inputStream = new ByteArrayInputStream(payload);

        HashMap<String, String> pkgStates;
        HashMap<String, LocalesInfo> pkgStates;
        try {
            // Parse the input blob into a list of BackupPackageState.
            final TypedXmlPullParser parser = Xml.newFastPullParser();
@@ -222,16 +246,17 @@ class LocaleManagerBackupHelper {
            StagedData stagedData = new StagedData(mClock.millis(), new HashMap<>());

            for (String pkgName : pkgStates.keySet()) {
                String languageTags = pkgStates.get(pkgName);
                LocalesInfo localesInfo = pkgStates.get(pkgName);
                // Check if the application is already installed for the concerned user.
                if (isPackageInstalledForUser(pkgName, userId)) {
                    // Don't apply the restore if the locales have already been set for the app.
                    checkExistingLocalesAndApplyRestore(pkgName, languageTags, userId);
                    checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
                } else {
                    // Stage the data if the app isn't installed.
                    stagedData.mPackageStates.put(pkgName, languageTags);
                    stagedData.mPackageStates.put(pkgName, localesInfo);
                    if (DEBUG) {
                        Slog.d(TAG, "Add locales=" + languageTags
                        Slog.d(TAG, "Add locales=" + localesInfo.mLocales
                                + " fromDelegate=" + localesInfo.mSetFromDelegate
                                + " package=" + pkgName + " for lazy restore.");
                    }
                }
@@ -276,9 +301,11 @@ class LocaleManagerBackupHelper {
     * {@link LocaleManagerServicePackageMonitor#onPackageDataCleared} when a package's data
     * is cleared.
     */
    void onPackageDataCleared() {
    void onPackageDataCleared(String packageName, int uid) {
        try {
            notifyBackupManager();
            int userId = UserHandle.getUserId(uid);
            removePackageFromPersistedInfo(packageName, userId);
        } catch (Exception e) {
            Slog.e(TAG, "Exception in onPackageDataCleared.", e);
        }
@@ -289,9 +316,11 @@ class LocaleManagerBackupHelper {
     * {@link LocaleManagerServicePackageMonitor#onPackageRemoved} when a package is removed
     * from device.
     */
    void onPackageRemoved() {
    void onPackageRemoved(String packageName, int uid) {
        try {
            notifyBackupManager();
            int userId = UserHandle.getUserId(uid);
            removePackageFromPersistedInfo(packageName, userId);
        } catch (Exception e) {
            Slog.e(TAG, "Exception in onPackageRemoved.", e);
        }
@@ -317,7 +346,12 @@ class LocaleManagerBackupHelper {
     * case, we want to keep the user settings and discard the restore.
     */
    private void checkExistingLocalesAndApplyRestore(@NonNull String pkgName,
            @NonNull String languageTags, int userId) {
            LocalesInfo localesInfo, int userId) {
        if (localesInfo == null) {
            Slog.w(TAG, "No locales info for " + pkgName);
            return;
        }

        try {
            LocaleList currLocales = mLocaleManagerService.getApplicationLocales(
                    pkgName,
@@ -332,9 +366,10 @@ class LocaleManagerBackupHelper {
        // Restore the locale immediately
        try {
            mLocaleManagerService.setApplicationLocales(pkgName, userId,
                    LocaleList.forLanguageTags(languageTags));
                    LocaleList.forLanguageTags(localesInfo.mLocales), localesInfo.mSetFromDelegate);
            if (DEBUG) {
                Slog.d(TAG, "Restored locales=" + languageTags + " for package=" + pkgName);
                Slog.d(TAG, "Restored locales=" + localesInfo.mLocales + " fromDelegate="
                        + localesInfo.mSetFromDelegate + " for package=" + pkgName);
            }
        } catch (RemoteException | IllegalArgumentException e) {
            Slog.e(TAG, "Could not restore locales for " + pkgName, e);
@@ -348,18 +383,21 @@ class LocaleManagerBackupHelper {
    /**
     * Parses the backup data from the serialized xml input stream.
     */
    private @NonNull HashMap<String, String> readFromXml(XmlPullParser parser)
    private @NonNull HashMap<String, LocalesInfo> readFromXml(TypedXmlPullParser parser)
            throws IOException, XmlPullParserException {
        HashMap<String, String> packageStates = new HashMap<>();
        HashMap<String, LocalesInfo> packageStates = new HashMap<>();
        int depth = parser.getDepth();
        while (XmlUtils.nextElementWithin(parser, depth)) {
            if (parser.getName().equals(PACKAGE_XML_TAG)) {
                String packageName = parser.getAttributeValue(/* namespace= */ null,
                        ATTR_PACKAGE_NAME);
                String languageTags = parser.getAttributeValue(/* namespace= */ null, ATTR_LOCALES);
                boolean delegateSelector = parser.getAttributeBoolean(/* namespace= */ null,
                        ATTR_DELEGATE_SELECTOR);

                if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(languageTags)) {
                    packageStates.put(packageName, languageTags);
                    LocalesInfo localesInfo = new LocalesInfo(languageTags, delegateSelector);
                    packageStates.put(packageName, localesInfo);
                }
            }
        }
@@ -369,8 +407,8 @@ class LocaleManagerBackupHelper {
    /**
     * Converts the list of app backup data into a serialized xml stream.
     */
    private static void writeToXml(OutputStream stream, @NonNull HashMap<String, String> pkgStates)
            throws IOException {
    private static void writeToXml(OutputStream stream,
            @NonNull HashMap<String, LocalesInfo> pkgStates) throws IOException {
        if (pkgStates.isEmpty()) {
            // No need to write anything at all if pkgStates is empty.
            return;
@@ -384,7 +422,9 @@ class LocaleManagerBackupHelper {
        for (String pkg : pkgStates.keySet()) {
            out.startTag(/* namespace= */ null, PACKAGE_XML_TAG);
            out.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, pkg);
            out.attribute(/* namespace= */ null, ATTR_LOCALES, pkgStates.get(pkg));
            out.attribute(/* namespace= */ null, ATTR_LOCALES, pkgStates.get(pkg).mLocales);
            out.attributeBoolean(/* namespace= */ null, ATTR_DELEGATE_SELECTOR,
                    pkgStates.get(pkg).mSetFromDelegate);
            out.endTag(/*namespace= */ null, PACKAGE_XML_TAG);
        }

@@ -394,14 +434,24 @@ class LocaleManagerBackupHelper {

    static class StagedData {
        final long mCreationTimeMillis;
        final HashMap<String, String> mPackageStates;
        final HashMap<String, LocalesInfo> mPackageStates;

        StagedData(long creationTimeMillis, HashMap<String, String> pkgStates) {
        StagedData(long creationTimeMillis, HashMap<String, LocalesInfo> pkgStates) {
            mCreationTimeMillis = creationTimeMillis;
            mPackageStates = pkgStates;
        }
    }

    static class LocalesInfo {
        final String mLocales;
        final boolean mSetFromDelegate;

        LocalesInfo(String locales, boolean setFromDelegate) {
            mLocales = locales;
            mSetFromDelegate = setFromDelegate;
        }
    }

    /**
     * Broadcast listener to capture user removed event.
     *
@@ -416,6 +466,7 @@ class LocaleManagerBackupHelper {
                    final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
                    synchronized (mStagedDataLock) {
                        deleteStagedDataLocked(userId);
                        removeProfileFromPersistedInfo(userId);
                    }
                }
            } catch (Exception e) {
@@ -443,11 +494,11 @@ class LocaleManagerBackupHelper {

        StagedData stagedData = mStagedData.get(userId);
        for (String pkgName : stagedData.mPackageStates.keySet()) {
            String languageTags = stagedData.mPackageStates.get(pkgName);
            LocalesInfo localesInfo = stagedData.mPackageStates.get(pkgName);

            if (pkgName.equals(packageName)) {

                checkExistingLocalesAndApplyRestore(pkgName, languageTags, userId);
                checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);

                // Remove the restored entry from the staged data list.
                stagedData.mPackageStates.remove(pkgName);
@@ -463,4 +514,98 @@ class LocaleManagerBackupHelper {
            }
        }
    }

    SharedPreferences createPersistedInfo() {
        final File prefsFile = new File(
                Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
                LOCALES_FROM_DELEGATE_PREFS);
        return mContext.createDeviceProtectedStorageContext().getSharedPreferences(prefsFile,
                Context.MODE_PRIVATE);
    }

    public SharedPreferences getPersistedInfo() {
        return mDelegateAppLocalePackages;
    }

    private void removePackageFromPersistedInfo(String packageName, @UserIdInt int userId) {
        if (mDelegateAppLocalePackages == null) {
            Slog.w(TAG, "Failed to persist data into the shared preference!");
            return;
        }

        String key = Integer.toString(userId);
        Set<String> packageNames = new ArraySet<>(
                mDelegateAppLocalePackages.getStringSet(key, new ArraySet<>()));
        if (packageNames.contains(packageName)) {
            if (DEBUG) {
                Slog.d(TAG, "remove " + packageName + " from persisted info");
            }
            packageNames.remove(packageName);
            SharedPreferences.Editor editor = mDelegateAppLocalePackages.edit();
            editor.putStringSet(key, packageNames);

            // commit and log the result.
            if (!editor.commit()) {
                Slog.e(TAG, "Failed to commit data!");
            }
        }
    }

    private void removeProfileFromPersistedInfo(@UserIdInt int userId) {
        String key = Integer.toString(userId);

        if (mDelegateAppLocalePackages == null || !mDelegateAppLocalePackages.contains(key)) {
            Slog.w(TAG, "The profile is not existed in the persisted info");
            return;
        }

        if (!mDelegateAppLocalePackages.edit().remove(key).commit()) {
            Slog.e(TAG, "Failed to commit data!");
        }
    }

    /**
     * Persists the package name of per-app locales set from a delegate selector.
     *
     * <p>This information is used when the user has set per-app locales for a specific application
     * from the delegate selector, and then the LocaleConfig of that application is removed in the
     * upgraded version, the per-app locales needs to be reset to system default locales to avoid
     * the user being unable to change system locales setting.
     */
    void persistLocalesModificationInfo(@UserIdInt int userId, String packageName,
            boolean fromDelegate, boolean emptyLocales) {
        if (mDelegateAppLocalePackages == null) {
            Slog.w(TAG, "Failed to persist data into the shared preference!");
            return;
        }

        SharedPreferences.Editor editor = mDelegateAppLocalePackages.edit();
        String key = Integer.toString(userId);
        Set<String> packageNames = new ArraySet<>(
                mDelegateAppLocalePackages.getStringSet(key, new ArraySet<>()));
        if (fromDelegate && !emptyLocales) {
            if (!packageNames.contains(packageName)) {
                if (DEBUG) {
                    Slog.d(TAG, "persist package: " + packageName);
                }
                packageNames.add(packageName);
                editor.putStringSet(key, packageNames);
            }
        } else {
            // Remove the package name if per-app locales was not set from the delegate selector
            // or they were set to empty.
            if (packageNames.contains(packageName)) {
                if (DEBUG) {
                    Slog.d(TAG, "remove package: " + packageName);
                }
                packageNames.remove(packageName);
                editor.putStringSet(key, packageNames);
            }
        }

        // commit and log the result.
        if (!editor.commit()) {
            Slog.e(TAG, "failed to commit locale setter info");
        }
    }
}
+7 −3
Original line number Diff line number Diff line
@@ -147,8 +147,9 @@ public class LocaleManagerService extends SystemService {
    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);
                @NonNull LocaleList locales, boolean fromDelegate) throws RemoteException {
            LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales,
                    fromDelegate);
        }

        @Override
@@ -178,7 +179,8 @@ public class LocaleManagerService extends SystemService {
     * Sets the current UI locales for a specified app.
     */
    public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
            @NonNull LocaleList locales) throws RemoteException, IllegalArgumentException {
            @NonNull LocaleList locales, boolean fromDelegate)
            throws RemoteException, IllegalArgumentException {
        AppLocaleChangedAtomRecord atomRecordForMetrics = new
                AppLocaleChangedAtomRecord(Binder.getCallingUid());
        try {
@@ -203,6 +205,8 @@ public class LocaleManagerService extends SystemService {
                enforceChangeConfigurationPermission(atomRecordForMetrics);
            }

            mBackupHelper.persistLocalesModificationInfo(userId, appPackageName, fromDelegate,
                    locales.isEmpty());
            final long token = Binder.clearCallingIdentity();
            try {
                setApplicationLocalesUnchecked(appPackageName, userId, locales,
+2 −2
Original line number Diff line number Diff line
@@ -48,12 +48,12 @@ final class LocaleManagerServicePackageMonitor extends PackageMonitor {

    @Override
    public void onPackageDataCleared(String packageName, int uid) {
        mBackupHelper.onPackageDataCleared();
        mBackupHelper.onPackageDataCleared(packageName, uid);
    }

    @Override
    public void onPackageRemoved(String packageName, int uid) {
        mBackupHelper.onPackageRemoved();
        mBackupHelper.onPackageRemoved(packageName, uid);
    }

    @Override
Loading