blob: 11b2c1b0ba5b35a0fe90ce08a8e61e1cd54ec8f4 [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.StorageVolume;
import android.text.TextUtils;
import android.util.ArrayMap;
import androidx.core.util.Preconditions;
import java.io.File;
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<CacheQuotaHintExtend>> 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<CacheQuotaHintExtend> listForUuid = byUuid.get(uuid);
if (listForUuid == null) {
listForUuid = new ArrayList<>();
byUuid.put(uuid, listForUuid);
}
listForUuid.add(convertToCacheQuotaHintExtend(request));
}
List<CacheQuotaHint> processed = new ArrayList<>();
byUuid.entrySet().forEach(
requestListEntry -> {
// Collapse all usage stats to the same uid.
Map<Integer, List<CacheQuotaHintExtend>> byUid = requestListEntry.getValue()
.stream()
.collect(Collectors.groupingBy(CacheQuotaHintExtend::getUid));
byUid.values().forEach(uidGroupedList -> {
int size = uidGroupedList.size();
if (size < 2) {
return;
}
CacheQuotaHintExtend 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.mTotalTimeInForeground +=
uidGroupedList.get(i).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<CacheQuotaHintExtend> flattenedRequests =
byUid.values()
.stream()
.map(entryList -> entryList.get(0))
.filter(entry -> entry.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).mCacheQuotaHint;
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...
long freeBytes = 0;
if (TextUtils.isEmpty(uuid)) { // regular equals because of null
freeBytes = Environment.getDataDirectory().getUsableSpace();
} else {
final StorageManager storageManager = getSystemService(StorageManager.class);
final List<StorageVolume> storageVolumes = storageManager.getStorageVolumes();
final int volumeCount = storageVolumes.size();
for (int i = 0; i < volumeCount; i++) {
final StorageVolume volume = storageVolumes.get(i);
if (TextUtils.equals(volume.getUuid(), uuid)) {
final File directory = volume.getDirectory();
freeBytes = (directory != null) ? directory.getUsableSpace() : 0;
break;
}
}
}
return Math.round(freeBytes * CACHE_RESERVE_RATIO);
}
// Compares based upon foreground time.
private static Comparator<CacheQuotaHintExtend> sCacheQuotaRequestComparator =
new Comparator<CacheQuotaHintExtend>() {
@Override
public int compare(CacheQuotaHintExtend o, CacheQuotaHintExtend t1) {
return (t1.mTotalTimeInForeground < o.mTotalTimeInForeground) ?
-1 : ((t1.mTotalTimeInForeground == o.mTotalTimeInForeground) ? 0 : 1);
}
};
private CacheQuotaHintExtend convertToCacheQuotaHintExtend(CacheQuotaHint cacheQuotaHint) {
Preconditions.checkNotNull(cacheQuotaHint);
return new CacheQuotaHintExtend(cacheQuotaHint);
}
private final class CacheQuotaHintExtend {
public final CacheQuotaHint mCacheQuotaHint;
public long mTotalTimeInForeground;
public CacheQuotaHintExtend (CacheQuotaHint cacheQuotaHint) {
mCacheQuotaHint = cacheQuotaHint;
mTotalTimeInForeground = (cacheQuotaHint.getUsageStats() != null) ?
cacheQuotaHint.getUsageStats().getTotalTimeInForeground() : 0;
}
public int getUid() {
return mCacheQuotaHint.getUid();
}
}
}