[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:
Valentin Iftime
2023-02-15 20:39:44 +01:00
committed by Iavor-Valentin Iftime
parent 240d62f76f
commit b9cd15ad8a
3 changed files with 92 additions and 25 deletions

View File

@@ -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 {

View File

@@ -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);
} }

View File

@@ -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();
} }
} }