diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 5b8e25ebb3b..a6e7021d339 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -15,6 +15,7 @@ import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.gfx.PanZoomController; import org.mozilla.gecko.health.BrowserHealthReporter; import org.mozilla.gecko.menu.GeckoMenu; +import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.FloatUtils; import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.HardwareUtils; @@ -510,7 +511,7 @@ abstract public class BrowserApp extends GeckoApp public boolean onContextItemSelected(MenuItem item) { switch(item.getItemId()) { case R.id.pasteandgo: { - String text = GeckoAppShell.getClipboardText(); + String text = Clipboard.getText(); if (!TextUtils.isEmpty(text)) { Tabs.getInstance().loadUrl(text); } @@ -521,7 +522,7 @@ abstract public class BrowserApp extends GeckoApp return true; } case R.id.paste: { - String text = GeckoAppShell.getClipboardText(); + String text = Clipboard.getText(); if (!TextUtils.isEmpty(text)) { showAwesomebar(AwesomeBar.Target.CURRENT_TAB, text); } @@ -549,7 +550,7 @@ abstract public class BrowserApp extends GeckoApp if (tab != null) { String url = tab.getURL(); if (url != null) { - GeckoAppShell.setClipboardText(url); + Clipboard.setText(url); } } return true; diff --git a/mobile/android/base/BrowserToolbar.java b/mobile/android/base/BrowserToolbar.java index 67f461d06e4..2644cfe1de6 100644 --- a/mobile/android/base/BrowserToolbar.java +++ b/mobile/android/base/BrowserToolbar.java @@ -11,6 +11,7 @@ import org.mozilla.gecko.gfx.ImmutableViewportMetrics; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.menu.MenuPopup; +import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.StringUtils; import org.mozilla.gecko.util.HardwareUtils; @@ -198,7 +199,7 @@ public class BrowserToolbar implements Tabs.OnTabsChangedListener, MenuInflater inflater = mActivity.getMenuInflater(); inflater.inflate(R.menu.titlebar_contextmenu, menu); - String clipboard = GeckoAppShell.getClipboardText(); + String clipboard = Clipboard.getText(); if (TextUtils.isEmpty(clipboard)) { menu.findItem(R.id.pasteandgo).setVisible(false); menu.findItem(R.id.paste).setVisible(false); diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 9b58794e1a1..e3d23e1a33b 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -20,7 +20,6 @@ import android.app.Activity; import android.app.ActivityManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; -import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -102,7 +101,6 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.StringTokenizer; import java.util.TreeMap; -import java.util.concurrent.SynchronousQueue; public class GeckoAppShell { @@ -1231,85 +1229,6 @@ public class GeckoAppShell return intent; } - /* On some devices, access to the clipboard service needs to happen - * on a thread with a looper, so this function requires a looper is - * present on the thread. */ - private static String getClipboardTextImpl() { - Context context = getContext(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - android.content.ClipboardManager cm = (android.content.ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE); - if (cm.hasPrimaryClip()) { - ClipData clip = cm.getPrimaryClip(); - if (clip != null) { - ClipData.Item item = clip.getItemAt(0); - return item.coerceToText(context).toString(); - } - } - } else { - android.text.ClipboardManager cm = (android.text.ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE); - if (cm.hasText()) { - return cm.getText().toString(); - } - } - return null; - } - - private static SynchronousQueue sClipboardQueue = new SynchronousQueue(); - - static String getClipboardText() { - // If we're on the UI thread or the background thread, we have a looper on the thread - // and can just call this directly. For any other threads, post the call to the - // background thread. - - if (ThreadUtils.isOnUiThread() || ThreadUtils.isOnBackgroundThread()) { - return getClipboardTextImpl(); - } - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - String text = getClipboardTextImpl(); - try { - sClipboardQueue.put(text != null ? text : ""); - } catch (InterruptedException ie) {} - } - }); - try { - return sClipboardQueue.take(); - } catch (InterruptedException ie) { - return ""; - } - } - - static void setClipboardText(String copiedText) { - // Copy an empty string instead of null to avoid clipboard crashes. - // AndroidBridge::EmptyClipboard() passes null to clear the clipboard's current contents. - final String text = (copiedText != null) ? copiedText : ""; - - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - Context context = getContext(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - android.content.ClipboardManager cm = (android.content.ClipboardManager) - context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Text", text); - try { - cm.setPrimaryClip(clip); - } catch (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. - } - } else { - android.text.ClipboardManager cm = (android.text.ClipboardManager) - context.getSystemService(Context.CLIPBOARD_SERVICE); - cm.setText(text); - } - } - }); - } - public static void setNotificationClient(NotificationClient client) { if (sNotificationClient == null) { sNotificationClient = client; diff --git a/mobile/android/base/GeckoApplication.java b/mobile/android/base/GeckoApplication.java index f16e473420b..b7c35df9ff5 100644 --- a/mobile/android/base/GeckoApplication.java +++ b/mobile/android/base/GeckoApplication.java @@ -7,6 +7,7 @@ package org.mozilla.gecko; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.mozglue.GeckoLoader; +import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.ThreadUtils; @@ -79,6 +80,7 @@ public class GeckoApplication extends Application { @Override public void onCreate() { HardwareUtils.init(getApplicationContext()); + Clipboard.init(getApplicationContext()); GeckoLoader.loadMozGlue(getApplicationContext()); super.onCreate(); } diff --git a/mobile/android/base/GeckoInputConnection.java b/mobile/android/base/GeckoInputConnection.java index 628ae37a997..2d20af657f5 100644 --- a/mobile/android/base/GeckoInputConnection.java +++ b/mobile/android/base/GeckoInputConnection.java @@ -6,6 +6,7 @@ package org.mozilla.gecko; import org.mozilla.gecko.gfx.InputConnectionHandler; +import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.ThreadUtils; @@ -271,10 +272,10 @@ class GeckoInputConnection // If selection is empty, we'll select everything if (selStart == selEnd) { // Fill the clipboard - GeckoAppShell.setClipboardText(editable.toString()); + Clipboard.setText(editable); editable.clear(); } else { - GeckoAppShell.setClipboardText( + Clipboard.setText( editable.toString().substring( Math.min(selStart, selEnd), Math.max(selStart, selEnd))); @@ -282,7 +283,7 @@ class GeckoInputConnection } break; case R.id.paste: - commitText(GeckoAppShell.getClipboardText(), 1); + commitText(Clipboard.getText(), 1); break; case R.id.copy: // Copy the current selection or the empty string if nothing is selected. @@ -290,7 +291,7 @@ class GeckoInputConnection editable.toString().substring( Math.min(selStart, selEnd), Math.max(selStart, selEnd)); - GeckoAppShell.setClipboardText(copiedText); + Clipboard.setText(copiedText); break; } return true; diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 7ae54cf077f..0cfac4edd39 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -27,6 +27,7 @@ MOZGLUE_PP_JAVA_FILES := \ UTIL_JAVA_FILES := \ util/ActivityResultHandler.java \ util/ActivityResultHandlerMap.java \ + util/Clipboard.java \ util/EventDispatcher.java \ util/FloatUtils.java \ util/GamepadUtils.java \ diff --git a/mobile/android/base/util/Clipboard.java b/mobile/android/base/util/Clipboard.java new file mode 100644 index 00000000000..521e54f8fc2 --- /dev/null +++ b/mobile/android/base/util/Clipboard.java @@ -0,0 +1,110 @@ +/* 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.util; + +import android.content.ClipData; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import java.util.concurrent.SynchronousQueue; + +public final class Clipboard { + private static Context mContext; + private final static String LOG_TAG = "Clipboard"; + private final static SynchronousQueue sClipboardQueue = new SynchronousQueue(); + + private Clipboard() { + } + + public static void init(Context c) { + if (mContext != null) { + Log.w(LOG_TAG, "Clipboard.init() called twice!"); + return; + } + mContext = c; + } + + public static String getText() { + // If we're on the UI thread or the background thread, we have a looper on the thread + // and can just call this directly. For any other threads, post the call to the + // background thread. + + if (ThreadUtils.isOnUiThread() || ThreadUtils.isOnBackgroundThread()) { + return getClipboardTextImpl(); + } + + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + String text = getClipboardTextImpl(); + try { + sClipboardQueue.put(text != null ? text : ""); + } catch (InterruptedException ie) {} + } + }); + try { + return sClipboardQueue.take(); + } catch (InterruptedException ie) { + return ""; + } + } + + public static void setText(final CharSequence text) { + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + @SuppressWarnings("deprecation") + public void run() { + if (Build.VERSION.SDK_INT >= 11) { + android.content.ClipboardManager cm = getClipboardManager11(mContext); + ClipData clip = ClipData.newPlainText("Text", text); + try { + cm.setPrimaryClip(clip); + } catch (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. + } + } else { + android.text.ClipboardManager cm = getClipboardManager(mContext); + cm.setText(text); + } + } + }); + } + + private static android.content.ClipboardManager getClipboardManager11(Context context) { + // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager, + // which is a subclass of android.text.ClipboardManager. + return (android.content.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); + } + + private static android.text.ClipboardManager getClipboardManager(Context context) { + return (android.text.ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); + } + + /* On some devices, access to the clipboard service needs to happen + * on a thread with a looper, so this function requires a looper is + * present on the thread. */ + @SuppressWarnings("deprecation") + private static String getClipboardTextImpl() { + if (Build.VERSION.SDK_INT >= 11) { + android.content.ClipboardManager cm = getClipboardManager11(mContext); + if (cm.hasPrimaryClip()) { + ClipData clip = cm.getPrimaryClip(); + if (clip != null) { + ClipData.Item item = clip.getItemAt(0); + return item.coerceToText(mContext).toString(); + } + } + } else { + android.text.ClipboardManager cm = getClipboardManager(mContext); + if (cm.hasText()) { + return cm.getText().toString(); + } + } + return null; + } +} diff --git a/widget/android/AndroidBridge.cpp b/widget/android/AndroidBridge.cpp index b4f7fbdcabf..375d5b58b7a 100644 --- a/widget/android/AndroidBridge.cpp +++ b/widget/android/AndroidBridge.cpp @@ -123,8 +123,6 @@ AndroidBridge::Init(JNIEnv *jEnv, jGetMimeTypeFromExtensions = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "getMimeTypeFromExtensions", "(Ljava/lang/String;)Ljava/lang/String;"); jGetExtensionFromMimeType = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "getExtensionFromMimeType", "(Ljava/lang/String;)Ljava/lang/String;"); jMoveTaskToBack = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "moveTaskToBack", "()V"); - jGetClipboardText = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "getClipboardText", "()Ljava/lang/String;"); - jSetClipboardText = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "setClipboardText", "(Ljava/lang/String;)V"); jShowAlertNotification = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "showAlertNotification", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); jShowFilePickerForExtensions = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "showFilePickerForExtensions", "(Ljava/lang/String;)Ljava/lang/String;"); jShowFilePickerForMimeType = (jmethodID) jEnv->GetStaticMethodID(jGeckoAppShellClass, "showFilePickerForMimeType", "(Ljava/lang/String;)Ljava/lang/String;"); @@ -242,6 +240,10 @@ AndroidBridge::Init(JNIEnv *jEnv, jGetContext = (jmethodID)jEnv->GetStaticMethodID(jGeckoAppShellClass, "getContext", "()Landroid/content/Context;"); + jClipboardClass = (jclass) jEnv->NewGlobalRef(jEnv->FindClass("org/mozilla/gecko/util/Clipboard")); + jClipboardGetText = (jmethodID) jEnv->GetStaticMethodID(jClipboardClass, "getText", "()Ljava/lang/String;"); + jClipboardSetText = (jmethodID) jEnv->GetStaticMethodID(jClipboardClass, "setText", "(Ljava/lang/CharSequence;)V"); + InitAndroidJavaWrappers(jEnv); // jEnv should NOT be cached here by anything -- the jEnv here @@ -632,8 +634,8 @@ AndroidBridge::GetClipboardText(nsAString& aText) AutoLocalJNIFrame jniFrame(env); jstring jstrType = static_cast( - env->CallStaticObjectMethod(mGeckoAppShellClass, - jGetClipboardText)); + env->CallStaticObjectMethod(jClipboardClass, + jClipboardGetText)); if (jniFrame.CheckForException() || !jstrType) return false; @@ -653,7 +655,7 @@ AndroidBridge::SetClipboardText(const nsAString& aText) AutoLocalJNIFrame jniFrame(env); jstring jstr = NewJavaString(&jniFrame, aText); - env->CallStaticVoidMethod(mGeckoAppShellClass, jSetClipboardText, jstr); + env->CallStaticVoidMethod(jClipboardClass, jClipboardSetText, jstr); } bool @@ -667,8 +669,8 @@ AndroidBridge::ClipboardHasText() AutoLocalJNIFrame jniFrame(env); jstring jstrType = static_cast( - env->CallStaticObjectMethod(mGeckoAppShellClass, - jGetClipboardText)); + env->CallStaticObjectMethod(jClipboardClass, + jClipboardGetText)); if (jniFrame.CheckForException() || !jstrType) return false; @@ -685,7 +687,7 @@ AndroidBridge::EmptyClipboard() return; AutoLocalJNIFrame jniFrame(env, 0); - env->CallStaticVoidMethod(mGeckoAppShellClass, jSetClipboardText, nullptr); + env->CallStaticVoidMethod(jClipboardClass, jClipboardSetText, nullptr); } void diff --git a/widget/android/AndroidBridge.h b/widget/android/AndroidBridge.h index 82dfc98d355..213147930c9 100644 --- a/widget/android/AndroidBridge.h +++ b/widget/android/AndroidBridge.h @@ -473,8 +473,6 @@ protected: jmethodID jGetMimeTypeFromExtensions; jmethodID jGetExtensionFromMimeType; jmethodID jMoveTaskToBack; - jmethodID jGetClipboardText; - jmethodID jSetClipboardText; jmethodID jShowAlertNotification; jmethodID jShowFilePickerForExtensions; jmethodID jShowFilePickerForMimeType; @@ -562,6 +560,10 @@ protected: jmethodID jRequestContentRepaint; jmethodID jPostDelayedCallback; + jclass jClipboardClass; + jmethodID jClipboardGetText; + jmethodID jClipboardSetText; + // some convinient types to have around jclass jStringClass;