Home | History | Annotate | Download | only in pm
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.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,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.server.pm;
     17 
     18 import android.annotation.NonNull;
     19 import android.annotation.Nullable;
     20 import android.content.pm.ShortcutInfo;
     21 import android.graphics.Bitmap;
     22 import android.graphics.Bitmap.CompressFormat;
     23 import android.graphics.drawable.Icon;
     24 import android.os.SystemClock;
     25 import android.util.Log;
     26 import android.util.Slog;
     27 
     28 import com.android.internal.annotations.GuardedBy;
     29 import com.android.internal.util.Preconditions;
     30 import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
     31 
     32 import libcore.io.IoUtils;
     33 
     34 import java.io.ByteArrayOutputStream;
     35 import java.io.File;
     36 import java.io.IOException;
     37 import java.io.PrintWriter;
     38 import java.util.Deque;
     39 import java.util.concurrent.CountDownLatch;
     40 import java.util.concurrent.Executor;
     41 import java.util.concurrent.LinkedBlockingDeque;
     42 import java.util.concurrent.LinkedBlockingQueue;
     43 import java.util.concurrent.ThreadPoolExecutor;
     44 import java.util.concurrent.TimeUnit;
     45 
     46 /**
     47  * Class to save shortcut bitmaps on a worker thread.
     48  *
     49  * The methods with the "Locked" prefix must be called with the service lock held.
     50  */
     51 public class ShortcutBitmapSaver {
     52     private static final String TAG = ShortcutService.TAG;
     53     private static final boolean DEBUG = ShortcutService.DEBUG;
     54 
     55     private static final boolean ADD_DELAY_BEFORE_SAVE_FOR_TEST = false; // DO NOT submit with true.
     56     private static final long SAVE_DELAY_MS_FOR_TEST = 1000; // DO NOT submit with true.
     57 
     58     /**
     59      * Before saving shortcuts.xml, and returning icons to the launcher, we wait for all pending
     60      * saves to finish.  However if it takes more than this long, we just give up and proceed.
     61      */
     62     private final long SAVE_WAIT_TIMEOUT_MS = 30 * 1000;
     63 
     64     private final ShortcutService mService;
     65 
     66     /**
     67      * Bitmaps are saved on this thread.
     68      *
     69      * Note: Just before saving shortcuts into the XML, we need to wait on all pending saves to
     70      * finish, and we need to do it with the service lock held, which would still block incoming
     71      * binder calls, meaning saving bitmaps *will* still actually block API calls too, which is
     72      * not ideal but fixing it would be tricky, so this is still a known issue on the current
     73      * version.
     74      *
     75      * In order to reduce the conflict, we use an own thread for this purpose, rather than
     76      * reusing existing background threads, and also to avoid possible deadlocks.
     77      */
     78     private final Executor mExecutor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
     79             new LinkedBlockingQueue<>());
     80 
     81     /** Represents a bitmap to save. */
     82     private static class PendingItem {
     83         /** Hosting shortcut. */
     84         public final ShortcutInfo shortcut;
     85 
     86         /** Compressed bitmap data. */
     87         public final byte[] bytes;
     88 
     89         /** Instantiated time, only for dogfooding. */
     90         private final long mInstantiatedUptimeMillis; // Only for dumpsys.
     91 
     92         private PendingItem(ShortcutInfo shortcut, byte[] bytes) {
     93             this.shortcut = shortcut;
     94             this.bytes = bytes;
     95             mInstantiatedUptimeMillis = SystemClock.uptimeMillis();
     96         }
     97 
     98         @Override
     99         public String toString() {
    100             return "PendingItem{size=" + bytes.length
    101                     + " age=" + (SystemClock.uptimeMillis() - mInstantiatedUptimeMillis) + "ms"
    102                     + " shortcut=" + shortcut.toInsecureString()
    103                     + "}";
    104         }
    105     }
    106 
    107     @GuardedBy("mPendingItems")
    108     private final Deque<PendingItem> mPendingItems = new LinkedBlockingDeque<>();
    109 
    110     public ShortcutBitmapSaver(ShortcutService service) {
    111         mService = service;
    112         // mLock = lock;
    113     }
    114 
    115     public boolean waitForAllSavesLocked() {
    116         final CountDownLatch latch = new CountDownLatch(1);
    117 
    118         mExecutor.execute(() -> latch.countDown());
    119 
    120         try {
    121             if (latch.await(SAVE_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
    122                 return true;
    123             }
    124             mService.wtf("Timed out waiting on saving bitmaps.");
    125         } catch (InterruptedException e) {
    126             Slog.w(TAG, "interrupted");
    127         }
    128         return false;
    129     }
    130 
    131     /**
    132      * Wait for all pending saves to finish, and then return the given shortcut's bitmap path.
    133      */
    134     @Nullable
    135     public String getBitmapPathMayWaitLocked(ShortcutInfo shortcut) {
    136         final boolean success = waitForAllSavesLocked();
    137         if (success && shortcut.hasIconFile()) {
    138             return shortcut.getBitmapPath();
    139         } else {
    140             return null;
    141         }
    142     }
    143 
    144     public void removeIcon(ShortcutInfo shortcut) {
    145         // Do not remove the actual bitmap file yet, because if the device crashes before saving
    146         // the XML we'd lose the icon.  We just remove all dangling files after saving the XML.
    147         shortcut.setIconResourceId(0);
    148         shortcut.setIconResName(null);
    149         shortcut.setBitmapPath(null);
    150         shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE |
    151                 ShortcutInfo.FLAG_ADAPTIVE_BITMAP | ShortcutInfo.FLAG_HAS_ICON_RES |
    152                 ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
    153     }
    154 
    155     public void saveBitmapLocked(ShortcutInfo shortcut,
    156             int maxDimension, CompressFormat format, int quality) {
    157         final Icon icon = shortcut.getIcon();
    158         Preconditions.checkNotNull(icon);
    159 
    160         final Bitmap original = icon.getBitmap();
    161         if (original == null) {
    162             Log.e(TAG, "Missing icon: " + shortcut);
    163             return;
    164         }
    165 
    166         // Compress it and enqueue to the requests.
    167         final byte[] bytes;
    168         try {
    169             final Bitmap shrunk = mService.shrinkBitmap(original, maxDimension);
    170             try {
    171                 try (final ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024)) {
    172                     if (!shrunk.compress(format, quality, out)) {
    173                         Slog.wtf(ShortcutService.TAG, "Unable to compress bitmap");
    174                     }
    175                     out.flush();
    176                     bytes = out.toByteArray();
    177                     out.close();
    178                 }
    179             } finally {
    180                 if (shrunk != original) {
    181                     shrunk.recycle();
    182                 }
    183             }
    184         } catch (IOException | RuntimeException | OutOfMemoryError e) {
    185             Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
    186             return;
    187         }
    188 
    189         shortcut.addFlags(
    190                 ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
    191 
    192         if (icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
    193             shortcut.addFlags(ShortcutInfo.FLAG_ADAPTIVE_BITMAP);
    194         }
    195 
    196         // Enqueue a pending save.
    197         final PendingItem item = new PendingItem(shortcut, bytes);
    198         synchronized (mPendingItems) {
    199             mPendingItems.add(item);
    200         }
    201 
    202         if (DEBUG) {
    203             Slog.d(TAG, "Scheduling to save: " + item);
    204         }
    205 
    206         mExecutor.execute(mRunnable);
    207     }
    208 
    209     private final Runnable mRunnable = () -> {
    210         // Process all pending items.
    211         while (processPendingItems()) {
    212         }
    213     };
    214 
    215     /**
    216      * Takes a {@link PendingItem} from {@link #mPendingItems} and process it.
    217      *
    218      * Must be called {@link #mExecutor}.
    219      *
    220      * @return true if it processed an item, false if the queue is empty.
    221      */
    222     private boolean processPendingItems() {
    223         if (ADD_DELAY_BEFORE_SAVE_FOR_TEST) {
    224             Slog.w(TAG, "*** ARTIFICIAL SLEEP ***");
    225             try {
    226                 Thread.sleep(SAVE_DELAY_MS_FOR_TEST);
    227             } catch (InterruptedException e) {
    228             }
    229         }
    230 
    231         // NOTE:
    232         // Ideally we should be holding the service lock when accessing shortcut instances,
    233         // but that could cause a deadlock so we don't do it.
    234         //
    235         // Instead, waitForAllSavesLocked() uses a latch to make sure changes made on this
    236         // thread is visible on the caller thread.
    237 
    238         ShortcutInfo shortcut = null;
    239         try {
    240             final PendingItem item;
    241 
    242             synchronized (mPendingItems) {
    243                 if (mPendingItems.size() == 0) {
    244                     return false;
    245                 }
    246                 item = mPendingItems.pop();
    247             }
    248 
    249             shortcut = item.shortcut;
    250 
    251             // See if the shortcut is still relevant. (It might have been removed already.)
    252             if (!shortcut.isIconPendingSave()) {
    253                 return true;
    254             }
    255 
    256             if (DEBUG) {
    257                 Slog.d(TAG, "Saving bitmap: " + item);
    258             }
    259 
    260             File file = null;
    261             try {
    262                 final FileOutputStreamWithPath out = mService.openIconFileForWrite(
    263                         shortcut.getUserId(), shortcut);
    264                 file = out.getFile();
    265 
    266                 try {
    267                     out.write(item.bytes);
    268                 } finally {
    269                     IoUtils.closeQuietly(out);
    270                 }
    271 
    272                 shortcut.setBitmapPath(file.getAbsolutePath());
    273 
    274             } catch (IOException | RuntimeException e) {
    275                 Slog.e(ShortcutService.TAG, "Unable to write bitmap to file", e);
    276 
    277                 if (file != null && file.exists()) {
    278                     file.delete();
    279                 }
    280                 return true;
    281             }
    282         } finally {
    283             if (DEBUG) {
    284                 Slog.d(TAG, "Saved bitmap.");
    285             }
    286             if (shortcut != null) {
    287                 if (shortcut.getBitmapPath() == null) {
    288                     removeIcon(shortcut);
    289                 }
    290 
    291                 // Whatever happened, remove this flag.
    292                 shortcut.clearFlags(ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
    293             }
    294         }
    295         return true;
    296     }
    297 
    298     public void dumpLocked(@NonNull PrintWriter pw, @NonNull String prefix) {
    299         synchronized (mPendingItems) {
    300             final int N = mPendingItems.size();
    301             pw.print(prefix);
    302             pw.println("Pending saves: Num=" + N + " Executor=" + mExecutor);
    303 
    304             for (PendingItem item : mPendingItems) {
    305                 pw.print(prefix);
    306                 pw.print("  ");
    307                 pw.println(item);
    308             }
    309         }
    310     }
    311 }
    312