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 com.android.server.storage;
18 
19 import android.annotation.MainThread;
20 import android.app.usage.CacheQuotaHint;
21 import android.app.usage.CacheQuotaService;
22 import android.app.usage.ICacheQuotaService;
23 import android.app.usage.UsageStats;
24 import android.app.usage.UsageStatsManager;
25 import android.app.usage.UsageStatsManagerInternal;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.ServiceConnection;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageManager;
32 import android.content.pm.ResolveInfo;
33 import android.content.pm.ServiceInfo;
34 import android.content.pm.UserInfo;
35 import android.os.AsyncTask;
36 import android.os.Bundle;
37 import android.os.Environment;
38 import android.os.IBinder;
39 import android.os.RemoteCallback;
40 import android.os.RemoteException;
41 import android.os.UserHandle;
42 import android.os.UserManager;
43 import android.text.format.DateUtils;
44 import android.util.ArrayMap;
45 import android.util.Pair;
46 import android.util.Slog;
47 import android.util.SparseLongArray;
48 import android.util.Xml;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.internal.os.AtomicFile;
52 import com.android.internal.util.FastXmlSerializer;
53 import com.android.internal.util.Preconditions;
54 import com.android.server.pm.Installer;
55 
56 import org.xmlpull.v1.XmlPullParser;
57 import org.xmlpull.v1.XmlPullParserException;
58 import org.xmlpull.v1.XmlSerializer;
59 
60 import java.io.File;
61 import java.io.FileInputStream;
62 import java.io.FileNotFoundException;
63 import java.io.FileOutputStream;
64 import java.io.IOException;
65 import java.io.InputStream;
66 import java.nio.charset.StandardCharsets;
67 import java.util.ArrayList;
68 import java.util.List;
69 
70 /**
71  * CacheQuotaStrategy is a strategy for determining cache quotas using usage stats and foreground
72  * time using the calculation as defined in the refuel rocket.
73  */
74 public class CacheQuotaStrategy implements RemoteCallback.OnResultListener {
75     private static final String TAG = "CacheQuotaStrategy";
76 
77     private final Object mLock = new Object();
78 
79     // XML Constants
80     private static final String CACHE_INFO_TAG = "cache-info";
81     private static final String ATTR_PREVIOUS_BYTES = "previousBytes";
82     private static final String TAG_QUOTA = "quota";
83     private static final String ATTR_UUID = "uuid";
84     private static final String ATTR_UID = "uid";
85     private static final String ATTR_QUOTA_IN_BYTES = "bytes";
86 
87     private final Context mContext;
88     private final UsageStatsManagerInternal mUsageStats;
89     private final Installer mInstaller;
90     private final ArrayMap<String, SparseLongArray> mQuotaMap;
91     private ServiceConnection mServiceConnection;
92     private ICacheQuotaService mRemoteService;
93     private AtomicFile mPreviousValuesFile;
94 
CacheQuotaStrategy( Context context, UsageStatsManagerInternal usageStatsManager, Installer installer, ArrayMap<String, SparseLongArray> quotaMap)95     public CacheQuotaStrategy(
96             Context context, UsageStatsManagerInternal usageStatsManager, Installer installer,
97             ArrayMap<String, SparseLongArray> quotaMap) {
98         mContext = Preconditions.checkNotNull(context);
99         mUsageStats = Preconditions.checkNotNull(usageStatsManager);
100         mInstaller = Preconditions.checkNotNull(installer);
101         mQuotaMap = Preconditions.checkNotNull(quotaMap);
102         mPreviousValuesFile = new AtomicFile(new File(
103                 new File(Environment.getDataDirectory(), "system"), "cachequota.xml"));
104     }
105 
106     /**
107      * Recalculates the quotas and stores them to installd.
108      */
recalculateQuotas()109     public void recalculateQuotas() {
110         createServiceConnection();
111 
112         ComponentName component = getServiceComponentName();
113         if (component != null) {
114             Intent intent = new Intent();
115             intent.setComponent(component);
116             mContext.bindServiceAsUser(
117                     intent, mServiceConnection, Context.BIND_AUTO_CREATE, UserHandle.CURRENT);
118         }
119     }
120 
createServiceConnection()121     private void createServiceConnection() {
122         // If we're already connected, don't create a new connection.
123         if (mServiceConnection != null) {
124             return;
125         }
126 
127         mServiceConnection = new ServiceConnection() {
128             @Override
129             @MainThread
130             public void onServiceConnected(ComponentName name, IBinder service) {
131                 Runnable runnable = new Runnable() {
132                     @Override
133                     public void run() {
134                         synchronized (mLock) {
135                             mRemoteService = ICacheQuotaService.Stub.asInterface(service);
136                             List<CacheQuotaHint> requests = getUnfulfilledRequests();
137                             final RemoteCallback remoteCallback =
138                                     new RemoteCallback(CacheQuotaStrategy.this);
139                             try {
140                                 mRemoteService.computeCacheQuotaHints(remoteCallback, requests);
141                             } catch (RemoteException ex) {
142                                 Slog.w(TAG,
143                                         "Remote exception occurred while trying to get cache quota",
144                                         ex);
145                             }
146                         }
147                     }
148                 };
149                 AsyncTask.execute(runnable);
150             }
151 
152             @Override
153             @MainThread
154             public void onServiceDisconnected(ComponentName name) {
155                 synchronized (mLock) {
156                     mRemoteService = null;
157                 }
158             }
159         };
160     }
161 
162     /**
163      * Returns a list of CacheQuotaHints which do not have their quotas filled out for apps
164      * which have been used in the last year.
165      */
getUnfulfilledRequests()166     private List<CacheQuotaHint> getUnfulfilledRequests() {
167         long timeNow = System.currentTimeMillis();
168         long oneYearAgo = timeNow - DateUtils.YEAR_IN_MILLIS;
169 
170         List<CacheQuotaHint> requests = new ArrayList<>();
171         UserManager um = mContext.getSystemService(UserManager.class);
172         final List<UserInfo> users = um.getUsers();
173         final int userCount = users.size();
174         final PackageManager packageManager = mContext.getPackageManager();
175         for (int i = 0; i < userCount; i++) {
176             UserInfo info = users.get(i);
177             List<UsageStats> stats =
178                     mUsageStats.queryUsageStatsForUser(info.id, UsageStatsManager.INTERVAL_BEST,
179                             oneYearAgo, timeNow, /*obfuscateInstantApps=*/ false);
180             if (stats == null) {
181                 continue;
182             }
183 
184             for (UsageStats stat : stats) {
185                 String packageName = stat.getPackageName();
186                 try {
187                     // We need the app info to determine the uid and the uuid of the volume
188                     // where the app is installed.
189                     ApplicationInfo appInfo = packageManager.getApplicationInfoAsUser(
190                             packageName, 0, info.id);
191                     requests.add(
192                             new CacheQuotaHint.Builder()
193                                     .setVolumeUuid(appInfo.volumeUuid)
194                                     .setUid(appInfo.uid)
195                                     .setUsageStats(stat)
196                                     .setQuota(CacheQuotaHint.QUOTA_NOT_SET)
197                                     .build());
198                 } catch (PackageManager.NameNotFoundException e) {
199                     // This may happen if an app has a recorded usage, but has been uninstalled.
200                     continue;
201                 }
202             }
203         }
204         return requests;
205     }
206 
207     @Override
onResult(Bundle data)208     public void onResult(Bundle data) {
209         final List<CacheQuotaHint> processedRequests =
210                 data.getParcelableArrayList(
211                         CacheQuotaService.REQUEST_LIST_KEY);
212         pushProcessedQuotas(processedRequests);
213         writeXmlToFile(processedRequests);
214     }
215 
pushProcessedQuotas(List<CacheQuotaHint> processedRequests)216     private void pushProcessedQuotas(List<CacheQuotaHint> processedRequests) {
217         final int requestSize = processedRequests.size();
218         for (int i = 0; i < requestSize; i++) {
219             CacheQuotaHint request = processedRequests.get(i);
220             long proposedQuota = request.getQuota();
221             if (proposedQuota == CacheQuotaHint.QUOTA_NOT_SET) {
222                 continue;
223             }
224 
225             try {
226                 int uid = request.getUid();
227                 mInstaller.setAppQuota(request.getVolumeUuid(),
228                         UserHandle.getUserId(uid),
229                         UserHandle.getAppId(uid), proposedQuota);
230                 insertIntoQuotaMap(request.getVolumeUuid(),
231                         UserHandle.getUserId(uid),
232                         UserHandle.getAppId(uid), proposedQuota);
233             } catch (Installer.InstallerException ex) {
234                 Slog.w(TAG,
235                         "Failed to set cache quota for " + request.getUid(),
236                         ex);
237             }
238         }
239 
240         disconnectService();
241     }
242 
insertIntoQuotaMap(String volumeUuid, int userId, int appId, long quota)243     private void insertIntoQuotaMap(String volumeUuid, int userId, int appId, long quota) {
244         SparseLongArray volumeMap = mQuotaMap.get(volumeUuid);
245         if (volumeMap == null) {
246             volumeMap = new SparseLongArray();
247             mQuotaMap.put(volumeUuid, volumeMap);
248         }
249         volumeMap.put(UserHandle.getUid(userId, appId), quota);
250     }
251 
disconnectService()252     private void disconnectService() {
253         if (mServiceConnection != null) {
254             mContext.unbindService(mServiceConnection);
255             mServiceConnection = null;
256         }
257     }
258 
getServiceComponentName()259     private ComponentName getServiceComponentName() {
260         String packageName =
261                 mContext.getPackageManager().getServicesSystemSharedLibraryPackageName();
262         if (packageName == null) {
263             Slog.w(TAG, "could not access the cache quota service: no package!");
264             return null;
265         }
266 
267         Intent intent = new Intent(CacheQuotaService.SERVICE_INTERFACE);
268         intent.setPackage(packageName);
269         ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
270                 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
271         if (resolveInfo == null || resolveInfo.serviceInfo == null) {
272             Slog.w(TAG, "No valid components found.");
273             return null;
274         }
275         ServiceInfo serviceInfo = resolveInfo.serviceInfo;
276         return new ComponentName(serviceInfo.packageName, serviceInfo.name);
277     }
278 
writeXmlToFile(List<CacheQuotaHint> processedRequests)279     private void writeXmlToFile(List<CacheQuotaHint> processedRequests) {
280         FileOutputStream fileStream = null;
281         try {
282             XmlSerializer out = new FastXmlSerializer();
283             fileStream = mPreviousValuesFile.startWrite();
284             out.setOutput(fileStream, StandardCharsets.UTF_8.name());
285             saveToXml(out, processedRequests, 0);
286             mPreviousValuesFile.finishWrite(fileStream);
287         } catch (Exception e) {
288             Slog.e(TAG, "An error occurred while writing the cache quota file.", e);
289             mPreviousValuesFile.failWrite(fileStream);
290         }
291     }
292 
293     /**
294      * Initializes the quotas from the file.
295      * @return the number of bytes that were free on the device when the quotas were last calced.
296      */
setupQuotasFromFile()297     public long setupQuotasFromFile() throws IOException {
298         Pair<Long, List<CacheQuotaHint>> cachedValues = null;
299         try (FileInputStream stream = mPreviousValuesFile.openRead()) {
300             try {
301                 cachedValues = readFromXml(stream);
302             } catch (XmlPullParserException e) {
303                 throw new IllegalStateException(e.getMessage());
304             }
305         } catch (FileNotFoundException e) {
306             // The file may not exist yet -- this isn't truly exceptional.
307             return -1;
308         }
309 
310         if (cachedValues == null) {
311             Slog.e(TAG, "An error occurred while parsing the cache quota file.");
312             return -1;
313         }
314         pushProcessedQuotas(cachedValues.second);
315 
316         return cachedValues.first;
317     }
318 
319     @VisibleForTesting
saveToXml(XmlSerializer out, List<CacheQuotaHint> requests, long bytesWhenCalculated)320     static void saveToXml(XmlSerializer out,
321             List<CacheQuotaHint> requests, long bytesWhenCalculated) throws IOException {
322         out.startDocument(null, true);
323         out.startTag(null, CACHE_INFO_TAG);
324         int requestSize = requests.size();
325         out.attribute(null, ATTR_PREVIOUS_BYTES, Long.toString(bytesWhenCalculated));
326 
327         for (int i = 0; i < requestSize; i++) {
328             CacheQuotaHint request = requests.get(i);
329             out.startTag(null, TAG_QUOTA);
330             String uuid = request.getVolumeUuid();
331             if (uuid != null) {
332                 out.attribute(null, ATTR_UUID, request.getVolumeUuid());
333             }
334             out.attribute(null, ATTR_UID, Integer.toString(request.getUid()));
335             out.attribute(null, ATTR_QUOTA_IN_BYTES, Long.toString(request.getQuota()));
336             out.endTag(null, TAG_QUOTA);
337         }
338         out.endTag(null, CACHE_INFO_TAG);
339         out.endDocument();
340     }
341 
readFromXml(InputStream inputStream)342     protected static Pair<Long, List<CacheQuotaHint>> readFromXml(InputStream inputStream)
343             throws XmlPullParserException, IOException {
344         XmlPullParser parser = Xml.newPullParser();
345         parser.setInput(inputStream, StandardCharsets.UTF_8.name());
346 
347         int eventType = parser.getEventType();
348         while (eventType != XmlPullParser.START_TAG &&
349                 eventType != XmlPullParser.END_DOCUMENT) {
350             eventType = parser.next();
351         }
352 
353         if (eventType == XmlPullParser.END_DOCUMENT) {
354             Slog.d(TAG, "No quotas found in quota file.");
355             return null;
356         }
357 
358         String tagName = parser.getName();
359         if (!CACHE_INFO_TAG.equals(tagName)) {
360             throw new IllegalStateException("Invalid starting tag.");
361         }
362 
363         final List<CacheQuotaHint> quotas = new ArrayList<>();
364         long previousBytes;
365         try {
366             previousBytes = Long.parseLong(parser.getAttributeValue(
367                     null, ATTR_PREVIOUS_BYTES));
368         } catch (NumberFormatException e) {
369             throw new IllegalStateException(
370                     "Previous bytes formatted incorrectly; aborting quota read.");
371         }
372 
373         eventType = parser.next();
374         do {
375             if (eventType == XmlPullParser.START_TAG) {
376                 tagName = parser.getName();
377                 if (TAG_QUOTA.equals(tagName)) {
378                     CacheQuotaHint request = getRequestFromXml(parser);
379                     if (request == null) {
380                         continue;
381                     }
382                     quotas.add(request);
383                 }
384             }
385             eventType = parser.next();
386         } while (eventType != XmlPullParser.END_DOCUMENT);
387         return new Pair<>(previousBytes, quotas);
388     }
389 
390     @VisibleForTesting
getRequestFromXml(XmlPullParser parser)391     static CacheQuotaHint getRequestFromXml(XmlPullParser parser) {
392         try {
393             String uuid = parser.getAttributeValue(null, ATTR_UUID);
394             int uid = Integer.parseInt(parser.getAttributeValue(null, ATTR_UID));
395             long bytes = Long.parseLong(parser.getAttributeValue(null, ATTR_QUOTA_IN_BYTES));
396             return new CacheQuotaHint.Builder()
397                     .setVolumeUuid(uuid).setUid(uid).setQuota(bytes).build();
398         } catch (NumberFormatException e) {
399             Slog.e(TAG, "Invalid cache quota request, skipping.");
400             return null;
401         }
402     }
403 }
404