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

Commit a0869ba7 authored by Mehdi Alizadeh's avatar Mehdi Alizadeh
Browse files

In ShortcutService, save data of each package in a separate file

This CL splits the main Xml file into individual files for each package.
This enables us to gain disk I/O performance improvements by selectively
updating only the parts that have been changed (coming in future CLs).
The old implementation was re-writing the entire shortcut data of a user
on every change.

Bug: 153680823
Test: atest com.android.server.pm.ShortcutManagerTest1
            com.android.server.pm.ShortcutManagerTest2
            com.android.server.pm.ShortcutManagerTest3
            com.android.server.pm.ShortcutManagerTest4
            com.android.server.pm.ShortcutManagerTest5
            com.android.server.pm.ShortcutManagerTest6
            com.android.server.pm.ShortcutManagerTest7
            com.android.server.pm.ShortcutManagerTest8
            com.android.server.pm.ShortcutManagerTest9
            com.android.server.pm.ShortcutManagerTest10
            com.android.server.pm.ShortcutManagerTest11
Change-Id: I3a29b80ecfa353eddb1bb778eb845d863f057e0a
parent a1ad29da
Loading
Loading
Loading
Loading
+56 −0
Original line number Diff line number Diff line
@@ -22,20 +22,29 @@ import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.Xml;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.pm.ShortcutService.DumpFilter;
import com.android.server.pm.ShortcutUser.PackageWithUser;

import libcore.io.IoUtils;

import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

@@ -254,6 +263,53 @@ class ShortcutLauncher extends ShortcutPackageItem {
        out.endTag(null, TAG_ROOT);
    }

    public static ShortcutLauncher loadFromFile(File path, ShortcutUser shortcutUser,
            int ownerUserId, boolean fromBackup) {

        final AtomicFile file = new AtomicFile(path);
        final FileInputStream in;
        try {
            in = file.openRead();
        } catch (FileNotFoundException e) {
            if (ShortcutService.DEBUG) {
                Slog.d(TAG, "Not found " + path);
            }
            return null;
        }

        try {
            final BufferedInputStream bis = new BufferedInputStream(in);

            ShortcutLauncher ret = null;
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(bis, StandardCharsets.UTF_8.name());

            int type;
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
                if (type != XmlPullParser.START_TAG) {
                    continue;
                }
                final int depth = parser.getDepth();

                final String tag = parser.getName();
                if (ShortcutService.DEBUG_LOAD) {
                    Slog.d(TAG, String.format("depth=%d type=%d name=%s", depth, type, tag));
                }
                if ((depth == 1) && TAG_ROOT.equals(tag)) {
                    ret = loadFromXml(parser, shortcutUser, ownerUserId, fromBackup);
                    continue;
                }
                ShortcutService.throwForInvalidTag(depth, tag);
            }
            return ret;
        } catch (IOException | XmlPullParserException e) {
            Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
            return null;
        } finally {
            IoUtils.closeQuietly(in);
        }
    }

    /**
     * Load.
     */
+55 −0
Original line number Diff line number Diff line
@@ -31,8 +31,10 @@ import android.os.PersistableBundle;
import android.text.format.Formatter;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Log;
import android.util.Slog;
import android.util.Xml;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
@@ -43,15 +45,21 @@ import com.android.server.pm.ShortcutService.DumpFilter;
import com.android.server.pm.ShortcutService.ShortcutOperation;
import com.android.server.pm.ShortcutService.Stats;

import libcore.io.IoUtils;

import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -1720,6 +1728,53 @@ class ShortcutPackage extends ShortcutPackageItem {
        out.endTag(null, TAG_SHORTCUT);
    }

    public static ShortcutPackage loadFromFile(ShortcutService s, ShortcutUser shortcutUser,
            File path, boolean fromBackup) {

        final AtomicFile file = new AtomicFile(path);
        final FileInputStream in;
        try {
            in = file.openRead();
        } catch (FileNotFoundException e) {
            if (ShortcutService.DEBUG) {
                Slog.d(TAG, "Not found " + path);
            }
            return null;
        }

        try {
            final BufferedInputStream bis = new BufferedInputStream(in);

            ShortcutPackage ret = null;
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(bis, StandardCharsets.UTF_8.name());

            int type;
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
                if (type != XmlPullParser.START_TAG) {
                    continue;
                }
                final int depth = parser.getDepth();

                final String tag = parser.getName();
                if (ShortcutService.DEBUG_LOAD) {
                    Slog.d(TAG, String.format("depth=%d type=%d name=%s", depth, type, tag));
                }
                if ((depth == 1) && TAG_ROOT.equals(tag)) {
                    ret = loadFromXml(s, shortcutUser, parser, fromBackup);
                    continue;
                }
                ShortcutService.throwForInvalidTag(depth, tag);
            }
            return ret;
        } catch (IOException | XmlPullParserException e) {
            Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
            return null;
        } finally {
            IoUtils.closeQuietly(in);
        }
    }

    public static ShortcutPackage loadFromXml(ShortcutService s, ShortcutUser shortcutUser,
            XmlPullParser parser, boolean fromBackup)
            throws IOException, XmlPullParserException {
+31 −0
Original line number Diff line number Diff line
@@ -18,8 +18,10 @@ package com.android.server.pm;
import android.annotation.NonNull;
import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
import android.util.AtomicFile;
import android.util.Slog;

import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.Preconditions;

import org.json.JSONException;
@@ -27,7 +29,11 @@ import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
@@ -143,6 +149,31 @@ abstract class ShortcutPackageItem {
    public abstract void saveToXml(@NonNull XmlSerializer out, boolean forBackup)
            throws IOException, XmlPullParserException;

    public void saveToFile(File path, boolean forBackup) {
        final AtomicFile file = new AtomicFile(path);
        FileOutputStream os = null;
        try {
            os = file.startWrite();
            final BufferedOutputStream bos = new BufferedOutputStream(os);

            // Write to XML
            XmlSerializer itemOut = new FastXmlSerializer();
            itemOut.setOutput(bos, StandardCharsets.UTF_8.name());
            itemOut.startDocument(null, true);

            saveToXml(itemOut, forBackup);

            itemOut.endDocument();

            bos.flush();
            os.flush();
            file.finishWrite(os);
        } catch (XmlPullParserException | IOException e) {
            Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
            file.failWrite(os);
        }
    }

    public JSONObject dumpCheckin(boolean clear) throws JSONException {
        final JSONObject result = new JSONObject();
        result.put(KEY_NAME, mPackageName);
+79 −5
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.pm.ShortcutManager;
import android.metrics.LogMaker;
import android.os.FileUtils;
import android.text.TextUtils;
import android.text.format.Formatter;
import android.util.ArrayMap;
@@ -30,7 +31,6 @@ import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.Preconditions;
import com.android.server.pm.ShortcutService.DumpFilter;
import com.android.server.pm.ShortcutService.InvalidFileFormatException;

@@ -55,6 +55,9 @@ import java.util.function.Consumer;
class ShortcutUser {
    private static final String TAG = ShortcutService.TAG;

    static final String DIRECTORY_PACKAGES = "packages";
    static final String DIRECTORY_LUANCHERS = "launchers";

    static final String TAG_ROOT = "user";
    private static final String TAG_LAUNCHER = "launcher";

@@ -354,6 +357,13 @@ class ShortcutUser {
                    mService.injectBuildFingerprint());
        }

        if (!forBackup) {
            // Since we are not handling package deletion yet, or any single package changes, just
            // clean the directory and rewrite all the ShortcutPackageItems.
            final File root = mService.injectUserDataPath(mUserId);
            FileUtils.deleteContents(new File(root, DIRECTORY_PACKAGES));
            FileUtils.deleteContents(new File(root, DIRECTORY_LUANCHERS));
        }
        // Can't use forEachPackageItem due to the checked exceptions.
        {
            final int size = mLaunchers.size();
@@ -371,20 +381,47 @@ class ShortcutUser {
        out.endTag(null, TAG_ROOT);
    }

    private void saveShortcutPackageItem(XmlSerializer out,
            ShortcutPackageItem spi, boolean forBackup) throws IOException, XmlPullParserException {
    private void saveShortcutPackageItem(XmlSerializer out, ShortcutPackageItem spi,
            boolean forBackup) throws IOException, XmlPullParserException {
        if (forBackup) {
            if (spi.getPackageUserId() != spi.getOwnerUserId()) {
                return; // Don't save cross-user information.
            }
        }
            spi.saveToXml(out, forBackup);
        } else {
            // Save each ShortcutPackageItem in a separate Xml file.
            final File path = getShortcutPackageItemFile(spi);
            if (ShortcutService.DEBUG) {
                Slog.d(TAG, "Saving package item " + spi.getPackageName() + " to " + path);
            }

            path.getParentFile().mkdirs();
            spi.saveToFile(path, forBackup);
        }
    }

    private File getShortcutPackageItemFile(ShortcutPackageItem spi) {
        boolean isShortcutLauncher = spi instanceof ShortcutLauncher;

        final File path = new File(mService.injectUserDataPath(mUserId),
                isShortcutLauncher ? DIRECTORY_LUANCHERS : DIRECTORY_PACKAGES);

        final String fileName;
        if (isShortcutLauncher) {
            // Package user id and owner id can have different values for ShortcutLaunchers. Adding
            // user Id to the file name to create a unique path. Owner id is used in the root path.
            fileName = spi.getPackageName() + spi.getPackageUserId() + ".xml";
        } else {
            fileName = spi.getPackageName() + ".xml";
        }

        return new File(path, fileName);
    }

    public static ShortcutUser loadFromXml(ShortcutService s, XmlPullParser parser, int userId,
            boolean fromBackup) throws IOException, XmlPullParserException, InvalidFileFormatException {
        final ShortcutUser ret = new ShortcutUser(s, userId);

        boolean readShortcutItems = false;
        try {
            ret.mKnownLocales = ShortcutService.parseStringAttribute(parser,
                    ATTR_KNOWN_LOCALES);
@@ -422,12 +459,14 @@ class ShortcutUser {

                            // Don't use addShortcut(), we don't need to save the icon.
                            ret.mPackages.put(shortcuts.getPackageName(), shortcuts);
                            readShortcutItems = true;
                            continue;
                        }

                        case ShortcutLauncher.TAG_ROOT: {
                            ret.addLauncher(
                                    ShortcutLauncher.loadFromXml(parser, ret, userId, fromBackup));
                            readShortcutItems = true;
                            continue;
                        }
                    }
@@ -438,9 +477,44 @@ class ShortcutUser {
            throw new ShortcutService.InvalidFileFormatException(
                    "Unable to parse file", e);
        }

        if (readShortcutItems) {
            // If the shortcuts info was read from the main Xml, skip reading from individual files.
            // Data will get stored in the new format during the next call to saveToXml().
            // TODO: ret.forAllPackageItems((ShortcutPackageItem item) -> item.markDirty());
            s.scheduleSaveUser(userId);
        } else {
            final File root = s.injectUserDataPath(userId);

            forAllFilesIn(new File(root, DIRECTORY_PACKAGES), (File f) -> {
                final ShortcutPackage sp = ShortcutPackage.loadFromFile(s, ret, f, fromBackup);
                if (sp != null) {
                    ret.mPackages.put(sp.getPackageName(), sp);
                }
            });

            forAllFilesIn(new File(root, DIRECTORY_LUANCHERS), (File f) -> {
                final ShortcutLauncher sl =
                        ShortcutLauncher.loadFromFile(f, ret, userId, fromBackup);
                if (sl != null) {
                    ret.addLauncher(sl);
                }
            });
        }

        return ret;
    }

    private static void forAllFilesIn(File path, Consumer<File> callback) {
        if (!path.exists()) {
            return;
        }
        File[] list = path.listFiles();
        for (File f : list) {
            callback.accept(f);
        }
    }

    public ComponentName getLastKnownLauncher() {
        return mLastKnownLauncher;
    }