Save/load calculated cache quotas to a file.
This will prevent us from unnecessarily redoing calculation work by loading the last caches on boot and shoving them down to installd. Bug: 33965858 Test: Framework services tests Change-Id: Ie94e269aa72bceb1ebe87911eaa42e2d826c1123
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +232,10 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener {
|
||||
}
|
||||
|
||||
private void disconnectService() {
|
||||
mContext.unbindService(mServiceConnection);
|
||||
mServiceConnection = null;
|
||||
if (mServiceConnection != null) {
|
||||
mContext.unbindService(mServiceConnection);
|
||||
mServiceConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
private ComponentName getServiceComponentName() {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user