Home | History | Annotate | Download | only in storage
      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 
     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      */
    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 
    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      */
    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
    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 
    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 
    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 
    253     private void disconnectService() {
    254         if (mServiceConnection != null) {
    255             mContext.unbindService(mServiceConnection);
    256             mServiceConnection = null;
    257         }
    258     }
    259 
    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 
    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      */
    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
    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 
    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
    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