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;
     }