1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.ext.services.storage; 18 19 import android.app.usage.CacheQuotaHint; 20 import android.app.usage.CacheQuotaService; 21 import android.os.Environment; 22 import android.os.storage.StorageManager; 23 import android.os.storage.StorageVolume; 24 import android.text.TextUtils; 25 import android.util.ArrayMap; 26 27 import androidx.core.util.Preconditions; 28 29 import java.io.File; 30 import java.util.ArrayList; 31 import java.util.Comparator; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.stream.Collectors; 35 36 /** 37 * CacheQuotaServiceImpl implements the CacheQuotaService with a strategy for populating the quota 38 * of {@link CacheQuotaHint}. 39 */ 40 public class CacheQuotaServiceImpl extends CacheQuotaService { 41 private static final double CACHE_RESERVE_RATIO = 0.15; 42 43 @Override onComputeCacheQuotaHints(List<CacheQuotaHint> requests)44 public List<CacheQuotaHint> onComputeCacheQuotaHints(List<CacheQuotaHint> requests) { 45 ArrayMap<String, List<CacheQuotaHintExtend>> byUuid = new ArrayMap<>(); 46 final int requestCount = requests.size(); 47 for (int i = 0; i < requestCount; i++) { 48 CacheQuotaHint request = requests.get(i); 49 String uuid = request.getVolumeUuid(); 50 List<CacheQuotaHintExtend> listForUuid = byUuid.get(uuid); 51 if (listForUuid == null) { 52 listForUuid = new ArrayList<>(); 53 byUuid.put(uuid, listForUuid); 54 } 55 listForUuid.add(convertToCacheQuotaHintExtend(request)); 56 } 57 58 List<CacheQuotaHint> processed = new ArrayList<>(); 59 byUuid.entrySet().forEach( 60 requestListEntry -> { 61 // Collapse all usage stats to the same uid. 62 Map<Integer, List<CacheQuotaHintExtend>> byUid = requestListEntry.getValue() 63 .stream() 64 .collect(Collectors.groupingBy(CacheQuotaHintExtend::getUid)); 65 byUid.values().forEach(uidGroupedList -> { 66 int size = uidGroupedList.size(); 67 if (size < 2) { 68 return; 69 } 70 CacheQuotaHintExtend first = uidGroupedList.get(0); 71 for (int i = 1; i < size; i++) { 72 /* Note: We can't use the UsageStats built-in addition function because 73 UIDs may span multiple packages and usage stats adding has 74 matching package names as a precondition. */ 75 first.mTotalTimeInForeground += 76 uidGroupedList.get(i).mTotalTimeInForeground; 77 } 78 }); 79 80 // Because the foreground stats have been added to the first element, we need 81 // a list of only the first values (which contain the merged foreground time). 82 List<CacheQuotaHintExtend> flattenedRequests = 83 byUid.values() 84 .stream() 85 .map(entryList -> entryList.get(0)) 86 .filter(entry -> entry.mTotalTimeInForeground != 0) 87 .sorted(sCacheQuotaRequestComparator) 88 .collect(Collectors.toList()); 89 90 // Because the elements are sorted, we can use the index to also be the sorted 91 // index for cache quota calculation. 92 double sum = getSumOfFairShares(flattenedRequests.size()); 93 String uuid = requestListEntry.getKey(); 94 long reservedSize = getReservedCacheSize(uuid); 95 for (int count = 0; count < flattenedRequests.size(); count++) { 96 double share = getFairShareForPosition(count) / sum; 97 CacheQuotaHint entry = flattenedRequests.get(count).mCacheQuotaHint; 98 CacheQuotaHint.Builder builder = new CacheQuotaHint.Builder(entry); 99 builder.setQuota(Math.round(share * reservedSize)); 100 processed.add(builder.build()); 101 } 102 } 103 ); 104 105 return processed.stream() 106 .filter(request -> request.getQuota() > 0).collect(Collectors.toList()); 107 } 108 getFairShareForPosition(int position)109 private double getFairShareForPosition(int position) { 110 double value = 1.0 / Math.log(position + 3) - 0.285; 111 return (value > 0.01) ? value : 0.01; 112 } 113 getSumOfFairShares(int size)114 private double getSumOfFairShares(int size) { 115 double sum = 0; 116 for (int i = 0; i < size; i++) { 117 sum += getFairShareForPosition(i); 118 } 119 return sum; 120 } 121 getReservedCacheSize(String uuid)122 private long getReservedCacheSize(String uuid) { 123 // TODO: Revisit the cache size after running more storage tests. 124 // TODO: Figure out how to ensure ExtServices has the permissions to call 125 // StorageStatsManager, because this is ignoring the cache... 126 long freeBytes = 0; 127 if (TextUtils.isEmpty(uuid)) { // regular equals because of null 128 freeBytes = Environment.getDataDirectory().getUsableSpace(); 129 } else { 130 final StorageManager storageManager = getSystemService(StorageManager.class); 131 final List<StorageVolume> storageVolumes = storageManager.getStorageVolumes(); 132 final int volumeCount = storageVolumes.size(); 133 for (int i = 0; i < volumeCount; i++) { 134 final StorageVolume volume = storageVolumes.get(i); 135 if (TextUtils.equals(volume.getUuid(), uuid)) { 136 final File directory = volume.getDirectory(); 137 freeBytes = (directory != null) ? directory.getUsableSpace() : 0; 138 break; 139 } 140 } 141 } 142 return Math.round(freeBytes * CACHE_RESERVE_RATIO); 143 } 144 145 // Compares based upon foreground time. 146 private static Comparator<CacheQuotaHintExtend> sCacheQuotaRequestComparator = 147 new Comparator<CacheQuotaHintExtend>() { 148 @Override 149 public int compare(CacheQuotaHintExtend o, CacheQuotaHintExtend t1) { 150 return (t1.mTotalTimeInForeground < o.mTotalTimeInForeground) ? 151 -1 : ((t1.mTotalTimeInForeground == o.mTotalTimeInForeground) ? 0 : 1); 152 } 153 }; 154 convertToCacheQuotaHintExtend(CacheQuotaHint cacheQuotaHint)155 private CacheQuotaHintExtend convertToCacheQuotaHintExtend(CacheQuotaHint cacheQuotaHint) { 156 Preconditions.checkNotNull(cacheQuotaHint); 157 return new CacheQuotaHintExtend(cacheQuotaHint); 158 } 159 160 private final class CacheQuotaHintExtend { 161 public final CacheQuotaHint mCacheQuotaHint; 162 public long mTotalTimeInForeground; 163 CacheQuotaHintExtend(CacheQuotaHint cacheQuotaHint)164 public CacheQuotaHintExtend (CacheQuotaHint cacheQuotaHint) { 165 mCacheQuotaHint = cacheQuotaHint; 166 mTotalTimeInForeground = (cacheQuotaHint.getUsageStats() != null) ? 167 cacheQuotaHint.getUsageStats().getTotalTimeInForeground() : 0; 168 } 169 getUid()170 public int getUid() { 171 return mCacheQuotaHint.getUid(); 172 } 173 } 174 } 175