tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }