Loading core/java/android/app/usage/CacheQuotaHint.java +21 −3 Original line number Diff line number Diff line Loading @@ -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} Loading Loading @@ -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; Loading @@ -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; } Loading @@ -117,7 +136,6 @@ public final class CacheQuotaHint implements Parcelable { } public @NonNull CacheQuotaHint build() { Preconditions.checkNotNull(mUsageStats); return new CacheQuotaHint(this); } } Loading services/core/java/com/android/server/storage/CacheQuotaStrategy.java +165 −4 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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")); } /** Loading Loading @@ -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() { Loading Loading @@ -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); Loading @@ -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 = Loading @@ -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; } } } services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java 0 → 100644 +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 services/usage/java/com/android/server/usage/StorageStatsService.java +31 −8 Original line number Diff line number Diff line Loading @@ -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"; Loading Loading @@ -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 Loading Loading @@ -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; Loading @@ -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) { Loading @@ -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; Loading @@ -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); } } Loading Loading
core/java/android/app/usage/CacheQuotaHint.java +21 −3 Original line number Diff line number Diff line Loading @@ -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} Loading Loading @@ -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; Loading @@ -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; } Loading @@ -117,7 +136,6 @@ public final class CacheQuotaHint implements Parcelable { } public @NonNull CacheQuotaHint build() { Preconditions.checkNotNull(mUsageStats); return new CacheQuotaHint(this); } } Loading
services/core/java/com/android/server/storage/CacheQuotaStrategy.java +165 −4 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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")); } /** Loading Loading @@ -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() { Loading Loading @@ -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); Loading @@ -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 = Loading @@ -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; } } }
services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java 0 → 100644 +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
services/usage/java/com/android/server/usage/StorageStatsService.java +31 −8 Original line number Diff line number Diff line Loading @@ -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"; Loading Loading @@ -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 Loading Loading @@ -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; Loading @@ -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) { Loading @@ -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; Loading @@ -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); } } Loading