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

Commit b52ba99d authored by Daniel Nishi's avatar Daniel Nishi Committed by Android (Google) Code Review
Browse files

Merge "Save/load calculated cache quotas to a file."

parents fa308f32 e40da3c1
Loading
Loading
Loading
Loading
+21 −3
Original line number Diff line number Diff line
@@ -24,8 +24,10 @@ import android.os.Parcelable;

import com.android.internal.util.Preconditions;

import java.util.Objects;

/**
 * CacheQuotaRequest represents a triplet of a uid, the volume UUID it is stored upon, and
 * CacheQuotaHint represents a triplet of a uid, the volume UUID it is stored upon, and
 * its usage stats. When processed, it obtains a cache quota as defined by the system which
 * allows apps to understand how much cache to use.
 * {@hide}
@@ -78,6 +80,23 @@ public final class CacheQuotaHint implements Parcelable {
        return 0;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof CacheQuotaHint) {
            final CacheQuotaHint other = (CacheQuotaHint) o;
            return Objects.equals(mUuid, other.mUuid)
                    && Objects.equals(mUsageStats, other.mUsageStats)
                    && mUid == other.mUid && mQuota == other.mQuota;
        }

        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.mUuid, this.mUid, this.mUsageStats, this.mQuota);
    }

    public static final class Builder {
        private String mUuid;
        private int mUid;
@@ -100,7 +119,7 @@ public final class CacheQuotaHint implements Parcelable {
        }

        public @NonNull Builder setUid(int uid) {
            Preconditions.checkArgumentPositive(uid, "Proposed uid was not positive.");
            Preconditions.checkArgumentNonnegative(uid, "Proposed uid was negative.");
            mUid = uid;
            return this;
        }
@@ -117,7 +136,6 @@ public final class CacheQuotaHint implements Parcelable {
        }

        public @NonNull CacheQuotaHint build() {
            Preconditions.checkNotNull(mUsageStats);
            return new CacheQuotaHint(this);
        }
    }
+165 −4
Original line number Diff line number Diff line
@@ -34,21 +34,37 @@ import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.format.DateUtils;
import android.util.Pair;
import android.util.Slog;
import android.util.Xml;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.AtomicFile;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.Preconditions;
import com.android.server.pm.Installer;

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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;


/**
 * CacheQuotaStrategy is a strategy for determining cache quotas using usage stats and foreground
 * time using the calculation as defined in the refuel rocket.
@@ -58,17 +74,28 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener {

    private final Object mLock = new Object();

    // XML Constants
    private static final String CACHE_INFO_TAG = "cache-info";
    private static final String ATTR_PREVIOUS_BYTES = "previousBytes";
    private static final String TAG_QUOTA = "quota";
    private static final String ATTR_UUID = "uuid";
    private static final String ATTR_UID = "uid";
    private static final String ATTR_QUOTA_IN_BYTES = "bytes";

    private final Context mContext;
    private final UsageStatsManagerInternal mUsageStats;
    private final Installer mInstaller;
    private ServiceConnection mServiceConnection;
    private ICacheQuotaService mRemoteService;
    private AtomicFile mPreviousValuesFile;

    public CacheQuotaStrategy(
            Context context, UsageStatsManagerInternal usageStatsManager, Installer installer) {
        mContext = Preconditions.checkNotNull(context);
        mUsageStats = Preconditions.checkNotNull(usageStatsManager);
        mInstaller = Preconditions.checkNotNull(installer);
        mPreviousValuesFile = new AtomicFile(new File(
                new File(Environment.getDataDirectory(), "system"), "cachequota.xml"));
    }

    /**
@@ -128,7 +155,7 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener {
    }

    /**
     * Returns a list of CacheQuotaRequests which do not have their quotas filled out for apps
     * Returns a list of CacheQuotaHints which do not have their quotas filled out for apps
     * which have been used in the last year.
     */
    private List<CacheQuotaHint> getUnfulfilledRequests() {
@@ -176,6 +203,11 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener {
        final List<CacheQuotaHint> processedRequests =
                data.getParcelableArrayList(
                        CacheQuotaService.REQUEST_LIST_KEY);
        pushProcessedQuotas(processedRequests);
        writeXmlToFile(processedRequests);
    }

    private void pushProcessedQuotas(List<CacheQuotaHint> processedRequests) {
        final int requestSize = processedRequests.size();
        for (int i = 0; i < requestSize; i++) {
            CacheQuotaHint request = processedRequests.get(i);
@@ -200,9 +232,11 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener {
    }

    private void disconnectService() {
        if (mServiceConnection != null) {
            mContext.unbindService(mServiceConnection);
            mServiceConnection = null;
        }
    }

    private ComponentName getServiceComponentName() {
        String packageName =
@@ -223,4 +257,131 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener {
        ServiceInfo serviceInfo = resolveInfo.serviceInfo;
        return new ComponentName(serviceInfo.packageName, serviceInfo.name);
    }

    private void writeXmlToFile(List<CacheQuotaHint> processedRequests) {
        FileOutputStream fileStream = null;
        try {
            XmlSerializer out = new FastXmlSerializer();
            fileStream = mPreviousValuesFile.startWrite();
            out.setOutput(fileStream, StandardCharsets.UTF_8.name());
            saveToXml(out, processedRequests, 0);
            mPreviousValuesFile.finishWrite(fileStream);
        } catch (Exception e) {
            Slog.e(TAG, "An error occurred while writing the cache quota file.", e);
            mPreviousValuesFile.failWrite(fileStream);
        }
    }

    /**
     * Initializes the quotas from the file.
     * @return the number of bytes that were free on the device when the quotas were last calced.
     */
    public long setupQuotasFromFile() throws IOException {
        FileInputStream stream;
        try {
            stream = mPreviousValuesFile.openRead();
        } catch (FileNotFoundException e) {
            // The file may not exist yet -- this isn't truly exceptional.
            return -1;
        }

        Pair<Long, List<CacheQuotaHint>> cachedValues = null;
        try {
            cachedValues = readFromXml(stream);
        } catch (XmlPullParserException e) {
            throw new IllegalStateException(e.getMessage());
        }

        if (cachedValues == null) {
            Slog.e(TAG, "An error occurred while parsing the cache quota file.");
            return -1;
        }
        pushProcessedQuotas(cachedValues.second);
        return cachedValues.first;
    }

    @VisibleForTesting
    static void saveToXml(XmlSerializer out,
            List<CacheQuotaHint> requests, long bytesWhenCalculated) throws IOException {
        out.startDocument(null, true);
        out.startTag(null, CACHE_INFO_TAG);
        int requestSize = requests.size();
        out.attribute(null, ATTR_PREVIOUS_BYTES, Long.toString(bytesWhenCalculated));

        for (int i = 0; i < requestSize; i++) {
            CacheQuotaHint request = requests.get(i);
            out.startTag(null, TAG_QUOTA);
            String uuid = request.getVolumeUuid();
            if (uuid != null) {
                out.attribute(null, ATTR_UUID, request.getVolumeUuid());
            }
            out.attribute(null, ATTR_UID, Integer.toString(request.getUid()));
            out.attribute(null, ATTR_QUOTA_IN_BYTES, Long.toString(request.getQuota()));
            out.endTag(null, TAG_QUOTA);
        }
        out.endTag(null, CACHE_INFO_TAG);
        out.endDocument();
    }

    protected static Pair<Long, List<CacheQuotaHint>> readFromXml(InputStream inputStream)
            throws XmlPullParserException, IOException {
        XmlPullParser parser = Xml.newPullParser();
        parser.setInput(inputStream, StandardCharsets.UTF_8.name());

        int eventType = parser.getEventType();
        while (eventType != XmlPullParser.START_TAG &&
                eventType != XmlPullParser.END_DOCUMENT) {
            eventType = parser.next();
        }

        if (eventType == XmlPullParser.END_DOCUMENT) {
            Slog.d(TAG, "No quotas found in quota file.");
            return null;
        }

        String tagName = parser.getName();
        if (!CACHE_INFO_TAG.equals(tagName)) {
            throw new IllegalStateException("Invalid starting tag.");
        }

        final List<CacheQuotaHint> quotas = new ArrayList<>();
        long previousBytes;
        try {
            previousBytes = Long.parseLong(parser.getAttributeValue(
                    null, ATTR_PREVIOUS_BYTES));
        } catch (NumberFormatException e) {
            throw new IllegalStateException(
                    "Previous bytes formatted incorrectly; aborting quota read.");
        }

        eventType = parser.next();
        do {
            if (eventType == XmlPullParser.START_TAG) {
                tagName = parser.getName();
                if (TAG_QUOTA.equals(tagName)) {
                    CacheQuotaHint request = getRequestFromXml(parser);
                    if (request == null) {
                        continue;
                    }
                    quotas.add(request);
                }
            }
            eventType = parser.next();
        } while (eventType != XmlPullParser.END_DOCUMENT);
        return new Pair<>(previousBytes, quotas);
    }

    @VisibleForTesting
    static CacheQuotaHint getRequestFromXml(XmlPullParser parser) {
        try {
            String uuid = parser.getAttributeValue(null, ATTR_UUID);
            int uid = Integer.parseInt(parser.getAttributeValue(null, ATTR_UID));
            long bytes = Long.parseLong(parser.getAttributeValue(null, ATTR_QUOTA_IN_BYTES));
            return new CacheQuotaHint.Builder()
                    .setVolumeUuid(uuid).setUid(uid).setQuota(bytes).build();
        } catch (NumberFormatException e) {
            Slog.e(TAG, "Invalid cache quota request, skipping.");
            return null;
        }
    }
}
+128 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.storage;

import static com.google.common.truth.Truth.assertThat;

import android.app.usage.CacheQuotaHint;
import android.test.AndroidTestCase;
import android.util.Pair;

import com.android.internal.util.FastXmlSerializer;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

@RunWith(JUnit4.class)
public class CacheQuotaStrategyTest extends AndroidTestCase {
    StringWriter mWriter;
    FastXmlSerializer mOut;

    @Before
    public void setUp() throws Exception {
        mWriter = new StringWriter();
        mOut = new FastXmlSerializer();
        mOut.setOutput(mWriter);
    }

    @Test
    public void testEmptyWrite() throws Exception {
        CacheQuotaStrategy.saveToXml(mOut, new ArrayList<>(), 0);
        mOut.flush();

        assertThat(mWriter.toString()).isEqualTo(
                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
                        "<cache-info previousBytes=\"0\" />\n");
    }

    @Test
    public void testWriteOneQuota() throws Exception {
        ArrayList<CacheQuotaHint> requests = new ArrayList<>();
        requests.add(buildCacheQuotaHint("uuid", 0, 100));

        CacheQuotaStrategy.saveToXml(mOut, requests, 1000);
        mOut.flush();

        assertThat(mWriter.toString()).isEqualTo(
                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
                "<cache-info previousBytes=\"1000\">\n"
                        + "<quota uuid=\"uuid\" uid=\"0\" bytes=\"100\" />\n"
                        + "</cache-info>\n");
    }

    @Test
    public void testWriteMultipleQuotas() throws Exception {
        ArrayList<CacheQuotaHint> requests = new ArrayList<>();
        requests.add(buildCacheQuotaHint("uuid", 0, 100));
        requests.add(buildCacheQuotaHint("uuid2", 10, 250));

        CacheQuotaStrategy.saveToXml(mOut, requests, 1000);
        mOut.flush();

        assertThat(mWriter.toString()).isEqualTo(
                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
                        "<cache-info previousBytes=\"1000\">\n"
                        + "<quota uuid=\"uuid\" uid=\"0\" bytes=\"100\" />\n"
                        + "<quota uuid=\"uuid2\" uid=\"10\" bytes=\"250\" />\n"
                        + "</cache-info>\n");
    }

    @Test
    public void testNullUuidDoesntCauseCrash() throws Exception {
        ArrayList<CacheQuotaHint> requests = new ArrayList<>();
        requests.add(buildCacheQuotaHint(null, 0, 100));
        requests.add(buildCacheQuotaHint(null, 10, 250));

        CacheQuotaStrategy.saveToXml(mOut, requests, 1000);
        mOut.flush();

        assertThat(mWriter.toString()).isEqualTo(
                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
                        "<cache-info previousBytes=\"1000\">\n"
                        + "<quota uid=\"0\" bytes=\"100\" />\n"
                        + "<quota uid=\"10\" bytes=\"250\" />\n"
                        + "</cache-info>\n");
    }

    @Test
    public void testReadMultipleQuotas() throws Exception {
        String input = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
                + "<cache-info previousBytes=\"1000\">\n"
                + "<quota uuid=\"uuid\" uid=\"0\" bytes=\"100\" />\n"
                + "<quota uuid=\"uuid2\" uid=\"10\" bytes=\"250\" />\n"
                + "</cache-info>\n";

        Pair<Long, List<CacheQuotaHint>> output =
                CacheQuotaStrategy.readFromXml(new ByteArrayInputStream(input.getBytes("UTF-8")));

        assertThat(output.first).isEqualTo(1000);
        assertThat(output.second).containsExactly(buildCacheQuotaHint("uuid", 0, 100),
                buildCacheQuotaHint("uuid2", 10, 250));
    }

    private CacheQuotaHint buildCacheQuotaHint(String volumeUuid, int uid, long quota) {
        return new CacheQuotaHint.Builder()
                .setVolumeUuid(volumeUuid).setUid(uid).setQuota(quota).build();
    }
}
 No newline at end of file
+31 −8
Original line number Diff line number Diff line
@@ -55,6 +55,8 @@ import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.storage.CacheQuotaStrategy;

import java.io.IOException;

public class StorageStatsService extends IStorageStatsManager.Stub {
    private static final String TAG = "StorageStatsService";

@@ -97,7 +99,7 @@ public class StorageStatsService extends IStorageStatsManager.Stub {
        invalidateMounts();

        mHandler = new H(IoThread.get().getLooper());
        mHandler.sendEmptyMessageDelayed(H.MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
        mHandler.sendEmptyMessageDelayed(H.MSG_LOAD_CACHED_QUOTAS_FROM_FILE, DELAY_IN_MILLIS);

        mStorage.registerListener(new StorageEventListener() {
            @Override
@@ -343,12 +345,14 @@ public class StorageStatsService extends IStorageStatsManager.Stub {

    private class H extends Handler {
        private static final int MSG_CHECK_STORAGE_DELTA = 100;
        private static final int MSG_LOAD_CACHED_QUOTAS_FROM_FILE = 101;
        /**
         * By only triggering a re-calculation after the storage has changed sizes, we can avoid
         * recalculating quotas too often. Minimum change delta defines the percentage of change
         * we need to see before we recalculate.
         */
        private static final double MINIMUM_CHANGE_DELTA = 0.05;
        private static final int UNSET = -1;
        private static final boolean DEBUG = false;

        private final StatFs mStats;
@@ -361,7 +365,6 @@ public class StorageStatsService extends IStorageStatsManager.Stub {
            mStats = new StatFs(Environment.getDataDirectory().getAbsolutePath());
            mPreviousBytes = mStats.getFreeBytes();
            mMinimumThresholdBytes = mStats.getTotalBytes() * MINIMUM_CHANGE_DELTA;
            // TODO: Load cache quotas from a file to avoid re-doing work.
        }

        public void handleMessage(Message msg) {
@@ -378,7 +381,26 @@ public class StorageStatsService extends IStorageStatsManager.Stub {
                    long bytesDelta = Math.abs(mPreviousBytes - mStats.getFreeBytes());
                    if (bytesDelta > mMinimumThresholdBytes) {
                        mPreviousBytes = mStats.getFreeBytes();
                        recalculateQuotas();
                        recalculateQuotas(getInitializedStrategy());
                    }
                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
                    break;
                }
                case MSG_LOAD_CACHED_QUOTAS_FROM_FILE: {
                    CacheQuotaStrategy strategy = getInitializedStrategy();
                    mPreviousBytes = UNSET;
                    try {
                        mPreviousBytes = strategy.setupQuotasFromFile();
                    } catch (IOException e) {
                        Slog.e(TAG, "An error occurred while reading the cache quota file.", e);
                    } catch (IllegalStateException e) {
                        Slog.e(TAG, "Cache quota XML file is malformed?", e);
                    }

                    // If errors occurred getting the quotas from disk, let's re-calc them.
                    if (mPreviousBytes < 0) {
                        mPreviousBytes = mStats.getFreeBytes();
                        recalculateQuotas(strategy);
                    }
                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
                    break;
@@ -391,17 +413,18 @@ public class StorageStatsService extends IStorageStatsManager.Stub {
            }
        }

        private void recalculateQuotas() {
        private void recalculateQuotas(CacheQuotaStrategy strategy) {
            if (DEBUG) {
                Slog.v(TAG, ">>> recalculating quotas ");
            }

            strategy.recalculateQuotas();
        }

        private CacheQuotaStrategy getInitializedStrategy() {
            UsageStatsManagerInternal usageStatsManager =
                    LocalServices.getService(UsageStatsManagerInternal.class);
            CacheQuotaStrategy strategy = new CacheQuotaStrategy(
                    mContext, usageStatsManager, mInstaller);
            // TODO: Save cache quotas to an XML file.
            strategy.recalculateQuotas();
            return new CacheQuotaStrategy(mContext, usageStatsManager, mInstaller);
        }
    }