| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.components.browser_ui.photo_picker; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.support.test.InstrumentationRegistry; |
| |
| import androidx.test.filters.LargeTest; |
| import androidx.test.filters.SmallTest; |
| |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.base.test.BaseJUnit4ClassRunner; |
| import org.chromium.base.test.util.CallbackHelper; |
| import org.chromium.base.test.util.MinAndroidSdkLevel; |
| import org.chromium.base.test.util.UrlUtils; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| |
| import java.io.File; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Tests for the DecoderServiceHost. |
| */ |
| @RunWith(BaseJUnit4ClassRunner.class) |
| @MinAndroidSdkLevel(Build.VERSION_CODES.N) |
| public class DecoderServiceHostTest implements DecoderServiceHost.DecoderStatusCallback, |
| DecoderServiceHost.ImagesDecodedCallback { |
| // The timeout (in milliseconds) to wait for the decoding. |
| private static final int WAIT_TIMEOUT_MS = 7500; |
| |
| // The base test file path. |
| private static final String TEST_FILE_PATH = "chrome/test/data/android/photo_picker/"; |
| |
| private Context mContext; |
| |
| // A callback that fires when the decoder is ready. |
| public final CallbackHelper mOnDecoderReadyCallback = new CallbackHelper(); |
| |
| // A callback that fires when something is finished decoding in the dialog. |
| public final CallbackHelper mOnDecodedCallback = new CallbackHelper(); |
| |
| private String mLastDecodedPath; |
| private boolean mLastIsVideo; |
| private Bitmap mLastInitialFrame; |
| private int mLastFrameCount; |
| private String mLastVideoDuration; |
| private float mLastRatio; |
| |
| @Before |
| public void setUp() throws Exception { |
| mContext = InstrumentationRegistry.getTargetContext(); |
| |
| TestThreadUtils.runOnUiThreadBlocking(() -> { |
| DecoderServiceHost.setIntentSupplier( |
| () -> { return new Intent(mContext, TestImageDecoderService.class); }); |
| }); |
| |
| DecoderServiceHost.setStatusCallback(this); |
| } |
| |
| // DecoderServiceHost.DecoderStatusCallback: |
| |
| @Override |
| public void serviceReady() { |
| mOnDecoderReadyCallback.notifyCalled(); |
| } |
| |
| @Override |
| public void decoderIdle() {} |
| |
| // DecoderServiceHost.ImagesDecodedCallback: |
| |
| @Override |
| public void imagesDecodedCallback(String filePath, boolean isVideo, boolean isZoomedIn, |
| List<Bitmap> bitmaps, String videoDuration, float ratio) { |
| mLastDecodedPath = filePath; |
| mLastIsVideo = isVideo; |
| mLastFrameCount = bitmaps != null ? bitmaps.size() : -1; |
| mLastInitialFrame = bitmaps != null ? bitmaps.get(0) : null; |
| mLastVideoDuration = videoDuration; |
| mLastRatio = ratio; |
| |
| mOnDecodedCallback.notifyCalled(); |
| } |
| |
| private void waitForDecoder() throws Exception { |
| int callCount = mOnDecoderReadyCallback.getCallCount(); |
| mOnDecoderReadyCallback.waitForCallback( |
| callCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } |
| |
| private void waitForThumbnailDecode() throws Exception { |
| int callCount = mOnDecodedCallback.getCallCount(); |
| mOnDecodedCallback.waitForCallback(callCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } |
| |
| private void decodeImage(DecoderServiceHost host, Uri uri, @PickerBitmap.TileTypes int fileType, |
| int width, boolean fullWidth, DecoderServiceHost.ImagesDecodedCallback callback) { |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> host.decodeImage(uri, fileType, width, fullWidth, callback)); |
| } |
| |
| private void cancelDecodeImage(DecoderServiceHost host, String filePath) { |
| TestThreadUtils.runOnUiThreadBlocking(() -> host.cancelDecodeImage(filePath)); |
| } |
| |
| @Test |
| @SmallTest |
| public void testRequestComparator() throws Throwable { |
| Uri uri = Uri.parse("http://example.com"); |
| int width = 100; |
| boolean fullWidth = true; |
| DecoderServiceHost.ImagesDecodedCallback callback = null; |
| |
| DecoderServiceHost.DecoderServiceParams higherPri; |
| DecoderServiceHost.DecoderServiceParams lowerPri; |
| |
| // Still image decoding has higher priority than first frame video decoding. |
| higherPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.PICTURE, |
| /* firstFrame= */ true, callback); |
| lowerPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.VIDEO, |
| /* firstFrame= */ true, callback); |
| DecoderServiceHost host = new DecoderServiceHost(this, mContext); |
| Assert.assertTrue("Still images have priority over requests for initial video frame", |
| host.mRequestComparator.compare(higherPri, lowerPri) < 0); |
| |
| // Still image decoding has higher priority than decoding remaining video frames. |
| higherPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.PICTURE, |
| /* firstFrame= */ true, callback); |
| lowerPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.VIDEO, |
| /* firstFrame= */ false, callback); |
| Assert.assertTrue("Still images have priority over requests for remaining video frames", |
| host.mRequestComparator.compare(higherPri, lowerPri) < 0); |
| |
| // First frame video request have priority over remaining video frames. |
| higherPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.VIDEO, |
| /* firstFrame= */ true, callback); |
| lowerPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.VIDEO, |
| /* firstFrame= */ false, callback); |
| Assert.assertTrue("Initial video frames have priority over remaining video frames", |
| host.mRequestComparator.compare(higherPri, lowerPri) < 0); |
| |
| // Enforce FIFO principle for two identical still image requests. |
| higherPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.PICTURE, |
| /* firstFrame= */ true, callback); |
| lowerPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.PICTURE, |
| /* firstFrame= */ true, callback); |
| Assert.assertTrue("Identical still image requests should be processed FIFO", |
| host.mRequestComparator.compare(higherPri, lowerPri) < 0); |
| |
| // Enforce FIFO principle for two identical video requests (initial frames). |
| higherPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.VIDEO, |
| /* firstFrame= */ true, callback); |
| lowerPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.VIDEO, |
| /* firstFrame= */ true, callback); |
| Assert.assertTrue("Identical video requests (initial frames) should be processed FIFO", |
| host.mRequestComparator.compare(higherPri, lowerPri) < 0); |
| |
| // Enforce FIFO principle for two identical video requests (remaining frames). |
| higherPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.VIDEO, |
| /* firstFrame= */ false, callback); |
| lowerPri = new DecoderServiceHost.DecoderServiceParams(uri, width, fullWidth, |
| PickerBitmap.TileTypes.VIDEO, |
| /* firstFrame= */ false, callback); |
| Assert.assertTrue("Identical video requests (remanining frames) should be processed FIFO", |
| host.mRequestComparator.compare(higherPri, lowerPri) < 0); |
| } |
| |
| @Test |
| @LargeTest |
| public void testDecodingOrder() throws Throwable { |
| DecoderServiceHost host = new DecoderServiceHost(this, mContext); |
| host.bind(mContext); |
| waitForDecoder(); |
| |
| String video1 = "noogler.mp4"; |
| String video2 = "noogler2.mp4"; |
| String jpg1 = "blue100x100.jpg"; |
| File file1 = new File(UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + video1)); |
| File file2 = new File(UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + video2)); |
| File file3 = new File(UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + jpg1)); |
| |
| decodeImage(host, Uri.fromFile(file1), PickerBitmap.TileTypes.VIDEO, 10, |
| /*fullWidth=*/false, this); |
| decodeImage(host, Uri.fromFile(file2), PickerBitmap.TileTypes.VIDEO, 10, |
| /*fullWidth=*/false, this); |
| decodeImage(host, Uri.fromFile(file3), PickerBitmap.TileTypes.PICTURE, 10, |
| /*fullWidth=*/false, this); |
| |
| // First decoding result should be first frame of video 1. Even though still images take |
| // priority over video decoding, video 1 will be the only item in the queue when the first |
| // decoding request is kicked off (as a result of calling decodeImage). |
| waitForThumbnailDecode(); |
| Assert.assertTrue(mLastDecodedPath.contains(video1)); |
| Assert.assertEquals(true, mLastIsVideo); |
| Assert.assertEquals("0:00", mLastVideoDuration); |
| Assert.assertEquals(1, mLastFrameCount); |
| |
| // When the decoder is finished with the first frame of video 1, there will be two new |
| // requests available for processing. Video2 was added first, but that will be skipped in |
| // favor of the still image, so the jpg is expected to be decoded next. |
| waitForThumbnailDecode(); |
| Assert.assertTrue(mLastDecodedPath.contains(jpg1)); |
| Assert.assertEquals(false, mLastIsVideo); |
| Assert.assertEquals(null, mLastVideoDuration); |
| Assert.assertEquals(1, mLastFrameCount); |
| |
| // Third decoding result is first frame of video 2, because that's higher priority than the |
| // rest of video 1. |
| waitForThumbnailDecode(); |
| Assert.assertTrue(mLastDecodedPath.contains(video2)); |
| Assert.assertEquals(true, mLastIsVideo); |
| Assert.assertEquals("0:00", mLastVideoDuration); |
| Assert.assertEquals(1, mLastFrameCount); |
| |
| // Remaining frames of video 1. |
| waitForThumbnailDecode(); |
| Assert.assertTrue(mLastDecodedPath.contains(video1)); |
| Assert.assertEquals(true, mLastIsVideo); |
| Assert.assertEquals("0:00", mLastVideoDuration); |
| Assert.assertEquals(10, mLastFrameCount); |
| |
| // Remaining frames of video 2. |
| waitForThumbnailDecode(); |
| Assert.assertTrue(mLastDecodedPath.contains(video2)); |
| Assert.assertEquals(true, mLastIsVideo); |
| Assert.assertEquals("0:00", mLastVideoDuration); |
| Assert.assertEquals(10, mLastFrameCount); |
| |
| host.unbind(mContext); |
| } |
| |
| @Test |
| @LargeTest |
| public void testDecodingSizes() throws Throwable { |
| DecoderServiceHost host = new DecoderServiceHost(this, mContext); |
| host.bind(mContext); |
| waitForDecoder(); |
| |
| String video1 = "noogler.mp4"; // 1920 x 1080 video. |
| String jpg1 = "blue100x100.jpg"; |
| File file1 = new File(UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + video1)); |
| File file2 = new File(UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + jpg1)); |
| |
| // Thumbnail photo. 100 x 100 -> 10 x 10. |
| decodeImage(host, Uri.fromFile(file2), PickerBitmap.TileTypes.PICTURE, 10, |
| /*fullWidth=*/false, this); |
| waitForThumbnailDecode(); |
| Assert.assertTrue(mLastDecodedPath.contains(jpg1)); |
| Assert.assertEquals(false, mLastIsVideo); |
| Assert.assertEquals(null, mLastVideoDuration); |
| Assert.assertEquals(1, mLastFrameCount); |
| Assert.assertEquals(1.0f, mLastRatio, 0.1f); |
| Assert.assertEquals(10, mLastInitialFrame.getWidth()); |
| Assert.assertEquals(10, mLastInitialFrame.getHeight()); |
| |
| // Full-width photo. 100 x 100 -> 200 x 200. |
| decodeImage(host, Uri.fromFile(file2), PickerBitmap.TileTypes.PICTURE, 200, |
| /*fullWidth=*/true, this); |
| waitForThumbnailDecode(); |
| Assert.assertTrue(mLastDecodedPath.contains(jpg1)); |
| Assert.assertEquals(false, mLastIsVideo); |
| Assert.assertEquals(null, mLastVideoDuration); |
| Assert.assertEquals(1, mLastFrameCount); |
| Assert.assertEquals(1.0f, mLastRatio, 0.1f); |
| Assert.assertEquals(200, mLastInitialFrame.getWidth()); |
| Assert.assertEquals(200, mLastInitialFrame.getHeight()); |
| |
| // Thumbnail video. 1920 x 1080 -> 10 x 10. |
| decodeImage(host, Uri.fromFile(file1), PickerBitmap.TileTypes.VIDEO, 10, |
| /*fullWidth=*/false, this); |
| waitForThumbnailDecode(); // Initial frame. |
| Assert.assertTrue(mLastDecodedPath.contains(video1)); |
| Assert.assertEquals(true, mLastIsVideo); |
| Assert.assertEquals("0:00", mLastVideoDuration); |
| Assert.assertEquals(1, mLastFrameCount); |
| Assert.assertEquals(0.5625f, mLastRatio, 0.0001f); |
| Assert.assertEquals(10, mLastInitialFrame.getWidth()); |
| Assert.assertEquals(10, mLastInitialFrame.getHeight()); |
| waitForThumbnailDecode(); // Rest of frames. |
| Assert.assertTrue(mLastDecodedPath.contains(video1)); |
| Assert.assertEquals(true, mLastIsVideo); |
| Assert.assertEquals("0:00", mLastVideoDuration); |
| Assert.assertEquals(10, mLastFrameCount); |
| Assert.assertEquals(0.5625f, mLastRatio, 0.0001f); |
| Assert.assertEquals(10, mLastInitialFrame.getWidth()); |
| Assert.assertEquals(10, mLastInitialFrame.getHeight()); |
| |
| // Full-width video. 1920 x 1080 -> 2000 x 1125. |
| decodeImage(host, Uri.fromFile(file1), PickerBitmap.TileTypes.VIDEO, 2000, |
| /*fullWidth=*/true, this); |
| waitForThumbnailDecode(); // Initial frame. |
| Assert.assertTrue(mLastDecodedPath.contains(video1)); |
| Assert.assertEquals(true, mLastIsVideo); |
| Assert.assertEquals("0:00", mLastVideoDuration); |
| Assert.assertEquals(1, mLastFrameCount); |
| Assert.assertEquals(0.5625f, mLastRatio, 0.0001f); |
| Assert.assertEquals(2000, mLastInitialFrame.getWidth()); |
| Assert.assertEquals(1125, mLastInitialFrame.getHeight()); |
| waitForThumbnailDecode(); // Rest of frames. |
| Assert.assertTrue(mLastDecodedPath.contains(video1)); |
| Assert.assertEquals(true, mLastIsVideo); |
| Assert.assertEquals("0:00", mLastVideoDuration); |
| Assert.assertEquals(10, mLastFrameCount); |
| Assert.assertEquals(0.5625f, mLastRatio, 0.0001f); |
| Assert.assertEquals(2000, mLastInitialFrame.getWidth()); |
| Assert.assertEquals(1125, mLastInitialFrame.getHeight()); |
| |
| host.unbind(mContext); |
| } |
| |
| @Test |
| @LargeTest |
| public void testCancelation() throws Throwable { |
| DecoderServiceHost host = new DecoderServiceHost(this, mContext); |
| host.bind(mContext); |
| waitForDecoder(); |
| |
| String green = "green100x100.jpg"; |
| String yellow = "yellow100x100.jpg"; |
| String red = "red100x100.jpg"; |
| String greenPath = UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + green); |
| String yellowPath = UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + yellow); |
| String redPath = UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + red); |
| |
| decodeImage(host, Uri.fromFile(new File(greenPath)), PickerBitmap.TileTypes.PICTURE, 10, |
| /*fullWidth=*/false, this); |
| decodeImage(host, Uri.fromFile(new File(yellowPath)), PickerBitmap.TileTypes.PICTURE, 10, |
| /*fullWidth=*/false, this); |
| |
| // Now add and subsequently remove the request. |
| decodeImage(host, Uri.fromFile(new File(redPath)), PickerBitmap.TileTypes.PICTURE, 10, |
| /*fullWidth=*/false, this); |
| cancelDecodeImage(host, redPath); |
| |
| // First decoding result should be the green image. |
| waitForThumbnailDecode(); |
| Assert.assertEquals(greenPath, mLastDecodedPath); |
| |
| // Next is the yellow image, and asserts in DecoderServiceHost (designed to catch when |
| // multiple simultaneous decoding requests are started) should not fire. |
| waitForThumbnailDecode(); |
| Assert.assertEquals(yellowPath, mLastDecodedPath); |
| |
| host.unbind(mContext); |
| } |
| |
| @Test |
| @LargeTest |
| public void testNoConnectionFailureMode() throws Throwable { |
| DecoderServiceHost host = new DecoderServiceHost(this, mContext); |
| |
| // Try decoding without a connection to the decoder. |
| String green = "green100x100.jpg"; |
| String greenPath = UrlUtils.getIsolatedTestFilePath(TEST_FILE_PATH + green); |
| decodeImage(host, Uri.fromFile(new File(greenPath)), PickerBitmap.TileTypes.PICTURE, 10, |
| /*fullWidth=*/false, this); |
| Assert.assertEquals(greenPath, mLastDecodedPath); |
| Assert.assertEquals(null, mLastInitialFrame); |
| } |
| |
| @Test |
| @LargeTest |
| public void testFileNotFoundFailureMode() throws Throwable { |
| DecoderServiceHost host = new DecoderServiceHost(this, mContext); |
| host.bind(mContext); |
| waitForDecoder(); |
| |
| // Try decoding a file that doesn't exist. |
| String noPath = "/nonexistentpath/nonexistentfile"; |
| decodeImage(host, Uri.fromFile(new File(noPath)), PickerBitmap.TileTypes.PICTURE, 10, |
| /*fullWidth=*/false, this); |
| Assert.assertEquals(noPath, mLastDecodedPath); |
| Assert.assertEquals(null, mLastInitialFrame); |
| |
| host.unbind(mContext); |
| } |
| } |