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