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 }