[DO NOT MERGE] Wait for preloading images to complete before inflating notifications
NotificationContentInflater waits on SysUiBg thread for images to load, with a timeout
of 1000ms.
Test: 1. Build a test app that posts MessagingStyle notifications with a huge image (8k+) set as data Uri.
2. SystemUi should not ANR
3. adb logcat | grep NotificationInlineImageCache - shows timeout/cancellation logs
Bug: 252766417
Bug: 223859644
Change-Id: I341db60223214cf2282b5c0270e343e1ce95fa01
(cherry picked from commit 195043f40e)
Merged-In: I341db60223214cf2282b5c0270e343e1ce95fa01
This commit is contained in:
committed by
Iavor-Valentin Iftime
parent
240d62f76f
commit
b9cd15ad8a
@@ -439,6 +439,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
CancellationSignal cancellationSignal = new CancellationSignal();
|
CancellationSignal cancellationSignal = new CancellationSignal();
|
||||||
cancellationSignal.setOnCancelListener(
|
cancellationSignal.setOnCancelListener(
|
||||||
() -> runningInflations.values().forEach(CancellationSignal::cancel));
|
() -> runningInflations.values().forEach(CancellationSignal::cancel));
|
||||||
|
|
||||||
return cancellationSignal;
|
return cancellationSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,6 +712,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
|
public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
|
||||||
implements InflationCallback, InflationTask {
|
implements InflationCallback, InflationTask {
|
||||||
|
|
||||||
|
private static final long IMG_PRELOAD_TIMEOUT_MS = 1000L;
|
||||||
private final NotificationEntry mEntry;
|
private final NotificationEntry mEntry;
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final boolean mInflateSynchronously;
|
private final boolean mInflateSynchronously;
|
||||||
@@ -804,7 +806,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
|
recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
|
||||||
mUsesIncreasedHeadsUpHeight, packageContext);
|
mUsesIncreasedHeadsUpHeight, packageContext);
|
||||||
InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState();
|
InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState();
|
||||||
return inflateSmartReplyViews(
|
InflationProgress result = inflateSmartReplyViews(
|
||||||
inflationProgress,
|
inflationProgress,
|
||||||
mReInflateFlags,
|
mReInflateFlags,
|
||||||
mEntry,
|
mEntry,
|
||||||
@@ -812,6 +814,11 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
packageContext,
|
packageContext,
|
||||||
previousSmartReplyState,
|
previousSmartReplyState,
|
||||||
mSmartRepliesInflater);
|
mSmartRepliesInflater);
|
||||||
|
|
||||||
|
// wait for image resolver to finish preloading
|
||||||
|
mRow.getImageResolver().waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
mError = e;
|
mError = e;
|
||||||
return null;
|
return null;
|
||||||
@@ -846,6 +853,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
mCallback.handleInflationException(mRow.getEntry(),
|
mCallback.handleInflationException(mRow.getEntry(),
|
||||||
new InflationException("Couldn't inflate contentViews" + e));
|
new InflationException("Couldn't inflate contentViews" + e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel any image loading tasks, not useful any more
|
||||||
|
mRow.getImageResolver().cancelRunningTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -872,6 +882,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder
|
|||||||
// Notify the resolver that the inflation task has finished,
|
// Notify the resolver that the inflation task has finished,
|
||||||
// try to purge unnecessary cached entries.
|
// try to purge unnecessary cached entries.
|
||||||
mRow.getImageResolver().purgeCache();
|
mRow.getImageResolver().purgeCache();
|
||||||
|
|
||||||
|
// Cancel any image loading tasks that have not completed at this point
|
||||||
|
mRow.getImageResolver().cancelRunningTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RtlEnabledContext extends ContextWrapper {
|
private class RtlEnabledContext extends ContextWrapper {
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ import android.util.Log;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cache for inline images of image messages.
|
* A cache for inline images of image messages.
|
||||||
@@ -57,12 +60,13 @@ public class NotificationInlineImageCache implements NotificationInlineImageReso
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Drawable get(Uri uri) {
|
public Drawable get(Uri uri, long timeoutMs) {
|
||||||
Drawable result = null;
|
Drawable result = null;
|
||||||
try {
|
try {
|
||||||
result = mCache.get(uri).get();
|
result = mCache.get(uri).get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
} catch (InterruptedException | ExecutionException ex) {
|
} catch (InterruptedException | ExecutionException
|
||||||
Log.d(TAG, "get: Failed get image from " + uri);
|
| TimeoutException | CancellationException ex) {
|
||||||
|
Log.d(TAG, "get: Failed get image from " + uri + " " + ex);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -73,6 +77,15 @@ public class NotificationInlineImageCache implements NotificationInlineImageReso
|
|||||||
mCache.entrySet().removeIf(entry -> !wantedSet.contains(entry.getKey()));
|
mCache.entrySet().removeIf(entry -> !wantedSet.contains(entry.getKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelRunningTasks() {
|
||||||
|
mCache.forEach((key, value) -> {
|
||||||
|
if (value.getStatus() != AsyncTask.Status.FINISHED) {
|
||||||
|
value.cancel(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static class PreloadImageTask extends AsyncTask<Uri, Void, Drawable> {
|
private static class PreloadImageTask extends AsyncTask<Uri, Void, Drawable> {
|
||||||
private final NotificationInlineImageResolver mResolver;
|
private final NotificationInlineImageResolver mResolver;
|
||||||
|
|
||||||
@@ -87,7 +100,7 @@ public class NotificationInlineImageCache implements NotificationInlineImageReso
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
drawable = mResolver.resolveImage(target);
|
drawable = mResolver.resolveImage(target);
|
||||||
} catch (IOException | SecurityException ex) {
|
} catch (Exception ex) {
|
||||||
Log.d(TAG, "PreloadImageTask: Resolve failed from " + target, ex);
|
Log.d(TAG, "PreloadImageTask: Resolve failed from " + target, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.graphics.drawable.Drawable;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.android.internal.R;
|
import com.android.internal.R;
|
||||||
@@ -46,6 +47,9 @@ import java.util.Set;
|
|||||||
public class NotificationInlineImageResolver implements ImageResolver {
|
public class NotificationInlineImageResolver implements ImageResolver {
|
||||||
private static final String TAG = NotificationInlineImageResolver.class.getSimpleName();
|
private static final String TAG = NotificationInlineImageResolver.class.getSimpleName();
|
||||||
|
|
||||||
|
// Timeout for loading images from ImageCache when calling from UI thread
|
||||||
|
private static final long MAX_UI_THREAD_TIMEOUT_MS = 100L;
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final ImageCache mImageCache;
|
private final ImageCache mImageCache;
|
||||||
private Set<Uri> mWantedUriSet;
|
private Set<Uri> mWantedUriSet;
|
||||||
@@ -111,30 +115,38 @@ public class NotificationInlineImageResolver implements ImageResolver {
|
|||||||
* To resolve image from specified uri directly. If the resulting image is larger than the
|
* To resolve image from specified uri directly. If the resulting image is larger than the
|
||||||
* maximum allowed size, scale it down.
|
* maximum allowed size, scale it down.
|
||||||
* @param uri Uri of the image.
|
* @param uri Uri of the image.
|
||||||
* @return Drawable of the image.
|
* @return Drawable of the image, or null if unable to load.
|
||||||
* @throws IOException Throws if failed at resolving the image.
|
|
||||||
*/
|
*/
|
||||||
Drawable resolveImage(Uri uri) throws IOException {
|
Drawable resolveImage(Uri uri) {
|
||||||
return LocalImageResolver.resolveImage(uri, mContext, mMaxImageWidth, mMaxImageHeight);
|
try {
|
||||||
|
return LocalImageResolver.resolveImage(uri, mContext, mMaxImageWidth, mMaxImageHeight);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Catch general Exception because ContentResolver can re-throw arbitrary Exception
|
||||||
|
// from remote process as a RuntimeException. See: Parcel#readException
|
||||||
|
Log.d(TAG, "resolveImage: Can't load image from " + uri, ex);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an image from the Uri.
|
||||||
|
* This method is synchronous and is usually called from the Main thread.
|
||||||
|
* It will time-out after MAX_UI_THREAD_TIMEOUT_MS.
|
||||||
|
*
|
||||||
|
* @param uri Uri of the target image.
|
||||||
|
* @return drawable of the image, null if loading failed/timeout
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Drawable loadImage(Uri uri) {
|
public Drawable loadImage(Uri uri) {
|
||||||
Drawable result = null;
|
return hasCache() ? loadImageFromCache(uri, MAX_UI_THREAD_TIMEOUT_MS) : resolveImage(uri);
|
||||||
try {
|
}
|
||||||
if (hasCache()) {
|
|
||||||
// if the uri isn't currently cached, try caching it first
|
private Drawable loadImageFromCache(Uri uri, long timeoutMs) {
|
||||||
if (!mImageCache.hasEntry(uri)) {
|
// if the uri isn't currently cached, try caching it first
|
||||||
mImageCache.preload((uri));
|
if (!mImageCache.hasEntry(uri)) {
|
||||||
}
|
mImageCache.preload((uri));
|
||||||
result = mImageCache.get(uri);
|
|
||||||
} else {
|
|
||||||
result = resolveImage(uri);
|
|
||||||
}
|
|
||||||
} catch (IOException | SecurityException ex) {
|
|
||||||
Log.d(TAG, "loadImage: Can't load image from " + uri, ex);
|
|
||||||
}
|
}
|
||||||
return result;
|
return mImageCache.get(uri, timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,6 +220,30 @@ public class NotificationInlineImageResolver implements ImageResolver {
|
|||||||
return mWantedUriSet;
|
return mWantedUriSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a maximum timeout for images to finish preloading
|
||||||
|
* @param timeoutMs total timeout time
|
||||||
|
*/
|
||||||
|
void waitForPreloadedImages(long timeoutMs) {
|
||||||
|
if (!hasCache()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<Uri> preloadedUris = getWantedUriSet();
|
||||||
|
if (preloadedUris != null) {
|
||||||
|
// Decrement remaining timeout after each image check
|
||||||
|
long endTimeMs = SystemClock.elapsedRealtime() + timeoutMs;
|
||||||
|
preloadedUris.forEach(
|
||||||
|
uri -> loadImageFromCache(uri, endTimeMs - SystemClock.elapsedRealtime()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelRunningTasks() {
|
||||||
|
if (!hasCache()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mImageCache.cancelRunningTasks();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A interface for internal cache implementation of this resolver.
|
* A interface for internal cache implementation of this resolver.
|
||||||
*/
|
*/
|
||||||
@@ -217,7 +253,7 @@ public class NotificationInlineImageResolver implements ImageResolver {
|
|||||||
* @param uri The uri of the image.
|
* @param uri The uri of the image.
|
||||||
* @return Drawable of the image.
|
* @return Drawable of the image.
|
||||||
*/
|
*/
|
||||||
Drawable get(Uri uri);
|
Drawable get(Uri uri, long timeoutMs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the image resolver that actually resolves image from specified uri.
|
* Set the image resolver that actually resolves image from specified uri.
|
||||||
@@ -242,6 +278,11 @@ public class NotificationInlineImageResolver implements ImageResolver {
|
|||||||
* Purge unnecessary entries in the cache.
|
* Purge unnecessary entries in the cache.
|
||||||
*/
|
*/
|
||||||
void purge();
|
void purge();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel all unfinished image loading tasks
|
||||||
|
*/
|
||||||
|
void cancelRunningTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user