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

Commit 7f994312 authored by Nishant Singh's avatar Nishant Singh Committed by Android (Google) Code Review
Browse files

Merge "Simplify app-locales B&R stage data retention."

parents 4217ed28 50b6f34e
Loading
Loading
Loading
Loading
+28 −157
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import static android.os.UserHandle.USER_NULL;
import static com.android.server.locales.LocaleManagerService.DEBUG;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.backup.BackupManager;
import android.content.BroadcastReceiver;
@@ -34,7 +33,6 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.BestClock;
import android.os.Binder;
import android.os.Environment;
import android.os.HandlerThread;
import android.os.LocaleList;
import android.os.Process;
@@ -42,7 +40,6 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedXmlPullParser;
@@ -53,17 +50,12 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.util.XmlUtils;

import libcore.io.IoUtils;

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.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
@@ -71,8 +63,6 @@ import java.time.Clock;
import java.time.Duration;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Helper class for managing backup and restore of app-specific locales.
@@ -87,33 +77,28 @@ class LocaleManagerBackupHelper {
    private static final String ATTR_LOCALES = "locales";
    private static final String ATTR_CREATION_TIME_MILLIS = "creationTimeMillis";

    private static final String STAGE_FILE_NAME = "staged_locales";
    private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android";

    private static final Pattern STAGE_FILE_NAME_PATTERN = Pattern.compile(
            TextUtils.formatSimple("(^%s_)(\\d+)(\\.xml$)", STAGE_FILE_NAME));
    private static final int USER_ID_GROUP_INDEX_IN_PATTERN = 2;
    private static final Duration STAGE_FILE_RETENTION_PERIOD = Duration.ofDays(3);
    // 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);

    private final LocaleManagerService mLocaleManagerService;
    private final PackageManagerInternal mPackageManagerInternal;
    private final File mStagedLocalesDir;
    private final Clock mClock;
    private final Context mContext;
    private final Object mStagedDataLock = new Object();

    // Staged data map keyed by user-id to handle multi-user scenario / work profiles. We are using
    // SparseArray because it is more memory-efficient than a HashMap.
    private final SparseArray<StagedData> mStagedData = new SparseArray<>();
    private final SparseArray<StagedData> mStagedData;

    private final PackageMonitor mPackageMonitor;
    private final BroadcastReceiver mUserMonitor;

    LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
            PackageManagerInternal pmInternal) {
        this(localeManagerService.mContext, localeManagerService, pmInternal,
                new File(Environment.getDataSystemCeDirectory(),
                        "app_locales"), getDefaultClock());
        this(localeManagerService.mContext, localeManagerService, pmInternal, getDefaultClock(),
                new SparseArray<>());
    }

    private static @NonNull Clock getDefaultClock() {
@@ -123,14 +108,12 @@ class LocaleManagerBackupHelper {

    @VisibleForTesting LocaleManagerBackupHelper(Context context,
            LocaleManagerService localeManagerService,
            PackageManagerInternal pmInternal, File stagedLocalesDir, Clock clock) {
            PackageManagerInternal pmInternal, Clock clock, SparseArray<StagedData> stagedData) {
        mContext = context;
        mLocaleManagerService = localeManagerService;
        mPackageManagerInternal = pmInternal;
        mClock = clock;
        mStagedLocalesDir = stagedLocalesDir;

        loadAllStageFiles();
        mStagedData = stagedData;

        HandlerThread broadcastHandlerThread = new HandlerThread(TAG,
                Process.THREAD_PRIORITY_BACKGROUND);
@@ -157,67 +140,6 @@ class LocaleManagerBackupHelper {
        return mPackageMonitor;
    }

    /**
     * Loads the staged data into memory by reading all the files in the staged directory.
     *
     * <p><b>Note:</b> We don't ned to hold the lock here because this is only called in the
     * constructor (before any broadcast receivers are registered).
     */
    private void loadAllStageFiles() {
        File[] files = mStagedLocalesDir.listFiles();
        if (files == null) {
            return;
        }
        for (File file : files) {
            String fileName = file.getName();
            Matcher matcher = STAGE_FILE_NAME_PATTERN.matcher(fileName);
            if (!matcher.matches()) {
                file.delete();
                Slog.w(TAG, TextUtils.formatSimple("Deleted %s. Reason: %s.", fileName,
                        "Unrecognized file"));
                continue;
            }
            try {
                final int userId = Integer.parseInt(matcher.group(USER_ID_GROUP_INDEX_IN_PATTERN));
                StagedData stagedData = readStageFile(file);
                if (stagedData != null) {
                    mStagedData.put(userId, stagedData);
                } else {
                    file.delete();
                    Slog.w(TAG, TextUtils.formatSimple("Deleted %s. Reason: %s.", fileName,
                            "Could not read file"));
                }
            } catch (NumberFormatException e) {
                file.delete();
                Slog.w(TAG, TextUtils.formatSimple("Deleted %s. Reason: %s.", fileName,
                        "Could not parse user id from file name"));
            }
        }
    }

    /**
     * Loads the stage file from the disk and parses it into a list of app backups.
     */
    private @Nullable StagedData readStageFile(@NonNull File file) {
        InputStream stagedDataInputStream = null;
        AtomicFile stageFile = new AtomicFile(file);
        try {
            stagedDataInputStream = stageFile.openRead();
            final TypedXmlPullParser parser = Xml.newFastPullParser();
            parser.setInput(stagedDataInputStream, StandardCharsets.UTF_8.name());

            XmlUtils.beginDocument(parser, LOCALES_XML_TAG);
            long creationTimeMillis = parser.getAttributeLong(/* namespace= */ null,
                    ATTR_CREATION_TIME_MILLIS);
            return new StagedData(creationTimeMillis, readFromXml(parser));
        } catch (IOException | XmlPullParserException e) {
            Slog.e(TAG, "Could not parse stage file ", e);
        } finally {
            IoUtils.closeQuietly(stagedDataInputStream);
        }
        return null;
    }

    /**
     * @see LocaleManagerInternal#getBackupPayload(int userId)
     */
@@ -261,9 +183,7 @@ class LocaleManagerBackupHelper {

        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            // Passing arbitrary value for creationTimeMillis since it is ignored when forStage
            // is false.
            writeToXml(out, pkgStates, /* forStage= */ false, /* creationTimeMillis= */ -1);
            writeToXml(out, pkgStates);
        } catch (IOException e) {
            Slog.e(TAG, "Could not write to xml for backup ", e);
            return null;
@@ -284,7 +204,7 @@ class LocaleManagerBackupHelper {
            int userId = mStagedData.keyAt(i);
            StagedData stagedData = mStagedData.get(userId);
            if (stagedData.mCreationTimeMillis
                    < mClock.millis() - STAGE_FILE_RETENTION_PERIOD.toMillis()) {
                    < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) {
                deleteStagedDataLocked(userId);
            }
        }
@@ -305,7 +225,7 @@ class LocaleManagerBackupHelper {

        final ByteArrayInputStream inputStream = new ByteArrayInputStream(payload);

        HashMap<String, String> pkgStates = new HashMap<>();
        HashMap<String, String> pkgStates;
        try {
            // Parse the input blob into a list of BackupPackageState.
            final TypedXmlPullParser parser = Xml.newFastPullParser();
@@ -315,6 +235,7 @@ class LocaleManagerBackupHelper {
            pkgStates = readFromXml(parser);
        } catch (IOException | XmlPullParserException e) {
            Slog.e(TAG, "Could not parse payload ", e);
            return;
        }

        // We need a lock here to prevent race conditions when accessing the stage file.
@@ -323,7 +244,7 @@ class LocaleManagerBackupHelper {
        // performed simultaneously.
        synchronized (mStagedDataLock) {
            // Backups for apps which are yet to be installed.
            mStagedData.put(userId, new StagedData(mClock.millis(), new HashMap<>()));
            StagedData stagedData = new StagedData(mClock.millis(), new HashMap<>());

            for (String pkgName : pkgStates.keySet()) {
                String languageTags = pkgStates.get(pkgName);
@@ -333,7 +254,7 @@ class LocaleManagerBackupHelper {
                    checkExistingLocalesAndApplyRestore(pkgName, languageTags, userId);
                } else {
                    // Stage the data if the app isn't installed.
                    mStagedData.get(userId).mPackageStates.put(pkgName, languageTags);
                    stagedData.mPackageStates.put(pkgName, languageTags);
                    if (DEBUG) {
                        Slog.d(TAG, "Add locales=" + languageTags
                                + " package=" + pkgName + " for lazy restore.");
@@ -341,7 +262,9 @@ class LocaleManagerBackupHelper {
                }
            }

            writeStageFileLocked(userId);
            if (!stagedData.mPackageStates.isEmpty()) {
                mStagedData.put(userId, stagedData);
            }
        }
    }

@@ -396,55 +319,10 @@ class LocaleManagerBackupHelper {
        }
    }

    /**
     * Converts the list of app backups into xml and writes it onto the disk.
     */
    private void writeStageFileLocked(int userId) {
        StagedData stagedData = mStagedData.get(userId);
        if (stagedData.mPackageStates.isEmpty()) {
            deleteStagedDataLocked(userId);
            return;
        }

        final FileOutputStream stagedDataOutputStream;
        AtomicFile stageFile = new AtomicFile(
                new File(mStagedLocalesDir,
                        TextUtils.formatSimple("%s_%d.xml", STAGE_FILE_NAME, userId)));
        try {
            stagedDataOutputStream = stageFile.startWrite();
        } catch (IOException e) {
            Slog.e(TAG, "Failed to save stage file");
            return;
        }

        try {
            writeToXml(stagedDataOutputStream, stagedData.mPackageStates,  /* forStage= */ true,
                    stagedData.mCreationTimeMillis);
            stageFile.finishWrite(stagedDataOutputStream);
            if (DEBUG) {
                Slog.d(TAG, "Stage file written.");
            }
        } catch (IOException e) {
            Slog.e(TAG, "Could not write stage file", e);
            stageFile.failWrite(stagedDataOutputStream);
        }
    }

    private void deleteStagedDataLocked(@UserIdInt int userId) {
        AtomicFile stageFile = getStageFileIfExistsLocked(userId);
        if (stageFile != null) {
            stageFile.delete();
        }
        mStagedData.remove(userId);
    }

    private @Nullable AtomicFile getStageFileIfExistsLocked(@UserIdInt int userId) {
        final File stageFile = new File(mStagedLocalesDir,
                TextUtils.formatSimple("%s_%d.xml", STAGE_FILE_NAME, userId));
        return stageFile.isFile() ? new AtomicFile(stageFile)
                : null;
    }

    /**
     * Parses the backup data from the serialized xml input stream.
     */
@@ -468,15 +346,8 @@ class LocaleManagerBackupHelper {

    /**
     * Converts the list of app backup data into a serialized xml stream.
     *
     * @param forStage Flag to indicate whether this method is called for the purpose of
     * staging the data. Note that if this is false, {@code creationTimeMillis} is ignored because
     * we only need it for the stage data.
     * @param creationTimeMillis The timestamp when the stage data was created. This is required
     * to determine when to delete the stage data.
     */
    private static void writeToXml(OutputStream stream,
            @NonNull HashMap<String, String> pkgStates, boolean forStage, long creationTimeMillis)
    private static void writeToXml(OutputStream stream, @NonNull HashMap<String, String> pkgStates)
            throws IOException {
        if (pkgStates.isEmpty()) {
            // No need to write anything at all if pkgStates is empty.
@@ -488,11 +359,6 @@ class LocaleManagerBackupHelper {
        out.startDocument(/* encoding= */ null, /* standalone= */ true);
        out.startTag(/* namespace= */ null, LOCALES_XML_TAG);

        if (forStage) {
            out.attribute(/* namespace= */ null, ATTR_CREATION_TIME_MILLIS,
                    Long.toString(creationTimeMillis));
        }

        for (String pkg : pkgStates.keySet()) {
            out.startTag(/* namespace= */ null, PACKAGE_XML_TAG);
            out.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, pkg);
@@ -504,7 +370,7 @@ class LocaleManagerBackupHelper {
        out.endDocument();
    }

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

@@ -517,7 +383,7 @@ class LocaleManagerBackupHelper {
    /**
     * Broadcast listener to capture user removed event.
     *
     * <p>The stage file is deleted when a user is removed.
     * <p>The stage data is deleted when a user is removed.
     */
    private final class UserMonitor extends BroadcastReceiver {
        @Override
@@ -546,6 +412,8 @@ class LocaleManagerBackupHelper {
        public void onPackageAdded(String packageName, int uid) {
            try {
                synchronized (mStagedDataLock) {
                    cleanStagedDataForOldEntriesLocked();

                    int userId = UserHandle.getUserId(uid);
                    if (mStagedData.contains(userId)) {
                        // Perform lazy restore only if the staged data exists.
@@ -589,7 +457,7 @@ class LocaleManagerBackupHelper {
        // Check if the package is installed indeed
        if (!isPackageInstalledForUser(packageName, userId)) {
            Slog.e(TAG, packageName + " not installed for user " + userId
                    + ". Could not restore locales from stage file");
                    + ". Could not restore locales from stage data");
            return;
        }

@@ -603,8 +471,11 @@ class LocaleManagerBackupHelper {

                // Remove the restored entry from the staged data list.
                stagedData.mPackageStates.remove(pkgName);
                // Update the file on the disk.
                writeStageFileLocked(userId);

                // Remove the stage data entry for user if there are no more packages to restore.
                if (stagedData.mPackageStates.isEmpty()) {
                    mStagedData.remove(userId);
                }

                // No need to loop further after restoring locales because the staged data will
                // contain at most one entry for the newly added package.
+123 −221

File changed.

Preview size limit exceeded, changes collapsed.

+4 −3
Original line number Diff line number Diff line
@@ -18,8 +18,8 @@ package com.android.server.locales;

import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.util.SparseArray;

import java.io.File;
import java.time.Clock;

/**
@@ -30,7 +30,8 @@ import java.time.Clock;
public class ShadowLocaleManagerBackupHelper extends LocaleManagerBackupHelper {
    ShadowLocaleManagerBackupHelper(Context context,
            LocaleManagerService localeManagerService,
            PackageManagerInternal pmInternal, File stagedLocalesDir, Clock clock) {
        super(context, localeManagerService, pmInternal, stagedLocalesDir, clock);
            PackageManagerInternal pmInternal, Clock clock,
            SparseArray<LocaleManagerBackupHelper.StagedData> stagedData) {
        super(context, localeManagerService, pmInternal, clock, stagedData);
    }
}