content: /*
   * Copyright (C) 2017 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.pm;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.pm.ShortcutInfo;
  import android.graphics.Bitmap;
  import android.graphics.Bitmap.CompressFormat;
  import android.graphics.drawable.Icon;
  import android.os.StrictMode;
  import android.os.StrictMode.ThreadPolicy;
  import android.os.SystemClock;
  import android.util.Log;
  import android.util.Slog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.util.Preconditions;
  import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
  
  import libcore.io.IoUtils;
  
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.util.Deque;
  import java.util.concurrent.CountDownLatch;
  import java.util.concurrent.Executor;
  import java.util.concurrent.LinkedBlockingDeque;
  import java.util.concurrent.LinkedBlockingQueue;
  import java.util.concurrent.ThreadPoolExecutor;
  import java.util.concurrent.TimeUnit;
  
  /**
   * Class to save shortcut bitmaps on a worker thread.
   *
   * The methods with the "Locked" prefix must be called with the service lock held.
   */
  public class ShortcutBitmapSaver {
      private static final String TAG = ShortcutService.TAG;
      private static final boolean DEBUG = ShortcutService.DEBUG;
  
      private static final boolean ADD_DELAY_BEFORE_SAVE_FOR_TEST = false; // DO NOT submit with true.
      private static final long SAVE_DELAY_MS_FOR_TEST = 1000; // DO NOT submit with true.
  
      /**
       * Before saving shortcuts.xml, and returning icons to the launcher, we wait for all pending
       * saves to finish.  However if it takes more than this long, we just give up and proceed.
       */
      private final long SAVE_WAIT_TIMEOUT_MS = 30 * 1000;
  
      private final ShortcutService mService;
  
      /**
       * Bitmaps are saved on this thread.
       *
       * Note: Just before saving shortcuts into the XML, we need to wait on all pending saves to
       * finish, and we need to do it with the service lock held, which would still block incoming
       * binder calls, meaning saving bitmaps *will* still actually block API calls too, which is
       * not ideal but fixing it would be tricky, so this is still a known issue on the current
       * version.
       *
       * In order to reduce the conflict, we use an own thread for this purpose, rather than
       * reusing existing background threads, and also to avoid possible deadlocks.
       */
      private final Executor mExecutor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
              new LinkedBlockingQueue<>());
  
      /** Represents a bitmap to save. */
      private static class PendingItem {
          /** Hosting shortcut. */
          public final ShortcutInfo shortcut;
  
          /** Compressed bitmap data. */
          public final byte[] bytes;
  
          /** Instantiated time, only for dogfooding. */
          private final long mInstantiatedUptimeMillis; // Only for dumpsys.
  
          private PendingItem(ShortcutInfo shortcut, byte[] bytes) {
              this.shortcut = shortcut;
              this.bytes = bytes;
              mInstantiatedUptimeMillis = SystemClock.uptimeMillis();
          }
  
          @Override
          public String toString() {
              return "PendingItem{size=" + bytes.length
                      + " age=" + (SystemClock.uptimeMillis() - mInstantiatedUptimeMillis) + "ms"
                      + " shortcut=" + shortcut.toInsecureString()
                      + "}";
          }
      }
  
      @GuardedBy("mPendingItems")
      private final Deque<PendingItem> mPendingItems = new LinkedBlockingDeque<>();
  
      public ShortcutBitmapSaver(ShortcutService service) {
          mService = service;
          // mLock = lock;
      }
  
      public boolean waitForAllSavesLocked() {
          final CountDownLatch latch = new CountDownLatch(1);
  
          mExecutor.execute(() -> latch.countDown());
  
          try {
              if (latch.await(SAVE_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
                  return true;
              }
              mService.wtf("Timed out waiting on saving bitmaps.");
          } catch (InterruptedException e) {
              Slog.w(TAG, "interrupted");
          }
          return false;
      }
  
      /**
       * Wait for all pending saves to finish, and then return the given shortcut's bitmap path.
       */
      @Nullable
      public String getBitmapPathMayWaitLocked(ShortcutInfo shortcut) {
          final boolean success = waitForAllSavesLocked();
          if (success && shortcut.hasIconFile()) {
              return shortcut.getBitmapPath();
          } else {
              return null;
          }
      }
  
      public void removeIcon(ShortcutInfo shortcut) {
          // Do not remove the actual bitmap file yet, because if the device crashes before saving
          // the XML we'd lose the icon.  We just remove all dangling files after saving the XML.
          shortcut.setIconResourceId(0);
          shortcut.setIconResName(null);
          shortcut.setBitmapPath(null);
          shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE |
                  ShortcutInfo.FLAG_ADAPTIVE_BITMAP | ShortcutInfo.FLAG_HAS_ICON_RES |
                  ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
      }
  
      public void saveBitmapLocked(ShortcutInfo shortcut,
              int maxDimension, CompressFormat format, int quality) {
          final Icon icon = shortcut.getIcon();
          Preconditions.checkNotNull(icon);
  
          final Bitmap original = icon.getBitmap();
          if (original == null) {
              Log.e(TAG, "Missing icon: " + shortcut);
              return;
          }
  
          // Compress it and enqueue to the requests.
          final byte[] bytes;
          final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
          try {
              // compress() triggers a slow call, but in this case it's needed to save RAM and also
              // the target bitmap is of an icon size, so let's just permit it.
              StrictMode.setThreadPolicy(new ThreadPolicy.Builder(oldPolicy)
                      .permitCustomSlowCalls()
                      .build());
              final Bitmap shrunk = mService.shrinkBitmap(original, maxDimension);
              try {
                  try (final ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024)) {
                      if (!shrunk.compress(format, quality, out)) {
                          Slog.wtf(ShortcutService.TAG, "Unable to compress bitmap");
                      }
                      out.flush();
                      bytes = out.toByteArray();
                      out.close();
                  }
              } finally {
                  if (shrunk != original) {
                      shrunk.recycle();
                  }
              }
          } catch (IOException | RuntimeException | OutOfMemoryError e) {
              Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
              return;
          } finally {
              StrictMode.setThreadPolicy(oldPolicy);
          }
  
          shortcut.addFlags(
                  ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
  
          if (icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
              shortcut.addFlags(ShortcutInfo.FLAG_ADAPTIVE_BITMAP);
          }
  
          // Enqueue a pending save.
          final PendingItem item = new PendingItem(shortcut, bytes);
          synchronized (mPendingItems) {
              mPendingItems.add(item);
          }
  
          if (DEBUG) {
              Slog.d(TAG, "Scheduling to save: " + item);
          }
  
          mExecutor.execute(mRunnable);
      }
  
      private final Runnable mRunnable = () -> {
          // Process all pending items.
          while (processPendingItems()) {
          }
      };
  
      /**
       * Takes a {@link PendingItem} from {@link #mPendingItems} and process it.
       *
       * Must be called {@link #mExecutor}.
       *
       * @return true if it processed an item, false if the queue is empty.
       */
      private boolean processPendingItems() {
          if (ADD_DELAY_BEFORE_SAVE_FOR_TEST) {
              Slog.w(TAG, "*** ARTIFICIAL SLEEP ***");
              try {
                  Thread.sleep(SAVE_DELAY_MS_FOR_TEST);
              } catch (InterruptedException e) {
              }
          }
  
          // NOTE:
          // Ideally we should be holding the service lock when accessing shortcut instances,
          // but that could cause a deadlock so we don't do it.
          //
          // Instead, waitForAllSavesLocked() uses a latch to make sure changes made on this
          // thread is visible on the caller thread.
  
          ShortcutInfo shortcut = null;
          try {
              final PendingItem item;
  
              synchronized (mPendingItems) {
                  if (mPendingItems.size() == 0) {
                      return false;
                  }
                  item = mPendingItems.pop();
              }
  
              shortcut = item.shortcut;
  
              // See if the shortcut is still relevant. (It might have been removed already.)
              if (!shortcut.isIconPendingSave()) {
                  return true;
              }
  
              if (DEBUG) {
                  Slog.d(TAG, "Saving bitmap: " + item);
              }
  
              File file = null;
              try {
                  final FileOutputStreamWithPath out = mService.openIconFileForWrite(
                          shortcut.getUserId(), shortcut);
                  file = out.getFile();
  
                  try {
                      out.write(item.bytes);
                  } finally {
                      IoUtils.closeQuietly(out);
                  }
  
                  shortcut.setBitmapPath(file.getAbsolutePath());
  
              } catch (IOException | RuntimeException e) {
                  Slog.e(ShortcutService.TAG, "Unable to write bitmap to file", e);
  
                  if (file != null && file.exists()) {
                      file.delete();
                  }
                  return true;
              }
          } finally {
              if (DEBUG) {
                  Slog.d(TAG, "Saved bitmap.");
              }
              if (shortcut != null) {
                  if (shortcut.getBitmapPath() == null) {
                      removeIcon(shortcut);
                  }
  
                  // Whatever happened, remove this flag.
                  shortcut.clearFlags(ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
              }
          }
          return true;
      }
  
      public void dumpLocked(@NonNull PrintWriter pw, @NonNull String prefix) {
          synchronized (mPendingItems) {
              final int N = mPendingItems.size();
              pw.print(prefix);
              pw.println("Pending saves: Num=" + N + " Executor=" + mExecutor);
  
              for (PendingItem item : mPendingItems) {
                  pw.print(prefix);
                  pw.print("  ");
                  pw.println(item);
              }
          }
      }
  }
  