/*
|
* 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);
|
}
|
}
|
}
|
}
|