1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE2.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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.server.storage;
18 
19 import android.app.job.JobInfo;
20 import android.app.job.JobParameters;
21 import android.app.job.JobScheduler;
22 import android.app.job.JobService;
23 import android.content.ComponentName;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.pm.PackageStats;
27 import android.os.AsyncTask;
28 import android.os.BatteryManager;
29 import android.os.Environment;
30 import android.os.Environment.UserEnvironment;
31 import android.os.UserHandle;
32 import android.os.storage.VolumeInfo;
33 import android.provider.Settings;
34 import android.util.Log;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.server.storage.FileCollector.MeasurementResult;
38 
39 import java.io.File;
40 import java.io.IOException;
41 import java.util.List;
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * DiskStatsLoggingService is a JobService which collects storage categorization information and
46  * app size information on a roughly daily cadence.
47  */
48 public class DiskStatsLoggingService extends JobService {
49     private static final String TAG = "DiskStatsLogService";
50     public static final String DUMPSYS_CACHE_PATH = "/data/system/diskstats_cache.json";
51     private static final int JOB_DISKSTATS_LOGGING = 0x4449534b; // DISK
52     private static ComponentName sDiskStatsLoggingService = new ComponentName(
53             "android",
54             DiskStatsLoggingService.class.getName());
55 
56     @Override
onStartJob(JobParameters params)57     public boolean onStartJob(JobParameters params) {
58         // We need to check the preconditions again because they may not be enforced for
59         // subsequent runs.
60         if (!isCharging(this) || !isDumpsysTaskEnabled(getContentResolver())) {
61             jobFinished(params, true);
62             return false;
63         }
64 
65 
66         VolumeInfo volume = getPackageManager().getPrimaryStorageCurrentVolume();
67         // volume is null if the primary storage is not yet mounted.
68         if (volume == null) {
69             return false;
70         }
71         AppCollector collector = new AppCollector(this, volume);
72 
73         final int userId = UserHandle.myUserId();
74         UserEnvironment environment = new UserEnvironment(userId);
75         LogRunnable task = new LogRunnable();
76         task.setDownloadsDirectory(
77                 environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
78         task.setSystemSize(FileCollector.getSystemSize(this));
79         task.setLogOutputFile(new File(DUMPSYS_CACHE_PATH));
80         task.setAppCollector(collector);
81         task.setJobService(this, params);
82         task.setContext(this);
83         AsyncTask.execute(task);
84         return true;
85     }
86 
87     @Override
onStopJob(JobParameters params)88     public boolean onStopJob(JobParameters params) {
89         // TODO: Try to stop being handled.
90         return false;
91     }
92 
93     /**
94      * Schedules a DiskStats collection task. This task only runs on device idle while charging
95      * once every 24 hours.
96      * @param context Context to use to get a job scheduler.
97      */
schedule(Context context)98     public static void schedule(Context context) {
99         JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
100 
101         js.schedule(new JobInfo.Builder(JOB_DISKSTATS_LOGGING, sDiskStatsLoggingService)
102                 .setRequiresDeviceIdle(true)
103                 .setRequiresCharging(true)
104                 .setPeriodic(TimeUnit.DAYS.toMillis(1))
105                 .build());
106     }
107 
isCharging(Context context)108     private static boolean isCharging(Context context) {
109         BatteryManager batteryManager =
110                 (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);
111         if (batteryManager != null) {
112             return batteryManager.isCharging();
113         }
114         return false;
115     }
116 
117     @VisibleForTesting
isDumpsysTaskEnabled(ContentResolver resolver)118     static boolean isDumpsysTaskEnabled(ContentResolver resolver) {
119         // The default is to treat the task as enabled.
120         return Settings.Global.getInt(resolver, Settings.Global.ENABLE_DISKSTATS_LOGGING, 1) != 0;
121     }
122 
123     @VisibleForTesting
124     static class LogRunnable implements Runnable {
125         private static final long TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(10);
126 
127         private JobService mJobService;
128         private JobParameters mParams;
129         private AppCollector mCollector;
130         private File mOutputFile;
131         private File mDownloadsDirectory;
132         private Context mContext;
133         private long mSystemSize;
134 
setDownloadsDirectory(File file)135         public void setDownloadsDirectory(File file) {
136             mDownloadsDirectory = file;
137         }
138 
setAppCollector(AppCollector collector)139         public void setAppCollector(AppCollector collector) {
140             mCollector = collector;
141         }
142 
setLogOutputFile(File file)143         public void setLogOutputFile(File file) {
144             mOutputFile = file;
145         }
146 
setSystemSize(long size)147         public void setSystemSize(long size) {
148             mSystemSize = size;
149         }
150 
setContext(Context context)151         public void setContext(Context context) {
152             mContext = context;
153         }
154 
setJobService(JobService jobService, JobParameters params)155         public void setJobService(JobService jobService, JobParameters params) {
156             mJobService = jobService;
157             mParams = params;
158         }
159 
run()160         public void run() {
161             FileCollector.MeasurementResult mainCategories;
162             try {
163                 mainCategories = FileCollector.getMeasurementResult(mContext);
164             } catch (IllegalStateException e) {
165                 // This can occur if installd has an issue.
166                 Log.e(TAG, "Error while measuring storage", e);
167                 finishJob(true);
168                 return;
169             }
170             FileCollector.MeasurementResult downloads =
171                     FileCollector.getMeasurementResult(mDownloadsDirectory);
172 
173             boolean needsReschedule = true;
174             List<PackageStats> stats = mCollector.getPackageStats(TIMEOUT_MILLIS);
175             if (stats != null) {
176                 needsReschedule = false;
177                 logToFile(mainCategories, downloads, stats, mSystemSize);
178             } else {
179                 Log.w(TAG, "Timed out while fetching package stats.");
180             }
181 
182             finishJob(needsReschedule);
183         }
184 
logToFile(MeasurementResult mainCategories, MeasurementResult downloads, List<PackageStats> stats, long systemSize)185         private void logToFile(MeasurementResult mainCategories, MeasurementResult downloads,
186                 List<PackageStats> stats, long systemSize) {
187             DiskStatsFileLogger logger = new DiskStatsFileLogger(mainCategories, downloads, stats,
188                     systemSize);
189             try {
190                 mOutputFile.createNewFile();
191                 logger.dumpToFile(mOutputFile);
192             } catch (IOException e) {
193                 Log.e(TAG, "Exception while writing opportunistic disk file cache.", e);
194             }
195         }
196 
finishJob(boolean needsReschedule)197         private void finishJob(boolean needsReschedule) {
198             if (mJobService != null) {
199                 mJobService.jobFinished(mParams, needsReschedule);
200             }
201         }
202     }
203 }