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