Commit 74b8c224 authored by Brandon Wylie's avatar Brandon Wylie Committed by Commit Bot

[IC] Fix null pointer exception when under memory pressure

The problem here is that between CachedImageFetcherImpl's FetchImage
invocation and completion, destroy was called on
InMemoryCachedImageFetcher. This causes an attempt to store the image
in a null bitmap cache. This just checks if the bitmap cache is null
before attempting to store an image.

Bug: 910080
Change-Id: I28b7aaee8588d883cfaf98328e2e58478d452375
Reviewed-on: https://chromium-review.googlesource.com/c/1355770Reviewed-by: default avatarTheresa <twellington@chromium.org>
Reviewed-by: default avatarSky Malice <skym@chromium.org>
Reviewed-by: default avatarFilip Gorski <fgorski@chromium.org>
Commit-Queue: Brandon Wylie <wylieb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#612857}
parent ad9ff3b0
...@@ -61,6 +61,7 @@ public class FeedImageLoader implements ImageLoaderApi { ...@@ -61,6 +61,7 @@ public class FeedImageLoader implements ImageLoaderApi {
public void destroy() { public void destroy() {
mCachedImageFetcher.destroy(); mCachedImageFetcher.destroy();
mCachedImageFetcher = null;
} }
@Override @Override
...@@ -82,7 +83,7 @@ public class FeedImageLoader implements ImageLoaderApi { ...@@ -82,7 +83,7 @@ public class FeedImageLoader implements ImageLoaderApi {
*/ */
private void loadDrawableWithIter( private void loadDrawableWithIter(
Iterator<String> urlsIter, int widthPx, int heightPx, Consumer<Drawable> consumer) { Iterator<String> urlsIter, int widthPx, int heightPx, Consumer<Drawable> consumer) {
if (!urlsIter.hasNext()) { if (!urlsIter.hasNext() || mCachedImageFetcher == null) {
// Post to ensure callback is not run synchronously. // Post to ensure callback is not run synchronously.
ThreadUtils.postOnUiThread(() -> consumer.accept(null)); ThreadUtils.postOnUiThread(() -> consumer.accept(null));
return; return;
......
...@@ -47,13 +47,21 @@ public class InMemoryCachedImageFetcher implements CachedImageFetcher { ...@@ -47,13 +47,21 @@ public class InMemoryCachedImageFetcher implements CachedImageFetcher {
@Override @Override
public void destroy() { public void destroy() {
mCachedImageFetcher.destroy(); mCachedImageFetcher.destroy();
mCachedImageFetcher = null;
mBitmapCache.destroy(); mBitmapCache.destroy();
mBitmapCache = null;
} }
@Override @Override
public void fetchImage(String url, int width, int height, Callback<Bitmap> callback) { public void fetchImage(String url, int width, int height, Callback<Bitmap> callback) {
Bitmap cachedBitmap = tryToGetBitmap(url, width, height); Bitmap cachedBitmap = tryToGetBitmap(url, width, height);
if (cachedBitmap == null) { if (cachedBitmap == null) {
if (mCachedImageFetcher == null) {
callback.onResult(null);
return;
}
mCachedImageFetcher.fetchImage(url, width, height, (Bitmap bitmap) -> { mCachedImageFetcher.fetchImage(url, width, height, (Bitmap bitmap) -> {
storeBitmap(bitmap, url, width, height); storeBitmap(bitmap, url, width, height);
callback.onResult(bitmap); callback.onResult(bitmap);
...@@ -76,7 +84,10 @@ public class InMemoryCachedImageFetcher implements CachedImageFetcher { ...@@ -76,7 +84,10 @@ public class InMemoryCachedImageFetcher implements CachedImageFetcher {
* @param height The height (in pixels) of the image. * @param height The height (in pixels) of the image.
* @return The Bitmap stored in memory or null. * @return The Bitmap stored in memory or null.
*/ */
private Bitmap tryToGetBitmap(String url, int width, int height) { @VisibleForTesting
Bitmap tryToGetBitmap(String url, int width, int height) {
if (mBitmapCache == null) return null;
String key = encodeCacheKey(url, width, height); String key = encodeCacheKey(url, width, height);
return mBitmapCache.getBitmap(key); return mBitmapCache.getBitmap(key);
} }
...@@ -89,7 +100,9 @@ public class InMemoryCachedImageFetcher implements CachedImageFetcher { ...@@ -89,7 +100,9 @@ public class InMemoryCachedImageFetcher implements CachedImageFetcher {
* @param height The height (in pixels) of the image. * @param height The height (in pixels) of the image.
*/ */
private void storeBitmap(Bitmap bitmap, String url, int width, int height) { private void storeBitmap(Bitmap bitmap, String url, int width, int height) {
if (bitmap == null) return; if (bitmap == null || mBitmapCache == null) {
return;
}
String key = encodeCacheKey(url, width, height); String key = encodeCacheKey(url, width, height);
mBitmapCache.putBitmap(key, bitmap); mBitmapCache.putBitmap(key, bitmap);
......
...@@ -4,16 +4,20 @@ ...@@ -4,16 +4,20 @@
package org.chromium.chrome.browser.cached_image_fetcher; package org.chromium.chrome.browser.cached_image_fetcher;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.support.test.filters.SmallTest; import android.support.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -23,36 +27,34 @@ import org.mockito.Mock; ...@@ -23,36 +27,34 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.Callback; import org.chromium.base.Callback;
import org.chromium.base.DiscardableReferencePool; import org.chromium.base.DiscardableReferencePool;
import org.chromium.base.task.test.BackgroundShadowAsyncTask;
import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.BitmapCache; import org.chromium.chrome.browser.BitmapCache;
import org.chromium.chrome.browser.util.test.ShadowUrlUtilities;
/** /**
* Unit tests for InMemoryCachedImageFetcher. * Unit tests for InMemoryCachedImageFetcher.
*/ */
@RunWith(BaseRobolectricTestRunner.class) @RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE, @Config(manifest = Config.NONE)
shadows = {ShadowUrlUtilities.class, BackgroundShadowAsyncTask.class})
public class InMemoryCachedImageFetcherTest { public class InMemoryCachedImageFetcherTest {
private static final String URL = "http://foo.bar"; private static final String URL = "http://foo.bar";
private static final int WIDTH_PX = 100; private static final int WIDTH_PX = 100;
private static final int HEIGHT_PX = 200; private static final int HEIGHT_PX = 200;
private static final int DEFAULT_CACHE_SIZE = 100; private static final int DEFAULT_CACHE_SIZE = 100;
private final DiscardableReferencePool mReferencePool = new DiscardableReferencePool();
private final Bitmap mBitmap = private final Bitmap mBitmap =
Bitmap.createBitmap(WIDTH_PX, HEIGHT_PX, Bitmap.Config.ARGB_8888); Bitmap.createBitmap(WIDTH_PX, HEIGHT_PX, Bitmap.Config.ARGB_8888);
private InMemoryCachedImageFetcher mInMemoryCachedImageFetcher; private InMemoryCachedImageFetcher mInMemoryCachedImageFetcher;
private BitmapCache mBitmapCache; private BitmapCache mBitmapCache;
private DiscardableReferencePool mReferencePool;
@Mock @Mock
private CachedImageFetcherImpl mCachedImageFetcherImpl; private CachedImageFetcherImpl mCachedImageFetcherImpl;
@Mock
private Callback<Bitmap> mCallback;
@Captor @Captor
private ArgumentCaptor<Integer> mWidthCaptor; private ArgumentCaptor<Integer> mWidthCaptor;
...@@ -64,32 +66,76 @@ public class InMemoryCachedImageFetcherTest { ...@@ -64,32 +66,76 @@ public class InMemoryCachedImageFetcherTest {
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mReferencePool = new DiscardableReferencePool();
mBitmapCache = new BitmapCache(mReferencePool, DEFAULT_CACHE_SIZE); mBitmapCache = new BitmapCache(mReferencePool, DEFAULT_CACHE_SIZE);
}
@After
public void tearDown() {
mBitmapCache.destroy();
mReferencePool.drain();
}
public void answerFetch(Bitmap bitmap, CachedImageFetcher cachedImageFetcher,
boolean deleteBitmapCacheOnFetch) {
mInMemoryCachedImageFetcher = mInMemoryCachedImageFetcher =
new InMemoryCachedImageFetcher(mBitmapCache, mCachedImageFetcherImpl); spy(new InMemoryCachedImageFetcher(mBitmapCache, cachedImageFetcher));
// clang-format off // clang-format off
doAnswer((InvocationOnMock invocation) -> { doAnswer((InvocationOnMock invocation) -> {
mCallbackCaptor.getValue().onResult(mBitmap); if (deleteBitmapCacheOnFetch) {
mInMemoryCachedImageFetcher.destroy();
mReferencePool.drain();
}
mCallbackCaptor.getValue().onResult(bitmap);
return null; return null;
}).when(mCachedImageFetcherImpl) }).when(mCachedImageFetcherImpl)
.fetchImage(eq(URL), mWidthCaptor.capture(), mHeightCaptor.capture(), .fetchImage(eq(URL), mWidthCaptor.capture(), mHeightCaptor.capture(),
mCallbackCaptor.capture()); mCallbackCaptor.capture());
// clang-format on // clang-format on
} }
@SuppressWarnings("unchecked")
@Test @Test
@SmallTest @SmallTest
public void testFetchImageCachesFirstCall() throws Exception { public void testFetchImageCachesFirstCall() throws Exception {
mInMemoryCachedImageFetcher.fetchImage( answerFetch(mBitmap, mCachedImageFetcherImpl, false);
URL, WIDTH_PX, HEIGHT_PX, (Bitmap bitmap) -> { assertEquals(bitmap, mBitmap); }); mInMemoryCachedImageFetcher.fetchImage(URL, WIDTH_PX, HEIGHT_PX, mCallback);
BackgroundShadowAsyncTask.runBackgroundTasks(); verify(mCallback).onResult(eq(mBitmap));
ShadowLooper.runUiThreadTasks();
mInMemoryCachedImageFetcher.fetchImage( reset(mCallback);
URL, WIDTH_PX, HEIGHT_PX, (Bitmap bitmap) -> { assertEquals(bitmap, mBitmap); }); mInMemoryCachedImageFetcher.fetchImage(URL, WIDTH_PX, HEIGHT_PX, mCallback);
BackgroundShadowAsyncTask.runBackgroundTasks(); verify(mCallback).onResult(eq(mBitmap));
ShadowLooper.runUiThreadTasks();
verify(mCachedImageFetcherImpl, /* Should only go to native the first time. */ times(1)) verify(mCachedImageFetcherImpl, /* Should only go to native the first time. */ times(1))
.fetchImage(eq(URL), eq(WIDTH_PX), eq(HEIGHT_PX), any()); .fetchImage(eq(URL), eq(WIDTH_PX), eq(HEIGHT_PX), any());
} }
@Test
@SmallTest
public void testFetchImageReturnsNullWhenFetcherIsNull() throws Exception {
answerFetch(mBitmap, null, false);
doReturn(null)
.when(mInMemoryCachedImageFetcher)
.tryToGetBitmap(eq(URL), eq(WIDTH_PX), eq(HEIGHT_PX));
mInMemoryCachedImageFetcher.fetchImage(URL, WIDTH_PX, HEIGHT_PX, mCallback);
verify(mCallback).onResult(eq(null));
verify(mCachedImageFetcherImpl, /* Shouldn't make the call at all. */ times(0))
.fetchImage(eq(URL), eq(WIDTH_PX), eq(HEIGHT_PX), any());
}
@Test
@SmallTest
public void testFetchImageDoesNotCacheAfterDestroy() {
try {
answerFetch(mBitmap, mCachedImageFetcherImpl, true);
// No exception should be thrown here when bitmap cache is null.
mInMemoryCachedImageFetcher.fetchImage(URL, WIDTH_PX, HEIGHT_PX, (Bitmap bitmap) -> {});
} catch (Exception e) {
fail("Destroy called in the middle of execution shouldn't throw");
}
}
} }
\ No newline at end of file
...@@ -88,8 +88,12 @@ public class FeedImageLoaderTest { ...@@ -88,8 +88,12 @@ public class FeedImageLoaderTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
setUpWithImageFetcher(mCachedImageFetcher);
}
public void setUpWithImageFetcher(CachedImageFetcher cachedImageFetcher) {
mImageLoader = Mockito.spy( mImageLoader = Mockito.spy(
new FeedImageLoader(ContextUtils.getApplicationContext(), mCachedImageFetcher)); new FeedImageLoader(ContextUtils.getApplicationContext(), cachedImageFetcher));
} }
private void answerFetchImage(String url, Bitmap bitmap) { private void answerFetchImage(String url, Bitmap bitmap) {
...@@ -138,6 +142,17 @@ public class FeedImageLoaderTest { ...@@ -138,6 +142,17 @@ public class FeedImageLoaderTest {
verify(mConsumer, times(1)).accept(eq(null)); verify(mConsumer, times(1)).accept(eq(null));
} }
@Test
@SmallTest
public void testLoadDrawableWithNullFetcher() {
setUpWithImageFetcher(null);
loadDrawable(HTTP_STRING1);
verify(mConsumer, times(1)).accept(eq(null));
verify(mImageLoader, times(0))
.fetchImage(eq(HTTP_STRING1), eq(ImageLoaderApi.DIMENSION_UNKNOWN),
eq(ImageLoaderApi.DIMENSION_UNKNOWN), any());
}
@Test @Test
@SmallTest @SmallTest
public void testLoadDrawableMultiple() { public void testLoadDrawableMultiple() {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment