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
.
Understanding this chapter requires that you have read the core chapters of this book.
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:
setText()
, to put text on the clipboardhasText()
, to indicate if the clipboard has something on itgetText()
, to retrieve the text on the clipboardThose methods still exist, but they have been deprecated as of API Level 11.
Their replacements are:
setPrimaryClip()
, to put something on the clipboardhasPrimaryClip()
, to indicate if the clipboard has something on itgetPrimaryClip()
, to retrieve something from the clipboardHere, the “something” winds up being in the form of ClipData
objects, which
can hold:
Uri
(e.g., to a piece of music)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>
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);
}
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);
}
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:
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();
}
}
}
}
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();
}
}
}
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.
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.
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));
}
}
Here, we:
ClipboardManager
in onCreate()
addPrimaryClipChangedListener()
in
onStart()
removePrimaryClipChangedListener()
in onStop()
getItemAt(0)
) of the primary clip (getPrimaryClip()
)
to text (coerceToText(this)
), and stuff the results into a TextView
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.
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 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 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.