blob: 4f5d1560bfb10b5a6b0b9783ee560b49a1979926 [file] [log] [blame]
/*
* 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.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;
try {
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;
}
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);
}
}
}
}