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 
PendingItem(ShortcutInfo shortcut, byte[] bytes)94         private PendingItem(ShortcutInfo shortcut, byte[] bytes) {
95             this.shortcut = shortcut;
96             this.bytes = bytes;
97             mInstantiatedUptimeMillis = SystemClock.uptimeMillis();
98         }
99 
100         @Override
toString()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 
ShortcutBitmapSaver(ShortcutService service)112     public ShortcutBitmapSaver(ShortcutService service) {
113         mService = service;
114         // mLock = lock;
115     }
116 
waitForAllSavesLocked()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
getBitmapPathMayWaitLocked(ShortcutInfo shortcut)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 
removeIcon(ShortcutInfo shortcut)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 
saveBitmapLocked(ShortcutInfo shortcut, int maxDimension, CompressFormat format, int quality)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      */
processPendingItems()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 
dumpLocked(@onNull PrintWriter pw, @NonNull String prefix)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