diff options
Diffstat (limited to 'packages/ExtServices')
6 files changed, 358 insertions, 3 deletions
diff --git a/packages/ExtServices/Android.mk b/packages/ExtServices/Android.mk index e8a40078ef0f..d0c2b9f4e0a5 100644 --- a/packages/ExtServices/Android.mk +++ b/packages/ExtServices/Android.mk @@ -34,7 +34,8 @@ LOCAL_PRIVILEGED_MODULE := true 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 f442219a2ffc..f3d8983f74b3 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 000000000000..18863ca4cefe --- /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 000000000000..cb3c352a6742 --- /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 000000000000..e6c7b9785784 --- /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 000000000000..cc1699a38d39 --- /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(); + } +} |