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