diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/browser_ui/webshare/android | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-85-based.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/browser_ui/webshare/android')
7 files changed, 780 insertions, 0 deletions
diff --git a/chromium/components/browser_ui/webshare/android/BUILD.gn b/chromium/components/browser_ui/webshare/android/BUILD.gn new file mode 100644 index 00000000000..05149697937 --- /dev/null +++ b/chromium/components/browser_ui/webshare/android/BUILD.gn @@ -0,0 +1,42 @@ +# Copyright 2020 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. + +import("//build/config/android/rules.gni") + +android_library("java") { + sources = [ + "java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java", + "java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java", + "java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java", + ] + deps = [ + "//base:base_java", + "//components/browser_ui/share/android:java", + "//content/public/android:content_java", + "//mojo/public/java:system_java", + "//mojo/public/java/system:system_impl_java", + "//third_party/blink/public/mojom:android_mojo_bindings_java", + "//ui/android:ui_java", + "//url/mojom:url_mojom_gurl_java", + ] +} + +java_library("junit") { + # Skip platform checks since Robolectric depends on requires_android targets. + bypass_platform_checks = true + testonly = true + sources = [ + "java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java", + "java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java", + ] + deps = [ + ":java", + "//base:base_java", + "//base:base_java_test_support", + "//base:base_junit_test_support", + "//base/test:test_support_java", + "//third_party/blink/public/mojom:android_mojo_bindings_java", + "//third_party/junit", + ] +} diff --git a/chromium/components/browser_ui/webshare/android/DEPS b/chromium/components/browser_ui/webshare/android/DEPS new file mode 100644 index 00000000000..bb311370365 --- /dev/null +++ b/chromium/components/browser_ui/webshare/android/DEPS @@ -0,0 +1,5 @@ +include_rules = [ + "+mojo/public/java/system", + "+content/public/android/java", + "+ui/android/java", +] diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java new file mode 100644 index 00000000000..e1008c3c68f --- /dev/null +++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java @@ -0,0 +1,177 @@ +// 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.webshare; + +import org.chromium.base.Callback; +import org.chromium.base.Log; +import org.chromium.base.StreamUtil; +import org.chromium.blink.mojom.Blob; +import org.chromium.blink.mojom.BlobReaderClient; +import org.chromium.mojo.system.Core; +import org.chromium.mojo.system.DataPipe; +import org.chromium.mojo.system.MojoException; +import org.chromium.mojo.system.MojoResult; +import org.chromium.mojo.system.Pair; +import org.chromium.mojo.system.ResultAnd; +import org.chromium.mojo.system.Watcher; +import org.chromium.mojo.system.impl.CoreImpl; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * Receives a blob over mojom and writes it to the stream. + */ +public class BlobReceiver implements BlobReaderClient { + private static final String TAG = "share"; + private static final int CHUNK_SIZE = 64 * 1024; + private static final int PIPE_CAPACITY = 2 * CHUNK_SIZE; + + private final ByteBuffer mBuffer; + private final OutputStream mOutputStream; + private long mMaximumContentSize; + private long mExpectedContentSize; + private long mReceivedContentSize; + private DataPipe.ConsumerHandle mConsumerHandle; + private Callback<Integer> mCallback; + + /** + * Constructs a BlobReceiver. + * + * @param outputStream the destination for the blob contents. + * @param maximumContentSize the maximum permitted length of the blob. + */ + public BlobReceiver(OutputStream outputStream, long maximumContentSize) { + mBuffer = ByteBuffer.allocateDirect(CHUNK_SIZE); + mOutputStream = outputStream; + mMaximumContentSize = maximumContentSize; + } + + /** + * Initiates reading of the blob contents. + * + * @param blob the blob to read. + * @param callback the callback to call when reading is complete. + */ + public void start(Blob blob, Callback<Integer> callback) { + mCallback = callback; + DataPipe.CreateOptions options = new DataPipe.CreateOptions(); + options.setElementNumBytes(1); + options.setCapacityNumBytes(PIPE_CAPACITY); + + Pair<DataPipe.ProducerHandle, DataPipe.ConsumerHandle> pipe = + CoreImpl.getInstance().createDataPipe(options); + mConsumerHandle = pipe.second; + blob.readAll(pipe.first, this); + } + + // Interface + @Override + public void close() {} + + // ConnectionErrorHandler + @Override + public void onConnectionError(MojoException e) { + if (mCallback == null) return; + reportError(e.getMojoResult(), "Connection error detected."); + } + + // BlobReaderClient + @Override + public void onCalculatedSize(long totalSize, long expectedContentSize) { + if (mCallback == null) return; + if (expectedContentSize > mMaximumContentSize) { + reportError(MojoResult.RESOURCE_EXHAUSTED, "Stream exceeds permitted size"); + return; + } + mExpectedContentSize = expectedContentSize; + if (mReceivedContentSize >= mExpectedContentSize) { + complete(); + return; + } + + Watcher watcher = CoreImpl.getInstance().getWatcher(); + watcher.start(mConsumerHandle, Core.HandleSignals.READABLE, new Watcher.Callback() { + @Override + public void onResult(int result) { + if (mCallback == null) return; + if (result == MojoResult.OK) { + read(); + } else { + reportError(result, "Watcher reported error."); + } + } + }); + } + + // BlobReaderClient + @Override + public void onComplete(int status, long dataLength) { + if (mCallback == null) return; + read(); + } + + private void read() { + try { + while (true) { + ResultAnd<Integer> result = + mConsumerHandle.readData(mBuffer, DataPipe.ReadFlags.NONE); + + if (result.getMojoResult() == MojoResult.SHOULD_WAIT) return; + + if (result.getMojoResult() != MojoResult.OK) { + reportError(result.getMojoResult(), "Failed to read from blob."); + return; + } + + Integer bytesRead = result.getValue(); + if (bytesRead <= 0) { + reportError(MojoResult.SHOULD_WAIT, "No data available"); + return; + } + try { + mOutputStream.write(mBuffer.array(), mBuffer.arrayOffset(), bytesRead); + } catch (IOException e) { + reportError(MojoResult.DATA_LOSS, "Failed to write to stream."); + return; + } + mReceivedContentSize += bytesRead; + if (mReceivedContentSize >= mExpectedContentSize) { + if (mReceivedContentSize == mExpectedContentSize) { + complete(); + } else { + reportError( + MojoResult.OUT_OF_RANGE, "Received more bytes than expected size."); + } + return; + } + } + } catch (MojoException e) { + reportError(e.getMojoResult(), "Failed to receive blob."); + } + } + + private void complete() { + try { + mOutputStream.close(); + } catch (IOException e) { + reportError(MojoResult.CANCELLED, "Failed to close stream."); + return; + } + mCallback.onResult(MojoResult.OK); + mCallback = null; + } + + private void reportError(int result, String message) { + if (result == MojoResult.OK) { + result = MojoResult.INVALID_ARGUMENT; + } + Log.w(TAG, message); + StreamUtil.closeQuietly(mOutputStream); + mCallback.onResult(result); + mCallback = null; + } +} diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java new file mode 100644 index 00000000000..dcdc8ddc798 --- /dev/null +++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java @@ -0,0 +1,290 @@ +// Copyright 2016 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.webshare; + +import android.app.Activity; +import android.content.ComponentName; +import android.net.Uri; + +import androidx.annotation.Nullable; + +import org.chromium.base.CollectionUtil; +import org.chromium.base.ContentUriUtils; +import org.chromium.base.FileUtils; +import org.chromium.base.Log; +import org.chromium.base.metrics.RecordHistogram; +import org.chromium.base.task.AsyncTask; +import org.chromium.base.task.PostTask; +import org.chromium.base.task.TaskRunner; +import org.chromium.base.task.TaskTraits; +import org.chromium.components.browser_ui.share.ShareImageFileUtils; +import org.chromium.components.browser_ui.share.ShareParams; +import org.chromium.content_public.browser.WebContents; +import org.chromium.mojo.system.MojoException; +import org.chromium.ui.base.WindowAndroid; +import org.chromium.url.mojom.Url; +import org.chromium.webshare.mojom.ShareError; +import org.chromium.webshare.mojom.ShareService; +import org.chromium.webshare.mojom.SharedFile; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; + +/** + * Android implementation of the ShareService service defined in + * third_party/blink/public/mojom/webshare/webshare.mojom. + */ +public class ShareServiceImpl implements ShareService { + private final WindowAndroid mWindow; + private final WebShareDelegate mDelegate; + + private static final String TAG = "share"; + + // These numbers are written to histograms. Keep in sync with WebShareMethod enum in + // histograms.xml, and don't reuse or renumber entries (except for the _COUNT entry). + private static final int WEBSHARE_METHOD_SHARE = 0; + // Count is technically 1, but recordEnumeratedHistogram requires a boundary of at least 2 + // (https://crbug.com/645032). + private static final int WEBSHARE_METHOD_COUNT = 2; + + // These numbers are written to histograms. Keep in sync with WebShareOutcome enum in + // histograms.xml, and don't reuse or renumber entries (except for the _COUNT entry). + private static final int WEBSHARE_OUTCOME_SUCCESS = 0; + private static final int WEBSHARE_OUTCOME_UNKNOWN_FAILURE = 1; + private static final int WEBSHARE_OUTCOME_CANCELED = 2; + private static final int WEBSHARE_OUTCOME_COUNT = 3; + + // These protect us if the renderer is compromised. + private static final int MAX_SHARED_FILE_COUNT = 10; + private static final int MAX_SHARED_FILE_BYTES = 50 * 1024 * 1024; + + // clang-format off + private static final Set<String> PERMITTED_EXTENSIONS = + Collections.unmodifiableSet(CollectionUtil.newHashSet( + "bmp", // image/bmp / image/x-ms-bmp + "css", // text/css + "csv", // text/csv / text/comma-separated-values + "ehtml", // text/html + "flac", // audio/flac + "gif", // image/gif + "htm", // text/html + "html", // text/html + "ico", // image/x-icon + "jfif", // image/jpeg + "jpeg", // image/jpeg + "jpg", // image/jpeg + "m4a", // audio/x-m4a + "m4v", // video/mp4 + "mp3", // audio/mp3 + "mp4", // video/mp4 + "mpeg", // video/mpeg + "mpg", // video/mpeg + "oga", // audio/ogg + "ogg", // audio/ogg + "ogm", // video/ogg + "ogv", // video/ogg + "opus", // audio/ogg + "pjp", // image/jpeg + "pjpeg", // image/jpeg + "png", // image/png + "shtm", // text/html + "shtml", // text/html + "svg", // image/svg+xml + "svgz", // image/svg+xml + "text", // text/plain + "tif", // image/tiff + "tiff", // image/tiff + "txt", // text/plain + "wav", // audio/wav + "weba", // audio/webm + "webm", // video/webm + "webp", // image/webp + "xbm" // image/x-xbitmap + )); + + private static final Set<String> PERMITTED_MIME_TYPES = + Collections.unmodifiableSet(CollectionUtil.newHashSet( + "audio/flac", + "audio/mp3", + "audio/ogg", + "audio/wav", + "audio/webm", + "audio/x-m4a", + "image/bmp", + "image/gif", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/tiff", + "image/webp", + "image/x-icon", + "image/x-ms-bmp", + "image/x-xbitmap", + "text/comma-separated-values", + "text/css", + "text/csv", + "text/html", + "text/plain", + "video/mp4", + "video/mpeg", + "video/ogg", + "video/webm" + )); + // clang-format on + + private static final TaskRunner TASK_RUNNER = + PostTask.createSequencedTaskRunner(TaskTraits.USER_BLOCKING); + + /** Delegate class that provides embedder-specific functionality. */ + public interface WebShareDelegate { + /** + * @return true if sharing is currently possible. + */ + public boolean canShare(); + + /** + * Overridden by the embedder to execute the share. + * @param params the share data. + */ + public void share(ShareParams params); + } + + public ShareServiceImpl(@Nullable WebContents webContents, WebShareDelegate delegate) { + mWindow = webContents.getTopLevelNativeWindow(); + mDelegate = delegate; + } + + @Override + public void close() {} + + @Override + public void onConnectionError(MojoException e) {} + + @Override + public void share(String title, String text, Url url, final SharedFile[] files, + final ShareResponse callback) { + RecordHistogram.recordEnumeratedHistogram( + "WebShare.ApiCount", WEBSHARE_METHOD_SHARE, WEBSHARE_METHOD_COUNT); + + if (!mDelegate.canShare()) { + RecordHistogram.recordEnumeratedHistogram("WebShare.ShareOutcome", + WEBSHARE_OUTCOME_UNKNOWN_FAILURE, WEBSHARE_OUTCOME_COUNT); + callback.call(ShareError.INTERNAL_ERROR); + return; + } + + ShareParams.TargetChosenCallback innerCallback = new ShareParams.TargetChosenCallback() { + @Override + public void onTargetChosen(ComponentName chosenComponent) { + RecordHistogram.recordEnumeratedHistogram( + "WebShare.ShareOutcome", WEBSHARE_OUTCOME_SUCCESS, WEBSHARE_OUTCOME_COUNT); + callback.call(ShareError.OK); + } + + @Override + public void onCancel() { + RecordHistogram.recordEnumeratedHistogram( + "WebShare.ShareOutcome", WEBSHARE_OUTCOME_CANCELED, WEBSHARE_OUTCOME_COUNT); + callback.call(ShareError.CANCELED); + } + }; + + final ShareParams.Builder paramsBuilder = new ShareParams.Builder(mWindow, title, url.url) + .setText(text) + .setCallback(innerCallback); + if (files == null || files.length == 0) { + mDelegate.share(paramsBuilder.build()); + return; + } + + if (files.length > MAX_SHARED_FILE_COUNT) { + callback.call(ShareError.PERMISSION_DENIED); + return; + } + + for (SharedFile file : files) { + if (isDangerousFilename(file.name) || isDangerousMimeType(file.blob.contentType)) { + Log.i(TAG, + "Cannot share potentially dangerous \"" + file.blob.contentType + + "\" file \"" + file.name + "\"."); + callback.call(ShareError.PERMISSION_DENIED); + return; + } + } + + new AsyncTask<Boolean>() { + @Override + protected void onPostExecute(Boolean result) { + if (result.equals(Boolean.FALSE)) { + callback.call(ShareError.INTERNAL_ERROR); + } + } + + @Override + protected Boolean doInBackground() { + ArrayList<Uri> fileUris = new ArrayList<>(files.length); + ArrayList<BlobReceiver> blobReceivers = new ArrayList<>(files.length); + try { + File sharePath = ShareImageFileUtils.getSharedFilesDirectory(); + + if (!sharePath.exists() && !sharePath.mkdir()) { + throw new IOException("Failed to create directory for shared file."); + } + + for (int index = 0; index < files.length; ++index) { + File tempFile = File.createTempFile("share", + "." + FileUtils.getExtension(files[index].name), sharePath); + fileUris.add(ContentUriUtils.getContentUriFromFile(tempFile)); + blobReceivers.add(new BlobReceiver( + new FileOutputStream(tempFile), MAX_SHARED_FILE_BYTES)); + } + + } catch (IOException ie) { + Log.w(TAG, "Error creating shared file", ie); + return false; + } + + paramsBuilder.setFileContentType(SharedFileCollator.commonMimeType(files)); + paramsBuilder.setFileUris(fileUris); + SharedFileCollator collator = new SharedFileCollator(files.length, success -> { + if (success) { + mDelegate.share(paramsBuilder.build()); + } else { + callback.call(ShareError.INTERNAL_ERROR); + } + }); + + for (int index = 0; index < files.length; ++index) { + blobReceivers.get(index).start(files[index].blob.blob, collator); + } + return true; + } + }.executeOnTaskRunner(TASK_RUNNER); + } + + static boolean isDangerousFilename(String name) { + // Reject filenames without a permitted extension. + return name.indexOf('.') <= 0 + || !PERMITTED_EXTENSIONS.contains(FileUtils.getExtension(name)); + } + + static boolean isDangerousMimeType(String contentType) { + return !PERMITTED_MIME_TYPES.contains(contentType); + } + + @Nullable + private static Activity activityFromWebContents(@Nullable WebContents webContents) { + if (webContents == null) return null; + + WindowAndroid window = webContents.getTopLevelNativeWindow(); + if (window == null) return null; + + return window.getActivity().get(); + } +} diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java new file mode 100644 index 00000000000..4c9d3359870 --- /dev/null +++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java @@ -0,0 +1,84 @@ +// 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.webshare; + +import androidx.test.filters.SmallTest; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import org.chromium.base.test.BaseRobolectricTestRunner; + +/** + * Unit tests for {@link ShareServiceImpl}. + */ +@RunWith(BaseRobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ShareServiceImplTest { + @Test + @SmallTest + public void testExtensionFormatting() { + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("foo/bar.txt")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("foo\\bar\u03C0.txt")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("foo\\bar.tx\u03C0t")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("https://example.com/a/b.html")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("foo/bar.txt/")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("foobar.tx\\t")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("hello")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("hellotxt")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename(".txt")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("https://example.com/a/.txt")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("/.txt")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("..")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename(".hello.txt")); + } + + @Test + @SmallTest + public void testExecutable() { + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("application.apk")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("application.dex")); + Assert.assertTrue(ShareServiceImpl.isDangerousFilename("application.sh")); + } + + @Test + @SmallTest + public void testContent() { + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("diagram.svg")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("greeting.txt")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("movie.mpeg")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("photo.jpeg")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("recording.wav")); + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("statistics.csv")); + } + + @Test + @SmallTest + public void testCompound() { + Assert.assertFalse(ShareServiceImpl.isDangerousFilename("powerless.sh.txt")); + } + + @Test + @SmallTest + public void testUnsupportedMime() { + Assert.assertTrue(ShareServiceImpl.isDangerousMimeType("application/x-shockwave-flash")); + Assert.assertTrue(ShareServiceImpl.isDangerousMimeType("image/wmf")); + Assert.assertTrue(ShareServiceImpl.isDangerousMimeType("text/calendar")); + Assert.assertTrue(ShareServiceImpl.isDangerousMimeType("video/H264")); + } + + @Test + @SmallTest + public void testSupportedMime() { + Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("audio/wav")); + Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("image/jpeg")); + Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("image/svg+xml")); + Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("text/csv")); + Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("text/plain")); + Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("video/mpeg")); + } +} diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java new file mode 100644 index 00000000000..268903ba59e --- /dev/null +++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java @@ -0,0 +1,74 @@ +// 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.webshare; + +import org.chromium.base.Callback; +import org.chromium.base.task.PostTask; +import org.chromium.content_public.browser.UiThreadTaskTraits; +import org.chromium.mojo.system.MojoResult; +import org.chromium.webshare.mojom.SharedFile; + +/** + * Initiates the share dialog when all files have been received. + */ +public class SharedFileCollator implements Callback<Integer> { + private static final String WILDCARD = "*/*"; + + private int mPending; + private Callback<Boolean> mCallback; + + /** + * Constructs a SharedFileCollator. + * + * @param params the share request to issue if blobs are successfully received. + * @param callback the callback to call if any blob is not successfully received. + */ + public SharedFileCollator(int pendingFileCount, Callback<Boolean> callback) { + mPending = pendingFileCount; + mCallback = callback; + + assert mPending > 0; + } + + /** + * Call with a MojoResult each time a blob has been received. + * + * @param result a MojoResult indicating if a blob was successfully received. + */ + @Override + public void onResult(final Integer result) { + if (mCallback == null) return; + + if (result == MojoResult.OK && --mPending > 0) return; + + final Callback<Boolean> callback = mCallback; + mCallback = null; + + PostTask.postTask( + UiThreadTaskTraits.DEFAULT, () -> { callback.onResult(result == MojoResult.OK); }); + } + + /** + * If the files have a common type and subtype, returns type / subtype + * Otherwise if the files have a common type, returns type / * + * Otherwise returns * / * + * + * @param files an array of files being shared. + */ + public static String commonMimeType(SharedFile[] files) { + if (files == null || files.length == 0) return WILDCARD; + String[] common = files[0].blob.contentType.split("/"); + if (common.length != 2) return WILDCARD; + for (int index = 1; index < files.length; ++index) { + String[] current = files[index].blob.contentType.split("/"); + if (current.length != 2) return WILDCARD; + if (!current[0].equals(common[0])) return WILDCARD; + if (!current[1].equals(common[1])) { + common[1] = "*"; + } + } + return common[0] + "/" + common[1]; + } +} diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java new file mode 100644 index 00000000000..3d1e9a2ebf1 --- /dev/null +++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java @@ -0,0 +1,108 @@ +// 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.webshare; + +import androidx.test.filters.SmallTest; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.blink.mojom.SerializedBlob; +import org.chromium.webshare.mojom.SharedFile; + +/** + * Unit tests for {@link SharedFileCollator}. + */ +@RunWith(BaseRobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class SharedFileCollatorTest { + @Test + @SmallTest + public void testDissimilar() { + Assert.assertEquals("*/*", SharedFileCollator.commonMimeType(new SharedFile[0])); + Assert.assertEquals( + "*/*", SharedFileCollator.commonMimeType(createFiles("text/plain", "image/jpeg"))); + Assert.assertEquals("*/*", + SharedFileCollator.commonMimeType( + createFiles("video/mpeg", "video/ogg", "text/html"))); + } + + @Test + @SmallTest + public void testMalformed() { + Assert.assertEquals("*/*", SharedFileCollator.commonMimeType(createFiles("invalid"))); + Assert.assertEquals( + "*/*", SharedFileCollator.commonMimeType(createFiles("text/xml/svg", "text/xml"))); + Assert.assertEquals("*/*", + SharedFileCollator.commonMimeType(createFiles("image/webp", "image/webp/jpeg"))); + } + + @Test + @SmallTest + public void testApplication() { + Assert.assertEquals("application/*", + SharedFileCollator.commonMimeType( + createFiles("application/rtf", "application/x-bzip2"))); + } + + @Test + @SmallTest + public void testAudio() { + Assert.assertEquals("audio/*", + SharedFileCollator.commonMimeType(createFiles("audio/mp3", "audio/wav"))); + } + + @Test + @SmallTest + public void testImage() { + Assert.assertEquals( + "image/jpeg", SharedFileCollator.commonMimeType(createFiles("image/jpeg"))); + Assert.assertEquals("image/gif", + SharedFileCollator.commonMimeType( + createFiles("image/gif", "image/gif", "image/gif"))); + Assert.assertEquals("image/*", + SharedFileCollator.commonMimeType(createFiles("image/gif", "image/jpeg"))); + Assert.assertEquals("image/*", + SharedFileCollator.commonMimeType( + createFiles("image/gif", "image/gif", "image/jpeg"))); + } + + @Test + @SmallTest + public void testText() { + Assert.assertEquals("text/css", SharedFileCollator.commonMimeType(createFiles("text/css"))); + Assert.assertEquals( + "text/csv", SharedFileCollator.commonMimeType(createFiles("text/csv", "text/csv"))); + Assert.assertEquals("text/*", + SharedFileCollator.commonMimeType( + createFiles("text/csv", "text/html", "text/csv"))); + } + + @Test + @SmallTest + public void testVideo() { + Assert.assertEquals( + "video/webm", SharedFileCollator.commonMimeType(createFiles("video/webm"))); + Assert.assertEquals("video/*", + SharedFileCollator.commonMimeType(createFiles("video/mpeg", "video/webm"))); + Assert.assertEquals("video/*", + SharedFileCollator.commonMimeType( + createFiles("video/mpeg", "video/webm", "video/webm"))); + } + + private static SharedFile[] createFiles(String... mimeTypeList) { + SharedFile[] result = new SharedFile[mimeTypeList.length]; + for (int i = 0; i < mimeTypeList.length; ++i) { + SerializedBlob blob = new SerializedBlob(); + blob.contentType = mimeTypeList[i]; + result[i] = new SharedFile(); + result[i].blob = blob; + } + return result; + } +} |