//
//src\com\example\android\notepad\NoteEditor.java
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.notepad;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.EditText;
/**
* This Activity handles "editing" a note, where editing is responding to
* {@link Intent#ACTION_VIEW} (request to view data), edit a note
* {@link Intent#ACTION_EDIT}, create a note {@link Intent#ACTION_INSERT}, or
* create a new note from the current contents of the clipboard {@link Intent#ACTION_PASTE}.
*
* NOTE: Notice that the provider operations in this Activity are taking place on the UI thread.
* This is not a good practice. It is only done here to make the code more readable. A real
* application should use the {@link android.content.AsyncQueryHandler}
* or {@link android.os.AsyncTask} object to perform operations asynchronously on a separate thread.
*/
public class NoteEditor extends Activity {
// For logging and debugging purposes
private static final String TAG = "NoteEditor";
/*
* Creates a projection that returns the note ID and the note contents.
*/
private static final String[] PROJECTION =
new String[] {
NotePad.Notes._ID,
NotePad.Notes.COLUMN_NAME_TITLE,
NotePad.Notes.COLUMN_NAME_NOTE
};
// A label for the saved state of the activity
private static final String ORIGINAL_CONTENT = "origContent";
// This Activity can be started by more than one action. Each action is represented
// as a "state" constant
private static final int STATE_EDIT = 0;
private static final int STATE_INSERT = 1;
// Global mutable variables
private int mState;
private Uri mUri;
private Cursor mCursor;
private EditText mText;
private String mOriginalContent;
/**
* Defines a custom EditText View that draws lines between each line of text that is displayed.
*/
public static class LinedEditText extends EditText {
private Rect mRect;
private Paint mPaint;
// This constructor is used by LayoutInflater
public LinedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
// Creates a Rect and a Paint object, and sets the style and color of the Paint object.
mRect = new Rect();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0x800000FF);
}
/**
* This is called to draw the LinedEditText object
* @param canvas The canvas on which the background is drawn.
*/
@Override
protected void onDraw(Canvas canvas) {
// Gets the number of lines of text in the View.
int count = getLineCount();
// Gets the global Rect and Paint objects
Rect r = mRect;
Paint paint = mPaint;
/*
* Draws one line in the rectangle for every line of text in the EditText
*/
for (int i = 0; i < count; i++) {
// Gets the baseline coordinates for the current line of text
int baseline = getLineBounds(i, r);
/*
* Draws a line in the background from the left of the rectangle to the right,
* at a vertical position one dip below the baseline, using the "paint" object
* for details.
*/
canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint);
}
// Finishes up by calling the parent method
super.onDraw(canvas);
}
}
/**
* This method is called by Android when the Activity is first started. From the incoming
* Intent, it determines what kind of editing is desired, and then does it.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
* Creates an Intent to use when the Activity object's result is sent back to the
* caller.
*/
final Intent intent = getIntent();
/*
* Sets up for the edit, based on the action specified for the incoming Intent.
*/
// Gets the action that triggered the intent filter for this Activity
final String action = intent.getAction();
// For an edit action:
if (Intent.ACTION_EDIT.equals(action)) {
// Sets the Activity state to EDIT, and gets the URI for the data to be edited.
mState = STATE_EDIT;
mUri = intent.getData();
// For an insert or paste action:
} else if (Intent.ACTION_INSERT.equals(action)
|| Intent.ACTION_PASTE.equals(action)) {
// Sets the Activity state to INSERT, gets the general note URI, and inserts an
// empty record in the provider
mState = STATE_INSERT;
mUri = getContentResolver().insert(intent.getData(), null);
/*
* If the attempt to insert the new note fails, shuts down this Activity. The
* originating Activity receives back RESULT_CANCELED if it requested a result.
* Logs that the insert failed.
*/
if (mUri == null) {
// Writes the log identifier, a message, and the URI that failed.
Log.e(TAG, "Failed to insert new note into " + getIntent().getData());
// Closes the activity.
finish();
return;
}
// Since the new entry was created, this sets the result to be returned
// set the result to be returned.
setResult(RESULT_OK, (new Intent()).setAction(mUri.toString()));
// If the action was other than EDIT or INSERT:
} else {
// Logs an error that the action was not understood, finishes the Activity, and
// returns RESULT_CANCELED to an originating Activity.
Log.e(TAG, "Unknown action, exiting");
finish();
return;
}
/*
* Using the URI passed in with the triggering Intent, gets the note or notes in
* the provider.
* Note: This is being done on the UI thread. It will block the thread until the query
* completes. In a sample app, going against a simple provider based on a local database,
* the block will be momentary, but in a real app you should use
* android.content.AsyncQueryHandler or android.os.AsyncTask.
*/
mCursor = managedQuery(
mUri, // The URI that gets multiple notes from the provider.
PROJECTION, // A projection that returns the note ID and note content for each note.
null, // No "where" clause selection criteria.
null, // No "where" clause selection values.
null // Use the default sort order (modification date, descending)
);
// For a paste, initializes the data from clipboard.
// (Must be done after mCursor is initialized.)
if (Intent.ACTION_PASTE.equals(action)) {
// Does the paste
performPaste();
// Switches the state to EDIT so the title can be modified.
mState = STATE_EDIT;
}
// Sets the layout for this Activity. See res/layout/note_editor.xml
setContentView(R.layout.note_editor);
// Gets a handle to the EditText in the the layout.
mText = (EditText) findViewById(R.id.note);
/*
* If this Activity had stopped previously, its state was written the ORIGINAL_CONTENT
* location in the saved Instance state. This gets the state.
*/
if (savedInstanceState != null) {
mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT);
}
}
/**
* This method is called when the Activity is about to come to the foreground. This happens
* when the Activity comes to the top of the task stack, OR when it is first starting.
*
* Moves to the first note in the list, sets an appropriate title for the action chosen by
* the user, puts the note contents into the TextView, and saves the original text as a
* backup.
*/
@Override
protected void onResume() {
super.onResume();
/*
* mCursor is initialized, since onCreate() always precedes onResume for any running
* process. This tests that it's not null, since it should always contain data.
*/
if (mCursor != null) {
// Requery in case something changed while paused (such as the title)
mCursor.requery();
/* Moves to the first record. Always call moveToFirst() before accessing data in
* a Cursor for the first time. The semantics of using a Cursor are that when it is
* created, its internal index is pointing to a "place" immediately before the first
* record.
*/
mCursor.moveToFirst();
// Modifies the window title for the Activity according to the current Activity state.
if (mState == STATE_EDIT) {
// Set the title of the Activity to include the note title
int colTitleIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE);
String title = mCursor.getString(colTitleIndex);
Resources res = getResources();
String text = String.format(res.getString(R.string.title_edit), title);
setTitle(text);
// Sets the title to "create" for inserts
} else if (mState == STATE_INSERT) {
setTitle(getText(R.string.title_create));
}
/*
* onResume() may have been called after the Activity lost focus (was paused).
* The user was either editing or creating a note when the Activity paused.
* The Activity should re-display the text that had been retrieved previously, but
* it should not move the cursor. This helps the user to continue editing or entering.
*/
// Gets the note text from the Cursor and puts it in the TextView, but doesn't change
// the text cursor's position.
int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE);
String note = mCursor.getString(colNoteIndex);
mText.setTextKeepState(note);
// Stores the original note text, to allow the user to revert changes.
if (mOriginalContent == null) {
mOriginalContent = note;
}
/*
* Something is wrong. The Cursor should always contain data. Report an error in the
* note.
*/
} else {
setTitle(getText(R.string.error_title));
mText.setText(getText(R.string.error_message));
}
}
/**
* This method is called when an Activity loses focus during its normal operation, and is then
* later on killed. The Activity has a chance to save its state so that the system can restore
* it.
*
* Notice that this method isn't a normal part of the Activity lifecycle. It won't be called
* if the user simply navigates away from the Activity.
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
// Save away the original text, so we still have it if the activity
// needs to be killed while paused.
outState.putString(ORIGINAL_CONTENT, mOriginalContent);
}
/**
* This method is called when the Activity loses focus.
*
* For Activity objects that edit information, onPause() may be the one place where changes are
* saved. The Android application model is predicated on the idea that "save" and "exit" aren't
* required actions. When users navigate away from an Activity, they shouldn't have to go back
* to it to complete their work. The act of going away should save everything and leave the
* Activity in a state where Android can destroy it if necessary.
*
* If the user hasn't done anything, then this deletes or clears out the note, otherwise it
* writes the user's work to the provider.
*/
@Override
protected void onPause() {
super.onPause();
/*
* Tests to see that the query operation didn't fail (see onCreate()). The Cursor object
* will exist, even if no records were returned, unless the query failed because of some
* exception or error.
*
*/
if (mCursor != null) {
// Get the current note text.
String text = mText.getText().toString();
int length = text.length();
/*
* If the Activity is in the midst of finishing and there is no text in the current
* note, returns a result of CANCELED to the caller, and deletes the note. This is done
* even if the note was being edited, the assumption being that the user wanted to
* "clear out" (delete) the note.
*/
if (isFinishing() && (length == 0)) {
setResult(RESULT_CANCELED);
deleteNote();
/*
* Writes the edits to the provider. The note has been edited if an existing note was
* retrieved into the editor *or* if a new note was inserted. In the latter case,
* onCreate() inserted a new empty note into the provider, and it is this new note
* that is being edited.
*/
} else if (mState == STATE_EDIT) {
// Creates a map to contain the new values for the columns
updateNote(text, null);
} else if (mState == STATE_INSERT) {
updateNote(text, text);
mState = STATE_EDIT;
}
}
}
/**
* This method is called when the user clicks the device's Menu button the first time for
* this Activity. Android passes in a Menu object that is populated with items.
*
* Builds the menus for editing and inserting, and adds in alternative actions that
* registered themselves to handle the MIME types for this application.
*
* @param menu A Menu object to which items should be added.
* @return True to display the menu.
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate menu from XML resource
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.editor_options_menu, menu);
// Only add extra menu items for a saved note
if (mState == STATE_EDIT) {
// Append to the
// menu items for any other activities that can do stuff with it
// as well. This does a query on the system for any activities that
// implement the ALTERNATIVE_ACTION for our data, adding a menu item
// for each one that is found.
Intent intent = new Intent(null, mUri);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
new ComponentName(this, NoteEditor.class), null, intent, 0, null);
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// Check if note has changed and enable/disable the revert option
int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE);
String savedNote = mCursor.getString(colNoteIndex);
String currentNote = mText.getText().toString();
if (savedNote.equals(currentNote)) {
menu.findItem(R.id.menu_revert).setVisible(false);
} else {
menu.findItem(R.id.menu_revert).setVisible(true);
}
return super.onPrepareOptionsMenu(menu);
}
/**
* This method is called when a menu item is selected. Android passes in the selected item.
* The switch statement in this method calls the appropriate method to perform the action the
* user chose.
*
* @param item The selected MenuItem
* @return True to indicate that the item was processed, and no further work is necessary. False
* to proceed to further processing as indicated in the MenuItem object.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle all of the possible menu actions.
switch (item.getItemId()) {
case R.id.menu_save:
String text = mText.getText().toString();
updateNote(text, null);
finish();
break;
case R.id.menu_delete:
deleteNote();
finish();
break;
case R.id.menu_revert:
cancelNote();
break;
}
return super.onOptionsItemSelected(item);
}
/**
* A helper method that replaces the note's data with the contents of the clipboard.
*/
private final void performPaste() {
// Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
// Gets a content resolver instance
ContentResolver cr = getContentResolver();
// Gets the clipboard data from the clipboard
ClipData clip = clipboard.getPrimaryClip();
if (clip != null) {
String text=null;
String title=null;
// Gets the first item from the clipboard data
ClipData.Item item = clip.getItemAt(0);
// Tries to get the item's contents as a URI pointing to a note
Uri uri = item.getUri();
// Tests to see that the item actually is an URI, and that the URI
// is a content URI pointing to a provider whose MIME type is the same
// as the MIME type supported by the Note pad provider.
if (uri != null && NotePad.Notes.CONTENT_ITEM_TYPE.equals(cr.getType(uri))) {
// The clipboard holds a reference to data with a note MIME type. This copies it.
Cursor orig = cr.query(
uri, // URI for the content provider
PROJECTION, // Get the columns referred to in the projection
null, // No selection variables
null, // No selection variables, so no criteria are needed
null // Use the default sort order
);
// If the Cursor is not null, and it contains at least one record
// (moveToFirst() returns true), then this gets the note data from it.
if (orig != null) {
if (orig.moveToFirst()) {
int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE);
int colTitleIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE);
text = orig.getString(colNoteIndex);
title = orig.getString(colTitleIndex);
}
// Closes the cursor.
orig.close();
}
}
// If the contents of the clipboard wasn't a reference to a note, then
// this converts whatever it is to text.
if (text == null) {
text = item.coerceToText(this).toString();
}
// Updates the current note with the retrieved title and text.
updateNote(text, title);
}
}
/**
* Replaces the current note contents with the text and title provided as arguments.
* @param text The new note contents to use.
* @param title The new note title to use
*/
private final void updateNote(String text, String title) {
// Sets up a map to contain values to be updated in the provider.
ContentValues values = new ContentValues();
values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, System.currentTimeMillis());
// If the action is to insert a new note, this creates an initial title for it.
if (mState == STATE_INSERT) {
// If no title was provided as an argument, create one from the note text.
if (title == null) {
// Get the note's length
int length = text.length();
// Sets the title by getting a substring of the text that is 31 characters long
// or the number of characters in the note plus one, whichever is smaller.
title = text.substring(0, Math.min(30, length));
// If the resulting length is more than 30 characters, chops off any
// trailing spaces
if (length > 30) {
int lastSpace = title.lastIndexOf(' ');
if (lastSpace > 0) {
title = title.substring(0, lastSpace);
}
}
}
// In the values map, sets the value of the title
values.put(NotePad.Notes.COLUMN_NAME_TITLE, title);
} else if (title != null) {
// In the values map, sets the value of the title
values.put(NotePad.Notes.COLUMN_NAME_TITLE, title);
}
// This puts the desired notes text into the map.
values.put(NotePad.Notes.COLUMN_NAME_NOTE, text);
/*
* Updates the provider with the new values in the map. The ListView is updated
* automatically. The provider sets this up by setting the notification URI for
* query Cursor objects to the incoming URI. The content resolver is thus
* automatically notified when the Cursor for the URI changes, and the UI is
* updated.
* Note: This is being done on the UI thread. It will block the thread until the
* update completes. In a sample app, going against a simple provider based on a
* local database, the block will be momentary, but in a real app you should use
* android.content.AsyncQueryHandler or android.os.AsyncTask.
*/
getContentResolver().update(
mUri, // The URI for the record to update.
values, // The map of column names and new values to apply to them.
null, // No selection criteria are used, so no where columns are necessary.
null // No where columns are used, so no where arguments are necessary.
);
}
/**
* This helper method cancels the work done on a note. It deletes the note if it was
* newly created, or reverts to the original text of the note i
*/
private final void cancelNote() {
if (mCursor != null) {
if (mState == STATE_EDIT) {
// Put the original note text back into the database
mCursor.close();
mCursor = null;
ContentValues values = new ContentValues();
values.put(NotePad.Notes.COLUMN_NAME_NOTE, mOriginalContent);
getContentResolver().update(mUri, values, null, null);
} else if (mState == STATE_INSERT) {
// We inserted an empty note, make sure to delete it
deleteNote();
}
}
setResult(RESULT_CANCELED);
finish();
}
/**
* Take care of deleting a note. Simply deletes the entry.
*/
private final void deleteNote() {
if (mCursor != null) {
mCursor.close();
mCursor = null;
getContentResolver().delete(mUri, null, null);
mText.setText("");
}
}
}
//src\com\example\android\notepad\NotePad.java
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.notepad;
import android.net.Uri;
import android.provider.BaseColumns;
/**
* Defines a contract between the Note Pad content provider and its clients. A contract defines the
* information that a client needs to access the provider as one or more data tables. A contract
* is a public, non-extendable (final) class that contains constants defining column names and
* URIs. A well-written client depends only on the constants in the contract.
*/
public final class NotePad {
public static final String AUTHORITY = "com.google.provider.NotePad";
// This class cannot be instantiated
private NotePad() {
}
/**
* Notes table contract
*/
public static final class Notes implements BaseColumns {
// This class cannot be instantiated
private Notes() {}
/**
* The table name offered by this provider
*/
public static final String TABLE_NAME = "notes";
/*
* URI definitions
*/
/**
* The scheme part for this provider's URI
*/
private static final String SCHEME = "content://";
/**
* Path parts for the URIs
*/
/**
* Path part for the Notes URI
*/
private static final String PATH_NOTES = "/notes";
/**
* Path part for the Note ID URI
*/
private static final String PATH_NOTE_ID = "/notes/";
/**
* 0-relative position of a note ID segment in the path part of a note ID URI
*/
public static final int NOTE_ID_PATH_POSITION = 1;
/**
* Path part for the Live Folder URI
*/
private static final String PATH_LIVE_FOLDER = "/live_folders/notes";
/**
* The content:// style URL for this table
*/
public static final Uri CONTENT_URI = Uri.parse(SCHEME + AUTHORITY + PATH_NOTES);
/**
* The content URI base for a single note. Callers must
* append a numeric note id to this Uri to retrieve a note
*/
public static final Uri CONTENT_ID_URI_BASE
= Uri.parse(SCHEME + AUTHORITY + PATH_NOTE_ID);
/**
* The content URI match pattern for a single note, specified by its ID. Use this to match
* incoming URIs or to construct an Intent.
*/
public static final Uri CONTENT_ID_URI_PATTERN
= Uri.parse(SCHEME + AUTHORITY + PATH_NOTE_ID + "/#");
/**
* The content Uri pattern for a notes listing for live folders
*/
public static final Uri LIVE_FOLDER_URI
= Uri.parse(SCHEME + AUTHORITY + PATH_LIVE_FOLDER);
/*
* MIME type definitions
*/
/**
* The MIME type of {@link #CONTENT_URI} providing a directory of notes.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.note";
/**
* The MIME type of a {@link #CONTENT_URI} sub-directory of a single
* note.
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.google.note";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = "modified DESC";
/*
* Column definitions
*/
/**
* Column name for the title of the note
* Type: TEXT
*/
public static final String COLUMN_NAME_TITLE = "title";
/**
* Column name of the note content
* Type: TEXT
*/
public static final String COLUMN_NAME_NOTE = "note";
/**
* Column name for the creation timestamp
* Type: INTEGER (long from System.curentTimeMillis())
*/
public static final String COLUMN_NAME_CREATE_DATE = "created";
/**
* Column name for the modification timestamp
* Type: INTEGER (long from System.curentTimeMillis())
*/
public static final String COLUMN_NAME_MODIFICATION_DATE = "modified";
}
}
//src\com\example\android\notepad\NotePadProvider.java
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.notepad;
import com.example.android.notepad.NotePad;
import android.content.ClipDescription;
import android.content.ContentProvider;
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;
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 implements PipeDataWriter {
// Used for debugging and logging
private static final String TAG = "NotePadProvider";
/**
* The database that the provider uses as its underlying data store
*/
private static final String DATABASE_NAME = "note_pad.db";
/**
* The database version
*/
private static final int DATABASE_VERSION = 2;
/**
* A projection map used to select columns from the database
*/
private static HashMap sNotesProjectionMap;
/**
* A projection map used to select columns from the database
*/
private static HashMap sLiveFolderProjectionMap;
/**
* Standard projection for the interesting columns of a normal note.
*/
private static final String[] READ_NOTE_PROJECTION = new String[] {
NotePad.Notes._ID, // Projection position 0, the note's id
NotePad.Notes.COLUMN_NAME_NOTE, // Projection position 1, the note's content
NotePad.Notes.COLUMN_NAME_TITLE, // Projection position 2, the note's title
};
private static final int READ_NOTE_NOTE_INDEX = 1;
private static final int READ_NOTE_TITLE_INDEX = 2;
/*
* Constants used by the Uri matcher to choose an action based on the pattern
* of the incoming URI
*/
// The incoming URI matches the Notes URI pattern
private static final int NOTES = 1;
// The incoming URI matches the Note ID URI pattern
private static final int NOTE_ID = 2;
// The incoming URI matches the Live Folder URI pattern
private static final int LIVE_FOLDER_NOTES = 3;
/**
* A UriMatcher instance
*/
private static final UriMatcher sUriMatcher;
// Handle to a new DatabaseHelper.
private DatabaseHelper mOpenHelper;
/**
* A block that instantiates and sets static objects
*/
static {
/*
* Creates and initializes the URI matcher
*/
// Create a new instance
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// Add a pattern that routes URIs terminated with "notes" to a NOTES operation
sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
// Add a pattern that routes URIs terminated with "notes" plus an integer
// to a note ID operation
sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
// Add a pattern that routes URIs terminated with live_folders/notes to a
// live folder operation
sUriMatcher.addURI(NotePad.AUTHORITY, "live_folders/notes", LIVE_FOLDER_NOTES);
/*
* Creates and initializes a projection map that returns all columns
*/
// Creates a new projection map instance. The map returns a column name
// given a string. The two are usually equal.
sNotesProjectionMap = new HashMap();
// Maps the string "_ID" to the column name "_ID"
sNotesProjectionMap.put(NotePad.Notes._ID, NotePad.Notes._ID);
// Maps "title" to "title"
sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_TITLE, NotePad.Notes.COLUMN_NAME_TITLE);
// Maps "note" to "note"
sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_NOTE, NotePad.Notes.COLUMN_NAME_NOTE);
// Maps "created" to "created"
sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE,
NotePad.Notes.COLUMN_NAME_CREATE_DATE);
// Maps "modified" to "modified"
sNotesProjectionMap.put(
NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE,
NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE);
/*
* Creates an initializes a projection map for handling Live Folders
*/
// Creates a new projection map instance
sLiveFolderProjectionMap = new HashMap();
// Maps "_ID" to "_ID AS _ID" for a live folder
sLiveFolderProjectionMap.put(LiveFolders._ID, NotePad.Notes._ID + " AS " + LiveFolders._ID);
// Maps "NAME" to "title AS NAME"
sLiveFolderProjectionMap.put(LiveFolders.NAME, NotePad.Notes.COLUMN_NAME_TITLE + " AS " +
LiveFolders.NAME);
}
/**
*
* This class helps open, create, and upgrade the database file. Set to package visibility
* for testing purposes.
*/
static class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context) {
// calls the super constructor, requesting the default cursor factory.
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
/**
*
* Creates the underlying database with table name and column names taken from the
* NotePad class.
*/
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + NotePad.Notes.TABLE_NAME + " ("
+ NotePad.Notes._ID + " INTEGER PRIMARY KEY,"
+ NotePad.Notes.COLUMN_NAME_TITLE + " TEXT,"
+ NotePad.Notes.COLUMN_NAME_NOTE + " TEXT,"
+ NotePad.Notes.COLUMN_NAME_CREATE_DATE + " INTEGER,"
+ NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE + " INTEGER"
+ ");");
}
/**
*
* Demonstrates that the provider must consider what happens when the
* underlying datastore is changed. In this sample, the database is upgraded the database
* by destroying the existing data.
* A real application should upgrade the database in place.
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Logs that the database is being upgraded
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
// Kills the table and existing data
db.execSQL("DROP TABLE IF EXISTS notes");
// Recreates the database with a new version
onCreate(db);
}
}
/**
*
* Initializes the provider by creating a new DatabaseHelper. onCreate() is called
* automatically when Android creates the provider in response to a resolver request from a
* client.
*/
@Override
public boolean onCreate() {
// Creates a new helper object. Note that the database itself isn't opened until
// something tries to access it, and it's only created if it doesn't already exist.
mOpenHelper = new DatabaseHelper(getContext());
// Assumes that any failures will be reported by a thrown exception.
return true;
}
/**
* This method is called when a client calls
* {@link android.content.ContentResolver#query(Uri, String[], String, String[], String)}.
* Queries the database and returns a cursor containing the results.
*
* @return A cursor containing the results of the query. The cursor exists but is empty if
* the query returns no results or an exception occurs.
* @throws IllegalArgumentException if the incoming URI pattern is invalid.
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// Constructs a new query builder and sets its table name
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(NotePad.Notes.TABLE_NAME);
/**
* Choose the projection and adjust the "where" clause based on URI pattern-matching.
*/
switch (sUriMatcher.match(uri)) {
// If the incoming URI is for notes, chooses the Notes projection
case NOTES:
qb.setProjectionMap(sNotesProjectionMap);
break;
/* If the incoming URI is for a single note identified by its ID, chooses the
* note ID projection, and appends "_ID = " to the where clause, so that
* it selects that single note
*/
case NOTE_ID:
qb.setProjectionMap(sNotesProjectionMap);
qb.appendWhere(
NotePad.Notes._ID + // the name of the ID column
"=" +
// the position of the note ID itself in the incoming URI
uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION));
break;
case LIVE_FOLDER_NOTES:
// If the incoming URI is from a live folder, chooses the live folder projection.
qb.setProjectionMap(sLiveFolderProjectionMap);
break;
default:
// If the URI doesn't match any of the known patterns, throw an exception.
throw new IllegalArgumentException("Unknown URI " + uri);
}
String orderBy;
// If no sort order is specified, uses the default
if (TextUtils.isEmpty(sortOrder)) {
orderBy = NotePad.Notes.DEFAULT_SORT_ORDER;
} else {
// otherwise, uses the incoming sort order
orderBy = sortOrder;
}
// Opens the database object in "read" mode, since no writes need to be done.
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
/*
* Performs the query. If no problems occur trying to read the database, then a Cursor
* object is returned; otherwise, the cursor variable contains null. If no records were
* selected, then the Cursor object is empty, and Cursor.getCount() returns 0.
*/
Cursor c = qb.query(
db, // The database to query
projection, // The columns to return from the query
selection, // The columns for the where clause
selectionArgs, // The values for the where clause
null, // don't group the rows
null, // don't filter by row groups
orderBy // The sort order
);
// Tells the Cursor what URI to watch, so it knows when its source data changes
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
/**
* This is called when a client calls {@link android.content.ContentResolver#getType(Uri)}.
* Returns the MIME data type of the URI given as a parameter.
*
* @param uri The URI whose MIME type is desired.
* @return The MIME type of the URI.
* @throws IllegalArgumentException if the incoming URI pattern is invalid.
*/
@Override
public String getType(Uri uri) {
/**
* Chooses the MIME type based on the incoming URI pattern
*/
switch (sUriMatcher.match(uri)) {
// If the pattern is for notes or live folders, returns the general content type.
case NOTES:
case LIVE_FOLDER_NOTES:
return NotePad.Notes.CONTENT_TYPE;
// If the pattern is for note IDs, returns the note ID content type.
case NOTE_ID:
return NotePad.Notes.CONTENT_ITEM_TYPE;
// If the URI pattern doesn't match any permitted patterns, throws an exception.
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
/**
* This describes the MIME types that are supported for opening a note
* URI as a stream.
*/
static ClipDescription NOTE_STREAM_TYPES = new ClipDescription(null,
new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN });
/**
* Returns the types of available data streams. URIs to specific notes are supported.
* The application can convert such a note to a plain text stream.
*
* @param uri the URI to analyze
* @param mimeTypeFilter The MIME type to check for. This method only returns a data stream
* type for MIME types that match the filter. Currently, only text/plain MIME types match.
* @return a data stream MIME type. Currently, only text/plan is returned.
* @throws IllegalArgumentException if the URI pattern doesn't match any supported patterns.
*/
@Override
public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
/**
* Chooses the data stream type based on the incoming URI pattern.
*/
switch (sUriMatcher.match(uri)) {
// If the pattern is for notes or live folders, return null. Data streams are not
// supported for this type of URI.
case NOTES:
case LIVE_FOLDER_NOTES:
return null;
// If the pattern is for note IDs and the MIME filter is text/plain, then return
// text/plain
case NOTE_ID:
return NOTE_STREAM_TYPES.filterMimeTypes(mimeTypeFilter);
// If the URI pattern doesn't match any permitted patterns, throws an exception.
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
/**
* Returns a stream of data for each supported stream type. This method does a query on the
* incoming URI, then uses
* {@link android.content.ContentProvider#openPipeHelper(Uri, String, Bundle, Object,
* PipeDataWriter)} to start another thread in which to convert the data into a stream.
*
* @param uri The URI pattern that points to the data stream
* @param mimeTypeFilter A String containing a MIME type. This method tries to get a stream of
* data with this MIME type.
* @param opts Additional options supplied by the caller. Can be interpreted as
* desired by the content provider.
* @return AssetFileDescriptor A handle to the file.
* @throws FileNotFoundException if there is no file associated with the incoming URI.
*/
@Override
public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
throws FileNotFoundException {
// Checks to see if the MIME type filter matches a supported MIME type.
String[] mimeTypes = getStreamTypes(uri, mimeTypeFilter);
// If the MIME type is supported
if (mimeTypes != null) {
// Retrieves the note for this URI. Uses the query method defined for this provider,
// rather than using the database query method.
Cursor c = query(
uri, // The URI of a note
READ_NOTE_PROJECTION, // Gets a projection containing the note's ID, title,
// and contents
null, // No WHERE clause, get all matching records
null, // Since there is no WHERE clause, no selection criteria
null // Use the default sort order (modification date,
// descending
);
// If the query fails or the cursor is empty, stop
if (c == null || !c.moveToFirst()) {
// If the cursor is empty, simply close the cursor and return
if (c != null) {
c.close();
}
// If the cursor is null, throw an exception
throw new FileNotFoundException("Unable to query " + uri);
}
// Start a new thread that pipes the stream data back to the caller.
return new AssetFileDescriptor(
openPipeHelper(uri, mimeTypes[0], opts, c, this), 0,
AssetFileDescriptor.UNKNOWN_LENGTH);
}
// If the MIME type is not supported, return a read-only handle to the file.
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) {
}
}
}
/**
* This is called when a client calls
* {@link android.content.ContentResolver#insert(Uri, ContentValues)}.
* Inserts a new row into the database. This method sets up default values for any
* columns that are not included in the incoming map.
* If rows were inserted, then listeners are notified of the change.
* @return The row ID of the inserted row.
* @throws SQLException if the insertion fails.
*/
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
// Validates the incoming URI. Only the full provider URI is allowed for inserts.
if (sUriMatcher.match(uri) != NOTES) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
// A map to hold the new record's values.
ContentValues values;
// If the incoming values map is not null, uses it for the new values.
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
// Otherwise, create a new value map
values = new ContentValues();
}
// Gets the current system time in milliseconds
Long now = Long.valueOf(System.currentTimeMillis());
// If the values map doesn't contain the creation date, sets the value to the current time.
if (values.containsKey(NotePad.Notes.COLUMN_NAME_CREATE_DATE) == false) {
values.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, now);
}
// If the values map doesn't contain the modification date, sets the value to the current
// time.
if (values.containsKey(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE) == false) {
values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, now);
}
// If the values map doesn't contain a title, sets the value to the default title.
if (values.containsKey(NotePad.Notes.COLUMN_NAME_TITLE) == false) {
Resources r = Resources.getSystem();
values.put(NotePad.Notes.COLUMN_NAME_TITLE, r.getString(android.R.string.untitled));
}
// If the values map doesn't contain note text, sets the value to an empty string.
if (values.containsKey(NotePad.Notes.COLUMN_NAME_NOTE) == false) {
values.put(NotePad.Notes.COLUMN_NAME_NOTE, "");
}
// Opens the database object in "write" mode.
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
// Performs the insert and returns the ID of the new note.
long rowId = db.insert(
NotePad.Notes.TABLE_NAME, // The table to insert into.
NotePad.Notes.COLUMN_NAME_NOTE, // A hack, SQLite sets this column value to null
// if values is empty.
values // A map of column names, and the values to insert
// into the columns.
);
// If the insert succeeded, the row ID exists.
if (rowId > 0) {
// Creates a URI with the note ID pattern and the new row ID appended to it.
Uri noteUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, rowId);
// Notifies observers registered against this provider that the data changed.
getContext().getContentResolver().notifyChange(noteUri, null);
return noteUri;
}
// If the insert didn't succeed, then the rowID is <= 0. Throws an exception.
throw new SQLException("Failed to insert row into " + uri);
}
/**
* This is called when a client calls
* {@link android.content.ContentResolver#delete(Uri, String, String[])}.
* Deletes records from the database. If the incoming URI matches the note ID URI pattern,
* this method deletes the one record specified by the ID in the URI. Otherwise, it deletes a
* a set of records. The record or records must also match the input selection criteria
* specified by where and whereArgs.
*
* If rows were deleted, then listeners are notified of the change.
* @return If a "where" clause is used, the number of rows affected is returned, otherwise
* 0 is returned. To delete all rows and get a row count, use "1" as the where clause.
* @throws IllegalArgumentException if the incoming URI pattern is invalid.
*/
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
// Opens the database object in "write" mode.
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
String finalWhere;
int count;
// Does the delete based on the incoming URI pattern.
switch (sUriMatcher.match(uri)) {
// If the incoming pattern matches the general pattern for notes, does a delete
// based on the incoming "where" columns and arguments.
case NOTES:
count = db.delete(
NotePad.Notes.TABLE_NAME, // The database table name
where, // The incoming where clause column names
whereArgs // The incoming where clause values
);
break;
// If the incoming URI matches a single note ID, does the delete based on the
// incoming data, but modifies the where clause to restrict it to the
// particular note ID.
case NOTE_ID:
/*
* Starts a final WHERE clause by restricting it to the
* desired note ID.
*/
finalWhere =
NotePad.Notes._ID + // The ID column name
" = " + // test for equality
uri.getPathSegments(). // the incoming note ID
get(NotePad.Notes.NOTE_ID_PATH_POSITION)
;
// If there were additional selection criteria, append them to the final
// WHERE clause
if (where != null) {
finalWhere = finalWhere + " AND " + where;
}
// Performs the delete.
count = db.delete(
NotePad.Notes.TABLE_NAME, // The database table name.
finalWhere, // The final WHERE clause
whereArgs // The incoming where clause values.
);
break;
// If the incoming pattern is invalid, throws an exception.
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
/*Gets a handle to the content resolver object for the current context, and notifies it
* that the incoming URI changed. The object passes this along to the resolver framework,
* and observers that have registered themselves for the provider are notified.
*/
getContext().getContentResolver().notifyChange(uri, null);
// Returns the number of rows deleted.
return count;
}
/**
* This is called when a client calls
* {@link android.content.ContentResolver#update(Uri,ContentValues,String,String[])}
* Updates records in the database. The column names specified by the keys in the values map
* are updated with new data specified by the values in the map. If the incoming URI matches the
* note ID URI pattern, then the method updates the one record specified by the ID in the URI;
* otherwise, it updates a set of records. The record or records must match the input
* selection criteria specified by where and whereArgs.
* If rows were updated, then listeners are notified of the change.
*
* @param uri The URI pattern to match and update.
* @param values A map of column names (keys) and new values (values).
* @param where An SQL "WHERE" clause that selects records based on their column values. If this
* is null, then all records that match the URI pattern are selected.
* @param whereArgs An array of selection criteria. If the "where" param contains value
* placeholders ("?"), then each placeholder is replaced by the corresponding element in the
* array.
* @return The number of rows updated.
* @throws IllegalArgumentException if the incoming URI pattern is invalid.
*/
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
// Opens the database object in "write" mode.
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
String finalWhere;
// Does the update based on the incoming URI pattern
switch (sUriMatcher.match(uri)) {
// If the incoming URI matches the general notes pattern, does the update based on
// the incoming data.
case NOTES:
// Does the update and returns the number of rows updated.
count = db.update(
NotePad.Notes.TABLE_NAME, // The database table name.
values, // A map of column names and new values to use.
where, // The where clause column names.
whereArgs // The where clause column values to select on.
);
break;
// If the incoming URI matches a single note ID, does the update based on the incoming
// data, but modifies the where clause to restrict it to the particular note ID.
case NOTE_ID:
// From the incoming URI, get the note ID
String noteId = uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION);
/*
* Starts creating the final WHERE clause by restricting it to the incoming
* note ID.
*/
finalWhere =
NotePad.Notes._ID + // The ID column name
" = " + // test for equality
uri.getPathSegments(). // the incoming note ID
get(NotePad.Notes.NOTE_ID_PATH_POSITION)
;
// If there were additional selection criteria, append them to the final WHERE
// clause
if (where !=null) {
finalWhere = finalWhere + " AND " + where;
}
// Does the update and returns the number of rows updated.
count = db.update(
NotePad.Notes.TABLE_NAME, // The database table name.
values, // A map of column names and new values to use.
finalWhere, // The final WHERE clause to use
// placeholders for whereArgs
whereArgs // The where clause column values to select on, or
// null if the values are in the where argument.
);
break;
// If the incoming pattern is invalid, throws an exception.
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
/*Gets a handle to the content resolver object for the current context, and notifies it
* that the incoming URI changed. The object passes this along to the resolver framework,
* and observers that have registered themselves for the provider are notified.
*/
getContext().getContentResolver().notifyChange(uri, null);
// Returns the number of rows updated.
return count;
}
/**
* A test package can call this to get a handle to the database underlying NotePadProvider,
* so it can insert test data into the database. The test case class is responsible for
* instantiating the provider in a test context; {@link android.test.ProviderTestCase2} does
* this during the call to setUp()
*
* @return a handle to the database helper object for the provider's data.
*/
DatabaseHelper getOpenHelperForTest() {
return mOpenHelper;
}
}
//src\com\example\android\notepad\NotesList.java
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.notepad;
import com.example.android.notepad.NotePad;
import android.app.ListActivity;
import android.content.ClipboardManager;
import android.content.ClipData;
import android.content.ComponentName;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
/**
* Displays a list of notes. Will display notes from the {@link Uri}
* provided in the incoming Intent if there is one, otherwise it defaults to displaying the
* contents of the {@link NotePadProvider}.
*
* NOTE: Notice that the provider operations in this Activity are taking place on the UI thread.
* This is not a good practice. It is only done here to make the code more readable. A real
* application should use the {@link android.content.AsyncQueryHandler} or
* {@link android.os.AsyncTask} object to perform operations asynchronously on a separate thread.
*/
public class NotesList extends ListActivity {
// For logging and debugging
private static final String TAG = "NotesList";
/**
* The columns needed by the cursor adapter
*/
private static final String[] PROJECTION = new String[] {
NotePad.Notes._ID, // 0
NotePad.Notes.COLUMN_NAME_TITLE, // 1
};
/** The index of the title column */
private static final int COLUMN_INDEX_TITLE = 1;
/**
* onCreate is called when Android starts this Activity from scratch.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// The user does not need to hold down the key to use menu shortcuts.
setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
/* If no data is given in the Intent that started this Activity, then this Activity
* was started when the intent filter matched a MAIN action. We should use the default
* provider URI.
*/
// Gets the intent that started this Activity.
Intent intent = getIntent();
// If there is no data associated with the Intent, sets the data to the default URI, which
// accesses a list of notes.
if (intent.getData() == null) {
intent.setData(NotePad.Notes.CONTENT_URI);
}
/*
* Sets the callback for context menu activation for the ListView. The listener is set
* to be this Activity. The effect is that context menus are enabled for items in the
* ListView, and the context menu is handled by a method in NotesList.
*/
getListView().setOnCreateContextMenuListener(this);
/* Performs a managed query. The Activity handles closing and requerying the cursor
* when needed.
*
* Please see the introductory note about performing provider operations on the UI thread.
*/
Cursor cursor = managedQuery(
getIntent().getData(), // Use the default content URI for the provider.
PROJECTION, // Return the note ID and title for each note.
null, // No where clause, return all records.
null, // No where clause, therefore no where column values.
NotePad.Notes.DEFAULT_SORT_ORDER // Use the default sort order.
);
/*
* The following two arrays create a "map" between columns in the cursor and view IDs
* for items in the ListView. Each element in the dataColumns array represents
* a column name; each element in the viewID array represents the ID of a View.
* The SimpleCursorAdapter maps them in ascending order to determine where each column
* value will appear in the ListView.
*/
// The names of the cursor columns to display in the view, initialized to the title column
String[] dataColumns = { NotePad.Notes.COLUMN_NAME_TITLE } ;
// The view IDs that will display the cursor columns, initialized to the TextView in
// noteslist_item.xml
int[] viewIDs = { android.R.id.text1 };
// Creates the backing adapter for the ListView.
SimpleCursorAdapter adapter
= new SimpleCursorAdapter(
this, // The Context for the ListView
R.layout.noteslist_item, // Points to the XML for a list item
cursor, // The cursor to get items from
dataColumns,
viewIDs
);
// Sets the ListView's adapter to be the cursor adapter that was just created.
setListAdapter(adapter);
}
/**
* Called when the user clicks the device's Menu button the first time for
* this Activity. Android passes in a Menu object that is populated with items.
*
* Sets up a menu that provides the Insert option plus a list of alternative actions for
* this Activity. Other applications that want to handle notes can "register" themselves in
* Android by providing an intent filter that includes the category ALTERNATIVE and the
* mimeTYpe NotePad.Notes.CONTENT_TYPE. If they do this, the code in onCreateOptionsMenu()
* will add the Activity that contains the intent filter to its list of options. In effect,
* the menu will offer the user other applications that can handle notes.
* @param menu A Menu object, to which menu items should be added.
* @return True, always. The menu should be displayed.
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate menu from XML resource
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.list_options_menu, menu);
// 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
// our menu with their own actions.
Intent intent = new Intent(null, getIntent().getData());
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
new ComponentName(this, NotesList.class), null, intent, 0, null);
return super.onCreateOptionsMenu(menu);
}
@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);
MenuItem mPasteItem = menu.findItem(R.id.menu_paste);
// If the clipboard contains an item, enables the Paste option on the menu.
if (clipboard.hasPrimaryClip()) {
mPasteItem.setEnabled(true);
} else {
// If the clipboard is empty, disables the menu's Paste option.
mPasteItem.setEnabled(false);
}
// Gets the number of notes currently being displayed.
final boolean haveItems = getListAdapter().getCount() > 0;
// If there are any notes in the list (which implies that one of
// them is selected), then we need to generate the actions that
// can be performed on the current selection. This will be a combination
// of our own specific actions along with any extensions that can be
// found.
if (haveItems) {
// This is the selected item.
Uri uri = ContentUris.withAppendedId(getIntent().getData(), getSelectedItemId());
// Creates an array of Intents with one element. This will be used to send an Intent
// based on the selected menu item.
Intent[] specifics = new Intent[1];
// Sets the Intent in the array to be an EDIT action on the URI of the selected note.
specifics[0] = new Intent(Intent.ACTION_EDIT, uri);
// Creates an array of menu items with one element. This will contain the EDIT option.
MenuItem[] items = new MenuItem[1];
// Creates an Intent with no specific action, using the URI of the selected note.
Intent intent = new Intent(null, uri);
/* Adds the category ALTERNATIVE to the Intent, with the note ID URI as its
* data. This prepares the Intent as a place to group alternative options in the
* menu.
*/
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
/*
* Add alternatives to the menu
*/
menu.addIntentOptions(
Menu.CATEGORY_ALTERNATIVE, // Add the Intents as options in the alternatives group.
Menu.NONE, // A unique item ID is not required.
Menu.NONE, // The alternatives don't need to be in order.
null, // The caller's name is not excluded from the group.
specifics, // These specific options must appear first.
intent, // These Intent objects map to the options in specifics.
Menu.NONE, // No flags are required.
items // The menu items generated from the specifics-to-
// Intents mapping
);
// If the Edit menu item exists, adds shortcuts for it.
if (items[0] != null) {
// Sets the Edit menu item shortcut to numeric "1", letter "e"
items[0].setShortcut('1', 'e');
}
} else {
// If the list is empty, removes any existing alternative actions from the menu
menu.removeGroup(Menu.CATEGORY_ALTERNATIVE);
}
// Displays the menu
return true;
}
/**
* This method is called when the user selects an option from the menu, but no item
* in the list is selected. If the option was INSERT, then a new Intent is sent out with action
* ACTION_INSERT. The data from the incoming Intent is put into the new Intent. In effect,
* this triggers the NoteEditor activity in the NotePad application.
*
* If the item was not INSERT, then most likely it was an alternative option from another
* application. The parent method is called to process the item.
* @param item The menu item that was selected by the user
* @return True, if the INSERT menu item was selected; otherwise, the result of calling
* the parent method.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_add:
/*
* Launches a new Activity using an Intent. The intent filter for the Activity
* has to have action ACTION_INSERT. No category is set, so DEFAULT is assumed.
* In effect, this starts the NoteEditor Activity in NotePad.
*/
startActivity(new Intent(Intent.ACTION_INSERT, getIntent().getData()));
return true;
case R.id.menu_paste:
/*
* Launches a new Activity using an Intent. The intent filter for the Activity
* has to have action ACTION_PASTE. No category is set, so DEFAULT is assumed.
* In effect, this starts the NoteEditor Activity in NotePad.
*/
startActivity(new Intent(Intent.ACTION_PASTE, getIntent().getData()));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* This method is called when the user context-clicks a note in the list. NotesList registers
* itself as the handler for context menus in its ListView (this is done in onCreate()).
*
* The only available options are COPY and DELETE.
*
* Context-click is equivalent to long-press.
*
* @param menu A ContexMenu object to which items should be added.
* @param view The View for which the context menu is being constructed.
* @param menuInfo Data associated with view.
* @throws ClassCastException
*/
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
// The data from the menu item.
AdapterView.AdapterContextMenuInfo info;
// Tries to get the position of the item in the ListView that was long-pressed.
try {
// Casts the incoming data object into the type for AdapterView objects.
info = (AdapterView.AdapterContextMenuInfo) menuInfo;
} catch (ClassCastException e) {
// If the menu object can't be cast, logs an error.
Log.e(TAG, "bad menuInfo", e);
return;
}
/*
* Gets the data associated with the item at the selected position. getItem() returns
* whatever the backing adapter of the ListView has associated with the item. In NotesList,
* the adapter associated all of the data for a note with its list item. As a result,
* getItem() returns that data as a Cursor.
*/
Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
// If the cursor is empty, then for some reason the adapter can't get the data from the
// provider, so returns null to the caller.
if (cursor == null) {
// For some reason the requested item isn't available, do nothing
return;
}
// Inflate menu from XML resource
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.list_context_menu, menu);
// Sets the menu header to be the title of the selected note.
menu.setHeaderTitle(cursor.getString(COLUMN_INDEX_TITLE));
// Append to the
// menu items for any other activities that can do stuff with it
// as well. This does a query on the system for any activities that
// implement the ALTERNATIVE_ACTION for our data, adding a menu item
// for each one that is found.
Intent intent = new Intent(null, Uri.withAppendedPath(getIntent().getData(),
Integer.toString((int) info.id) ));
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
new ComponentName(this, NotesList.class), null, intent, 0, null);
}
/**
* This method is called when the user selects an item from the context menu
* (see onCreateContextMenu()). The only menu items that are actually handled are DELETE and
* COPY. Anything else is an alternative option, for which default handling should be done.
*
* @param item The selected menu item
* @return True if the menu item was DELETE, and no default processing is need, otherwise false,
* which triggers the default handling of the item.
* @throws ClassCastException
*/
@Override
public boolean onContextItemSelected(MenuItem item) {
// The data from the menu item.
AdapterView.AdapterContextMenuInfo info;
/*
* Gets the extra info from the menu item. When an note in the Notes list is long-pressed, a
* context menu appears. The menu items for the menu automatically get the data
* associated with the note that was long-pressed. The data comes from the provider that
* backs the list.
*
* The note's data is passed to the context menu creation routine in a ContextMenuInfo
* object.
*
* When one of the context menu items is clicked, the same data is passed, along with the
* note ID, to onContextItemSelected() via the item parameter.
*/
try {
// Casts the data object in the item into the type for AdapterView objects.
info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
} catch (ClassCastException e) {
// If the object can't be cast, logs an error
Log.e(TAG, "bad menuInfo", e);
// Triggers default processing of the menu item.
return false;
}
// Appends the selected note's ID to the URI sent with the incoming Intent.
Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id);
/*
* Gets the menu item's ID and compares it to known actions.
*/
switch (item.getItemId()) {
case R.id.context_open:
// Launch activity to view/edit the currently selected item
startActivity(new Intent(Intent.ACTION_EDIT, noteUri));
return true;
case R.id.context_copy:
// Gets a handle to the clipboard service.
ClipboardManager clipboard = (ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
// Copies the notes URI to the clipboard. In effect, this copies the note itself
clipboard.setPrimaryClip(ClipData.newUri( // new clipboard item holding a URI
getContentResolver(), // resolver to retrieve URI info
"Note", // label for the clip
noteUri) // the URI
);
// Returns to the caller and skips further processing.
return true;
case R.id.context_delete:
// Deletes the note from the provider by passing in a URI in note ID format.
// Please see the introductory note about performing provider operations on the
// UI thread.
getContentResolver().delete(
noteUri, // The URI of the provider
null, // No where clause is needed, since only a single note ID is being
// passed in.
null // No where clause is used, so no where arguments are needed.
);
// Returns to the caller and skips further processing.
return true;
default:
return super.onContextItemSelected(item);
}
}
/**
* This method is called when the user clicks a note in the displayed list.
*
* This method handles incoming actions of either PICK (get data from the provider) or
* GET_CONTENT (get or create data). If the incoming action is EDIT, this method sends a
* new Intent to start NoteEditor.
* @param l The ListView that contains the clicked item
* @param v The View of the individual item
* @param position The position of v in the displayed list
* @param id The row ID of the clicked item
*/
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
// Constructs a new URI from the incoming URI and the row ID
Uri uri = ContentUris.withAppendedId(getIntent().getData(), id);
// Gets the action from the incoming Intent
String action = getIntent().getAction();
// Handles requests for note data
if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)) {
// Sets the result to return to the component that called this Activity. The
// result contains the new URI
setResult(RESULT_OK, new Intent().setData(uri));
} else {
// Sends out an Intent to start an Activity that can handle ACTION_EDIT. The
// Intent's data is the note ID URI. The effect is to call NoteEdit.
startActivity(new Intent(Intent.ACTION_EDIT, uri));
}
}
}
//src\com\example\android\notepad\NotesLiveFolder.java
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.notepad;
import com.example.android.notepad.NotePad;
import android.app.Activity;
import android.content.Intent;
import android.content.Intent.ShortcutIconResource;
import android.os.Bundle;
import android.provider.LiveFolders;
/**
* This Activity creates a live folder Intent and
* sends it back to HOME. From the data in the Intent, HOME creates a live folder and displays
* its icon in the Home view.
* When the user clicks the icon, Home uses the data it got from the Intent to retrieve information
* from a content provider and display it in a View.
*
* The intent filter for this Activity is set to ACTION_CREATE_LIVE_FOLDER, which
* HOME sends in response to a long press and selection of Live Folder.
*/
public class NotesLiveFolder extends Activity {
/**
* All of the work is done in onCreate(). The Activity doesn't actually display a UI.
* Instead, it sets up an Intent and returns it to its caller (the HOME activity).
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
* Gets the incoming Intent and its action. If the incoming Intent was
* ACTION_CREATE_LIVE_FOLDER, then create an outgoing Intent with the
* necessary data and send back OK. Otherwise, send back CANCEL.
*/
final Intent intent = getIntent();
final String action = intent.getAction();
if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action)) {
// Creates a new Intent.
final Intent liveFolderIntent = new Intent();
/*
* The following statements put data into the outgoing Intent. Please see
* {@link android.provider.LiveFolders for a detailed description of these
* data values. From this data, HOME sets up a live folder.
*/
// Sets the URI pattern for the content provider backing the folder.
liveFolderIntent.setData(NotePad.Notes.LIVE_FOLDER_URI);
// Adds the display name of the live folder as an Extra string.
String foldername = getString(R.string.live_folder_name);
liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, foldername);
// Adds the display icon of the live folder as an Extra resource.
ShortcutIconResource foldericon =
Intent.ShortcutIconResource.fromContext(this, R.drawable.live_folder_notes);
liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON, foldericon);
// Add the display mode of the live folder as an integer. The specified
// mode causes the live folder to display as a list.
liveFolderIntent.putExtra(
LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE,
LiveFolders.DISPLAY_MODE_LIST);
/*
* Adds a base action for items in the live folder list, as an Intent. When the
* user clicks an individual note in the list, the live folder fires this Intent.
*
* Its action is ACTION_EDIT, so it triggers the Note Editor activity. Its
* data is the URI pattern for a single note identified by its ID. The live folder
* automatically adds the ID value of the selected item to the URI pattern.
*
* As a result, Note Editor is triggered and gets a single note to retrieve by ID.
*/
Intent returnIntent
= new Intent(Intent.ACTION_EDIT, NotePad.Notes.CONTENT_ID_URI_PATTERN);
liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT, returnIntent);
/* Creates an ActivityResult object to propagate back to HOME. Set its result indicator
* to OK, and sets the returned Intent to the live folder Intent that was just
* constructed.
*/
setResult(RESULT_OK, liveFolderIntent);
} else {
// If the original action was not ACTION_CREATE_LIVE_FOLDER, creates an
// ActivityResult with the indicator set to CANCELED, but do not return an Intent
setResult(RESULT_CANCELED);
}
// Closes the Activity. The ActivityObject is propagated back to the caller.
finish();
}
}
//src\com\example\android\notepad\TitleEditor.java
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.notepad;
import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
/**
* This Activity allows the user to edit a note's title. It displays a floating window
* containing an EditText.
*
* NOTE: Notice that the provider operations in this Activity are taking place on the UI thread.
* This is not a good practice. It is only done here to make the code more readable. A real
* application should use the {@link android.content.AsyncQueryHandler}
* or {@link android.os.AsyncTask} object to perform operations asynchronously on a separate thread.
*/
public class TitleEditor extends Activity {
/**
* This is a special intent action that means "edit the title of a note".
*/
public static final String EDIT_TITLE_ACTION = "com.android.notepad.action.EDIT_TITLE";
// Creates a projection that returns the note ID and the note contents.
private static final String[] PROJECTION = new String[] {
NotePad.Notes._ID, // 0
NotePad.Notes.COLUMN_NAME_TITLE, // 1
};
// The position of the title column in a Cursor returned by the provider.
private static final int COLUMN_INDEX_TITLE = 1;
// A Cursor object that will contain the results of querying the provider for a note.
private Cursor mCursor;
// An EditText object for preserving the edited title.
private EditText mText;
// A URI object for the note whose title is being edited.
private Uri mUri;
/**
* This method is called by Android when the Activity is first started. From the incoming
* Intent, it determines what kind of editing is desired, and then does it.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set the View for this Activity object's UI.
setContentView(R.layout.title_editor);
// Get the Intent that activated this Activity, and from it get the URI of the note whose
// title we need to edit.
mUri = getIntent().getData();
/*
* Using the URI passed in with the triggering Intent, gets the note.
*
* Note: This is being done on the UI thread. It will block the thread until the query
* completes. In a sample app, going against a simple provider based on a local database,
* the block will be momentary, but in a real app you should use
* android.content.AsyncQueryHandler or android.os.AsyncTask.
*/
mCursor = managedQuery(
mUri, // The URI for the note that is to be retrieved.
PROJECTION, // The columns to retrieve
null, // No selection criteria are used, so no where columns are needed.
null, // No where columns are used, so no where values are needed.
null // No sort order is needed.
);
// Gets the View ID for the EditText box
mText = (EditText) this.findViewById(R.id.title);
}
/**
* This method is called when the Activity is about to come to the foreground. This happens
* when the Activity comes to the top of the task stack, OR when it is first starting.
*
* Displays the current title for the selected note.
*/
@Override
protected void onResume() {
super.onResume();
// Verifies that the query made in onCreate() actually worked. If it worked, then the
// Cursor object is not null. If it is *empty*, then mCursor.getCount() == 0.
if (mCursor != null) {
// The Cursor was just retrieved, so its index is set to one record *before* the first
// record retrieved. This moves it to the first record.
mCursor.moveToFirst();
// Displays the current title text in the EditText object.
mText.setText(mCursor.getString(COLUMN_INDEX_TITLE));
}
}
/**
* This method is called when the Activity loses focus.
*
* For Activity objects that edit information, onPause() may be the one place where changes are
* saved. The Android application model is predicated on the idea that "save" and "exit" aren't
* required actions. When users navigate away from an Activity, they shouldn't have to go back
* to it to complete their work. The act of going away should save everything and leave the
* Activity in a state where Android can destroy it if necessary.
*
* Updates the note with the text currently in the text box.
*/
@Override
protected void onPause() {
super.onPause();
// Verifies that the query made in onCreate() actually worked. If it worked, then the
// Cursor object is not null. If it is *empty*, then mCursor.getCount() == 0.
if (mCursor != null) {
// Creates a values map for updating the provider.
ContentValues values = new ContentValues();
// In the values map, sets the title to the current contents of the edit box.
values.put(NotePad.Notes.COLUMN_NAME_TITLE, mText.getText().toString());
/*
* Updates the provider with the note's new title.
*
* Note: This is being done on the UI thread. It will block the thread until the
* update completes. In a sample app, going against a simple provider based on a
* local database, the block will be momentary, but in a real app you should use
* android.content.AsyncQueryHandler or android.os.AsyncTask.
*/
getContentResolver().update(
mUri, // The URI for the note to update.
values, // The values map containing the columns to update and the values to use.
null, // No selection criteria is used, so no "where" columns are needed.
null // No "where" columns are used, so no "where" values are needed.
);
}
}
public void onClickOk(View v) {
finish();
}
}
//
//res\layout\note_editor.xml
class="com.example.android.notepad.NoteEditor$LinedEditText"
android:id="@+id/note"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:padding="5dp"
android:scrollbars="vertical"
android:fadingEdge="vertical"
android:gravity="top"
android:textSize="22sp"
android:capitalize="sentences"
/>
//res\layout\noteslist_item.xml
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center_vertical"
android:paddingLeft="5dip"
android:singleLine="true"
/>
//res\layout\title_editor.xml
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="6dip"
android:paddingRight="6dip"
android:paddingBottom="3dip">
android:maxLines="1"
android:layout_marginTop="2dp"
android:layout_marginBottom="15dp"
android:layout_width="wrap_content"
android:ems="25"
android:layout_height="wrap_content"
android:autoText="true"
android:capitalize="sentences"
android:scrollHorizontally="true" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="@string/button_ok"
android:onClick="onClickOk" />
//
//res\menu\editor_options_menu.xml
//res\menu\list_context_menu.xml
//res\menu\list_options_menu.xml
//
//res\values\strings.xml
NotePad
Notes
Note title:
New note
Edit: %1$s
Notes
New note
Save
Delete
Open
Revert changes
Copy
Paste
OK
Title:
Edit note
Edit title
Error
Error loading note
There is nothing to save