Working with the Clipboard

Being able to copy and paste is something that mobile device users seem to want almost as much as their desktop brethren. Most of the time, we think of this as copying and pasting text, but one could copy and paste other things, such as Uri values pointing to more elaborate forms of content.

In this chapter, we will explore how to work with the modern clipboard APIs. Here, “modern” refers to android.content.ClipboardManager. Android 1.x and 2.x used android.text.ClipboardManager, which still exists in the Android SDK for backwards-compatibility reasons. However, most modern development should use android.content.ClipboardManager.

Prerequisites

Understanding this chapter requires that you have read the core chapters of this book.

Working with the Clipboard

ClipboardManager can be obtained via a call to getSystemService() on any handy Context.

The old Android 1.x/2.x API was dominated by three methods, all focused on plain text:

Those methods still exist, but they have been deprecated as of API Level 11.

Their replacements are:

Here, the “something” winds up being in the form of ClipData objects, which can hold:

  1. plain text
  2. a Uri (e.g., to a piece of music)
  3. an Intent

The Uri means that you can put anything on the clipboard that can be referenced by a Uri… and if there is nothing in Android that lets you reference some data via a Uri, you can invent your own content provider to handle that chore for you. Furthermore, a single ClipData can actually hold as many of these as you want, each represented as individual ClipData.Item objects. As such, the possibilities are endless.

There are static factory methods on ClipData, such as newUri(), that you can use to create your ClipData objects. In fact, that is what we use in the SystemServices/ClipMusic sample project and the MusicClipper activity.

MusicClipper has the classic two-big-button layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  >
  <Button android:id="@+id/pick"
    android:layout_width="match_parent" 
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:text="Pick"
    android:onClick="pickMusic"
  />
  <Button android:id="@+id/view"
    android:layout_width="match_parent" 
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:text="Play"
    android:onClick="playMusic"
  />
</LinearLayout>
(from SystemServices/ClipMusic/app/src/main/res/layout-land/main.xml)

The Music Clipper main screen
Figure 831: The Music Clipper main screen

In onCreate(), we get our hands on our ClipboardManager system service:

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    clipboard=(ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
  }
(from SystemServices/ClipMusic/app/src/main/java/com/commonsware/android/clip/music/MusicClipper.java)

Tapping the “Pick” button will let you pick a piece of music, courtesy of the pickMusic() method wired to that Button object:

  public void pickMusic(View v) {
    Intent i=new Intent(Intent.ACTION_GET_CONTENT);

    i.setType("audio/*");
    startActivityForResult(i, PICK_REQUEST);
  }
(from SystemServices/ClipMusic/app/src/main/java/com/commonsware/android/clip/music/MusicClipper.java)

Here, we tell Android to let us pick a piece of music from any available audio MIME type (audio/*). Fortunately, Android has an activity that lets us do that:

The XOOM tablets music track picker
Figure 832: The XOOM tablet’s music track picker

We get the result in onActivityResult(), since we used startActivityForResult() to pick the music. There, we package up the content:// Uri to the music into a ClipData object and put it on the clipboard:

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
                                  Intent data) {
    if (requestCode == PICK_REQUEST) {
      if (resultCode == RESULT_OK) {
        ClipData clip=
            ClipData.newUri(getContentResolver(), "Some music",
                            data.getData());

        try {
          clipboard.setPrimaryClip(clip);
        }
        catch (Exception e) {
          Log.e(getClass().getSimpleName(), "Exception clipping Uri", e);
          Toast.makeText(this, "Exception: " + e.getMessage(),
                         Toast.LENGTH_SHORT).show();
        }
      }
    }
  }
(from SystemServices/ClipMusic/app/src/main/java/com/commonsware/android/clip/music/MusicClipper.java)

Note that there is a significant bug in Android 4.3 that, until it is fixed, will require you to do a bit more error-handling with your clipboard operations. That is why we have our setPrimaryClip() call wrapped in a try/catch blog, even though setPrimaryClip() does not throw a checked exception. The rationale for this will be discussed later in this chapter.

The catch with rich data on the clipboard is that somebody has to know about the sort of information you are placing on the clipboard. Eventually, the Android development community will work out common practices in this area. Right now, though, you can certainly use it within your own application (e.g., clipping a note and pasting it into another folder).

Since putting ClipData onto the clipboard involves a call to setPrimaryClip(), it should not be surprising that the reverse operation — getting a ClipData from the clipboard — uses getPrimaryClip(). However, since you do not know where this clip came from, you need to validate that it has what you expect and to let the user know when the clipboard contents are not something you can leverage.

The “Play” button in our UI is wired to a playMusic() method. This will only work when we have pasted a Uri ClipData to the clipboard pointing to a piece of music. Since we cannot be sure that the user has done that, we have to sniff around:

  public void playMusic(View v) {
    ClipData clip=clipboard.getPrimaryClip();

    if (clip == null) {
      Toast.makeText(this, "There is no clip!", Toast.LENGTH_LONG)
           .show();
    }
    else {
      ClipData.Item item=clip.getItemAt(0);
      Uri song=item.getUri();

      if (song != null
          && getContentResolver().getType(song).startsWith("audio/")) {
        startActivity(new Intent(Intent.ACTION_VIEW, song));
      }
      else {
        Toast.makeText(this, "There is no song!", Toast.LENGTH_LONG)
             .show();
      }
    }
  }
(from SystemServices/ClipMusic/app/src/main/java/com/commonsware/android/clip/music/MusicClipper.java)

First, there may be nothing on the clipboard, in which case the ClipData returned by getPrimaryClip() would be null. Or, there may be stuff on the clipboard, but it may not have a Uri associated with it (getUri() on ClipData). Even then, the Uri may point to something other than music, so even if we get a Uri, we need to use a ContentResolver to check the MIME type (getContentResolver().getType()) and make sure it seems like it is music (e.g., starts with audio/). Then, and only then, does it make sense to try to start an ACTION_VIEW activity on that Uri and hope that something useful happens. Assuming you clipped a piece of music with the “Pick” button, “Play” will kick off playback of that song.

ClipData and Drag-and-Drop

API Level 11 also introduced Android’s first built-in drag-and-drop framework. One might expect that this would be related entirely to View and ViewGroup objects and have nothing to do with the clipboard. In reality, the drag-and-drop framework leverages ClipData to say what it is that is being dragged and dropped. You call startDrag() on a View, supplying a ClipData object, along with some objects to help render the “shadow” that is the visual representation of this drag operation. A View that can receive objects “dropped” via drag-and-drop needs to register an OnDragListener to receive drag events as the user slides the shadow over the top of the View in question. If the user lifts their finger, thereby dropping the shadow, the recipient View will get an ACTION_DROP drag event, and can get the ClipData out of the event.

The chapter on drag-and-drop goes into this in much greater detail.

Monitoring the Clipboard

API Level 11 added the capability for an app to monitor what is put on the clipboard, including things put on the clipboard by other apps.

This is a somewhat esoteric feature, but one that perhaps has some valid use cases. Mostly, it would be used by something not in the foreground, since the foreground activity is probably what is adding material to the clipboard. A service, or perhaps an activity that has moved to the background, could use this feature to find out about new clipboard entries.

To monitor the clipboard, you simply call addPrimaryClipChangedListener() on ClipboardMonitor, passing an implementation of an OnPrimaryClipChangedListener interface. That object, in turn, will be called with onPrimaryClipChanged() whenever there is a new clipboard entry. Later on, you can call removePrimaryClipChangedListener() to stop being notified about new clipboard entries.

For example, here is MainActivity from the SystemServices/ClipboardMonitor sample project:

package com.commonsware.android.clipmon;

import android.app.Activity;
import android.content.ClipboardManager;
import android.content.ClipboardManager.OnPrimaryClipChangedListener;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity implements
    OnPrimaryClipChangedListener {
  private ClipboardManager cm=null;
  private TextView lastClip=null;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    lastClip=(TextView)findViewById(R.id.last_clip);
    cm=(ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
  }
  
  @Override
  public void onStart() {
    super.onStart();
    cm.addPrimaryClipChangedListener(this);
  }
  
  @Override
  public void onStop() {
    cm.removePrimaryClipChangedListener(this);
    super.onStop();
  }

  @Override
  public void onPrimaryClipChanged() {
    lastClip.setText(cm.getPrimaryClip().getItemAt(0)
                       .coerceToText(this));
  }
}
(from SystemServices/ClipboardMonitor/app/src/main/java/com/commonsware/android/clipmon/MainActivity.java)

Here, we:

In theory, this activity will display new clipboard entries as they arrive. In practice, it will only do so while it is in the foreground, and so it would require something in the background to add something to the clipboard. That is not a particularly useful example… except to test the bug outlined in the next section.

The Android 4.3 Clipboard Bug

AndroidPolice reported on a fairly unpleasant bug in Android 4.3. While this bug was fixed in Android 4.4, there is little evidence that Google will be releasing a fix for Android 4.3 devices, which means that this problem will plague developers into 2015 and perhaps beyond.

The bug stems from the clipboard monitoring facility. If an app has used addPrimaryClipChangedListener(), any other app that tries to paste to the clipboard will crash.

The first crash will be a SecurityException:

java.lang.SecurityException: uid ... does not have android.permission.UPDATE_APP_OPS_STATS

The second and subsequent times this occurs on the device, it will be an IllegalStateException:

java.lang.IllegalStateException: beginBroadcast() called while already in a broadcast

The only resolution is to unregister the clipboard listener… and hope that the first crash has not occurred. If it has, a full reboot of the device is required to fix the broken system.

If Your App Monitors the Clipboard…

If you have a component, such as a long-running service, that is monitoring the clipboard, please ensure that the users have an easy way to stop that behavior, even if it means stopping your whole service. While this may mean that your app has seriously degraded functionality, the alternative is that the user has to keep rebooting their device while your app is installed.

If Your App Pastes to the Clipboard…

If you are pasting to the clipboard, with setPrimaryClip() or the older setText(), you will want to throw a try/catch block around those calls, so you catch the RuntimeExceptions that will be thrown.

However, you will need to tell your users that they are now fairly well screwed, needing to both find the clipboard-monitoring app and learn how to control it (or uninstall/disable it, if needed), plus reboot their device, in order to paste to the clipboard again.