blob: 862f50b2b627c609c4c79553997359631e847c3c [file] [log] [blame]
/*
* 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().getUsableSpace();
} else {
final VolumeInfo vol = storageManager.findVolumeByUuid(uuid);
freeBytes = vol.getPath().getUsableSpace();
}
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);
}
};
}