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