content: /*
   * Copyright (C) 2007-2008 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/LICENSE-2.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.annotation.WorkerThread;
  import android.app.Notification;
  import android.app.NotificationChannel;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.os.Binder;
  import android.os.Environment;
  import android.os.FileObserver;
  import android.os.Handler;
  import android.os.HandlerThread;
  import android.os.Message;
  import android.os.ResultReceiver;
  import android.os.ServiceManager;
  import android.os.ShellCallback;
  import android.os.ShellCommand;
  import android.os.UserHandle;
  import android.os.storage.StorageManager;
  import android.os.storage.VolumeInfo;
  import android.text.format.DateUtils;
  import android.util.ArrayMap;
  import android.util.DataUnit;
  import android.util.Slog;
  import android.util.StatsLog;
  
  import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
  import com.android.internal.notification.SystemNotificationChannels;
  import com.android.internal.util.DumpUtils;
  import com.android.internal.util.IndentingPrintWriter;
  import com.android.server.EventLogTags;
  import com.android.server.SystemService;
  import com.android.server.pm.InstructionSets;
  import com.android.server.pm.PackageManagerService;
  
  import dalvik.system.VMRuntime;
  
  import java.io.File;
  import java.io.FileDescriptor;
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.util.Objects;
  import java.util.UUID;
  import java.util.concurrent.atomic.AtomicInteger;
  
  /**
   * Service that monitors and maintains free space on storage volumes.
   * <p>
   * As the free space on a volume nears the threshold defined by
   * {@link StorageManager#getStorageLowBytes(File)}, this service will clear out
   * cached data to keep the disk from entering this low state.
   */
  public class DeviceStorageMonitorService extends SystemService {
      private static final String TAG = "DeviceStorageMonitorService";
  
      /**
       * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:
       * Current int sequence number of the update.
       */
      public static final String EXTRA_SEQUENCE = "seq";
  
      private static final int MSG_CHECK = 1;
  
      private static final long DEFAULT_LOG_DELTA_BYTES = DataUnit.MEBIBYTES.toBytes(64);
      private static final long DEFAULT_CHECK_INTERVAL = DateUtils.MINUTE_IN_MILLIS;
  
      // com.android.internal.R.string.low_internal_storage_view_text_no_boot
      // hard codes 250MB in the message as the storage space required for the
      // boot image.
      private static final long BOOT_IMAGE_STORAGE_REQUIREMENT = DataUnit.MEBIBYTES.toBytes(250);
  
      private NotificationManager mNotifManager;
  
      /** Sequence number used for testing */
      private final AtomicInteger mSeq = new AtomicInteger(1);
      /** Forced level used for testing */
      private volatile int mForceLevel = State.LEVEL_UNKNOWN;
  
      /** Map from storage volume UUID to internal state */
      private final ArrayMap<UUID, State> mStates = new ArrayMap<>();
  
      /**
       * State for a specific storage volume, including the current "level" that
       * we've alerted the user and apps about.
       */
      private static class State {
          private static final int LEVEL_UNKNOWN = -1;
          private static final int LEVEL_NORMAL = 0;
          private static final int LEVEL_LOW = 1;
          private static final int LEVEL_FULL = 2;
  
          /** Last "level" that we alerted about */
          public int level = LEVEL_NORMAL;
          /** Last {@link File#getUsableSpace()} that we logged about */
          public long lastUsableBytes = Long.MAX_VALUE;
  
          /**
           * Test if the given level transition is "entering" a specific level.
           * <p>
           * As an example, a transition from {@link #LEVEL_NORMAL} to
           * {@link #LEVEL_FULL} is considered to "enter" both {@link #LEVEL_LOW}
           * and {@link #LEVEL_FULL}.
           */
          private static boolean isEntering(int level, int oldLevel, int newLevel) {
              return newLevel >= level && (oldLevel < level || oldLevel == LEVEL_UNKNOWN);
          }
  
          /**
           * Test if the given level transition is "leaving" a specific level.
           * <p>
           * As an example, a transition from {@link #LEVEL_FULL} to
           * {@link #LEVEL_NORMAL} is considered to "leave" both
           * {@link #LEVEL_FULL} and {@link #LEVEL_LOW}.
           */
          private static boolean isLeaving(int level, int oldLevel, int newLevel) {
              return newLevel < level && (oldLevel >= level || oldLevel == LEVEL_UNKNOWN);
          }
  
          private static String levelToString(int level) {
              switch (level) {
                  case State.LEVEL_UNKNOWN: return "UNKNOWN";
                  case State.LEVEL_NORMAL: return "NORMAL";
                  case State.LEVEL_LOW: return "LOW";
                  case State.LEVEL_FULL: return "FULL";
                  default: return Integer.toString(level);
              }
          }
      }
  
      private CacheFileDeletedObserver mCacheFileDeletedObserver;
  
      /**
       * This string is used for ServiceManager access to this class.
       */
      static final String SERVICE = "devicestoragemonitor";
  
      private static final String TV_NOTIFICATION_CHANNEL_ID = "devicestoragemonitor.tv";
  
      private final HandlerThread mHandlerThread;
      private final Handler mHandler;
  
      private State findOrCreateState(UUID uuid) {
          State state = mStates.get(uuid);
          if (state == null) {
              state = new State();
              mStates.put(uuid, state);
          }
          return state;
      }
  
      /**
       * Core logic that checks the storage state of every mounted private volume.
       * Since this can do heavy I/O, callers should invoke indirectly using
       * {@link #MSG_CHECK}.
       */
      @WorkerThread
      private void check() {
          final StorageManager storage = getContext().getSystemService(StorageManager.class);
          final int seq = mSeq.get();
  
          // Check every mounted private volume to see if they're low on space
          for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
              final File file = vol.getPath();
              final long fullBytes = storage.getStorageFullBytes(file);
              final long lowBytes = storage.getStorageLowBytes(file);
  
              // Automatically trim cached data when nearing the low threshold;
              // when it's within 150% of the threshold, we try trimming usage
              // back to 200% of the threshold.
              if (file.getUsableSpace() < (lowBytes * 3) / 2) {
                  final PackageManagerService pms = (PackageManagerService) ServiceManager
                          .getService("package");
                  try {
                      pms.freeStorage(vol.getFsUuid(), lowBytes * 2, 0);
                  } catch (IOException e) {
                      Slog.w(TAG, e);
                  }
              }
  
              // Send relevant broadcasts and show notifications based on any
              // recently noticed state transitions.
              final UUID uuid = StorageManager.convert(vol.getFsUuid());
              final State state = findOrCreateState(uuid);
              final long totalBytes = file.getTotalSpace();
              final long usableBytes = file.getUsableSpace();
  
              int oldLevel = state.level;
              int newLevel;
              if (mForceLevel != State.LEVEL_UNKNOWN) {
                  // When in testing mode, use unknown old level to force sending
                  // of any relevant broadcasts.
                  oldLevel = State.LEVEL_UNKNOWN;
                  newLevel = mForceLevel;
              } else if (usableBytes <= fullBytes) {
                  newLevel = State.LEVEL_FULL;
              } else if (usableBytes <= lowBytes) {
                  newLevel = State.LEVEL_LOW;
              } else if (StorageManager.UUID_DEFAULT.equals(uuid) && !isBootImageOnDisk()
                      && usableBytes < BOOT_IMAGE_STORAGE_REQUIREMENT) {
                  newLevel = State.LEVEL_LOW;
              } else {
                  newLevel = State.LEVEL_NORMAL;
              }
  
              // Log whenever we notice drastic storage changes
              if ((Math.abs(state.lastUsableBytes - usableBytes) > DEFAULT_LOG_DELTA_BYTES)
                      || oldLevel != newLevel) {
                  EventLogTags.writeStorageState(uuid.toString(), oldLevel, newLevel,
                          usableBytes, totalBytes);
                  state.lastUsableBytes = usableBytes;
              }
  
              updateNotifications(vol, oldLevel, newLevel);
              updateBroadcasts(vol, oldLevel, newLevel, seq);
  
              state.level = newLevel;
          }
  
          // Loop around to check again in future; we don't remove messages since
          // there might be an immediate request pending.
          if (!mHandler.hasMessages(MSG_CHECK)) {
              mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHECK),
                      DEFAULT_CHECK_INTERVAL);
          }
      }
  
      public DeviceStorageMonitorService(Context context) {
          super(context);
  
          mHandlerThread = new HandlerThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND);
          mHandlerThread.start();
  
          mHandler = new Handler(mHandlerThread.getLooper()) {
              @Override
              public void handleMessage(Message msg) {
                  switch (msg.what) {
                      case MSG_CHECK:
                          check();
                          return;
                  }
              }
          };
      }
  
      private static boolean isBootImageOnDisk() {
          for (String instructionSet : InstructionSets.getAllDexCodeInstructionSets()) {
              if (!VMRuntime.isBootClassPathOnDisk(instructionSet)) {
                  return false;
              }
          }
          return true;
      }
  
      @Override
      public void onStart() {
          final Context context = getContext();
          mNotifManager = context.getSystemService(NotificationManager.class);
  
          mCacheFileDeletedObserver = new CacheFileDeletedObserver();
          mCacheFileDeletedObserver.startWatching();
  
          // Ensure that the notification channel is set up
          PackageManager packageManager = context.getPackageManager();
          boolean isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
  
          if (isTv) {
              mNotifManager.createNotificationChannel(new NotificationChannel(
                      TV_NOTIFICATION_CHANNEL_ID,
                      context.getString(
                          com.android.internal.R.string.device_storage_monitor_notification_channel),
                      NotificationManager.IMPORTANCE_HIGH));
          }
  
          publishBinderService(SERVICE, mRemoteService);
          publishLocalService(DeviceStorageMonitorInternal.class, mLocalService);
  
          // Kick off pass to examine storage state
          mHandler.removeMessages(MSG_CHECK);
          mHandler.obtainMessage(MSG_CHECK).sendToTarget();
      }
  
      private final DeviceStorageMonitorInternal mLocalService = new DeviceStorageMonitorInternal() {
          @Override
          public void checkMemory() {
              // Kick off pass to examine storage state
              mHandler.removeMessages(MSG_CHECK);
              mHandler.obtainMessage(MSG_CHECK).sendToTarget();
          }
  
          @Override
          public boolean isMemoryLow() {
              return Environment.getDataDirectory().getUsableSpace() < getMemoryLowThreshold();
          }
  
          @Override
          public long getMemoryLowThreshold() {
              return getContext().getSystemService(StorageManager.class)
                      .getStorageLowBytes(Environment.getDataDirectory());
          }
      };
  
      private final Binder mRemoteService = new Binder() {
          @Override
          protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
              if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
              dumpImpl(fd, pw, args);
          }
  
          @Override
          public void onShellCommand(FileDescriptor in, FileDescriptor out,
                  FileDescriptor err, String[] args, ShellCallback callback,
                  ResultReceiver resultReceiver) {
              (new Shell()).exec(this, in, out, err, args, callback, resultReceiver);
          }
      };
  
      class Shell extends ShellCommand {
          @Override
          public int onCommand(String cmd) {
              return onShellCommand(this, cmd);
          }
  
          @Override
          public void onHelp() {
              PrintWriter pw = getOutPrintWriter();
              dumpHelp(pw);
          }
      }
  
      static final int OPTION_FORCE_UPDATE = 1<<0;
  
      int parseOptions(Shell shell) {
          String opt;
          int opts = 0;
          while ((opt = shell.getNextOption()) != null) {
              if ("-f".equals(opt)) {
                  opts |= OPTION_FORCE_UPDATE;
              }
          }
          return opts;
      }
  
      int onShellCommand(Shell shell, String cmd) {
          if (cmd == null) {
              return shell.handleDefaultCommands(cmd);
          }
          PrintWriter pw = shell.getOutPrintWriter();
          switch (cmd) {
              case "force-low": {
                  int opts = parseOptions(shell);
                  getContext().enforceCallingOrSelfPermission(
                          android.Manifest.permission.DEVICE_POWER, null);
                  mForceLevel = State.LEVEL_LOW;
                  int seq = mSeq.incrementAndGet();
                  if ((opts & OPTION_FORCE_UPDATE) != 0) {
                      mHandler.removeMessages(MSG_CHECK);
                      mHandler.obtainMessage(MSG_CHECK).sendToTarget();
                      pw.println(seq);
                  }
              } break;
              case "force-not-low": {
                  int opts = parseOptions(shell);
                  getContext().enforceCallingOrSelfPermission(
                          android.Manifest.permission.DEVICE_POWER, null);
                  mForceLevel = State.LEVEL_NORMAL;
                  int seq = mSeq.incrementAndGet();
                  if ((opts & OPTION_FORCE_UPDATE) != 0) {
                      mHandler.removeMessages(MSG_CHECK);
                      mHandler.obtainMessage(MSG_CHECK).sendToTarget();
                      pw.println(seq);
                  }
              } break;
              case "reset": {
                  int opts = parseOptions(shell);
                  getContext().enforceCallingOrSelfPermission(
                          android.Manifest.permission.DEVICE_POWER, null);
                  mForceLevel = State.LEVEL_UNKNOWN;
                  int seq = mSeq.incrementAndGet();
                  if ((opts & OPTION_FORCE_UPDATE) != 0) {
                      mHandler.removeMessages(MSG_CHECK);
                      mHandler.obtainMessage(MSG_CHECK).sendToTarget();
                      pw.println(seq);
                  }
              } break;
              default:
                  return shell.handleDefaultCommands(cmd);
          }
          return 0;
      }
  
      static void dumpHelp(PrintWriter pw) {
          pw.println("Device storage monitor service (devicestoragemonitor) commands:");
          pw.println("  help");
          pw.println("    Print this help text.");
          pw.println("  force-low [-f]");
          pw.println("    Force storage to be low, freezing storage state.");
          pw.println("    -f: force a storage change broadcast be sent, prints new sequence.");
          pw.println("  force-not-low [-f]");
          pw.println("    Force storage to not be low, freezing storage state.");
          pw.println("    -f: force a storage change broadcast be sent, prints new sequence.");
          pw.println("  reset [-f]");
          pw.println("    Unfreeze storage state, returning to current real values.");
          pw.println("    -f: force a storage change broadcast be sent, prints new sequence.");
      }
  
      void dumpImpl(FileDescriptor fd, PrintWriter _pw, String[] args) {
          final IndentingPrintWriter pw = new IndentingPrintWriter(_pw, "  ");
          if (args == null || args.length == 0 || "-a".equals(args[0])) {
              pw.println("Known volumes:");
              pw.increaseIndent();
              for (int i = 0; i < mStates.size(); i++) {
                  final UUID uuid = mStates.keyAt(i);
                  final State state = mStates.valueAt(i);
                  if (StorageManager.UUID_DEFAULT.equals(uuid)) {
                      pw.println("Default:");
                  } else {
                      pw.println(uuid + ":");
                  }
                  pw.increaseIndent();
                  pw.printPair("level", State.levelToString(state.level));
                  pw.printPair("lastUsableBytes", state.lastUsableBytes);
                  pw.println();
                  pw.decreaseIndent();
              }
              pw.decreaseIndent();
              pw.println();
  
              pw.printPair("mSeq", mSeq.get());
              pw.printPair("mForceState", State.levelToString(mForceLevel));
              pw.println();
              pw.println();
  
          } else {
              Shell shell = new Shell();
              shell.exec(mRemoteService, null, fd, null, args, null, new ResultReceiver(null));
          }
      }
  
      private void updateNotifications(VolumeInfo vol, int oldLevel, int newLevel) {
          final Context context = getContext();
          final UUID uuid = StorageManager.convert(vol.getFsUuid());
  
          if (State.isEntering(State.LEVEL_LOW, oldLevel, newLevel)) {
              Intent lowMemIntent = new Intent(StorageManager.ACTION_MANAGE_STORAGE);
              lowMemIntent.putExtra(StorageManager.EXTRA_UUID, uuid);
              lowMemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  
              final CharSequence title = context.getText(
                      com.android.internal.R.string.low_internal_storage_view_title);
  
              final CharSequence details;
              if (StorageManager.UUID_DEFAULT.equals(uuid)) {
                  details = context.getText(isBootImageOnDisk()
                          ? com.android.internal.R.string.low_internal_storage_view_text
                          : com.android.internal.R.string.low_internal_storage_view_text_no_boot);
              } else {
                  details = context.getText(
                          com.android.internal.R.string.low_internal_storage_view_text);
              }
  
              PendingIntent intent = PendingIntent.getActivityAsUser(context, 0, lowMemIntent, 0,
                      null, UserHandle.CURRENT);
              Notification notification =
                      new Notification.Builder(context, SystemNotificationChannels.ALERTS)
                              .setSmallIcon(com.android.internal.R.drawable.stat_notify_disk_full)
                              .setTicker(title)
                              .setColor(context.getColor(
                                  com.android.internal.R.color.system_notification_accent_color))
                              .setContentTitle(title)
                              .setContentText(details)
                              .setContentIntent(intent)
                              .setStyle(new Notification.BigTextStyle()
                                    .bigText(details))
                              .setVisibility(Notification.VISIBILITY_PUBLIC)
                              .setCategory(Notification.CATEGORY_SYSTEM)
                              .extend(new Notification.TvExtender()
                                      .setChannelId(TV_NOTIFICATION_CHANNEL_ID))
                              .build();
              notification.flags |= Notification.FLAG_NO_CLEAR;
              mNotifManager.notifyAsUser(uuid.toString(), SystemMessage.NOTE_LOW_STORAGE,
                      notification, UserHandle.ALL);
              StatsLog.write(StatsLog.LOW_STORAGE_STATE_CHANGED,
                      Objects.toString(vol.getDescription()),
                      StatsLog.LOW_STORAGE_STATE_CHANGED__STATE__ON);
          } else if (State.isLeaving(State.LEVEL_LOW, oldLevel, newLevel)) {
              mNotifManager.cancelAsUser(uuid.toString(), SystemMessage.NOTE_LOW_STORAGE,
                      UserHandle.ALL);
              StatsLog.write(StatsLog.LOW_STORAGE_STATE_CHANGED,
                      Objects.toString(vol.getDescription()),
                      StatsLog.LOW_STORAGE_STATE_CHANGED__STATE__OFF);
          }
      }
  
      private void updateBroadcasts(VolumeInfo vol, int oldLevel, int newLevel, int seq) {
          if (!Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, vol.getFsUuid())) {
              // We don't currently send broadcasts for secondary volumes
              return;
          }
  
          final Intent lowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_LOW)
                  .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                          | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
                          | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)
                  .putExtra(EXTRA_SEQUENCE, seq);
          final Intent notLowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_OK)
                  .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                          | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
                          | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)
                  .putExtra(EXTRA_SEQUENCE, seq);
  
          if (State.isEntering(State.LEVEL_LOW, oldLevel, newLevel)) {
              getContext().sendStickyBroadcastAsUser(lowIntent, UserHandle.ALL);
          } else if (State.isLeaving(State.LEVEL_LOW, oldLevel, newLevel)) {
              getContext().removeStickyBroadcastAsUser(lowIntent, UserHandle.ALL);
              getContext().sendBroadcastAsUser(notLowIntent, UserHandle.ALL);
          }
  
          final Intent fullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_FULL)
                  .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)
                  .putExtra(EXTRA_SEQUENCE, seq);
          final Intent notFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_NOT_FULL)
                  .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)
                  .putExtra(EXTRA_SEQUENCE, seq);
  
          if (State.isEntering(State.LEVEL_FULL, oldLevel, newLevel)) {
              getContext().sendStickyBroadcastAsUser(fullIntent, UserHandle.ALL);
          } else if (State.isLeaving(State.LEVEL_FULL, oldLevel, newLevel)) {
              getContext().removeStickyBroadcastAsUser(fullIntent, UserHandle.ALL);
              getContext().sendBroadcastAsUser(notFullIntent, UserHandle.ALL);
          }
      }
  
      private static class CacheFileDeletedObserver extends FileObserver {
          public CacheFileDeletedObserver() {
              super(Environment.getDownloadCacheDirectory().getAbsolutePath(), FileObserver.DELETE);
          }
  
          @Override
          public void onEvent(int event, String path) {
              EventLogTags.writeCacheFileDeleted(path);
          }
      }
  }
  