summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java284
1 files changed, 284 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java
new file mode 100644
index 0000000000..6ef2dd3073
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java
@@ -0,0 +1,284 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.ClipboardManager.OnPrimaryClipChangedListener;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicLong;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public final class Clipboard {
+ private static final String HTML_MIME = "text/html";
+ private static final String PLAINTEXT_MIME = "text/plain";
+ private static final String LOGTAG = "GeckoClipboard";
+ private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+ private static OnPrimaryClipChangedListener sClipboardChangedListener = null;
+ private static final AtomicLong sClipboardSequenceNumber = new AtomicLong();
+
+ private Clipboard() {}
+
+ /**
+ * Get the text on the primary clip on Android clipboard
+ *
+ * @param context application context.
+ * @return a plain text string of clipboard data.
+ */
+ public static String getText(final Context context) {
+ return getTextData(context, PLAINTEXT_MIME);
+ }
+
+ /**
+ * Get the text data on the primary clip on clipboard
+ *
+ * @param context application context
+ * @param mimeType the mime type we want. This supports text/html and text/plain only. If other
+ * type, we do nothing.
+ * @return a string into clipboard.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static String getTextData(final Context context, final String mimeType) {
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm.hasPrimaryClip()) {
+ final ClipData clip = cm.getPrimaryClip();
+ if (clip == null || clip.getItemCount() == 0) {
+ return null;
+ }
+
+ final ClipDescription description = clip.getDescription();
+ if (HTML_MIME.equals(mimeType)
+ && description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
+ final CharSequence data = clip.getItemAt(0).getHtmlText();
+ if (data == null) {
+ return null;
+ }
+ return data.toString();
+ }
+ if (PLAINTEXT_MIME.equals(mimeType)) {
+ try {
+ return clip.getItemAt(0).coerceToText(context).toString();
+ } catch (final SecurityException e) {
+ Log.e(LOGTAG, "Couldn't get clip data from clipboard", e);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the blob data on the primary clip on clipboard
+ *
+ * @param mimeType the mime type we want.
+ * @return a byte array into clipboard.
+ */
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "nsresult")
+ private static byte[] getRawData(final String mimeType) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm.hasPrimaryClip()) {
+ final ClipData clip = cm.getPrimaryClip();
+ if (clip == null || clip.getItemCount() == 0) {
+ return null;
+ }
+
+ final ClipDescription description = clip.getDescription();
+ if (description.hasMimeType(mimeType)) {
+ return getRawDataFromClipData(context, clip);
+ }
+ }
+ return null;
+ }
+
+ private static byte[] getRawDataFromClipData(final Context context, final ClipData clipData) {
+ try (final AssetFileDescriptor descriptor =
+ context
+ .getContentResolver()
+ .openAssetFileDescriptor(clipData.getItemAt(0).getUri(), "r");
+ final InputStream inputStream = new FileInputStream(descriptor.getFileDescriptor());
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ final byte[] data = new byte[DEFAULT_BUFFER_SIZE];
+ int readed;
+ while ((readed = inputStream.read(data)) != -1) {
+ outputStream.write(data, 0, readed);
+ }
+ return outputStream.toByteArray();
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Couldn't get clip data from clipboard due to I/O error", e);
+ } catch (final OutOfMemoryError e) {
+ Log.e(LOGTAG, "Couldn't get clip data from clipboard due to OOM", e);
+ }
+ return null;
+ }
+
+ /**
+ * Set plain text to clipboard
+ *
+ * @param context application context
+ * @param text a plain text to set to clipboard
+ * @return true if copy is successful.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public static boolean setText(final Context context, final CharSequence text) {
+ return setData(context, ClipData.newPlainText("text", text));
+ }
+
+ /**
+ * Store HTML to clipboard
+ *
+ * @param context application context
+ * @param text a plain text to set to clipboard
+ * @param html a html text to set to clipboard
+ * @return true if copy is successful.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean setHTML(
+ final Context context, final CharSequence text, final String htmlText) {
+ return setData(context, ClipData.newHtmlText("html", text, htmlText));
+ }
+
+ /**
+ * Store {@link android.content.ClipData} to clipboard
+ *
+ * @param context application context
+ * @param clipData a {@link android.content.ClipData} to set to clipboard
+ * @return true if copy is successful.
+ */
+ private static boolean setData(final Context context, final ClipData clipData) {
+ // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
+ // which is a subclass of android.text.ClipboardManager.
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ try {
+ cm.setPrimaryClip(clipData);
+ } catch (final NullPointerException e) {
+ // Bug 776223: This is a Samsung clipboard bug. setPrimaryClip() can throw
+ // a NullPointerException if Samsung's /data/clipboard directory is full.
+ // Fortunately, the text is still successfully copied to the clipboard.
+ } catch (final RuntimeException e) {
+ // If clipData is too large, TransactionTooLargeException occurs.
+ Log.e(LOGTAG, "Couldn't set clip data to clipboard", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check whether primary clipboard has given MIME type.
+ *
+ * @param context application context
+ * @param mimeType MIME type
+ * @return true if the clipboard is nonempty, false otherwise.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean hasData(final Context context, final String mimeType) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+ if (HTML_MIME.equals(mimeType) || PLAINTEXT_MIME.equals(mimeType)) {
+ return !TextUtils.isEmpty(getTextData(context, mimeType));
+ }
+ }
+
+ // Calling getPrimaryClip causes a toast message from Android 12.
+ // https://developer.android.com/about/versions/12/behavior-changes-all#clipboard-access-notifications
+
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+
+ if (!cm.hasPrimaryClip()) {
+ return false;
+ }
+
+ final ClipDescription description = cm.getPrimaryClipDescription();
+ if (description == null) {
+ return false;
+ }
+
+ if (HTML_MIME.equals(mimeType)) {
+ return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
+ }
+
+ if (PLAINTEXT_MIME.equals(mimeType)) {
+ // We cannot check content in data at this time to avoid toast message.
+ return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)
+ || description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
+ }
+
+ return description.hasMimeType(mimeType);
+ }
+
+ /**
+ * Deletes all data from the clipboard.
+ *
+ * @param context application context
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void clear(final Context context) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+ setText(context, null);
+ return;
+ }
+ // Although we don't know more details of https://crbug.com/1203377, Blink doesn't use
+ // clearPrimaryClip on Android P since this may throw an exception, even if it is supported
+ // on Android P.
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.clearPrimaryClip();
+ }
+
+ /**
+ * Start monitor clipboard sequence number.
+ *
+ * @param context application context
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void startTrackingClipboardData(final Context context) {
+ if (sClipboardChangedListener != null) {
+ return;
+ }
+
+ sClipboardChangedListener =
+ new OnPrimaryClipChangedListener() {
+ @Override
+ public void onPrimaryClipChanged() {
+ Clipboard.sClipboardSequenceNumber.incrementAndGet();
+ }
+ };
+
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.addPrimaryClipChangedListener(sClipboardChangedListener);
+ }
+
+ /** Stop monitor clipboard sequence number. */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void stopTrackingClipboardData(final Context context) {
+ if (sClipboardChangedListener == null) {
+ return;
+ }
+
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.removePrimaryClipChangedListener(sClipboardChangedListener);
+ sClipboardChangedListener = null;
+ }
+
+ /** Get clipboard sequence number. */
+ @WrapForJNI(calledFrom = "gecko")
+ private static long getSequenceNumber(final Context context) {
+ return sClipboardSequenceNumber.get();
+ }
+}