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