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