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.server.pm.ShortcutService.FileOutputStreamWithPath;
32 
33 import libcore.io.IoUtils;
34 
35 import java.io.ByteArrayOutputStream;
36 import java.io.File;
37 import java.io.IOException;
38 import java.io.PrintWriter;
39 import java.util.Deque;
40 import java.util.Objects;
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 = 5 * 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.setIconUri(null);
153         shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE |
154                 ShortcutInfo.FLAG_ADAPTIVE_BITMAP | ShortcutInfo.FLAG_HAS_ICON_RES |
155                 ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE | ShortcutInfo.FLAG_HAS_ICON_URI);
156     }
157 
saveBitmapLocked(ShortcutInfo shortcut, int maxDimension, CompressFormat format, int quality)158     public void saveBitmapLocked(ShortcutInfo shortcut,
159             int maxDimension, CompressFormat format, int quality) {
160         final Icon icon = shortcut.getIcon();
161         Objects.requireNonNull(icon);
162 
163         final Bitmap original = icon.getBitmap();
164         if (original == null) {
165             Log.e(TAG, "Missing icon: " + shortcut);
166             return;
167         }
168 
169         // Compress it and enqueue to the requests.
170         final byte[] bytes;
171         final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
172         try {
173             // compress() triggers a slow call, but in this case it's needed to save RAM and also
174             // the target bitmap is of an icon size, so let's just permit it.
175             StrictMode.setThreadPolicy(new ThreadPolicy.Builder(oldPolicy)
176                     .permitCustomSlowCalls()
177                     .build());
178             final Bitmap shrunk = mService.shrinkBitmap(original, maxDimension);
179             try {
180                 try (final ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024)) {
181                     if (!shrunk.compress(format, quality, out)) {
182                         Slog.wtf(ShortcutService.TAG, "Unable to compress bitmap");
183                     }
184                     out.flush();
185                     bytes = out.toByteArray();
186                     out.close();
187                 }
188             } finally {
189                 if (shrunk != original) {
190                     shrunk.recycle();
191                 }
192             }
193         } catch (IOException | RuntimeException | OutOfMemoryError e) {
194             Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
195             return;
196         } finally {
197             StrictMode.setThreadPolicy(oldPolicy);
198         }
199 
200         shortcut.addFlags(
201                 ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
202 
203         if (icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
204             shortcut.addFlags(ShortcutInfo.FLAG_ADAPTIVE_BITMAP);
205         }
206 
207         // Enqueue a pending save.
208         final PendingItem item = new PendingItem(shortcut, bytes);
209         synchronized (mPendingItems) {
210             mPendingItems.add(item);
211         }
212 
213         if (DEBUG) {
214             Slog.d(TAG, "Scheduling to save: " + item);
215         }
216 
217         mExecutor.execute(mRunnable);
218     }
219 
220     private final Runnable mRunnable = () -> {
221         // Process all pending items.
222         while (processPendingItems()) {
223         }
224     };
225 
226     /**
227      * Takes a {@link PendingItem} from {@link #mPendingItems} and process it.
228      *
229      * Must be called {@link #mExecutor}.
230      *
231      * @return true if it processed an item, false if the queue is empty.
232      */
processPendingItems()233     private boolean processPendingItems() {
234         if (ADD_DELAY_BEFORE_SAVE_FOR_TEST) {
235             Slog.w(TAG, "*** ARTIFICIAL SLEEP ***");
236             try {
237                 Thread.sleep(SAVE_DELAY_MS_FOR_TEST);
238             } catch (InterruptedException e) {
239             }
240         }
241 
242         // NOTE:
243         // Ideally we should be holding the service lock when accessing shortcut instances,
244         // but that could cause a deadlock so we don't do it.
245         //
246         // Instead, waitForAllSavesLocked() uses a latch to make sure changes made on this
247         // thread is visible on the caller thread.
248 
249         ShortcutInfo shortcut = null;
250         try {
251             final PendingItem item;
252 
253             synchronized (mPendingItems) {
254                 if (mPendingItems.size() == 0) {
255                     return false;
256                 }
257                 item = mPendingItems.pop();
258             }
259 
260             shortcut = item.shortcut;
261 
262             // See if the shortcut is still relevant. (It might have been removed already.)
263             if (!shortcut.isIconPendingSave()) {
264                 return true;
265             }
266 
267             if (DEBUG) {
268                 Slog.d(TAG, "Saving bitmap: " + item);
269             }
270 
271             File file = null;
272             try {
273                 final FileOutputStreamWithPath out = mService.openIconFileForWrite(
274                         shortcut.getUserId(), shortcut);
275                 file = out.getFile();
276 
277                 try {
278                     out.write(item.bytes);
279                 } finally {
280                     IoUtils.closeQuietly(out);
281                 }
282 
283                 final String path = file.getAbsolutePath();
284                 shortcut.setBitmapPath(path);
285 
286             } catch (IOException | RuntimeException e) {
287                 Slog.e(ShortcutService.TAG, "Unable to write bitmap to file", e);
288 
289                 if (file != null && file.exists()) {
290                     file.delete();
291                 }
292                 return true;
293             }
294         } finally {
295             if (DEBUG) {
296                 Slog.d(TAG, "Saved bitmap.");
297             }
298             if (shortcut != null) {
299                 if (shortcut.getBitmapPath() == null) {
300                     removeIcon(shortcut);
301                 }
302 
303                 // Whatever happened, remove this flag.
304                 shortcut.clearFlags(ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
305             }
306         }
307         return true;
308     }
309 
dumpLocked(@onNull PrintWriter pw, @NonNull String prefix)310     public void dumpLocked(@NonNull PrintWriter pw, @NonNull String prefix) {
311         synchronized (mPendingItems) {
312             final int N = mPendingItems.size();
313             pw.print(prefix);
314             pw.println("Pending saves: Num=" + N + " Executor=" + mExecutor);
315 
316             for (PendingItem item : mPendingItems) {
317                 pw.print(prefix);
318                 pw.print("  ");
319                 pw.println(item);
320             }
321         }
322     }
323 }
324