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