| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.server.pm; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.pm.ShortcutInfo; |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap.CompressFormat; |
| import android.graphics.drawable.Icon; |
| import android.os.StrictMode; |
| import android.os.StrictMode.ThreadPolicy; |
| import android.os.SystemClock; |
| import android.util.Log; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.util.Preconditions; |
| import com.android.server.pm.ShortcutService.FileOutputStreamWithPath; |
| |
| import libcore.io.IoUtils; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.util.Deque; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.LinkedBlockingDeque; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.ThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Class to save shortcut bitmaps on a worker thread. |
| * |
| * The methods with the "Locked" prefix must be called with the service lock held. |
| */ |
| public class ShortcutBitmapSaver { |
| private static final String TAG = ShortcutService.TAG; |
| private static final boolean DEBUG = ShortcutService.DEBUG; |
| |
| private static final boolean ADD_DELAY_BEFORE_SAVE_FOR_TEST = false; // DO NOT submit with true. |
| private static final long SAVE_DELAY_MS_FOR_TEST = 1000; // DO NOT submit with true. |
| |
| /** |
| * Before saving shortcuts.xml, and returning icons to the launcher, we wait for all pending |
| * saves to finish. However if it takes more than this long, we just give up and proceed. |
| */ |
| private final long SAVE_WAIT_TIMEOUT_MS = 30 * 1000; |
| |
| private final ShortcutService mService; |
| |
| /** |
| * Bitmaps are saved on this thread. |
| * |
| * Note: Just before saving shortcuts into the XML, we need to wait on all pending saves to |
| * finish, and we need to do it with the service lock held, which would still block incoming |
| * binder calls, meaning saving bitmaps *will* still actually block API calls too, which is |
| * not ideal but fixing it would be tricky, so this is still a known issue on the current |
| * version. |
| * |
| * In order to reduce the conflict, we use an own thread for this purpose, rather than |
| * reusing existing background threads, and also to avoid possible deadlocks. |
| */ |
| private final Executor mExecutor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, |
| new LinkedBlockingQueue<>()); |
| |
| /** Represents a bitmap to save. */ |
| private static class PendingItem { |
| /** Hosting shortcut. */ |
| public final ShortcutInfo shortcut; |
| |
| /** Compressed bitmap data. */ |
| public final byte[] bytes; |
| |
| /** Instantiated time, only for dogfooding. */ |
| private final long mInstantiatedUptimeMillis; // Only for dumpsys. |
| |
| private PendingItem(ShortcutInfo shortcut, byte[] bytes) { |
| this.shortcut = shortcut; |
| this.bytes = bytes; |
| mInstantiatedUptimeMillis = SystemClock.uptimeMillis(); |
| } |
| |
| @Override |
| public String toString() { |
| return "PendingItem{size=" + bytes.length |
| + " age=" + (SystemClock.uptimeMillis() - mInstantiatedUptimeMillis) + "ms" |
| + " shortcut=" + shortcut.toInsecureString() |
| + "}"; |
| } |
| } |
| |
| @GuardedBy("mPendingItems") |
| private final Deque<PendingItem> mPendingItems = new LinkedBlockingDeque<>(); |
| |
| public ShortcutBitmapSaver(ShortcutService service) { |
| mService = service; |
| // mLock = lock; |
| } |
| |
| public boolean waitForAllSavesLocked() { |
| final CountDownLatch latch = new CountDownLatch(1); |
| |
| mExecutor.execute(() -> latch.countDown()); |
| |
| try { |
| if (latch.await(SAVE_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { |
| return true; |
| } |
| mService.wtf("Timed out waiting on saving bitmaps."); |
| } catch (InterruptedException e) { |
| Slog.w(TAG, "interrupted"); |
| } |
| return false; |
| } |
| |
| /** |
| * Wait for all pending saves to finish, and then return the given shortcut's bitmap path. |
| */ |
| @Nullable |
| public String getBitmapPathMayWaitLocked(ShortcutInfo shortcut) { |
| final boolean success = waitForAllSavesLocked(); |
| if (success && shortcut.hasIconFile()) { |
| return shortcut.getBitmapPath(); |
| } else { |
| return null; |
| } |
| } |
| |
| public void removeIcon(ShortcutInfo shortcut) { |
| // Do not remove the actual bitmap file yet, because if the device crashes before saving |
| // the XML we'd lose the icon. We just remove all dangling files after saving the XML. |
| shortcut.setIconResourceId(0); |
| shortcut.setIconResName(null); |
| shortcut.setBitmapPath(null); |
| shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | |
| ShortcutInfo.FLAG_ADAPTIVE_BITMAP | ShortcutInfo.FLAG_HAS_ICON_RES | |
| ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE); |
| } |
| |
| public void saveBitmapLocked(ShortcutInfo shortcut, |
| int maxDimension, CompressFormat format, int quality) { |
| final Icon icon = shortcut.getIcon(); |
| Preconditions.checkNotNull(icon); |
| |
| final Bitmap original = icon.getBitmap(); |
| if (original == null) { |
| Log.e(TAG, "Missing icon: " + shortcut); |
| return; |
| } |
| |
| // Compress it and enqueue to the requests. |
| final byte[] bytes; |
| final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); |
| try { |
| // compress() triggers a slow call, but in this case it's needed to save RAM and also |
| // the target bitmap is of an icon size, so let's just permit it. |
| StrictMode.setThreadPolicy(new ThreadPolicy.Builder(oldPolicy) |
| .permitCustomSlowCalls() |
| .build()); |
| final Bitmap shrunk = mService.shrinkBitmap(original, maxDimension); |
| try { |
| try (final ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024)) { |
| if (!shrunk.compress(format, quality, out)) { |
| Slog.wtf(ShortcutService.TAG, "Unable to compress bitmap"); |
| } |
| out.flush(); |
| bytes = out.toByteArray(); |
| out.close(); |
| } |
| } finally { |
| if (shrunk != original) { |
| shrunk.recycle(); |
| } |
| } |
| } catch (IOException | RuntimeException | OutOfMemoryError e) { |
| Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e); |
| return; |
| } finally { |
| StrictMode.setThreadPolicy(oldPolicy); |
| } |
| |
| shortcut.addFlags( |
| ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE); |
| |
| if (icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { |
| shortcut.addFlags(ShortcutInfo.FLAG_ADAPTIVE_BITMAP); |
| } |
| |
| // Enqueue a pending save. |
| final PendingItem item = new PendingItem(shortcut, bytes); |
| synchronized (mPendingItems) { |
| mPendingItems.add(item); |
| } |
| |
| if (DEBUG) { |
| Slog.d(TAG, "Scheduling to save: " + item); |
| } |
| |
| mExecutor.execute(mRunnable); |
| } |
| |
| private final Runnable mRunnable = () -> { |
| // Process all pending items. |
| while (processPendingItems()) { |
| } |
| }; |
| |
| /** |
| * Takes a {@link PendingItem} from {@link #mPendingItems} and process it. |
| * |
| * Must be called {@link #mExecutor}. |
| * |
| * @return true if it processed an item, false if the queue is empty. |
| */ |
| private boolean processPendingItems() { |
| if (ADD_DELAY_BEFORE_SAVE_FOR_TEST) { |
| Slog.w(TAG, "*** ARTIFICIAL SLEEP ***"); |
| try { |
| Thread.sleep(SAVE_DELAY_MS_FOR_TEST); |
| } catch (InterruptedException e) { |
| } |
| } |
| |
| // NOTE: |
| // Ideally we should be holding the service lock when accessing shortcut instances, |
| // but that could cause a deadlock so we don't do it. |
| // |
| // Instead, waitForAllSavesLocked() uses a latch to make sure changes made on this |
| // thread is visible on the caller thread. |
| |
| ShortcutInfo shortcut = null; |
| try { |
| final PendingItem item; |
| |
| synchronized (mPendingItems) { |
| if (mPendingItems.size() == 0) { |
| return false; |
| } |
| item = mPendingItems.pop(); |
| } |
| |
| shortcut = item.shortcut; |
| |
| // See if the shortcut is still relevant. (It might have been removed already.) |
| if (!shortcut.isIconPendingSave()) { |
| return true; |
| } |
| |
| if (DEBUG) { |
| Slog.d(TAG, "Saving bitmap: " + item); |
| } |
| |
| File file = null; |
| try { |
| final FileOutputStreamWithPath out = mService.openIconFileForWrite( |
| shortcut.getUserId(), shortcut); |
| file = out.getFile(); |
| |
| try { |
| out.write(item.bytes); |
| } finally { |
| IoUtils.closeQuietly(out); |
| } |
| |
| shortcut.setBitmapPath(file.getAbsolutePath()); |
| |
| } catch (IOException | RuntimeException e) { |
| Slog.e(ShortcutService.TAG, "Unable to write bitmap to file", e); |
| |
| if (file != null && file.exists()) { |
| file.delete(); |
| } |
| return true; |
| } |
| } finally { |
| if (DEBUG) { |
| Slog.d(TAG, "Saved bitmap."); |
| } |
| if (shortcut != null) { |
| if (shortcut.getBitmapPath() == null) { |
| removeIcon(shortcut); |
| } |
| |
| // Whatever happened, remove this flag. |
| shortcut.clearFlags(ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE); |
| } |
| } |
| return true; |
| } |
| |
| public void dumpLocked(@NonNull PrintWriter pw, @NonNull String prefix) { |
| synchronized (mPendingItems) { |
| final int N = mPendingItems.size(); |
| pw.print(prefix); |
| pw.println("Pending saves: Num=" + N + " Executor=" + mExecutor); |
| |
| for (PendingItem item : mPendingItems) { |
| pw.print(prefix); |
| pw.print(" "); |
| pw.println(item); |
| } |
| } |
| } |
| } |