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