summaryrefslogtreecommitdiff
path: root/chromium/components/browser_ui/share
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/components/browser_ui/share')
-rw-r--r--chromium/components/browser_ui/share/DEPS7
-rw-r--r--chromium/components/browser_ui/share/OWNERS5
-rw-r--r--chromium/components/browser_ui/share/android/BUILD.gn53
-rw-r--r--chromium/components/browser_ui/share/android/java/res/layout/share_dialog_item.xml29
-rw-r--r--chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareDialogAdapter.java52
-rw-r--r--chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareHelper.java394
-rw-r--r--chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java449
-rw-r--r--chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtilsTest.java348
-rw-r--r--chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareParams.java246
9 files changed, 1583 insertions, 0 deletions
diff --git a/chromium/components/browser_ui/share/DEPS b/chromium/components/browser_ui/share/DEPS
new file mode 100644
index 00000000000..876f1347c5f
--- /dev/null
+++ b/chromium/components/browser_ui/share/DEPS
@@ -0,0 +1,7 @@
+include_rules = [
+ "+chrome/android/java/src/org/chromium/chrome/browser/FileProviderHelper.java",
+ "+components/dom_distiller/core/android",
+ "+content/public/android",
+ "+content/public/test/android/javatests",
+ "+ui/android",
+]
diff --git a/chromium/components/browser_ui/share/OWNERS b/chromium/components/browser_ui/share/OWNERS
new file mode 100644
index 00000000000..109f4d68517
--- /dev/null
+++ b/chromium/components/browser_ui/share/OWNERS
@@ -0,0 +1,5 @@
+dtrainor@chromium.org
+
+file://components/send_tab_to_self/OWNERS
+
+# COMPONENT: UI>Browser>Sharing
diff --git a/chromium/components/browser_ui/share/android/BUILD.gn b/chromium/components/browser_ui/share/android/BUILD.gn
new file mode 100644
index 00000000000..6c370e7cba3
--- /dev/null
+++ b/chromium/components/browser_ui/share/android/BUILD.gn
@@ -0,0 +1,53 @@
+# 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/share/ShareDialogAdapter.java",
+ "java/src/org/chromium/components/browser_ui/share/ShareHelper.java",
+ "java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java",
+ "java/src/org/chromium/components/browser_ui/share/ShareParams.java",
+ ]
+ deps = [
+ ":java_resources",
+ "//base:base_java",
+ "//components/browser_ui/util/android:java",
+ "//components/dom_distiller/core/android:dom_distiller_core_java",
+ "//content/public/android:content_java",
+ "//third_party/android_deps:androidx_annotation_annotation_java",
+ "//third_party/android_deps:androidx_appcompat_appcompat_java",
+ "//third_party/android_deps:androidx_core_core_java",
+ "//ui/android:ui_java",
+ ]
+}
+
+android_resources("java_resources") {
+ custom_package = "org.chromium.components.browser_ui.share"
+ sources = [ "java/res/layout/share_dialog_item.xml" ]
+ deps = [
+ "//components/browser_ui/strings/android:browser_ui_strings_grd",
+ "//components/browser_ui/styles/android:java_resources",
+ ]
+}
+
+android_library("javatests") {
+ testonly = true
+
+ sources = [ "java/src/org/chromium/components/browser_ui/share/ShareImageFileUtilsTest.java" ]
+ deps = [
+ ":java",
+ "//base:base_java",
+ "//base:base_java_test_support",
+ "//chrome/android:chrome_java",
+ "//content/public/test/android:content_java_test_support",
+ "//third_party/android_deps:androidx_appcompat_appcompat_java",
+ "//third_party/android_deps:androidx_core_core_java",
+ "//third_party/hamcrest:hamcrest_java",
+ "//third_party/junit",
+ "//ui/android:ui_java",
+ "//ui/android:ui_java_test_support",
+ ]
+}
diff --git a/chromium/components/browser_ui/share/android/java/res/layout/share_dialog_item.xml b/chromium/components/browser_ui/share/android/java/res/layout/share_dialog_item.xml
new file mode 100644
index 00000000000..7a716c41522
--- /dev/null
+++ b/chromium/components/browser_ui/share/android/java/res/layout/share_dialog_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 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. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/selectableItemBackground"
+ android:gravity="center_vertical"
+ android:orientation="horizontal" >
+ <ImageView android:id="@+id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="12dp"
+ android:padding="4dp"
+ android:scaleType="fitCenter"
+ tools:ignore="ContentDescription" />
+ <TextView android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="12dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+</LinearLayout> \ No newline at end of file
diff --git a/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareDialogAdapter.java b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareDialogAdapter.java
new file mode 100644
index 00000000000..811dfdd3d81
--- /dev/null
+++ b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareDialogAdapter.java
@@ -0,0 +1,52 @@
+// Copyright 2014 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.share;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * Adapter that provides the list of activities via which a web page can be shared.
+ */
+class ShareDialogAdapter extends ArrayAdapter<ResolveInfo> {
+ private final LayoutInflater mInflater;
+ private final PackageManager mManager;
+
+ /**
+ * @param context Context used to for layout inflation.
+ * @param manager PackageManager used to query for activity information.
+ * @param objects The list of possible share intents.
+ */
+ public ShareDialogAdapter(Context context, PackageManager manager, List<ResolveInfo> objects) {
+ super(context, R.layout.share_dialog_item, objects);
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mManager = manager;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+ if (convertView == null) {
+ view = mInflater.inflate(R.layout.share_dialog_item, parent, false);
+ } else {
+ view = convertView;
+ }
+ TextView text = (TextView) view.findViewById(R.id.text);
+ ImageView icon = (ImageView) view.findViewById(R.id.icon);
+
+ text.setText(getItem(position).loadLabel(mManager));
+ icon.setImageDrawable(ShareHelper.loadIconForResolveInfo(getItem(position), mManager));
+ return view;
+ }
+}
diff --git a/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareHelper.java b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareHelper.java
new file mode 100644
index 00000000000..993e5d7c165
--- /dev/null
+++ b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareHelper.java
@@ -0,0 +1,394 @@
+// 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.
+
+package org.chromium.components.browser_ui.share;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ClipData;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AlertDialog;
+
+import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.PackageManagerUtils;
+import org.chromium.components.browser_ui.share.ShareParams.TargetChosenCallback;
+import org.chromium.ui.UiUtils;
+import org.chromium.ui.base.WindowAndroid;
+import org.chromium.ui.base.WindowAndroid.IntentCallback;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A helper class that helps to start an intent to share titles and URLs.
+ */
+public class ShareHelper {
+ /** Interface that receives intents for testing (to fake out actually sending them). */
+ public interface FakeIntentReceiver {
+ /** Sets the intent to send back in the broadcast. */
+ public void setIntentToSendBack(Intent intent);
+
+ /** Called when a custom chooser dialog is shown. */
+ public void onCustomChooserShown(AlertDialog dialog);
+
+ /**
+ * Simulates firing the given intent, without actually doing so.
+ *
+ * @param context The context that will receive broadcasts from the simulated activity.
+ * @param intent The intent to send to the system.
+ */
+ public void fireIntent(Context context, Intent intent);
+ }
+
+ /** The task ID of the activity that triggered the share action. */
+ public static final String EXTRA_TASK_ID = "org.chromium.chrome.extra.TASK_ID";
+
+ private static final String EXTRA_SHARE_SCREENSHOT_AS_STREAM = "share_screenshot_as_stream";
+
+ /** Force the use of a Chrome-specific intent chooser, not the system chooser. */
+ private static boolean sForceCustomChooserForTesting;
+
+ /** If non-null, will be used instead of the real activity. */
+ private static FakeIntentReceiver sFakeIntentReceiverForTesting;
+
+ protected ShareHelper() {}
+
+ /**
+ * Fire the intent to share content with the target app.
+ *
+ * @param window The current window.
+ * @param intent The intent to fire.
+ * @param callback The callback to be triggered when the calling activity has finished. This
+ * allows the target app to identify Chrome as the source.
+ */
+ protected static void fireIntent(
+ WindowAndroid window, Intent intent, @Nullable IntentCallback callback) {
+ if (sFakeIntentReceiverForTesting != null) {
+ sFakeIntentReceiverForTesting.fireIntent(ContextUtils.getApplicationContext(), intent);
+ } else if (callback != null) {
+ window.showIntent(intent, callback, null);
+ } else {
+ // TODO(tedchoc): Allow startActivity w/o intent via Window.
+ Activity activity = window.getActivity().get();
+ activity.startActivity(intent);
+ }
+ }
+
+ /**
+ * Force the use of a Chrome-specific intent chooser, not the system chooser.
+ *
+ * This emulates the behavior on pre Lollipop-MR1 systems, where the system chooser is not
+ * available.
+ */
+ public static void setForceCustomChooserForTesting(boolean enabled) {
+ sForceCustomChooserForTesting = enabled;
+ }
+
+ /**
+ * Uses a FakeIntentReceiver instead of actually sending intents to the system.
+ *
+ * @param receiver The object to send intents to. If null, resets back to the default behavior
+ * (really send intents).
+ */
+ public static void setFakeIntentReceiverForTesting(FakeIntentReceiver receiver) {
+ sFakeIntentReceiverForTesting = receiver;
+ }
+
+ /**
+ * Receiver to record the chosen component when sharing an Intent.
+ */
+ public static class TargetChosenReceiver extends BroadcastReceiver implements IntentCallback {
+ private static final String EXTRA_RECEIVER_TOKEN = "receiver_token";
+ private static final Object LOCK = new Object();
+
+ private static String sTargetChosenReceiveAction;
+ private static TargetChosenReceiver sLastRegisteredReceiver;
+
+ @Nullable
+ private TargetChosenCallback mCallback;
+
+ private TargetChosenReceiver(@Nullable TargetChosenCallback callback) {
+ mCallback = callback;
+ }
+
+ public static boolean isSupported() {
+ return !sForceCustomChooserForTesting
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
+ public static void sendChooserIntent(WindowAndroid window, Intent sharingIntent,
+ @Nullable TargetChosenCallback callback) {
+ final Context context = ContextUtils.getApplicationContext();
+ final String packageName = context.getPackageName();
+ synchronized (LOCK) {
+ if (sTargetChosenReceiveAction == null) {
+ sTargetChosenReceiveAction =
+ packageName + "/" + TargetChosenReceiver.class.getName() + "_ACTION";
+ }
+ if (sLastRegisteredReceiver != null) {
+ context.unregisterReceiver(sLastRegisteredReceiver);
+ // Must cancel the callback (to satisfy guarantee that exactly one method of
+ // TargetChosenCallback is called).
+ sLastRegisteredReceiver.cancel();
+ }
+ sLastRegisteredReceiver = new TargetChosenReceiver(callback);
+ context.registerReceiver(
+ sLastRegisteredReceiver, new IntentFilter(sTargetChosenReceiveAction));
+ }
+
+ Intent intent = new Intent(sTargetChosenReceiveAction);
+ intent.setPackage(packageName);
+ intent.putExtra(EXTRA_RECEIVER_TOKEN, sLastRegisteredReceiver.hashCode());
+ Activity activity = window.getActivity().get();
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(activity, 0, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
+ Intent chooserIntent = Intent.createChooser(sharingIntent,
+ context.getString(R.string.share_link_chooser_title),
+ pendingIntent.getIntentSender());
+ if (sFakeIntentReceiverForTesting != null) {
+ sFakeIntentReceiverForTesting.setIntentToSendBack(intent);
+ }
+ fireIntent(window, chooserIntent, sLastRegisteredReceiver);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized (LOCK) {
+ if (sLastRegisteredReceiver != this) return;
+ ContextUtils.getApplicationContext().unregisterReceiver(sLastRegisteredReceiver);
+ sLastRegisteredReceiver = null;
+ }
+ if (!intent.hasExtra(EXTRA_RECEIVER_TOKEN)
+ || intent.getIntExtra(EXTRA_RECEIVER_TOKEN, 0) != this.hashCode()) {
+ return;
+ }
+
+ ComponentName target = intent.getParcelableExtra(Intent.EXTRA_CHOSEN_COMPONENT);
+ if (mCallback != null) {
+ mCallback.onTargetChosen(target);
+ mCallback = null;
+ }
+ }
+
+ @Override
+ public void onIntentCompleted(WindowAndroid window, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_CANCELED) {
+ cancel();
+ }
+ }
+
+ private void cancel() {
+ if (mCallback != null) {
+ mCallback.onCancel();
+ mCallback = null;
+ }
+ }
+ }
+
+ /**
+ * Creates and shows a custom share intent picker dialog.
+ *
+ * @param params The container holding the share parameters.
+ */
+ static void showCompatShareDialog(final ShareParams params) {
+ Intent intent = getShareLinkAppCompatibilityIntent();
+ List<ResolveInfo> resolveInfoList = PackageManagerUtils.queryIntentActivities(intent, 0);
+ assert resolveInfoList.size() > 0;
+ if (resolveInfoList.size() == 0) return;
+
+ final Context context = params.getWindow().getContext().get();
+ final PackageManager manager = context.getPackageManager();
+ Collections.sort(resolveInfoList, new ResolveInfo.DisplayNameComparator(manager));
+
+ final ShareDialogAdapter adapter =
+ new ShareDialogAdapter(context, manager, resolveInfoList);
+ AlertDialog.Builder builder = new UiUtils.CompatibleAlertDialogBuilder(
+ context, R.style.Theme_Chromium_AlertDialog);
+ builder.setTitle(context.getString(R.string.share_link_chooser_title));
+ builder.setAdapter(adapter, null);
+
+ final TargetChosenCallback callback = params.getCallback();
+ // Need a mutable object to record whether the callback has been fired.
+ final boolean[] callbackCalled = new boolean[1];
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+ dialog.getListView().setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ ResolveInfo info = adapter.getItem(position);
+ ActivityInfo ai = info.activityInfo;
+ ComponentName component =
+ new ComponentName(ai.applicationInfo.packageName, ai.name);
+
+ if (callback != null && !callbackCalled[0]) {
+ callback.onTargetChosen(component);
+ callbackCalled[0] = true;
+ }
+ shareDirectly(params, component);
+ dialog.dismiss();
+ }
+ });
+
+ dialog.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (callback != null && !callbackCalled[0]) {
+ callback.onCancel();
+ callbackCalled[0] = true;
+ }
+ }
+ });
+
+ if (sFakeIntentReceiverForTesting != null) {
+ sFakeIntentReceiverForTesting.onCustomChooserShown(dialog);
+ }
+ }
+
+ /**
+ * Shares the params using the system share sheet, or skipping the sheet and sharing directl if
+ * the target component is specified.
+ */
+ static void shareWithSystemSheet(ShareParams params) {
+ assert TargetChosenReceiver.isSupported();
+ TargetChosenReceiver.sendChooserIntent(
+ params.getWindow(), getShareLinkIntent(params), params.getCallback());
+ }
+
+ /**
+ * Shows a picker and allows the user to choose a share target.
+ *
+ * @param params The container holding the share parameters.
+ */
+ public static void shareWithUi(ShareParams params) {
+ if (TargetChosenReceiver.isSupported()) {
+ // On L+ open system share sheet.
+ shareWithSystemSheet(params);
+ } else {
+ // On K and below open custom share dialog.
+ showCompatShareDialog(params);
+ }
+ }
+
+ /**
+ * Share directly with the provied share target.
+ * @param params The container holding the share parameters.
+ * @param component The component to share to, bypassing any UI.
+ */
+ public static void shareDirectly(
+ @NonNull ShareParams params, @NonNull ComponentName component) {
+ Intent intent = getShareLinkIntent(params);
+ intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ intent.setComponent(component);
+ fireIntent(params.getWindow(), intent, null);
+ }
+
+ @VisibleForTesting
+ public static Intent getShareLinkIntent(ShareParams params) {
+ final boolean isFileShare = (params.getFileUris() != null);
+ final boolean isMultipleFileShare = isFileShare && (params.getFileUris().size() > 1);
+ final String action =
+ isMultipleFileShare ? Intent.ACTION_SEND_MULTIPLE : Intent.ACTION_SEND;
+ Intent intent = new Intent(action);
+ intent.addFlags(ApiCompatibilityUtils.getActivityNewDocumentFlag()
+ | Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ intent.putExtra(EXTRA_TASK_ID, params.getWindow().getActivity().get().getTaskId());
+
+ Uri screenshotUri = params.getScreenshotUri();
+ if (screenshotUri != null) {
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ // To give read access to an Intent target, we need to put |screenshotUri| in clipData
+ // because adding Intent.FLAG_GRANT_READ_URI_PERMISSION doesn't work for
+ // EXTRA_SHARE_SCREENSHOT_AS_STREAM.
+ intent.setClipData(ClipData.newRawUri("", screenshotUri));
+ intent.putExtra(EXTRA_SHARE_SCREENSHOT_AS_STREAM, screenshotUri);
+ }
+
+ if (params.getOfflineUri() != null) {
+ intent.putExtra(Intent.EXTRA_SUBJECT, params.getTitle());
+ intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.putExtra(Intent.EXTRA_STREAM, params.getOfflineUri());
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.setType("multipart/related");
+ } else {
+ if (!TextUtils.equals(params.getText(), params.getTitle())) {
+ intent.putExtra(Intent.EXTRA_SUBJECT, params.getTitle());
+ }
+ intent.putExtra(Intent.EXTRA_TEXT, params.getText());
+
+ if (isFileShare) {
+ intent.setType(params.getFileContentType());
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ if (isMultipleFileShare) {
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, params.getFileUris());
+ } else {
+ intent.putExtra(Intent.EXTRA_STREAM, params.getFileUris().get(0));
+ }
+ } else {
+ intent.setType("text/plain");
+ }
+ }
+
+ return intent;
+ }
+
+ /**
+ * Convenience method to create an Intent to retrieve all the apps support sharing text.
+ */
+ public static Intent getShareLinkAppCompatibilityIntent() {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.addFlags(ApiCompatibilityUtils.getActivityNewDocumentFlag());
+ intent.putExtra(Intent.EXTRA_SUBJECT, "");
+ intent.putExtra(Intent.EXTRA_TEXT, "");
+ intent.setType("text/plain");
+ return intent;
+ }
+
+ /**
+ * Loads the icon for the provided ResolveInfo.
+ * @param info The ResolveInfo to load the icon for.
+ * @param manager The package manager to use to load the icon.
+ */
+ public static Drawable loadIconForResolveInfo(ResolveInfo info, PackageManager manager) {
+ try {
+ final int iconRes = info.getIconResource();
+ if (iconRes != 0) {
+ Resources res = manager.getResourcesForApplication(info.activityInfo.packageName);
+ Drawable icon = ApiCompatibilityUtils.getDrawable(res, iconRes);
+ return icon;
+ }
+ } catch (NameNotFoundException | NotFoundException e) {
+ // Could not find the icon. loadIcon call below will return the default app icon.
+ }
+ return info.loadIcon(manager);
+ }
+}
diff --git a/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java
new file mode 100644
index 00000000000..246b0c7a9b3
--- /dev/null
+++ b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java
@@ -0,0 +1,449 @@
+// 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.
+
+package org.chromium.components.browser_ui.share;
+
+import android.annotation.TargetApi;
+import android.app.DownloadManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.base.ApplicationState;
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.BuildInfo;
+import org.chromium.base.Callback;
+import org.chromium.base.ContentUriUtils;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.FileUtils;
+import org.chromium.base.Log;
+import org.chromium.base.StreamUtil;
+import org.chromium.base.task.AsyncTask;
+import org.chromium.components.browser_ui.util.DownloadUtils;
+import org.chromium.content_public.browser.RenderWidgetHostView;
+import org.chromium.content_public.browser.WebContents;
+import org.chromium.ui.UiUtils;
+import org.chromium.ui.base.Clipboard;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Locale;
+
+/**
+ * Utility class for file operations for image data.
+ */
+public class ShareImageFileUtils {
+ private static final String TAG = "share";
+
+ /**
+ * Directory name for shared images.
+ *
+ * Named "screenshot" for historical reasons as we only initially shared screenshot images.
+ * TODO(crbug.com/1055886): consider changing the directory name.
+ */
+ private static final String SHARE_IMAGES_DIRECTORY_NAME = "screenshot";
+ private static final String JPEG_EXTENSION = ".jpg";
+ private static final String FILE_NUMBER_FORMAT = " (%d)";
+ private static final String MIME_TYPE = "image/JPEG";
+
+ /**
+ * Check if the file related to |fileUri| is in the |folder|.
+ *
+ * @param fileUri The {@link Uri} related to the file to be checked.
+ * @param folder The folder that may contain the |fileUrl|.
+ * @return Whether the |fileUri| is in the |folder|.
+ */
+ private static boolean isUriInDirectory(Uri fileUri, File folder) {
+ if (fileUri == null) return false;
+
+ Uri chromeUriPrefix = ContentUriUtils.getContentUriFromFile(folder);
+ if (chromeUriPrefix == null) return false;
+
+ return fileUri.toString().startsWith(chromeUriPrefix.toString());
+ }
+
+ /**
+ * Check if the system clipboard contains a Uri that comes from Chrome. If yes, return the file
+ * name from the Uri, otherwise return null.
+ *
+ * @return The file name if system clipboard contains a Uri from Chrome, otherwise return null.
+ */
+ private static String getClipboardCurrentFilepath() throws IOException {
+ Uri clipboardUri = Clipboard.getInstance().getImageUri();
+ if (isUriInDirectory(clipboardUri, getSharedFilesDirectory())) {
+ return clipboardUri.getPath();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the directory where temporary files are stored to be shared with external
+ * applications. These files are deleted on startup and when there are no longer any active
+ * Activities.
+ *
+ * @return The directory where shared files are stored.
+ */
+ public static File getSharedFilesDirectory() throws IOException {
+ File imagePath = UiUtils.getDirectoryForImageCapture(ContextUtils.getApplicationContext());
+ return new File(imagePath, SHARE_IMAGES_DIRECTORY_NAME);
+ }
+
+ /**
+ * Clears all shared image files.
+ */
+ public static void clearSharedImages() {
+ AsyncTask.SERIAL_EXECUTOR.execute(() -> {
+ try {
+ String clipboardFilepath = getClipboardCurrentFilepath();
+ FileUtils.recursivelyDeleteFile(getSharedFilesDirectory(), (filepath) -> {
+ return filepath == null || clipboardFilepath == null
+ || !filepath.endsWith(clipboardFilepath);
+ });
+ } catch (IOException ie) {
+ // Ignore exception.
+ }
+ });
+ }
+
+ /**
+ * Temporarily saves the given set of image bytes and provides that URI to a callback for
+ * sharing.
+ *
+ * @param context The context used to trigger the share action.
+ * @param imageData The image data to be shared in |fileExtension| format.
+ * @param fileExtension File extension which |imageData| encoded to.
+ * @param callback A provided callback function which will act on the generated URI.
+ */
+ public static void generateTemporaryUriFromData(final Context context, final byte[] imageData,
+ String fileExtension, Callback<Uri> callback) {
+ if (imageData.length == 0) {
+ Log.w(TAG, "Share failed -- Received image contains no data.");
+ return;
+ }
+ OnImageSaveListener listener = new OnImageSaveListener() {
+ @Override
+ public void onImageSaved(Uri uri, String displayName) {
+ callback.onResult(uri);
+ }
+ @Override
+ public void onImageSaveError(String displayName) {}
+ };
+
+ String fileName = String.valueOf(System.currentTimeMillis());
+ // Path is passed as a function because in some cases getting the path should be run on a
+ // background thread.
+ saveImage(fileName,
+ ()
+ -> { return ""; },
+ listener, (fos) -> { writeImageData(fos, imageData); }, true, fileExtension);
+ }
+
+ /**
+ * Saves bitmap to external storage directory.
+ *
+ * @param context The Context to use for determining download location.
+ * @param filename The filename without extension.
+ * @param bitmap The Bitmap to download.
+ * @param listener The OnImageSaveListener to notify the download results.
+ */
+ public static void saveBitmapToExternalStorage(
+ final Context context, String fileName, Bitmap bitmap, OnImageSaveListener listener) {
+ // Passing the path as a function so that it can be called on a background thread in
+ // |saveImage|.
+ saveImage(fileName,
+ ()
+ -> {
+ return context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getPath();
+ },
+ listener, (fos) -> { writeBitmap(fos, bitmap); }, false, JPEG_EXTENSION);
+ }
+
+ /**
+ * Interface for notifying image download result.
+ */
+ public interface OnImageSaveListener {
+ void onImageSaved(Uri uri, String displayName);
+ void onImageSaveError(String displayName);
+ }
+
+ /**
+ * Interface for writing image information to a output stream.
+ */
+ private interface FileOutputStreamWriter {
+ void write(FileOutputStream fos) throws IOException;
+ }
+
+ /**
+ * Interface for providing file path. This is used for passing a function for getting the path
+ * to other function to be called while on a background thread. Should be used on a background
+ * thread.
+ */
+ private interface FilePathProvider {
+ String getPath();
+ }
+
+ /**
+ * Saves image to the given file.
+ *
+ * @param fileName The File instance of a destination file.
+ * @param filePathProvider The FilePathProvider for obtaining destination file path.
+ * @param listener The OnImageSaveListener to notify the download results.
+ * @param writer The FileOutputStreamWriter that writes to given stream.
+ * @param isTemporary Indicates whether image should be save to a temporary file.
+ * @param fileExtension The file's extension.
+ */
+ private static void saveImage(String fileName, FilePathProvider filePathProvider,
+ OnImageSaveListener listener, FileOutputStreamWriter writer, boolean isTemporary,
+ String fileExtension) {
+ new AsyncTask<Uri>() {
+ @Override
+ protected Uri doInBackground() {
+ FileOutputStream fOut = null;
+ File destFile = null;
+ try {
+ destFile = createFile(
+ fileName, filePathProvider.getPath(), isTemporary, fileExtension);
+ if (destFile != null && destFile.exists()) {
+ fOut = new FileOutputStream(destFile);
+ writer.write(fOut);
+ } else {
+ Log.w(TAG,
+ "Share failed -- Unable to create or write to destination file.");
+ }
+ } catch (IOException ie) {
+ cancel(true);
+ } finally {
+ StreamUtil.closeQuietly(fOut);
+ }
+
+ Uri uri = null;
+ if (!isTemporary) {
+ if (BuildInfo.isAtLeastQ()) {
+ uri = addToMediaStore(destFile);
+ } else {
+ long downloadId = addCompletedDownload(destFile);
+ DownloadManager manager =
+ (DownloadManager) ContextUtils.getApplicationContext()
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+ return manager.getUriForDownloadedFile(downloadId);
+ }
+ } else {
+ uri = FileUtils.getUriForFile(destFile);
+ }
+ return uri;
+ }
+
+ @Override
+ protected void onCancelled() {
+ listener.onImageSaveError(fileName);
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ if (uri == null) {
+ listener.onImageSaveError(fileName);
+ return;
+ }
+
+ if (ApplicationStatus.getStateForApplication()
+ == ApplicationState.HAS_DESTROYED_ACTIVITIES) {
+ return;
+ }
+
+ listener.onImageSaved(uri, fileName);
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ /**
+ * Creates file with specified path, name and extension.
+ *
+ * @param filePath The file path a destination file.
+ * @param fileName The file name a destination file.
+ * @param isTemporary Indicates whether image should be save to a temporary file.
+ * @param fileExtension The file's extension.
+ *
+ * @return The new File object.
+ */
+ private static File createFile(String fileName, String filePath, boolean isTemporary,
+ String fileExtension) throws IOException {
+ File path;
+ if (filePath.isEmpty()) {
+ path = getSharedFilesDirectory();
+ } else {
+ path = new File(filePath);
+ }
+
+ File newFile = null;
+ if (path.exists() || path.mkdir()) {
+ if (isTemporary) {
+ newFile = File.createTempFile(fileName, fileExtension, path);
+ } else {
+ newFile = getNextAvailableFile(filePath, fileName, fileExtension);
+ }
+ }
+
+ return newFile;
+ }
+
+ /**
+ * Returns next available file for the given fileName.
+ *
+ * @param filePath The file path a destination file.
+ * @param fileName The file name a destination file.
+ * @param extension The extension a destination file.
+ *
+ * @return The new File object.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public static File getNextAvailableFile(String filePath, String fileName, String extension)
+ throws IOException {
+ File destFile = new File(filePath, fileName + extension);
+ int num = 0;
+ while (destFile.exists()) {
+ destFile = new File(filePath,
+ fileName + String.format(Locale.getDefault(), FILE_NUMBER_FORMAT, ++num)
+ + extension);
+ }
+ destFile.createNewFile();
+
+ return destFile;
+ }
+
+ /**
+ * Writes given bitmap to into the given fos.
+ *
+ * @param fos The FileOutputStream to write to.
+ * @param bitmap The Bitmap to write.
+ */
+ private static void writeBitmap(FileOutputStream fos, Bitmap bitmap) throws IOException {
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
+ }
+
+ /**
+ * Writes given data to into the given fos.
+ *
+ * @param fos The FileOutputStream to write to.
+ * @param byte[] The byte[] to write.
+ */
+ private static void writeImageData(FileOutputStream fos, final byte[] data) throws IOException {
+ fos.write(data);
+ }
+
+ /**
+ * This is a pass through to the {@link AndroidDownloadManager} function of the same name.
+ * @param file The File corresponding to the download.
+ * @return the download ID of this item as assigned by the download manager.
+ */
+ public static long addCompletedDownload(File file) {
+ String title = file.getName();
+ String path = file.getPath();
+ long length = file.length();
+
+ return DownloadUtils.addCompletedDownload(
+ title, title, MIME_TYPE, path, length, null, null);
+ }
+
+ @TargetApi(29)
+ public static Uri addToMediaStore(File file) {
+ assert BuildInfo.isAtLeastQ();
+
+ final ContentValues contentValues = new ContentValues();
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, file.getName());
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, MIME_TYPE);
+ contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
+
+ ContentResolver database = ContextUtils.getApplicationContext().getContentResolver();
+ Uri insertUri = database.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
+
+ InputStream input = null;
+ OutputStream output = null;
+ try {
+ input = new FileInputStream(file);
+ if (insertUri != null) {
+ output = database.openOutputStream(insertUri);
+ }
+ if (output != null) {
+ byte[] buffer = new byte[4096];
+ int byteCount = 0;
+ while ((byteCount = input.read(buffer)) != -1) {
+ output.write(buffer, 0, byteCount);
+ }
+ }
+ file.delete();
+ } catch (IOException e) {
+ } finally {
+ StreamUtil.closeQuietly(input);
+ StreamUtil.closeQuietly(output);
+ }
+ return insertUri;
+ }
+
+ /**
+ * Captures a screenshot for the provided web contents, persists it and notifies the file
+ * provider that the file is ready to be accessed by the client.
+ *
+ * The screenshot is compressed to JPEG before being written to the file.
+ *
+ * @param contents The WebContents instance for which to capture a screenshot.
+ * @param width The desired width of the resulting screenshot, or 0 for "auto."
+ * @param height The desired height of the resulting screenshot, or 0 for "auto."
+ * @param callback The callback that will be called once the screenshot is saved.
+ */
+ public static void captureScreenshotForContents(
+ WebContents contents, int width, int height, Callback<Uri> callback) {
+ RenderWidgetHostView rwhv = contents.getRenderWidgetHostView();
+ if (rwhv == null) {
+ callback.onResult(null);
+ return;
+ }
+ try {
+ String path = UiUtils.getDirectoryForImageCapture(ContextUtils.getApplicationContext())
+ + File.separator + SHARE_IMAGES_DIRECTORY_NAME;
+ rwhv.writeContentBitmapToDiskAsync(
+ width, height, path, new ExternallyVisibleUriCallback(callback));
+ } catch (IOException e) {
+ Log.e(TAG, "Error getting content bitmap: ", e);
+ callback.onResult(null);
+ }
+ }
+
+ private static class ExternallyVisibleUriCallback implements Callback<String> {
+ private Callback<Uri> mComposedCallback;
+ ExternallyVisibleUriCallback(Callback<Uri> cb) {
+ mComposedCallback = cb;
+ }
+
+ @Override
+ public void onResult(final String path) {
+ if (TextUtils.isEmpty(path)) {
+ mComposedCallback.onResult(null);
+ return;
+ }
+
+ new AsyncTask<Uri>() {
+ @Override
+ protected Uri doInBackground() {
+ return ContentUriUtils.getContentUriFromFile(new File(path));
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ mComposedCallback.onResult(uri);
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+}
diff --git a/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtilsTest.java b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtilsTest.java
new file mode 100644
index 00000000000..087d53556ea
--- /dev/null
+++ b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtilsTest.java
@@ -0,0 +1,348 @@
+// 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.
+
+package org.chromium.components.browser_ui.share;
+
+import android.annotation.TargetApi;
+import android.app.DownloadManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Looper;
+import android.provider.MediaStore;
+
+import androidx.test.filters.SmallTest;
+
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.BuildInfo;
+import org.chromium.base.Callback;
+import org.chromium.base.ContentUriUtils;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.task.AsyncTask;
+import org.chromium.base.test.BaseJUnit4ClassRunner;
+import org.chromium.base.test.util.CallbackHelper;
+import org.chromium.base.test.util.DisableIf;
+import org.chromium.base.test.util.DisabledTest;
+import org.chromium.chrome.browser.FileProviderHelper;
+import org.chromium.content_public.browser.test.util.Criteria;
+import org.chromium.content_public.browser.test.util.CriteriaHelper;
+import org.chromium.ui.base.Clipboard;
+import org.chromium.ui.test.util.DummyUiActivityTestCase;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Tests of {@link ShareImageFileUtils}.
+ */
+@RunWith(BaseJUnit4ClassRunner.class)
+public class ShareImageFileUtilsTest extends DummyUiActivityTestCase {
+ private static final long WAIT_TIMEOUT_SECONDS = 30L;
+ private static final byte[] TEST_IMAGE_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+ private static final String TEST_IMAGE_FILE_NAME = "chrome-test-bitmap";
+ private static final String TEST_GIF_IMAGE_FILE_EXTENSION = ".gif";
+ private static final String TEST_JPG_IMAGE_FILE_EXTENSION = ".jpg";
+ private static final String TEST_PNG_IMAGE_FILE_EXTENSION = ".png";
+
+ private class GenerateUriCallback extends CallbackHelper implements Callback<Uri> {
+ private Uri mImageUri;
+
+ public Uri getImageUri() {
+ return mImageUri;
+ }
+
+ @Override
+ public void onResult(Uri uri) {
+ mImageUri = uri;
+ notifyCalled();
+ }
+ }
+
+ private class AsyncTaskRunnableHelper extends CallbackHelper implements Runnable {
+ @Override
+ public void run() {
+ notifyCalled();
+ }
+ }
+
+ @Override
+ public void setUpTest() throws Exception {
+ super.setUpTest();
+ Looper.prepare();
+ ContentUriUtils.setFileProviderUtil(new FileProviderHelper());
+ }
+
+ @Override
+ public void tearDownTest() throws Exception {
+ Clipboard.getInstance().setText("");
+ clearSharedImages();
+ deleteAllTestImages();
+ super.tearDownTest();
+ }
+
+ private int fileCount(File file) {
+ if (file.isFile()) {
+ return 1;
+ }
+
+ int count = 0;
+ if (file.isDirectory()) {
+ for (File f : file.listFiles()) count += fileCount(f);
+ }
+ return count;
+ }
+
+ private boolean filepathExists(File file, String filepath) {
+ if (file.isFile() && filepath.endsWith(file.getName())) {
+ return true;
+ }
+
+ if (file.isDirectory()) {
+ for (File f : file.listFiles()) {
+ if (filepathExists(f, filepath)) return true;
+ }
+ }
+ return false;
+ }
+
+ private Uri generateAnImageToClipboard(String fileExtension) throws TimeoutException {
+ GenerateUriCallback imageCallback = new GenerateUriCallback();
+ ShareImageFileUtils.generateTemporaryUriFromData(
+ getActivity(), TEST_IMAGE_DATA, fileExtension, imageCallback);
+ imageCallback.waitForCallback(0, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ Clipboard.getInstance().setImageUri(imageCallback.getImageUri());
+ CriteriaHelper.pollInstrumentationThread(() -> {
+ Criteria.checkThat(Clipboard.getInstance().getImageUri(),
+ Matchers.is(imageCallback.getImageUri()));
+ });
+ return imageCallback.getImageUri();
+ }
+
+ private Uri generateAnImageToClipboard() throws TimeoutException {
+ return generateAnImageToClipboard(TEST_JPG_IMAGE_FILE_EXTENSION);
+ }
+
+ private void clearSharedImages() throws TimeoutException {
+ ShareImageFileUtils.clearSharedImages();
+
+ // ShareImageFileUtils::clearSharedImages uses AsyncTask.SERIAL_EXECUTOR to schedule a
+ // clearing the shared folder job, so schedule a new job and wait for the new job finished
+ // to make sure ShareImageFileUtils::clearSharedImages's clearing folder job finished.
+ waitForAsync();
+ }
+
+ private void waitForAsync() throws TimeoutException {
+ AsyncTaskRunnableHelper runnableHelper = new AsyncTaskRunnableHelper();
+ AsyncTask.SERIAL_EXECUTOR.execute(runnableHelper);
+ runnableHelper.waitForCallback(0, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(runnableHelper);
+ runnableHelper.waitForCallback(0, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+
+ private void deleteAllTestImages() throws TimeoutException {
+ AsyncTask.SERIAL_EXECUTOR.execute(() -> {
+ if (BuildInfo.isAtLeastQ()) {
+ deleteMediaStoreFiles();
+ }
+ deleteExternalStorageFiles();
+ });
+ waitForAsync();
+ }
+
+ @TargetApi(29)
+ private void deleteMediaStoreFiles() {
+ ContentResolver contentResolver = ContextUtils.getApplicationContext().getContentResolver();
+ Cursor cursor =
+ contentResolver.query(MediaStore.Downloads.EXTERNAL_CONTENT_URI, null, null, null);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID));
+ Uri uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id);
+ contentResolver.delete(uri, null, null);
+ }
+ }
+
+ public void deleteExternalStorageFiles() {
+ File externalStorageDir = ContextUtils.getApplicationContext().getExternalFilesDir(
+ Environment.DIRECTORY_DOWNLOADS);
+ String[] children = externalStorageDir.list();
+ for (int i = 0; i < children.length; i++) {
+ new File(externalStorageDir, children[i]).delete();
+ }
+ }
+
+ private int fileCountInShareDirectory() throws IOException {
+ return fileCount(ShareImageFileUtils.getSharedFilesDirectory());
+ }
+
+ private boolean fileExistsInShareDirectory(Uri fileUri) throws IOException {
+ return filepathExists(ShareImageFileUtils.getSharedFilesDirectory(), fileUri.getPath());
+ }
+
+ private Bitmap getTestBitmap() {
+ int size = 10;
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint();
+ paint.setColor(android.graphics.Color.GREEN);
+ canvas.drawRect(0F, 0F, (float) size, (float) size, paint);
+ return bitmap;
+ }
+
+ @Test
+ @SmallTest
+ public void clipboardUriDoNotClearTest() throws TimeoutException, IOException {
+ Uri clipboardUri = generateAnImageToClipboard(TEST_GIF_IMAGE_FILE_EXTENSION);
+ Assert.assertTrue(clipboardUri.getPath().endsWith(TEST_GIF_IMAGE_FILE_EXTENSION));
+ clipboardUri = generateAnImageToClipboard(TEST_JPG_IMAGE_FILE_EXTENSION);
+ Assert.assertTrue(clipboardUri.getPath().endsWith(TEST_JPG_IMAGE_FILE_EXTENSION));
+ clipboardUri = generateAnImageToClipboard(TEST_PNG_IMAGE_FILE_EXTENSION);
+ Assert.assertTrue(clipboardUri.getPath().endsWith(TEST_PNG_IMAGE_FILE_EXTENSION));
+ Assert.assertEquals(3, fileCountInShareDirectory());
+
+ clearSharedImages();
+ Assert.assertEquals(1, fileCountInShareDirectory());
+ Assert.assertTrue(fileExistsInShareDirectory(clipboardUri));
+ }
+
+ @Test
+ @SmallTest
+ public void clearEverythingIfNoClipboardImageTest() throws TimeoutException, IOException {
+ generateAnImageToClipboard();
+ generateAnImageToClipboard();
+ generateAnImageToClipboard();
+ Assert.assertEquals(3, fileCountInShareDirectory());
+
+ Clipboard.getInstance().setText("");
+ clearSharedImages();
+ Assert.assertEquals(0, fileCountInShareDirectory());
+ }
+
+ @Test
+ @SmallTest
+ @DisabledTest(message = "crbug.com/1056059")
+ public void testSaveBitmap() throws IOException, TimeoutException {
+ String fileName = TEST_IMAGE_FILE_NAME + "_save_bitmap";
+ ShareImageFileUtils.OnImageSaveListener listener =
+ new ShareImageFileUtils.OnImageSaveListener() {
+ @Override
+ public void onImageSaved(Uri uri, String displayName) {
+ Assert.assertNotNull(uri);
+ Assert.assertEquals(fileName, displayName);
+ AsyncTask.SERIAL_EXECUTOR.execute(() -> {
+ File file = new File(uri.getPath());
+ Assert.assertTrue(file.exists());
+ Assert.assertTrue(file.isFile());
+ });
+
+ // Wait for the above checks to complete.
+ try {
+ waitForAsync();
+ } catch (TimeoutException ex) {
+ }
+ }
+
+ @Override
+ public void onImageSaveError(String displayName) {
+ Assert.fail();
+ }
+ };
+ ShareImageFileUtils.saveBitmapToExternalStorage(
+ getActivity(), fileName, getTestBitmap(), listener);
+ waitForAsync();
+ }
+
+ @Test
+ @SmallTest
+ @DisableIf.Build(sdk_is_less_than = 29)
+ public void testSaveBitmapAndMediaStore() throws IOException, TimeoutException {
+ String fileName = TEST_IMAGE_FILE_NAME + "_mediastore";
+ ShareImageFileUtils.OnImageSaveListener listener =
+ new ShareImageFileUtils.OnImageSaveListener() {
+ @Override
+ public void onImageSaved(Uri uri, String displayName) {
+ Assert.assertNotNull(uri);
+ Assert.assertEquals(fileName, displayName);
+ AsyncTask.SERIAL_EXECUTOR.execute(() -> {
+ Cursor cursor = getActivity().getContentResolver().query(
+ uri, null, null, null, null);
+ Assert.assertNotNull(cursor);
+ Assert.assertTrue(cursor.moveToFirst());
+ Assert.assertEquals(fileName + TEST_JPG_IMAGE_FILE_EXTENSION,
+ cursor.getString(cursor.getColumnIndex(
+ MediaStore.MediaColumns.DISPLAY_NAME)));
+ });
+
+ // Wait for the above checks to complete.
+ try {
+ waitForAsync();
+ } catch (TimeoutException ex) {
+ }
+ }
+
+ @Override
+ public void onImageSaveError(String displayName) {
+ Assert.fail();
+ }
+ };
+ ShareImageFileUtils.saveBitmapToExternalStorage(
+ getActivity(), fileName, getTestBitmap(), listener);
+ waitForAsync();
+ }
+
+ @Test
+ @SmallTest
+ public void testGetNextAvailableFile() throws IOException {
+ String fileName = TEST_IMAGE_FILE_NAME + "_next_availble";
+ File externalStorageDir =
+ getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+ File imageFile = ShareImageFileUtils.getNextAvailableFile(
+ externalStorageDir.getPath(), fileName, TEST_JPG_IMAGE_FILE_EXTENSION);
+ Assert.assertTrue(imageFile.exists());
+
+ File imageFile2 = ShareImageFileUtils.getNextAvailableFile(
+ externalStorageDir.getPath(), fileName, TEST_JPG_IMAGE_FILE_EXTENSION);
+ Assert.assertTrue(imageFile2.exists());
+ Assert.assertNotEquals(imageFile.getPath(), imageFile2.getPath());
+ }
+
+ @Test
+ @SmallTest
+ @DisableIf.Build(sdk_is_greater_than = 28)
+ public void testAddCompletedDownload() throws IOException {
+ String filename =
+ TEST_IMAGE_FILE_NAME + "_add_completed_download" + TEST_JPG_IMAGE_FILE_EXTENSION;
+ File externalStorageDir =
+ getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+ File qrcodeFile = new File(externalStorageDir, filename);
+ Assert.assertTrue(qrcodeFile.createNewFile());
+
+ long downloadId = ShareImageFileUtils.addCompletedDownload(qrcodeFile);
+ Assert.assertNotEquals(0L, downloadId);
+
+ DownloadManager downloadManager =
+ (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
+ DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
+ Cursor c = downloadManager.query(query);
+
+ Assert.assertNotNull(c);
+ Assert.assertTrue(c.moveToFirst());
+ Assert.assertEquals(
+ filename, c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE)));
+ c.close();
+ }
+}
diff --git a/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareParams.java b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareParams.java
new file mode 100644
index 00000000000..f26655eb4a8
--- /dev/null
+++ b/chromium/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareParams.java
@@ -0,0 +1,246 @@
+// Copyright 2017 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.share;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils;
+import org.chromium.ui.base.WindowAndroid;
+
+import java.util.ArrayList;
+
+/**
+ * A container object for passing share parameters to {@link ShareHelper}.
+ */
+public class ShareParams {
+ /** The window that triggered the share action. */
+ private final WindowAndroid mWindow;
+
+ /** The title of the page to be shared. */
+ private final String mTitle;
+
+ /**
+ * The text to be shared. If both |text| and |url| are supplied, they are concatenated with a
+ * space.
+ */
+ private final String mText;
+
+ /** The URL of the page to be shared. */
+ private final String mUrl;
+
+ /** The common MIME type of the files to be shared. A wildcard if they have differing types. */
+ private final String mFileContentType;
+
+ /** The list of Uris of the files to be shared. */
+ private final ArrayList<Uri> mFileUris;
+
+ /** The Uri to the offline MHTML file to be shared. */
+ private final Uri mOfflineUri;
+
+ /** The Uri of the screenshot of the page to be shared. */
+ private final Uri mScreenshotUri;
+
+ /**
+ * Optional callback to be called when user makes a choice. Will not be called if receiving a
+ * response when the user makes a choice is not supported (on older Android versions).
+ */
+ private TargetChosenCallback mCallback;
+
+ private ShareParams(WindowAndroid window, String title, String text, String url,
+ @Nullable String fileContentType, @Nullable ArrayList<Uri> fileUris,
+ @Nullable Uri offlineUri, @Nullable Uri screenshotUri,
+ @Nullable TargetChosenCallback callback) {
+ mWindow = window;
+ mTitle = title;
+ mText = text;
+ mUrl = url;
+ mFileContentType = fileContentType;
+ mFileUris = fileUris;
+ mOfflineUri = offlineUri;
+ mScreenshotUri = screenshotUri;
+ mCallback = callback;
+ }
+
+ /**
+ * @return The window that triggered share.
+ */
+ public WindowAndroid getWindow() {
+ return mWindow;
+ }
+
+ /**
+ * @return The title of the page to be shared.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * @return The text to be shared.
+ */
+ public String getText() {
+ return mText;
+ }
+
+ /**
+ * @return The URL of the page to be shared.
+ */
+ public String getUrl() {
+ return mUrl;
+ }
+
+ /**
+ * @return The MIME type to the arbitrary files to be shared.
+ */
+ @Nullable
+ public String getFileContentType() {
+ return mFileContentType;
+ }
+
+ /**
+ * @return The Uri to the arbitrary files to be shared.
+ */
+ @Nullable
+ public ArrayList<Uri> getFileUris() {
+ return mFileUris;
+ }
+
+ /**
+ * @return The Uri to the offline MHTML file to be shared.
+ */
+ @Nullable
+ public Uri getOfflineUri() {
+ return mOfflineUri;
+ }
+
+ /**
+ * @return The Uri of the screenshot of the page to be shared.
+ */
+ @Nullable
+ public Uri getScreenshotUri() {
+ return mScreenshotUri;
+ }
+
+ /**
+ * @return The callback to be called when user makes a choice.
+ */
+ @Nullable
+ public TargetChosenCallback getCallback() {
+ return mCallback;
+ }
+
+ /**
+ * @param callback To be called when user makes a choice.
+ */
+ public void setCallback(@Nullable TargetChosenCallback callback) {
+ mCallback = callback;
+ }
+
+ /** The builder for {@link ShareParams} objects. */
+ public static class Builder {
+ private WindowAndroid mWindow;
+ private String mTitle;
+ private String mText;
+ private String mUrl;
+ private String mFileContentType;
+ private ArrayList<Uri> mFileUris;
+ private Uri mOfflineUri;
+ private Uri mScreenshotUri;
+ private TargetChosenCallback mCallback;
+
+ public Builder(@NonNull WindowAndroid window, @NonNull String title, @NonNull String url) {
+ mWindow = window;
+ mUrl = url;
+ mTitle = title;
+ }
+
+ /**
+ * Sets the text to be shared.
+ */
+ public Builder setText(@NonNull String text) {
+ mText = text;
+ return this;
+ }
+
+ /**
+ * Sets the MIME type of the arbitrary files to be shared.
+ */
+ public Builder setFileContentType(@NonNull String fileContentType) {
+ mFileContentType = fileContentType;
+ return this;
+ }
+
+ /**
+ * Sets the Uri of the arbitrary files to be shared.
+ */
+ public Builder setFileUris(@Nullable ArrayList<Uri> fileUris) {
+ mFileUris = fileUris;
+ return this;
+ }
+
+ /**
+ * Sets the Uri of the offline MHTML file to be shared.
+ */
+ public Builder setOfflineUri(@Nullable Uri offlineUri) {
+ mOfflineUri = offlineUri;
+ return this;
+ }
+
+ /**
+ * Sets the Uri of the screenshot of the page to be shared.
+ */
+ public Builder setScreenshotUri(@Nullable Uri screenshotUri) {
+ mScreenshotUri = screenshotUri;
+ return this;
+ }
+
+ /**
+ * Sets the callback to be called when user makes a choice.
+ */
+ public Builder setCallback(@Nullable TargetChosenCallback callback) {
+ mCallback = callback;
+ return this;
+ }
+
+ /** @return A fully constructed {@link ShareParams} object. */
+ public ShareParams build() {
+ if (!TextUtils.isEmpty(mUrl)) {
+ mUrl = DomDistillerUrlUtils.getOriginalUrlFromDistillerUrl(mUrl);
+ if (!TextUtils.isEmpty(mText)) {
+ // Concatenate text and URL with a space.
+ mText = mText + " " + mUrl;
+ } else {
+ mText = mUrl;
+ }
+ }
+ return new ShareParams(mWindow, mTitle, mText, mUrl, mFileContentType, mFileUris,
+ mOfflineUri, mScreenshotUri, mCallback);
+ }
+ }
+
+ /**
+ * Callback interface for when a target is chosen.
+ */
+ public static interface TargetChosenCallback {
+ /**
+ * Called when the user chooses a target in the share dialog.
+ *
+ * Note that if the user cancels the share dialog, this callback is never called.
+ */
+ public void onTargetChosen(ComponentName chosenComponent);
+
+ /**
+ * Called when the user cancels the share dialog.
+ *
+ * Guaranteed that either this, or onTargetChosen (but not both) will be called, eventually.
+ */
+ public void onCancel();
+ }
+}