/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.server.storage; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageStats; import android.os.AsyncTask; import android.os.BatteryManager; import android.os.Environment; import android.os.Environment.UserEnvironment; import android.os.UserHandle; import android.os.storage.VolumeInfo; import android.provider.Settings; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.storage.FileCollector.MeasurementResult; import java.io.File; import java.io.IOException; import java.util.List; import java.util.concurrent.TimeUnit; /** * DiskStatsLoggingService is a JobService which collects storage categorization information and * app size information on a roughly daily cadence. */ public class DiskStatsLoggingService extends JobService { private static final String TAG = "DiskStatsLogService"; public static final String DUMPSYS_CACHE_PATH = "/data/system/diskstats_cache.json"; private static final int JOB_DISKSTATS_LOGGING = 0x4449534b; // DISK private static ComponentName sDiskStatsLoggingService = new ComponentName( "android", DiskStatsLoggingService.class.getName()); @Override public boolean onStartJob(JobParameters params) { // We need to check the preconditions again because they may not be enforced for // subsequent runs. if (!isCharging(this) || !isDumpsysTaskEnabled(getContentResolver())) { jobFinished(params, true); return false; } VolumeInfo volume = getPackageManager().getPrimaryStorageCurrentVolume(); // volume is null if the primary storage is not yet mounted. if (volume == null) { return false; } AppCollector collector = new AppCollector(this, volume); final int userId = UserHandle.myUserId(); UserEnvironment environment = new UserEnvironment(userId); LogRunnable task = new LogRunnable(); task.setDownloadsDirectory( environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)); task.setSystemSize(FileCollector.getSystemSize(this)); task.setLogOutputFile(new File(DUMPSYS_CACHE_PATH)); task.setAppCollector(collector); task.setJobService(this, params); task.setContext(this); AsyncTask.execute(task); return true; } @Override public boolean onStopJob(JobParameters params) { // TODO: Try to stop being handled. return false; } /** * Schedules a DiskStats collection task. This task only runs on device idle while charging * once every 24 hours. * @param context Context to use to get a job scheduler. */ public static void schedule(Context context) { JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); js.schedule(new JobInfo.Builder(JOB_DISKSTATS_LOGGING, sDiskStatsLoggingService) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .setPeriodic(TimeUnit.DAYS.toMillis(1)) .build()); } private static boolean isCharging(Context context) { BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); if (batteryManager != null) { return batteryManager.isCharging(); } return false; } @VisibleForTesting static boolean isDumpsysTaskEnabled(ContentResolver resolver) { // The default is to treat the task as enabled. return Settings.Global.getInt(resolver, Settings.Global.ENABLE_DISKSTATS_LOGGING, 1) != 0; } @VisibleForTesting static class LogRunnable implements Runnable { private static final long TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(10); private JobService mJobService; private JobParameters mParams; private AppCollector mCollector; private File mOutputFile; private File mDownloadsDirectory; private Context mContext; private long mSystemSize; public void setDownloadsDirectory(File file) { mDownloadsDirectory = file; } public void setAppCollector(AppCollector collector) { mCollector = collector; } public void setLogOutputFile(File file) { mOutputFile = file; } public void setSystemSize(long size) { mSystemSize = size; } public void setContext(Context context) { mContext = context; } public void setJobService(JobService jobService, JobParameters params) { mJobService = jobService; mParams = params; } public void run() { FileCollector.MeasurementResult mainCategories; try { mainCategories = FileCollector.getMeasurementResult(mContext); } catch (IllegalStateException e) { // This can occur if installd has an issue. Log.e(TAG, "Error while measuring storage", e); finishJob(true); return; } FileCollector.MeasurementResult downloads = FileCollector.getMeasurementResult(mDownloadsDirectory); boolean needsReschedule = true; List stats = mCollector.getPackageStats(TIMEOUT_MILLIS); if (stats != null) { needsReschedule = false; logToFile(mainCategories, downloads, stats, mSystemSize); } else { Log.w(TAG, "Timed out while fetching package stats."); } finishJob(needsReschedule); } private void logToFile(MeasurementResult mainCategories, MeasurementResult downloads, List stats, long systemSize) { DiskStatsFileLogger logger = new DiskStatsFileLogger(mainCategories, downloads, stats, systemSize); try { mOutputFile.createNewFile(); logger.dumpToFile(mOutputFile); } catch (IOException e) { Log.e(TAG, "Exception while writing opportunistic disk file cache.", e); } } private void finishJob(boolean needsReschedule) { if (mJobService != null) { mJobService.jobFinished(mParams, needsReschedule); } } } }