Update NotePad to support copying of an entire note to the clipboard.
Change-Id: Icbda36dcdb98d53395af1570e161dad727146f93
diff --git a/samples/NotePad/AndroidManifest.xml b/samples/NotePad/AndroidManifest.xml
index 04f4dbe..7d41dd5 100644
--- a/samples/NotePad/AndroidManifest.xml
+++ b/samples/NotePad/AndroidManifest.xml
@@ -65,9 +65,12 @@
</intent-filter>
<!-- This filter says that we can create a new note inside
- of a directory of notes. -->
+ of a directory of notes. The INSERT action creates an
+ empty note; the PASTE action initializes a new note from
+ the current contents of the clipboard. -->
<intent-filter>
<action android:name="android.intent.action.INSERT" />
+ <action android:name="android.intent.action.PASTE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
</intent-filter>
diff --git a/samples/NotePad/res/values/strings.xml b/samples/NotePad/res/values/strings.xml
index 168db92..43be4dd 100644
--- a/samples/NotePad/res/values/strings.xml
+++ b/samples/NotePad/res/values/strings.xml
@@ -15,8 +15,10 @@
-->
<resources>
+ <string name="menu_copy">Copy</string>
<string name="menu_delete">Delete</string>
<string name="menu_insert">Add note</string>
+ <string name="menu_paste">Paste</string>
<string name="menu_revert">Revert</string>
<string name="menu_discard">Discard</string>
diff --git a/samples/NotePad/src/com/example/android/notepad/NoteEditor.java b/samples/NotePad/src/com/example/android/notepad/NoteEditor.java
index e45efd8..57b4646 100644
--- a/samples/NotePad/src/com/example/android/notepad/NoteEditor.java
+++ b/samples/NotePad/src/com/example/android/notepad/NoteEditor.java
@@ -19,7 +19,10 @@
import com.example.android.notepad.NotePad.Notes;
import android.app.Activity;
+import android.content.ClipboardManager;
+import android.content.ClippedData;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -38,7 +41,9 @@
/**
* A generic activity for editing a note in a database. This can be used
* either to simply view a note {@link Intent#ACTION_VIEW}, view and edit a note
- * {@link Intent#ACTION_EDIT}, or create a new note {@link Intent#ACTION_INSERT}.
+ * {@link Intent#ACTION_EDIT}, or create a new empty note
+ * {@link Intent#ACTION_INSERT}, or create a new note from the current contents
+ * of the clipboard {@link Intent#ACTION_PASTE}.
*/
public class NoteEditor extends Activity {
private static final String TAG = "Notes";
@@ -49,9 +54,12 @@
private static final String[] PROJECTION = new String[] {
Notes._ID, // 0
Notes.NOTE, // 1
+ Notes.TITLE, // 2
};
/** The index of the note column */
private static final int COLUMN_INDEX_NOTE = 1;
+ /** The index of the title column */
+ private static final int COLUMN_INDEX_TITLE = 2;
// This is our state data that is stored when freezing.
private static final String ORIGINAL_CONTENT = "origContent";
@@ -64,6 +72,7 @@
// The different distinct states the activity can be run in.
private static final int STATE_EDIT = 0;
private static final int STATE_INSERT = 1;
+ private static final int STATE_PASTE = 2;
private int mState;
private boolean mNoteOnly = false;
@@ -118,7 +127,8 @@
// Requested to edit: set that state, and the data being edited.
mState = STATE_EDIT;
mUri = intent.getData();
- } else if (Intent.ACTION_INSERT.equals(action)) {
+ } else if (Intent.ACTION_INSERT.equals(action)
+ || Intent.ACTION_PASTE.equals(action)) {
// Requested to insert: set that state, and create a new entry
// in the container.
mState = STATE_INSERT;
@@ -137,6 +147,13 @@
// set the result to be returned.
setResult(RESULT_OK, (new Intent()).setAction(mUri.toString()));
+ // If pasting, initialize data from clipboard.
+ if (Intent.ACTION_PASTE.equals(action)) {
+ performPaste();
+ // Switch to paste mode; can no longer modify title.
+ mState = STATE_PASTE;
+ }
+
} else {
// Whoops, unknown action! Bail.
Log.e(TAG, "Unknown action, exiting");
@@ -173,7 +190,7 @@
// Modify our overall title depending on the mode we are running in.
if (mState == STATE_EDIT) {
setTitle(getText(R.string.title_edit));
- } else if (mState == STATE_INSERT) {
+ } else if (mState == STATE_INSERT || mState == STATE_PASTE) {
setTitle(getText(R.string.title_create));
}
@@ -224,34 +241,7 @@
// Get out updates into the provider.
} else {
- ContentValues values = new ContentValues();
-
- // This stuff is only done when working with a full-fledged note.
- if (!mNoteOnly) {
- // Bump the modification time to now.
- values.put(Notes.MODIFIED_DATE, System.currentTimeMillis());
-
- // If we are creating a new note, then we want to also create
- // an initial title for it.
- if (mState == STATE_INSERT) {
- String title = text.substring(0, Math.min(30, length));
- if (length > 30) {
- int lastSpace = title.lastIndexOf(' ');
- if (lastSpace > 0) {
- title = title.substring(0, lastSpace);
- }
- }
- values.put(Notes.TITLE, title);
- }
- }
-
- // Write our text back into the provider.
- values.put(Notes.NOTE, text);
-
- // Commit all of our changes to persistent storage. When the update completes
- // the content provider will notify the cursor of the change, which will
- // cause the UI to be updated.
- getContentResolver().update(mUri, values, null, null);
+ updateNote(text, null, !mNoteOnly);
}
}
}
@@ -311,6 +301,82 @@
return super.onOptionsItemSelected(item);
}
+//BEGIN_INCLUDE(paste)
+ /**
+ * Replace the note's data with the current contents of the clipboard.
+ */
+ private final void performPaste() {
+ ClipboardManager clipboard = (ClipboardManager)
+ getSystemService(Context.CLIPBOARD_SERVICE);
+ ContentResolver cr = getContentResolver();
+
+ ClippedData clip = clipboard.getPrimaryClip();
+ if (clip != null) {
+ String text=null, title=null;
+
+ ClippedData.Item item = clip.getItem(0);
+ Uri uri = item.getUri();
+ if (uri != null && NotePad.Notes.CONTENT_ITEM_TYPE.equals(cr.getType(uri))) {
+ // The clipboard holds a reference to a note. Copy it.
+ Cursor orig = cr.query(uri, PROJECTION, null, null, null);
+ if (orig != null) {
+ if (orig.moveToFirst()) {
+ text = orig.getString(COLUMN_INDEX_NOTE);
+ title = orig.getString(COLUMN_INDEX_TITLE);
+ }
+ orig.close();
+ }
+ }
+
+ // If we weren't able to load the clipped data as a note, then
+ // convert whatever it is to text.
+ if (text == null) {
+ text = item.coerceToText(this).toString();
+ }
+
+ updateNote(text, title, true);
+ }
+ }
+//END_INCLUDE(paste)
+
+ /**
+ * Replace the current note contents with the given data.
+ */
+ private final void updateNote(String text, String title, boolean updateTitle) {
+ ContentValues values = new ContentValues();
+
+ // This stuff is only done when working with a full-fledged note.
+ if (updateTitle) {
+ // Bump the modification time to now.
+ values.put(Notes.MODIFIED_DATE, System.currentTimeMillis());
+
+ // If we are creating a new note, then we want to also create
+ // an initial title for it.
+ if (mState == STATE_INSERT) {
+ if (title == null) {
+ int length = text.length();
+ title = text.substring(0, Math.min(30, length));
+ if (length > 30) {
+ int lastSpace = title.lastIndexOf(' ');
+ if (lastSpace > 0) {
+ title = title.substring(0, lastSpace);
+ }
+ }
+ }
+ values.put(Notes.TITLE, title);
+ }
+ }
+
+ // Write our text back into the provider.
+ values.put(Notes.NOTE, text);
+
+ // Commit all of our changes to persistent storage. When the update completes
+ // the content provider will notify the cursor of the change, which will
+ // cause the UI to be updated.
+ getContentResolver().update(mUri, values, null, null);
+
+ }
+
/**
* Take care of canceling work on a note. Deletes the note if we
* had created it, otherwise reverts to the original text.
diff --git a/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java b/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java
index 58cdc8f..d79be24 100644
--- a/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java
+++ b/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java
@@ -23,6 +23,8 @@
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
+import android.content.ContentProvider.PipeDataWriter;
+import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.SQLException;
@@ -30,17 +32,25 @@
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
import android.provider.LiveFolders;
import android.text.TextUtils;
import android.util.Log;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
import java.util.HashMap;
/**
* Provides access to a database of notes. Each note has a title, the note
* itself, a creation date and a modified data.
*/
-public class NotePadProvider extends ContentProvider {
+public class NotePadProvider extends ContentProvider implements PipeDataWriter<Cursor> {
private static final String TAG = "NotePadProvider";
@@ -150,6 +160,102 @@
}
}
+//BEGIN_INCLUDE(stream)
+ /**
+ * Return the types of data streams we can return. Currently we only
+ * support URIs to specific notes, and can convert such a note to a
+ * plain text stream.
+ */
+ @Override
+ public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
+ switch (sUriMatcher.match(uri)) {
+ case NOTES:
+ case LIVE_FOLDER_NOTES:
+ return null;
+
+ case NOTE_ID:
+ if (compareMimeTypes("text/plain", mimeTypeFilter)) {
+ return new String[] { "text/plain" };
+ }
+ return null;
+
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
+ /**
+ * Standard projection for the interesting columns of a normal note.
+ */
+ private static final String[] READ_NOTE_PROJECTION = new String[] {
+ Notes._ID, // 0
+ Notes.NOTE, // 1
+ NotePad.Notes.TITLE, // 2
+ };
+ private static final int READ_NOTE_NOTE_INDEX = 1;
+ private static final int READ_NOTE_TITLE_INDEX = 2;
+
+ /**
+ * Implement the other side of getStreamTypes: for each stream time we
+ * report to support, we need to actually be able to return a stream of
+ * data. This function simply retrieves a cursor for the URI of interest,
+ * and uses ContentProvider's openPipeHelper() to start the work of
+ * convering the data off into another thread.
+ */
+ @Override
+ public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
+ throws FileNotFoundException {
+ // Check if we support a stream MIME type for this URI.
+ String[] mimeTypes = getStreamTypes(uri, mimeTypeFilter);
+ if (mimeTypes != null) {
+ // Retrieve the note for this URI.
+ Cursor c = query(uri, READ_NOTE_PROJECTION, null, null, null);
+ if (c == null || !c.moveToFirst()) {
+ if (c != null) {
+ c.close();
+ }
+ throw new FileNotFoundException("Unable to query " + uri);
+ }
+ // Start a thread to pipe the data back to the client.
+ return new AssetFileDescriptor(
+ openPipeHelper(uri, mimeTypes[0], opts, c, this), 0,
+ AssetFileDescriptor.UNKNOWN_LENGTH);
+ }
+ return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
+ }
+
+ /**
+ * Implementation of {@link android.content.ContentProvider.PipeDataWriter}
+ * to perform the actual work of converting the data in one of cursors to a
+ * stream of data for the client to read.
+ */
+ @Override
+ public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType,
+ Bundle opts, Cursor c) {
+ // We currently only support conversion-to-text from a single note entry,
+ // so no need for cursor data type checking here.
+ FileOutputStream fout = new FileOutputStream(output.getFileDescriptor());
+ PrintWriter pw = null;
+ try {
+ pw = new PrintWriter(new OutputStreamWriter(fout, "UTF-8"));
+ pw.println(c.getString(READ_NOTE_TITLE_INDEX));
+ pw.println("");
+ pw.println(c.getString(READ_NOTE_NOTE_INDEX));
+ } catch (UnsupportedEncodingException e) {
+ Log.w(TAG, "Ooops", e);
+ } finally {
+ c.close();
+ if (pw != null) {
+ pw.flush();
+ }
+ try {
+ fout.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+//END_INCLUDE(stream)
+
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
// Validate the requested uri
diff --git a/samples/NotePad/src/com/example/android/notepad/NotesList.java b/samples/NotePad/src/com/example/android/notepad/NotesList.java
index ceaaa3c..bbcb936 100644
--- a/samples/NotePad/src/com/example/android/notepad/NotesList.java
+++ b/samples/NotePad/src/com/example/android/notepad/NotesList.java
@@ -19,8 +19,11 @@
import com.example.android.notepad.NotePad.Notes;
import android.app.ListActivity;
+import android.content.ClipboardManager;
+import android.content.ClippedData;
import android.content.ComponentName;
import android.content.ContentUris;
+import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
@@ -45,7 +48,9 @@
// Menu item ids
public static final int MENU_ITEM_DELETE = Menu.FIRST;
- public static final int MENU_ITEM_INSERT = Menu.FIRST + 1;
+ public static final int MENU_ITEM_COPY = Menu.FIRST + 1;
+ public static final int MENU_ITEM_INSERT = Menu.FIRST + 2;
+ public static final int MENU_ITEM_PASTE = Menu.FIRST + 3;
/**
* The columns we are interested in from the database
@@ -58,6 +63,8 @@
/** The index of the title column */
private static final int COLUMN_INDEX_TITLE = 1;
+ private MenuItem mPasteItem;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -95,6 +102,11 @@
.setShortcut('3', 'a')
.setIcon(android.R.drawable.ic_menu_add);
+ // If there is currently data in the clipboard, we can paste it
+ // as a new note.
+ mPasteItem = menu.add(0, MENU_ITEM_PASTE, 0, R.string.menu_paste)
+ .setShortcut('4', 'p');
+
// Generate any additional actions that can be performed on the
// overall list. In a normal install, there are no additional
// actions found here, but this allows other applications to extend
@@ -110,6 +122,16 @@
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
+
+ // The paste menu item is enabled if there is data on the clipboard.
+ ClipboardManager clipboard = (ClipboardManager)
+ getSystemService(Context.CLIPBOARD_SERVICE);
+ if (clipboard.hasPrimaryClip()) {
+ mPasteItem.setEnabled(true);
+ } else {
+ mPasteItem.setEnabled(false);
+ }
+
final boolean haveItems = getListAdapter().getCount() > 0;
// If there are any notes in the list (which implies that one of
@@ -150,6 +172,10 @@
// Launch activity to insert a new item
startActivity(new Intent(Intent.ACTION_INSERT, getIntent().getData()));
return true;
+ case MENU_ITEM_PASTE:
+ // Launch activity to insert a new item
+ startActivity(new Intent(Intent.ACTION_PASTE, getIntent().getData()));
+ return true;
}
return super.onOptionsItemSelected(item);
}
@@ -173,6 +199,9 @@
// Setup the menu header
menu.setHeaderTitle(cursor.getString(COLUMN_INDEX_TITLE));
+ // Add a menu item to copy the note
+ menu.add(0, MENU_ITEM_COPY, 0, R.string.menu_copy);
+
// Add a menu item to delete the note
menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_delete);
}
@@ -194,6 +223,17 @@
getContentResolver().delete(noteUri, null, null);
return true;
}
+//BEGIN_INCLUDE(copy)
+ case MENU_ITEM_COPY: {
+ // Copy the note that the context menu is for on to the clipboard
+ ClipboardManager clipboard = (ClipboardManager)
+ getSystemService(Context.CLIPBOARD_SERVICE);
+ Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id);
+ clipboard.setPrimaryClip(new ClippedData(null, null, new ClippedData.Item(
+ noteUri)));
+ return true;
+ }
+//END_INCLUDE(copy)
}
return false;
}