| // Copyright 2013 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.ui.base; |
| |
| import android.content.ClipData; |
| import android.content.ClipboardManager; |
| import android.content.Context; |
| |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.JNINamespace; |
| import org.chromium.base.annotations.SuppressFBWarnings; |
| import org.chromium.base.metrics.RecordHistogram; |
| import org.chromium.base.metrics.RecordUserAction; |
| import org.chromium.ui.R; |
| import org.chromium.ui.widget.Toast; |
| |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.Arrays; |
| |
| /** |
| * Simple proxy that provides C++ code with an access pathway to the Android clipboard. |
| */ |
| @JNINamespace("ui") |
| public class Clipboard implements ClipboardManager.OnPrimaryClipChangedListener { |
| private static Clipboard sInstance; |
| |
| private static final String TAG = "Clipboard"; |
| |
| // Necessary for coercing clipboard contents to text if they require |
| // access to network resources, etceteras (e.g., URI in clipboard) |
| private final Context mContext; |
| |
| private final ClipboardManager mClipboardManager; |
| |
| // A message hasher that's used to hash clipboard contents so we can tell |
| // when a clipboard changes without storing the full contents. |
| private MessageDigest mMd5Hasher; |
| // The hash of the current clipboard. |
| // TODO(mpearson): unsuppress this warning once saving and restoring |
| // the hash from prefs is added. |
| @SuppressFBWarnings("URF_UNREAD_FIELD") |
| private byte[] mClipboardMd5; |
| // The time when the clipboard was last updated. Set to 0 if unknown. |
| private long mClipboardChangeTime; |
| |
| /** |
| * Get the singleton Clipboard instance (creating it if needed). |
| */ |
| @CalledByNative |
| public static Clipboard getInstance() { |
| if (sInstance == null) { |
| sInstance = new Clipboard(); |
| } |
| return sInstance; |
| } |
| |
| private Clipboard() { |
| mContext = ContextUtils.getApplicationContext(); |
| mClipboardManager = |
| (ClipboardManager) ContextUtils.getApplicationContext().getSystemService( |
| Context.CLIPBOARD_SERVICE); |
| mClipboardManager.addPrimaryClipChangedListener(this); |
| try { |
| mMd5Hasher = MessageDigest.getInstance("MD5"); |
| mClipboardMd5 = weakMd5Hash(); |
| } catch (NoSuchAlgorithmException e) { |
| Log.e(TAG, |
| "Unable to construct MD5 MessageDigest: %s; assume " |
| + "clipboard last update time is start of epoch.", |
| e); |
| mMd5Hasher = null; |
| mClipboardMd5 = new byte[] {}; |
| } |
| RecordHistogram.recordBooleanHistogram("Clipboard.ConstructedHasher", mMd5Hasher != null); |
| mClipboardChangeTime = 0; |
| } |
| |
| /** |
| * Emulates the behavior of the now-deprecated |
| * {@link android.text.ClipboardManager#getText()} by invoking |
| * {@link android.content.ClipData.Item#coerceToText(Context)} on the first |
| * item in the clipboard (if any) and returning the result as a string. |
| * <p> |
| * This is quite different than simply calling {@link Object#toString()} on |
| * the clip; consumers of this API should familiarize themselves with the |
| * process described in |
| * {@link android.content.ClipData.Item#coerceToText(Context)} before using |
| * this method. |
| * |
| * @return a string representation of the first item on the clipboard, if |
| * the clipboard currently has an item and coercion of the item into |
| * a string is possible; otherwise, <code>null</code> |
| */ |
| @SuppressWarnings("javadoc") |
| @CalledByNative |
| private String getCoercedText() { |
| // getPrimaryClip() has been observed to throw unexpected exceptions for some devices (see |
| // crbug.com/654802 and b/31501780) |
| try { |
| return mClipboardManager.getPrimaryClip() |
| .getItemAt(0) |
| .coerceToText(mContext) |
| .toString(); |
| } catch (Exception e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Gets the HTML text of top item on the primary clip on the Android clipboard. |
| * |
| * @return a Java string with the html text if any, or null if there is no html |
| * text or no entries on the primary clip. |
| */ |
| @CalledByNative |
| private String getHTMLText() { |
| // getPrimaryClip() has been observed to throw unexpected exceptions for some devices (see |
| // crbug/654802 and b/31501780) |
| try { |
| return mClipboardManager.getPrimaryClip().getItemAt(0).getHtmlText(); |
| } catch (Exception e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Gets the time the clipboard content last changed. |
| * |
| * This is calculated according to the device's clock. E.g., it continues |
| * increasing when the device is suspended. Likewise, it can be in the |
| * future if the user's clock updated after this information was recorded. |
| * |
| * @return a Java long recording the last changed time in milliseconds since |
| * epoch, or 0 if the time could not be determined. |
| */ |
| @CalledByNative |
| public long getClipboardContentChangeTimeInMillis() { |
| return mClipboardChangeTime; |
| } |
| |
| /** |
| * Emulates the behavior of the now-deprecated |
| * {@link android.text.ClipboardManager#setText(CharSequence)}, setting the |
| * clipboard's current primary clip to a plain-text clip that consists of |
| * the specified string. |
| * @param text will become the content of the clipboard's primary clip |
| */ |
| @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") |
| @CalledByNative |
| public void setText(final String text) { |
| setPrimaryClipNoException(ClipData.newPlainText("text", text)); |
| } |
| |
| /** |
| * Writes HTML to the clipboard, together with a plain-text representation |
| * of that very data. |
| * |
| * @param html The HTML content to be pasted to the clipboard. |
| * @param text Plain-text representation of the HTML content. |
| */ |
| @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") |
| @CalledByNative |
| private void setHTMLText(final String html, final String text) { |
| setPrimaryClipNoException(ClipData.newHtmlText("html", text, html)); |
| } |
| |
| /** |
| * Clears the Clipboard Primary clip. |
| * |
| */ |
| @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") |
| @CalledByNative |
| private void clear() { |
| setPrimaryClipNoException(ClipData.newPlainText(null, null)); |
| } |
| |
| private void setPrimaryClipNoException(ClipData clip) { |
| try { |
| mClipboardManager.setPrimaryClip(clip); |
| } catch (Exception ex) { |
| // Ignore any exceptions here as certain devices have bugs and will fail. |
| String text = mContext.getString(R.string.copy_to_clipboard_failure_message); |
| Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show(); |
| } |
| } |
| |
| /** |
| * Updates mClipboardMd5 and mClipboardChangeTime when the clipboard updates. |
| * |
| * Implements OnPrimaryClipChangedListener to listen for clipboard updates. |
| */ |
| @Override |
| public void onPrimaryClipChanged() { |
| if (mMd5Hasher == null) return; |
| RecordUserAction.record("MobileOmniboxClipboardChanged"); |
| mClipboardMd5 = weakMd5Hash(); |
| // Always update the clipboard change time even if the clipboard |
| // content hasn't changed. This is because if the user put something |
| // in the clipboard recently (even if it was not necessary because it |
| // was already there), that content should be considered recent. |
| mClipboardChangeTime = System.currentTimeMillis(); |
| } |
| |
| /** |
| * Returns a weak hash of getCoercedText(). |
| * |
| * @return a Java byte[] with the weak hash. |
| */ |
| private byte[] weakMd5Hash() { |
| if (getCoercedText() == null) { |
| return new byte[] {}; |
| } |
| // Compute a hash consisting of the first 4 bytes of the MD5 hash of |
| // getCoercedText(). This value is used to detect clipboard content |
| // change. Keeping only 4 bytes is a privacy requirement to introduce |
| // collision and allow deniability of having copied a given string. |
| return Arrays.copyOfRange(mMd5Hasher.digest(getCoercedText().getBytes()), 0, 4); |
| } |
| } |