blob: 815f8851e8e5290f561354e49b8e2616ad6d29bc [file] [log] [blame]
Makoto Onuki475c3652017-05-08 14:29:03 -07001/*
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 */
16package com.android.server.pm;
17
18import android.annotation.NonNull;
19import android.annotation.Nullable;
20import android.content.pm.ShortcutInfo;
21import android.graphics.Bitmap;
22import android.graphics.Bitmap.CompressFormat;
23import android.graphics.drawable.Icon;
Makoto Onukic03127e2017-10-11 11:47:16 -070024import android.os.StrictMode;
25import android.os.StrictMode.ThreadPolicy;
Makoto Onuki475c3652017-05-08 14:29:03 -070026import android.os.SystemClock;
27import android.util.Log;
28import android.util.Slog;
29
30import com.android.internal.annotations.GuardedBy;
31import com.android.internal.util.Preconditions;
32import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
33
34import libcore.io.IoUtils;
35
36import java.io.ByteArrayOutputStream;
37import java.io.File;
38import java.io.IOException;
39import java.io.PrintWriter;
40import java.util.Deque;
41import java.util.concurrent.CountDownLatch;
42import java.util.concurrent.Executor;
43import java.util.concurrent.LinkedBlockingDeque;
44import java.util.concurrent.LinkedBlockingQueue;
45import java.util.concurrent.ThreadPoolExecutor;
46import 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 */
53public 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;
Makoto Onukic03127e2017-10-11 11:47:16 -0700170 final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
Makoto Onuki475c3652017-05-08 14:29:03 -0700171 try {
Makoto Onukic03127e2017-10-11 11:47:16 -0700172 // 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());
Makoto Onuki475c3652017-05-08 14:29:03 -0700177 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;
Makoto Onukic03127e2017-10-11 11:47:16 -0700195 } finally {
196 StrictMode.setThreadPolicy(oldPolicy);
Makoto Onuki475c3652017-05-08 14:29:03 -0700197 }
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}