content: /*
   * 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<PackageStats> 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<PackageStats> 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);
              }
          }
      }
  }