First pass at adding the cache quota suggestions.
This currently integrates with installd, but not with
any framework API to expose this information to apps.
The first pass, as per the design doc, adds a service
which polls for large changes in the file system free space.
If enough spaces changes, it begins a recalculation of the
cache quotas and pipes the information down to installd.
This calculation is done in the updateable ExtServices.
Further enhancements in later patches include integrating this
to listen to package install and removal events, caching the
last computed quota values into an XML file on disk to load
on boot, and exposing the information to apps.
Bug: 33965858
Test: ExtServices unit test
Change-Id: Ie39f228b73532cb6ce2f98529f7c5df0839202ae
diff --git a/Android.mk b/Android.mk
index 7a8b1b7..d3fe6ad 100644
--- a/Android.mk
+++ b/Android.mk
@@ -109,6 +109,7 @@
core/java/android/app/backup/IRestoreObserver.aidl \
core/java/android/app/backup/IRestoreSession.aidl \
core/java/android/app/backup/ISelectBackupTransportCallback.aidl \
+ core/java/android/app/usage/ICacheQuotaService.aidl \
core/java/android/app/usage/IStorageStatsManager.aidl \
core/java/android/app/usage/IUsageStatsManager.aidl \
core/java/android/bluetooth/IBluetooth.aidl \
@@ -706,6 +707,7 @@
frameworks/base/core/java/android/service/notification/StatusBarNotification.aidl \
frameworks/base/core/java/android/service/chooser/ChooserTarget.aidl \
frameworks/base/core/java/android/speech/tts/Voice.aidl \
+ frameworks/base/core/java/android/app/usage/CacheQuotaHint.aidl \
frameworks/base/core/java/android/app/usage/ExternalStorageStats.aidl \
frameworks/base/core/java/android/app/usage/StorageStats.aidl \
frameworks/base/core/java/android/app/usage/UsageEvents.aidl \
diff --git a/api/system-current.txt b/api/system-current.txt
index fc78b4b..1382541 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -7178,6 +7178,35 @@
package android.app.usage {
+ public final class CacheQuotaHint implements android.os.Parcelable {
+ ctor public CacheQuotaHint(android.app.usage.CacheQuotaHint.Builder);
+ method public int describeContents();
+ method public long getQuota();
+ method public int getUid();
+ method public android.app.usage.UsageStats getUsageStats();
+ method public java.lang.String getVolumeUuid();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.app.usage.CacheQuotaHint> CREATOR;
+ field public static final long QUOTA_NOT_SET = -1L; // 0xffffffffffffffffL
+ }
+
+ public static final class CacheQuotaHint.Builder {
+ ctor public CacheQuotaHint.Builder();
+ ctor public CacheQuotaHint.Builder(android.app.usage.CacheQuotaHint);
+ method public android.app.usage.CacheQuotaHint build();
+ method public android.app.usage.CacheQuotaHint.Builder setQuota(long);
+ method public android.app.usage.CacheQuotaHint.Builder setUid(int);
+ method public android.app.usage.CacheQuotaHint.Builder setUsageStats(android.app.usage.UsageStats);
+ method public android.app.usage.CacheQuotaHint.Builder setVolumeUuid(java.lang.String);
+ }
+
+ public abstract class CacheQuotaService extends android.app.Service {
+ ctor public CacheQuotaService();
+ method public android.os.IBinder onBind(android.content.Intent);
+ method public abstract java.util.List<android.app.usage.CacheQuotaHint> onComputeCacheQuotaHints(java.util.List<android.app.usage.CacheQuotaHint>);
+ field public static final java.lang.String SERVICE_INTERFACE = "android.app.usage.CacheQuotaService";
+ }
+
public final class ConfigurationStats implements android.os.Parcelable {
ctor public ConfigurationStats(android.app.usage.ConfigurationStats);
method public int describeContents();
diff --git a/core/java/android/app/usage/CacheQuotaHint.aidl b/core/java/android/app/usage/CacheQuotaHint.aidl
new file mode 100644
index 0000000..0470ea7
--- /dev/null
+++ b/core/java/android/app/usage/CacheQuotaHint.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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 android.app.usage;
+
+/** {@hide} */
+parcelable CacheQuotaHint;
\ No newline at end of file
diff --git a/core/java/android/app/usage/CacheQuotaHint.java b/core/java/android/app/usage/CacheQuotaHint.java
new file mode 100644
index 0000000..4b6f99b
--- /dev/null
+++ b/core/java/android/app/usage/CacheQuotaHint.java
@@ -0,0 +1,142 @@
+/*
+ * 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 android.app.usage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * CacheQuotaRequest 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}
+ */
+@SystemApi
+public final class CacheQuotaHint implements Parcelable {
+ public static final long QUOTA_NOT_SET = -1;
+ private final String mUuid;
+ private final int mUid;
+ private final UsageStats mUsageStats;
+ private final long mQuota;
+
+ /**
+ * Create a new request.
+ * @param builder A builder for this object.
+ */
+ public CacheQuotaHint(Builder builder) {
+ this.mUuid = builder.mUuid;
+ this.mUid = builder.mUid;
+ this.mUsageStats = builder.mUsageStats;
+ this.mQuota = builder.mQuota;
+ }
+
+ public String getVolumeUuid() {
+ return mUuid;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public long getQuota() {
+ return mQuota;
+ }
+
+ public UsageStats getUsageStats() {
+ return mUsageStats;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mUuid);
+ dest.writeInt(mUid);
+ dest.writeLong(mQuota);
+ dest.writeParcelable(mUsageStats, 0);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final class Builder {
+ private String mUuid;
+ private int mUid;
+ private UsageStats mUsageStats;
+ private long mQuota;
+
+ public Builder() {
+ }
+
+ public Builder(CacheQuotaHint hint) {
+ setVolumeUuid(hint.getVolumeUuid());
+ setUid(hint.getUid());
+ setUsageStats(hint.getUsageStats());
+ setQuota(hint.getQuota());
+ }
+
+ public @NonNull Builder setVolumeUuid(@Nullable String uuid) {
+ mUuid = uuid;
+ return this;
+ }
+
+ public @NonNull Builder setUid(int uid) {
+ Preconditions.checkArgumentPositive(uid, "Proposed uid was not positive.");
+ mUid = uid;
+ return this;
+ }
+
+ public @NonNull Builder setUsageStats(@Nullable UsageStats stats) {
+ mUsageStats = stats;
+ return this;
+ }
+
+ public @NonNull Builder setQuota(long quota) {
+ Preconditions.checkArgument((quota >= QUOTA_NOT_SET));
+ mQuota = quota;
+ return this;
+ }
+
+ public @NonNull CacheQuotaHint build() {
+ Preconditions.checkNotNull(mUsageStats);
+ return new CacheQuotaHint(this);
+ }
+ }
+
+ public static final Parcelable.Creator<CacheQuotaHint> CREATOR =
+ new Creator<CacheQuotaHint>() {
+ @Override
+ public CacheQuotaHint createFromParcel(Parcel in) {
+ final Builder builder = new Builder();
+ return builder.setVolumeUuid(in.readString())
+ .setUid(in.readInt())
+ .setQuota(in.readLong())
+ .setUsageStats(in.readParcelable(UsageStats.class.getClassLoader()))
+ .build();
+ }
+
+ @Override
+ public CacheQuotaHint[] newArray(int size) {
+ return new CacheQuotaHint[size];
+ }
+ };
+}
diff --git a/core/java/android/app/usage/CacheQuotaService.java b/core/java/android/app/usage/CacheQuotaService.java
new file mode 100644
index 0000000..b9430ab
--- /dev/null
+++ b/core/java/android/app/usage/CacheQuotaService.java
@@ -0,0 +1,112 @@
+/*
+ * 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 android.app.usage;
+
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.usage.ICacheQuotaService;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallback;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.List;
+
+/**
+ * CacheQuoteService defines a service which accepts cache quota requests and processes them,
+ * thereby filling out how much quota each request deserves.
+ * {@hide}
+ */
+@SystemApi
+public abstract class CacheQuotaService extends Service {
+ private static final String TAG = "CacheQuotaService";
+
+ /**
+ * The {@link Intent} action that must be declared as handled by a service
+ * in its manifest for the system to recognize it as a quota providing service.
+ */
+ public static final String SERVICE_INTERFACE = "android.app.usage.CacheQuotaService";
+
+ /** {@hide} **/
+ public static final String REQUEST_LIST_KEY = "requests";
+
+ private CacheQuotaServiceWrapper mWrapper;
+ private Handler mHandler;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWrapper = new CacheQuotaServiceWrapper();
+ mHandler = new ServiceHandler(getMainLooper());
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mWrapper;
+ }
+
+ /**
+ * Processes the cache quota list upon receiving a list of requests.
+ * @param requests A list of cache quotas to fulfill.
+ * @return A completed list of cache quota requests.
+ */
+ public abstract List<CacheQuotaHint> onComputeCacheQuotaHints(
+ List<CacheQuotaHint> requests);
+
+ private final class CacheQuotaServiceWrapper extends ICacheQuotaService.Stub {
+ @Override
+ public void computeCacheQuotaHints(
+ RemoteCallback callback, List<CacheQuotaHint> requests) {
+ final Pair<RemoteCallback, List<CacheQuotaHint>> pair =
+ Pair.create(callback, requests);
+ Message msg = mHandler.obtainMessage(ServiceHandler.MSG_SEND_LIST, pair);
+ mHandler.sendMessage(msg);
+ }
+ }
+
+ private final class ServiceHandler extends Handler {
+ public static final int MSG_SEND_LIST = 1;
+
+ public ServiceHandler(Looper looper) {
+ super(looper, null, true);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final int action = msg.what;
+ switch (action) {
+ case MSG_SEND_LIST:
+ final Pair<RemoteCallback, List<CacheQuotaHint>> pair =
+ (Pair<RemoteCallback, List<CacheQuotaHint>>) msg.obj;
+ List<CacheQuotaHint> processed = onComputeCacheQuotaHints(pair.second);
+ final Bundle data = new Bundle();
+ data.putParcelableList(REQUEST_LIST_KEY, processed);
+
+ final RemoteCallback callback = pair.first;
+ callback.sendResult(data);
+ break;
+ default:
+ Log.w(TAG, "Handling unknown message: " + action);
+ }
+ }
+ }
+}
diff --git a/core/java/android/app/usage/ICacheQuotaService.aidl b/core/java/android/app/usage/ICacheQuotaService.aidl
new file mode 100644
index 0000000..8d984e0
--- /dev/null
+++ b/core/java/android/app/usage/ICacheQuotaService.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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 android.app.usage;
+
+import android.os.RemoteCallback;
+import android.app.usage.CacheQuotaHint;
+
+/** {@hide} */
+oneway interface ICacheQuotaService {
+ void computeCacheQuotaHints(in RemoteCallback callback, in List<CacheQuotaHint> requests);
+}
diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java
index a6f91fe..08595dd 100644
--- a/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -19,7 +19,7 @@
import android.content.ComponentName;
import android.content.res.Configuration;
-import java.io.IOException;
+import java.util.List;
/**
* UsageStatsManager local system service interface.
@@ -127,4 +127,7 @@
public abstract void applyRestoredPayload(int user, String key, byte[] payload);
+ /* Cache Quota Service API */
+ public abstract List<UsageStats> queryUsageStatsForUser(
+ int userId, int interval, long beginTime, long endTime);
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 97fbfa5..7c5b8a6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3107,6 +3107,13 @@
<permission android:name="android.permission.BIND_DREAM_SERVICE"
android:protectionLevel="signature" />
+ <!-- Must be required by an {@link android.app.usage.CacheQuotaService} to ensure that only the
+ system can bind to it.
+ @hide This is not a third-party API (intended for OEMs and system apps).
+ -->
+ <permission android:name="android.permission.BIND_CACHE_QUOTA_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- @SystemApi Allows an application to call into a carrier setup flow. It is up to the
carrier setup application to enforce that this permission is required
@hide This is not a third-party API (intended for OEMs and system apps). -->
diff --git a/packages/ExtServices/Android.mk b/packages/ExtServices/Android.mk
index e8a4007..d0c2b9f 100644
--- a/packages/ExtServices/Android.mk
+++ b/packages/ExtServices/Android.mk
@@ -34,7 +34,8 @@
include $(BUILD_PACKAGE)
-
-
-
+# Use the following include to make our test apk.
+ifeq ($(strip $(LOCAL_PACKAGE_OVERRIDES)),)
+include $(call all-makefiles-under, $(LOCAL_PATH))
+endif
diff --git a/packages/ExtServices/AndroidManifest.xml b/packages/ExtServices/AndroidManifest.xml
index f442219..f3d8983 100644
--- a/packages/ExtServices/AndroidManifest.xml
+++ b/packages/ExtServices/AndroidManifest.xml
@@ -25,6 +25,13 @@
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true">
+ <service android:name=".storage.CacheQuotaServiceImpl"
+ android:permission="android.permission.BIND_CACHE_QUOTA_SERVICE">
+ <intent-filter>
+ <action android:name="android.app.usage.CacheQuotaService" />
+ </intent-filter>
+ </service>
+
<library android:name="android.ext.services"/>
</application>
diff --git a/packages/ExtServices/src/android/ext/services/storage/CacheQuotaServiceImpl.java b/packages/ExtServices/src/android/ext/services/storage/CacheQuotaServiceImpl.java
new file mode 100644
index 0000000..18863ca
--- /dev/null
+++ b/packages/ExtServices/src/android/ext/services/storage/CacheQuotaServiceImpl.java
@@ -0,0 +1,144 @@
+
+/*
+ * 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 android.ext.services.storage;
+
+import android.app.usage.CacheQuotaHint;
+import android.app.usage.CacheQuotaService;
+import android.os.Environment;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.util.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * CacheQuotaServiceImpl implements the CacheQuotaService with a strategy for populating the quota
+ * of {@link CacheQuotaHint}.
+ */
+public class CacheQuotaServiceImpl extends CacheQuotaService {
+ private static final double CACHE_RESERVE_RATIO = 0.15;
+
+ @Override
+ public List<CacheQuotaHint> onComputeCacheQuotaHints(List<CacheQuotaHint> requests) {
+ ArrayMap<String, List<CacheQuotaHint>> byUuid = new ArrayMap<>();
+ final int requestCount = requests.size();
+ for (int i = 0; i < requestCount; i++) {
+ CacheQuotaHint request = requests.get(i);
+ String uuid = request.getVolumeUuid();
+ List<CacheQuotaHint> listForUuid = byUuid.get(uuid);
+ if (listForUuid == null) {
+ listForUuid = new ArrayList<>();
+ byUuid.put(uuid, listForUuid);
+ }
+ listForUuid.add(request);
+ }
+
+ List<CacheQuotaHint> processed = new ArrayList<>();
+ byUuid.entrySet().forEach(
+ requestListEntry -> {
+ // Collapse all usage stats to the same uid.
+ Map<Integer, List<CacheQuotaHint>> byUid = requestListEntry.getValue()
+ .stream()
+ .collect(Collectors.groupingBy(CacheQuotaHint::getUid));
+ byUid.values().forEach(uidGroupedList -> {
+ int size = uidGroupedList.size();
+ if (size < 2) {
+ return;
+ }
+ CacheQuotaHint first = uidGroupedList.get(0);
+ for (int i = 1; i < size; i++) {
+ /* Note: We can't use the UsageStats built-in addition function because
+ UIDs may span multiple packages and usage stats adding has
+ matching package names as a precondition. */
+ first.getUsageStats().mTotalTimeInForeground +=
+ uidGroupedList.get(i).getUsageStats().mTotalTimeInForeground;
+ }
+ });
+
+ // Because the foreground stats have been added to the first element, we need
+ // a list of only the first values (which contain the merged foreground time).
+ List<CacheQuotaHint> flattenedRequests =
+ byUid.values()
+ .stream()
+ .map(entryList -> entryList.get(0))
+ .filter(entry -> entry.getUsageStats().mTotalTimeInForeground != 0)
+ .sorted(sCacheQuotaRequestComparator)
+ .collect(Collectors.toList());
+
+ // Because the elements are sorted, we can use the index to also be the sorted
+ // index for cache quota calculation.
+ double sum = getSumOfFairShares(flattenedRequests.size());
+ String uuid = requestListEntry.getKey();
+ long reservedSize = getReservedCacheSize(uuid);
+ for (int count = 0; count < flattenedRequests.size(); count++) {
+ double share = getFairShareForPosition(count) / sum;
+ CacheQuotaHint entry = flattenedRequests.get(count);
+ CacheQuotaHint.Builder builder = new CacheQuotaHint.Builder(entry);
+ builder.setQuota(Math.round(share * reservedSize));
+ processed.add(builder.build());
+ }
+ }
+ );
+
+ return processed.stream()
+ .filter(request -> request.getQuota() > 0).collect(Collectors.toList());
+ }
+
+ private double getFairShareForPosition(int position) {
+ double value = 1.0 / Math.log(position + 3) - 0.285;
+ return (value > 0.01) ? value : 0.01;
+ }
+
+ private double getSumOfFairShares(int size) {
+ double sum = 0;
+ for (int i = 0; i < size; i++) {
+ sum += getFairShareForPosition(i);
+ }
+ return sum;
+ }
+
+ private long getReservedCacheSize(String uuid) {
+ // TODO: Revisit the cache size after running more storage tests.
+ // TODO: Figure out how to ensure ExtServices has the permissions to call
+ // StorageStatsManager, because this is ignoring the cache...
+ StorageManager storageManager = getSystemService(StorageManager.class);
+ long freeBytes = 0;
+ if (uuid == StorageManager.UUID_PRIVATE_INTERNAL) { // regular equals because of null
+ freeBytes = Environment.getDataDirectory().getFreeSpace();
+ } else {
+ final VolumeInfo vol = storageManager.findVolumeByUuid(uuid);
+ freeBytes = vol.getPath().getFreeSpace();
+ }
+ return Math.round(freeBytes * CACHE_RESERVE_RATIO);
+ }
+
+ // Compares based upon foreground time.
+ private static Comparator<CacheQuotaHint> sCacheQuotaRequestComparator =
+ new Comparator<CacheQuotaHint>() {
+ @Override
+ public int compare(CacheQuotaHint o, CacheQuotaHint t1) {
+ long x = t1.getUsageStats().getTotalTimeInForeground();
+ long y = o.getUsageStats().getTotalTimeInForeground();
+ return (x < y) ? -1 : ((x == y) ? 0 : 1);
+ }
+ };
+}
diff --git a/packages/ExtServices/tests/Android.mk b/packages/ExtServices/tests/Android.mk
new file mode 100644
index 0000000..cb3c352
--- /dev/null
+++ b/packages/ExtServices/tests/Android.mk
@@ -0,0 +1,24 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+LOCAL_CERTIFICATE := platform
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ mockito-target-minus-junit4 \
+ espresso-core \
+ truth-prebuilt \
+ legacy-android-test
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := ExtServicesUnitTests
+
+LOCAL_INSTRUMENTATION_FOR := ExtServices
+
+include $(BUILD_PACKAGE)
diff --git a/packages/ExtServices/tests/AndroidManifest.xml b/packages/ExtServices/tests/AndroidManifest.xml
new file mode 100644
index 0000000..e6c7b97
--- /dev/null
+++ b/packages/ExtServices/tests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.ext.services.tests.unit">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.ext.services"
+ android:label="ExtServices Test Cases">
+ </instrumentation>
+
+</manifest>
\ No newline at end of file
diff --git a/packages/ExtServices/tests/src/android/ext/services/storage/CacheQuotaServiceImplTest.java b/packages/ExtServices/tests/src/android/ext/services/storage/CacheQuotaServiceImplTest.java
new file mode 100644
index 0000000..cc1699a
--- /dev/null
+++ b/packages/ExtServices/tests/src/android/ext/services/storage/CacheQuotaServiceImplTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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 android.ext.services.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.app.usage.CacheQuotaHint;
+import android.app.usage.UsageStats;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.test.ServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CacheQuotaServiceImplTest extends ServiceTestCase<CacheQuotaServiceImpl> {
+ private static final String sTestVolUuid = "uuid";
+ private static final String sSecondTestVolUuid = "otherUuid";
+
+ @Mock private Context mContext;
+ @Mock private File mFile;
+ @Mock private VolumeInfo mVolume;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS) private StorageManager mStorageManager;
+
+ public CacheQuotaServiceImplTest() {
+ super(CacheQuotaServiceImpl.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mContext = Mockito.spy(new ContextWrapper(getSystemContext()));
+ setContext(mContext);
+ when(mContext.getSystemService(Context.STORAGE_SERVICE)).thenReturn(mStorageManager);
+
+ when(mFile.getFreeSpace()).thenReturn(10000L);
+ when(mVolume.getPath()).thenReturn(mFile);
+ when(mStorageManager.findVolumeByUuid(sTestVolUuid)).thenReturn(mVolume);
+ when(mStorageManager.findVolumeByUuid(sSecondTestVolUuid)).thenReturn(mVolume);
+
+ Intent intent = new Intent(getContext(), CacheQuotaServiceImpl.class);
+ startService(intent);
+ }
+
+ @Test
+ public void testNoApps() {
+ CacheQuotaServiceImpl service = getService();
+ assertEquals(service.onComputeCacheQuotaHints(new ArrayList()).size(), 0);
+ }
+
+ @Test
+ public void testOneApp() throws Exception {
+ ArrayList<CacheQuotaHint> requests = new ArrayList<>();
+ CacheQuotaHint request = makeNewRequest("com.test", sTestVolUuid, 1001, 100L);
+ requests.add(request);
+
+ List<CacheQuotaHint> output = getService().onComputeCacheQuotaHints(requests);
+
+ assertThat(output).hasSize(1);
+ assertThat(output.get(0).getQuota()).isEqualTo(1500L);
+ }
+
+ @Test
+ public void testTwoAppsOneVolume() throws Exception {
+ ArrayList<CacheQuotaHint> requests = new ArrayList<>();
+ requests.add(makeNewRequest("com.test", sTestVolUuid, 1001, 100L));
+ requests.add(makeNewRequest("com.test2", sTestVolUuid, 1002, 99L));
+
+ List<CacheQuotaHint> output = getService().onComputeCacheQuotaHints(requests);
+
+ // Note that the sizes are just the cache area split up.
+ assertThat(output).hasSize(2);
+ assertThat(output.get(0).getQuota()).isEqualTo(883);
+ assertThat(output.get(1).getQuota()).isEqualTo(1500 - 883);
+ }
+
+ @Test
+ public void testTwoAppsTwoVolumes() throws Exception {
+ ArrayList<CacheQuotaHint> requests = new ArrayList<>();
+ requests.add(makeNewRequest("com.test", sTestVolUuid, 1001, 100L));
+ requests.add(makeNewRequest("com.test2", sSecondTestVolUuid, 1002, 99L));
+
+ List<CacheQuotaHint> output = getService().onComputeCacheQuotaHints(requests);
+
+ assertThat(output).hasSize(2);
+ assertThat(output.get(0).getQuota()).isEqualTo(1500);
+ assertThat(output.get(1).getQuota()).isEqualTo(1500);
+ }
+
+ @Test
+ public void testMultipleAppsPerUidIsCollated() throws Exception {
+ ArrayList<CacheQuotaHint> requests = new ArrayList<>();
+ requests.add(makeNewRequest("com.test", sTestVolUuid, 1001, 100L));
+ requests.add(makeNewRequest("com.test2", sTestVolUuid, 1001, 99L));
+
+ List<CacheQuotaHint> output = getService().onComputeCacheQuotaHints(requests);
+
+ assertThat(output).hasSize(1);
+ assertThat(output.get(0).getQuota()).isEqualTo(1500);
+ }
+
+ @Test
+ public void testTwoAppsTwoVolumesTwoUuidsShouldBESeparate() throws Exception {
+ ArrayList<CacheQuotaHint> requests = new ArrayList<>();
+ requests.add(makeNewRequest("com.test", sTestVolUuid, 1001, 100L));
+ requests.add(makeNewRequest("com.test2", sSecondTestVolUuid, 1001, 99L));
+
+ List<CacheQuotaHint> output = getService().onComputeCacheQuotaHints(requests);
+
+ assertThat(output).hasSize(2);
+ assertThat(output.get(0).getQuota()).isEqualTo(1500);
+ assertThat(output.get(1).getQuota()).isEqualTo(1500);
+ }
+
+ private CacheQuotaHint makeNewRequest(String packageName, String uuid, int uid, long foregroundTime) {
+ UsageStats stats = new UsageStats();
+ stats.mPackageName = packageName;
+ stats.mTotalTimeInForeground = foregroundTime;
+ return new CacheQuotaHint.Builder()
+ .setVolumeUuid(uuid).setUid(uid).setUsageStats(stats).setQuota(-1).build();
+ }
+}
diff --git a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java
new file mode 100644
index 0000000..10d30aa
--- /dev/null
+++ b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java
@@ -0,0 +1,226 @@
+/*
+ * 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 android.annotation.MainThread;
+import android.app.usage.CacheQuotaHint;
+import android.app.usage.CacheQuotaService;
+import android.app.usage.ICacheQuotaService;
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.UserInfo;
+import android.os.AsyncTask;
+import android.os.Bundle;
+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.Slog;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.pm.Installer;
+
+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.
+ */
+public class CacheQuotaStrategy implements RemoteCallback.OnResultListener {
+ private static final String TAG = "CacheQuotaStrategy";
+
+ private final Object mLock = new Object();
+
+ private final Context mContext;
+ private final UsageStatsManagerInternal mUsageStats;
+ private final Installer mInstaller;
+ private ServiceConnection mServiceConnection;
+ private ICacheQuotaService mRemoteService;
+
+ public CacheQuotaStrategy(
+ Context context, UsageStatsManagerInternal usageStatsManager, Installer installer) {
+ mContext = Preconditions.checkNotNull(context);
+ mUsageStats = Preconditions.checkNotNull(usageStatsManager);
+ mInstaller = Preconditions.checkNotNull(installer);
+ }
+
+ /**
+ * Recalculates the quotas and stores them to installd.
+ */
+ public void recalculateQuotas() {
+ createServiceConnection();
+
+ ComponentName component = getServiceComponentName();
+ if (component != null) {
+ Intent intent = new Intent();
+ intent.setComponent(component);
+ mContext.bindServiceAsUser(
+ intent, mServiceConnection, Context.BIND_AUTO_CREATE, UserHandle.CURRENT);
+ }
+ }
+
+ private void createServiceConnection() {
+ // If we're already connected, don't create a new connection.
+ if (mServiceConnection != null) {
+ return;
+ }
+
+ mServiceConnection = new ServiceConnection() {
+ @Override
+ @MainThread
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ mRemoteService = ICacheQuotaService.Stub.asInterface(service);
+ List<CacheQuotaHint> requests = getUnfulfilledRequests();
+ final RemoteCallback remoteCallback =
+ new RemoteCallback(CacheQuotaStrategy.this);
+ try {
+ mRemoteService.computeCacheQuotaHints(remoteCallback, requests);
+ } catch (RemoteException ex) {
+ Slog.w(TAG,
+ "Remote exception occurred while trying to get cache quota",
+ ex);
+ }
+ }
+ }
+ };
+ AsyncTask.execute(runnable);
+ }
+
+ @Override
+ @MainThread
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (mLock) {
+ mRemoteService = null;
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a list of CacheQuotaRequests which do not have their quotas filled out for apps
+ * which have been used in the last year.
+ */
+ private List<CacheQuotaHint> getUnfulfilledRequests() {
+ long timeNow = System.currentTimeMillis();
+ long oneYearAgo = timeNow - DateUtils.YEAR_IN_MILLIS;
+
+ List<CacheQuotaHint> requests = new ArrayList<>();
+ UserManager um = mContext.getSystemService(UserManager.class);
+ final List<UserInfo> users = um.getUsers();
+ final int userCount = users.size();
+ final PackageManager packageManager = mContext.getPackageManager();
+ for (int i = 0; i < userCount; i++) {
+ UserInfo info = users.get(i);
+ List<UsageStats> stats =
+ mUsageStats.queryUsageStatsForUser(info.id, UsageStatsManager.INTERVAL_BEST,
+ oneYearAgo, timeNow);
+ if (stats == null) {
+ continue;
+ }
+
+ for (UsageStats stat : stats) {
+ String packageName = stat.getPackageName();
+ try {
+ // We need the app info to determine the uid and the uuid of the volume
+ // where the app is installed.
+ ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
+ requests.add(
+ new CacheQuotaHint.Builder()
+ .setVolumeUuid(appInfo.volumeUuid)
+ .setUid(appInfo.uid)
+ .setUsageStats(stat)
+ .setQuota(CacheQuotaHint.QUOTA_NOT_SET)
+ .build());
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.w(TAG, "Unable to find package for quota calculation", e);
+ continue;
+ }
+ }
+ }
+ return requests;
+ }
+
+ @Override
+ public void onResult(Bundle data) {
+ final List<CacheQuotaHint> processedRequests =
+ data.getParcelableArrayList(
+ CacheQuotaService.REQUEST_LIST_KEY);
+ final int requestSize = processedRequests.size();
+ for (int i = 0; i < requestSize; i++) {
+ CacheQuotaHint request = processedRequests.get(i);
+ long proposedQuota = request.getQuota();
+ if (proposedQuota == CacheQuotaHint.QUOTA_NOT_SET) {
+ continue;
+ }
+
+ try {
+ int uid = request.getUid();
+ mInstaller.setAppQuota(request.getVolumeUuid(),
+ UserHandle.getUserId(uid),
+ UserHandle.getAppId(uid), proposedQuota);
+ } catch (Installer.InstallerException ex) {
+ Slog.w(TAG,
+ "Failed to set cache quota for " + request.getUid(),
+ ex);
+ }
+ }
+
+ disconnectService();
+ }
+
+ private void disconnectService() {
+ mContext.unbindService(mServiceConnection);
+ mServiceConnection = null;
+ }
+
+ private ComponentName getServiceComponentName() {
+ String packageName =
+ mContext.getPackageManager().getServicesSystemSharedLibraryPackageName();
+ if (packageName == null) {
+ Slog.w(TAG, "could not access the cache quota service: no package!");
+ return null;
+ }
+
+ Intent intent = new Intent(CacheQuotaService.SERVICE_INTERFACE);
+ intent.setPackage(packageName);
+ ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ Slog.w(TAG, "No valid components found.");
+ return null;
+ }
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ return new ComponentName(serviceInfo.packageName, serviceInfo.name);
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index 6826975..96907c3 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -20,6 +20,7 @@
import android.app.usage.ExternalStorageStats;
import android.app.usage.IStorageStatsManager;
import android.app.usage.StorageStats;
+import android.app.usage.UsageStatsManagerInternal;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -28,25 +29,35 @@
import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.StatFs;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageEventListener;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
+import android.text.format.DateUtils;
import android.util.Slog;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
+import com.android.server.IoThread;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
+import com.android.server.storage.CacheQuotaStrategy;
public class StorageStatsService extends IStorageStatsManager.Stub {
private static final String TAG = "StorageStatsService";
private static final String PROP_VERIFY_STORAGE = "fw.verify_storage";
+ private static final long DELAY_IN_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS;
+
public static class Lifecycle extends SystemService {
private StorageStatsService mService;
@@ -68,6 +79,7 @@
private final StorageManager mStorage;
private final Installer mInstaller;
+ private final H mHandler;
public StorageStatsService(Context context) {
mContext = Preconditions.checkNotNull(context);
@@ -80,6 +92,9 @@
mInstaller.onStart();
invalidateMounts();
+ mHandler = new H(IoThread.get().getLooper());
+ mHandler.sendEmptyMessageDelayed(H.MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
+
mStorage.registerListener(new StorageEventListener() {
@Override
public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
@@ -274,4 +289,63 @@
res.cacheBytes = stats.cacheSize + stats.externalCacheSize;
return res;
}
+
+ private class H extends Handler {
+ private static final int MSG_CHECK_STORAGE_DELTA = 100;
+ /**
+ * 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 boolean DEBUG = false;
+
+ private final StatFs mStats;
+ private long mPreviousBytes;
+ private double mMinimumThresholdBytes;
+
+ public H(Looper looper) {
+ super(looper);
+ // TODO: Handle all private volumes.
+ 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) {
+ if (DEBUG) {
+ Slog.v(TAG, ">>> handling " + msg.what);
+ }
+ switch (msg.what) {
+ case MSG_CHECK_STORAGE_DELTA: {
+ long bytesDelta = Math.abs(mPreviousBytes - mStats.getFreeBytes());
+ if (bytesDelta > mMinimumThresholdBytes) {
+ mPreviousBytes = mStats.getFreeBytes();
+ recalculateQuotas();
+ }
+ sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
+ break;
+ }
+ default:
+ if (DEBUG) {
+ Slog.v(TAG, ">>> default message case ");
+ }
+ return;
+ }
+ }
+
+ private void recalculateQuotas() {
+ if (DEBUG) {
+ Slog.v(TAG, ">>> recalculating quotas ");
+ }
+
+ UsageStatsManagerInternal usageStatsManager =
+ LocalServices.getService(UsageStatsManagerInternal.class);
+ CacheQuotaStrategy strategy = new CacheQuotaStrategy(
+ mContext, usageStatsManager, mInstaller);
+ // TODO: Save cache quotas to an XML file.
+ strategy.recalculateQuotas();
+ }
+ }
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 7a69803..3c743b5 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1603,5 +1603,12 @@
userStats.applyRestoredPayload(key, payload);
}
}
+
+ @Override
+ public List<UsageStats> queryUsageStatsForUser(
+ int userId, int intervalType, long beginTime, long endTime) {
+ return UsageStatsService.this.queryUsageStats(
+ userId, intervalType, beginTime, endTime);
+ }
}
}