Clipboard.java (11231B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package org.mozilla.gecko; 6 7 import android.content.ClipData; 8 import android.content.ClipDescription; 9 import android.content.ClipboardManager; 10 import android.content.ClipboardManager.OnPrimaryClipChangedListener; 11 import android.content.Context; 12 import android.content.res.AssetFileDescriptor; 13 import android.os.Build; 14 import android.os.PersistableBundle; 15 import android.text.TextUtils; 16 import android.util.Log; 17 import java.io.ByteArrayOutputStream; 18 import java.io.FileInputStream; 19 import java.io.IOException; 20 import java.io.InputStream; 21 import java.util.concurrent.atomic.AtomicLong; 22 import org.mozilla.gecko.annotation.WrapForJNI; 23 24 public final class Clipboard { 25 private static final String HTML_MIME = "text/html"; 26 private static final String PLAINTEXT_MIME = "text/plain"; 27 private static final String LOGTAG = "GeckoClipboard"; 28 private static final int DEFAULT_BUFFER_SIZE = 8192; 29 30 private static OnPrimaryClipChangedListener sClipboardChangedListener = null; 31 private static final AtomicLong sClipboardSequenceNumber = new AtomicLong(); 32 private static final AtomicLong sClipboardTimestamp = new AtomicLong(); 33 34 private Clipboard() {} 35 36 /** 37 * Get the text on the primary clip on Android clipboard 38 * 39 * @param context application context. 40 * @return a plain text string of clipboard data. 41 */ 42 public static String getText(final Context context) { 43 return getTextData(context, PLAINTEXT_MIME); 44 } 45 46 /** 47 * Get the text data on the primary clip on clipboard 48 * 49 * @param context application context 50 * @param mimeType the mime type we want. This supports text/html and text/plain only. If other 51 * type, we do nothing. 52 * @return a string into clipboard. 53 */ 54 @WrapForJNI(calledFrom = "gecko") 55 private static String getTextData(final Context context, final String mimeType) { 56 final ClipboardManager cm = 57 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 58 if (cm.hasPrimaryClip()) { 59 final ClipData clip = cm.getPrimaryClip(); 60 if (clip == null || clip.getItemCount() == 0) { 61 return null; 62 } 63 64 final ClipDescription description = clip.getDescription(); 65 if (HTML_MIME.equals(mimeType) 66 && description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) { 67 final CharSequence data = clip.getItemAt(0).getHtmlText(); 68 if (data == null) { 69 return null; 70 } 71 return data.toString(); 72 } 73 if (PLAINTEXT_MIME.equals(mimeType)) { 74 try { 75 return clip.getItemAt(0).coerceToText(context).toString(); 76 } catch (final SecurityException e) { 77 Log.e(LOGTAG, "Couldn't get clip data from clipboard", e); 78 } 79 } 80 } 81 return null; 82 } 83 84 /** 85 * Get the blob data on the primary clip on clipboard 86 * 87 * @param mimeType the mime type we want. 88 * @return a byte array into clipboard. 89 */ 90 @WrapForJNI(calledFrom = "gecko", exceptionMode = "nsresult") 91 private static byte[] getRawData(final String mimeType) { 92 final Context context = GeckoAppShell.getApplicationContext(); 93 final ClipboardManager cm = 94 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 95 if (cm.hasPrimaryClip()) { 96 final ClipData clip = cm.getPrimaryClip(); 97 if (clip == null || clip.getItemCount() == 0) { 98 return null; 99 } 100 101 final ClipDescription description = clip.getDescription(); 102 if (description.hasMimeType(mimeType)) { 103 return getRawDataFromClipData(context, clip); 104 } 105 } 106 return null; 107 } 108 109 private static byte[] getRawDataFromClipData(final Context context, final ClipData clipData) { 110 try (final AssetFileDescriptor descriptor = 111 context 112 .getContentResolver() 113 .openAssetFileDescriptor(clipData.getItemAt(0).getUri(), "r"); 114 final InputStream inputStream = new FileInputStream(descriptor.getFileDescriptor()); 115 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { 116 final byte[] data = new byte[DEFAULT_BUFFER_SIZE]; 117 int readed; 118 while ((readed = inputStream.read(data)) != -1) { 119 outputStream.write(data, 0, readed); 120 } 121 return outputStream.toByteArray(); 122 } catch (final IOException e) { 123 Log.e(LOGTAG, "Couldn't get clip data from clipboard due to I/O error", e); 124 } catch (final OutOfMemoryError e) { 125 Log.e(LOGTAG, "Couldn't get clip data from clipboard due to OOM", e); 126 } 127 return null; 128 } 129 130 /** 131 * Set plain text to clipboard 132 * 133 * @param context application context 134 * @param text a plain text to set to clipboard 135 * @return true if copy is successful. 136 */ 137 @WrapForJNI(calledFrom = "gecko") 138 public static boolean setText( 139 final Context context, final CharSequence text, final boolean isPrivateData) { 140 return setData(context, ClipData.newPlainText("text", text), isPrivateData); 141 } 142 143 /** 144 * Store HTML to clipboard 145 * 146 * @param context application context 147 * @param text a plain text to set to clipboard 148 * @param html a html text to set to clipboard 149 * @return true if copy is successful. 150 */ 151 @WrapForJNI(calledFrom = "gecko") 152 private static boolean setHTML( 153 final Context context, 154 final CharSequence text, 155 final String htmlText, 156 final boolean isPrivateData) { 157 return setData(context, ClipData.newHtmlText("html", text, htmlText), isPrivateData); 158 } 159 160 /** 161 * Store {@link android.content.ClipData} to clipboard 162 * 163 * @param context application context 164 * @param clipData a {@link android.content.ClipData} to set to clipboard 165 * @return true if copy is successful. 166 */ 167 private static boolean setData( 168 final Context context, final ClipData clipData, final boolean isPrivateData) { 169 // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager, 170 // which is a subclass of android.text.ClipboardManager. 171 final ClipboardManager cm = 172 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 173 if (isPrivateData) { 174 final PersistableBundle extras = new PersistableBundle(); 175 extras.putBoolean("android.content.extra.IS_SENSITIVE", true); 176 clipData.getDescription().setExtras(extras); 177 } 178 try { 179 cm.setPrimaryClip(clipData); 180 } catch (final NullPointerException e) { 181 // Bug 776223: This is a Samsung clipboard bug. setPrimaryClip() can throw 182 // a NullPointerException if Samsung's /data/clipboard directory is full. 183 // Fortunately, the text is still successfully copied to the clipboard. 184 } catch (final RuntimeException e) { 185 // If clipData is too large, TransactionTooLargeException occurs. 186 Log.e(LOGTAG, "Couldn't set clip data to clipboard", e); 187 return false; 188 } 189 updateSequenceNumber(context); 190 return true; 191 } 192 193 /** 194 * Check whether primary clipboard has given MIME type. 195 * 196 * @param context application context 197 * @param mimeType MIME type 198 * @return true if the clipboard is nonempty, false otherwise. 199 */ 200 @WrapForJNI(calledFrom = "gecko") 201 private static boolean hasData(final Context context, final String mimeType) { 202 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { 203 if (HTML_MIME.equals(mimeType) || PLAINTEXT_MIME.equals(mimeType)) { 204 return !TextUtils.isEmpty(getTextData(context, mimeType)); 205 } 206 } 207 208 // Calling getPrimaryClip causes a toast message from Android 12. 209 // https://developer.android.com/about/versions/12/behavior-changes-all#clipboard-access-notifications 210 211 final ClipboardManager cm = 212 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 213 214 if (!cm.hasPrimaryClip()) { 215 return false; 216 } 217 218 final ClipDescription description = cm.getPrimaryClipDescription(); 219 if (description == null) { 220 return false; 221 } 222 223 if (HTML_MIME.equals(mimeType)) { 224 return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML); 225 } 226 227 if (PLAINTEXT_MIME.equals(mimeType)) { 228 // We cannot check content in data at this time to avoid toast message. 229 return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML) 230 || description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN); 231 } 232 233 return description.hasMimeType(mimeType); 234 } 235 236 /** 237 * Deletes all data from the clipboard. 238 * 239 * @param context application context 240 */ 241 @WrapForJNI(calledFrom = "gecko") 242 private static void clear(final Context context) { 243 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { 244 setText(context, null, false); 245 return; 246 } 247 // Although we don't know more details of https://crbug.com/1203377, Blink doesn't use 248 // clearPrimaryClip on Android P since this may throw an exception, even if it is supported 249 // on Android P. 250 final ClipboardManager cm = 251 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 252 cm.clearPrimaryClip(); 253 } 254 255 /** 256 * Start monitor clipboard sequence number. 257 * 258 * @param context application context 259 */ 260 @WrapForJNI(calledFrom = "gecko") 261 private static void startTrackingClipboardData(final Context context) { 262 if (sClipboardChangedListener != null) { 263 return; 264 } 265 266 sClipboardChangedListener = 267 new OnPrimaryClipChangedListener() { 268 @Override 269 public void onPrimaryClipChanged() { 270 Clipboard.updateSequenceNumber(GeckoAppShell.getApplicationContext()); 271 } 272 }; 273 274 final ClipboardManager cm = 275 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 276 cm.addPrimaryClipChangedListener(sClipboardChangedListener); 277 } 278 279 /** Stop monitor clipboard sequence number. */ 280 @WrapForJNI(calledFrom = "gecko") 281 private static void stopTrackingClipboardData(final Context context) { 282 if (sClipboardChangedListener == null) { 283 return; 284 } 285 286 final ClipboardManager cm = 287 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 288 cm.removePrimaryClipChangedListener(sClipboardChangedListener); 289 sClipboardChangedListener = null; 290 } 291 292 private static long getClipboardTimestamp(final Context context) { 293 final ClipboardManager cm = 294 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 295 final ClipDescription description = cm.getPrimaryClipDescription(); 296 if (description == null) { 297 return 0; 298 } 299 return description.getTimestamp(); 300 } 301 302 public static void updateSequenceNumber(final Context context) { 303 final long timestamp = getClipboardTimestamp(context); 304 if (timestamp != 0) { 305 if (timestamp == sClipboardTimestamp.get()) { 306 return; 307 } 308 sClipboardTimestamp.set(timestamp); 309 } 310 311 sClipboardSequenceNumber.incrementAndGet(); 312 } 313 314 /** Get clipboard sequence number. */ 315 @WrapForJNI(calledFrom = "gecko") 316 private static long getSequenceNumber(final Context context) { 317 return sClipboardSequenceNumber.get(); 318 } 319 }