Update NotePad to support copying of an entire note to the clipboard.

Change-Id: Icbda36dcdb98d53395af1570e161dad727146f93
This commit is contained in:
Dianne Hackborn
2010-08-06 17:09:29 -07:00
parent 762a14fa3c
commit 4779ab6f9a
5 changed files with 251 additions and 34 deletions

View File

@@ -19,7 +19,10 @@ package com.example.android.notepad;
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 @@ import android.widget.EditText;
/**
* 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 @@ public class NoteEditor extends Activity {
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 @@ public class NoteEditor extends Activity {
// 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 @@ public class NoteEditor extends Activity {
// 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 @@ public class NoteEditor extends Activity {
// 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 @@ public class NoteEditor extends Activity {
// 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 @@ public class NoteEditor extends Activity {
// 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 @@ public class NoteEditor extends Activity {
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.

View File

@@ -23,6 +23,8 @@ import android.content.ContentUris;
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.SQLiteDatabase;
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 @@ public class NotePadProvider extends ContentProvider {
}
}
//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

View File

@@ -19,8 +19,11 @@ package com.example.android.notepad;
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 @@ public class NotesList extends ListActivity {
// 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 @@ public class NotesList extends ListActivity {
/** 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 @@ public class NotesList extends ListActivity {
.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 @@ public class NotesList extends ListActivity {
@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 @@ public class NotesList extends ListActivity {
// 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 @@ public class NotesList extends ListActivity {
// 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 @@ public class NotesList extends ListActivity {
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;
}