Revised Note Pad sample, new test app for Note Pad
Change-Id: Ia41a33d935ead704c1de439a0cfb0a55806cfe12
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2007 The Android Open Source Project
|
||||
<!-- Copyright (C) 2010 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.
|
||||
@@ -14,26 +14,69 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<!-- Declare the contents of this Android application. The namespace
|
||||
attribute brings in the Android platform namespace, and the package
|
||||
supplies a unique name for the application. When writing your
|
||||
own application, the package name must be changed from "com.example.*"
|
||||
to come from a domain that you own or have control over. -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.notepad"
|
||||
>
|
||||
<application android:icon="@drawable/app_notes"
|
||||
android:label="@string/app_name"
|
||||
>
|
||||
<provider android:name="NotePadProvider"
|
||||
android:authorities="com.google.provider.NotePad"
|
||||
/>
|
||||
<!--
|
||||
The manifest declares information about this Android application that is of
|
||||
interest to the system and to other applications.
|
||||
The namespace attribute brings in the Android platform namespace. The package name is
|
||||
an Android identifier for the application. It uses names in the Java package id format,
|
||||
but is not dependent on or linked to any of the Java package names in the application.
|
||||
It must be unique. If you use this manifest as part of your own application, you must
|
||||
change the name prefix from "com.example." to a domain that you own or control.
|
||||
|
||||
<activity android:name="NotesList" android:label="@string/title_notes_list">
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.notepad"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<!--
|
||||
Declares the attributes of the application.
|
||||
The icon and label are displayed in the Launcher and in
|
||||
other utilities that list applications.
|
||||
-->
|
||||
<application
|
||||
android:icon="@drawable/app_notes"
|
||||
android:label="@string/app_name" >
|
||||
|
||||
<!--
|
||||
Declares the content provider offered by this application. The name is an
|
||||
identifier. The authorities attribute is stored by the Android system in a global
|
||||
table, along with a handle to the current instance of the provider.
|
||||
Clients can then link to the provider through the global map by providing
|
||||
the authority string to Android.
|
||||
-->
|
||||
|
||||
<provider
|
||||
android:name="NotePadProvider"
|
||||
android:authorities="com.example.android.notepad.NotePad" />
|
||||
|
||||
<!--
|
||||
Declares the NotesList activity, and gives it a name and a label.
|
||||
The intent filters determine the types of intents that this activity will see.
|
||||
The NotesList activity displays notes and returns single notes instances to
|
||||
other activities.
|
||||
-->
|
||||
<activity
|
||||
android:name="NotesList"
|
||||
android:label="@string/title_notes_list">
|
||||
<!--
|
||||
Defines the main entry point into NotePad (MAIN), which doesn't
|
||||
require any additional information. LAUNCHER tells launchers to display this
|
||||
Activity.
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!--
|
||||
Defines actions the Activity can do on a directory of notes.
|
||||
It allows the user to view a list of notes, edit a list of notes, or pick one
|
||||
of the notes. The intent's MIME type must match the MIME type specified by the
|
||||
<data> element. That element specifies a cursor of data containing
|
||||
vnd.google.note objects (defined in this app).
|
||||
Notice that all intent filters that don't specify MAIN must specify
|
||||
DEFAULT as a category
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.EDIT" />
|
||||
@@ -41,6 +84,11 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
|
||||
</intent-filter>
|
||||
<!--
|
||||
Defines an action the Activity can do for a single note. If the action is
|
||||
GET_CONTENT, then the Activity can return a cursor containing at most 1
|
||||
note object (an item) identified as a vnd.google.note item.
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -48,14 +96,26 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="NoteEditor"
|
||||
<!--
|
||||
Defines the NoteEditor activity. This activity edits or displays a single note.
|
||||
The screen orientation displayed by this activity depends on the device orientation
|
||||
(screenOrientation).
|
||||
The activity uses a light theme (android:theme)
|
||||
When the keyboard is closed (or hidden) or the device orientation changes, the
|
||||
Activity will handle it. The Activity does not restart, but onConfigurationChanged() is
|
||||
called (This has been deprecated; developers should let Android call onDestroy() and
|
||||
then restore the app state in onCreate()
|
||||
-->
|
||||
<activity
|
||||
android:name="NoteEditor"
|
||||
android:theme="@android:style/Theme.Light"
|
||||
android:label="@string/title_note"
|
||||
android:screenOrientation="sensor"
|
||||
android:configChanges="keyboardHidden|orientation"
|
||||
>
|
||||
<!-- This filter says that we can view or edit the data of
|
||||
a single note -->
|
||||
android:configChanges="keyboardHidden|orientation" >
|
||||
<!--
|
||||
Allows viewing or editing data, including the application-created EDIT_NOTE
|
||||
intent, for a single note of type vnd.google.note
|
||||
-->
|
||||
<intent-filter android:label="@string/resolve_edit">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.EDIT" />
|
||||
@@ -64,10 +124,9 @@
|
||||
<data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- This filter says that we can create a new note inside
|
||||
of a directory of notes. The INSERT action creates an
|
||||
empty note; the PASTE action initializes a new note from
|
||||
the current contents of the clipboard. -->
|
||||
<!--
|
||||
Allows inserting a note into a list of notes of type vnd.google.note
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.INSERT" />
|
||||
<action android:name="android.intent.action.PASTE" />
|
||||
@@ -77,42 +136,53 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="TitleEditor"
|
||||
<!--
|
||||
Defines the title editor Activity. This Activity uses a dialog view, which
|
||||
looks like a regular dialog but is reachable by an Intent and uses
|
||||
intent filters. The soft keyboard is made visible when input is expected.
|
||||
-->
|
||||
<activity
|
||||
android:name="TitleEditor"
|
||||
android:label="@string/title_edit_title"
|
||||
android:theme="@android:style/Theme.Dialog"
|
||||
android:windowSoftInputMode="stateVisible">
|
||||
<!-- This activity implements an alternative action that can be
|
||||
performed on notes: editing their title. It can be used as
|
||||
a default operation if the user invokes this action, and is
|
||||
available as an alternative action for any note data. -->
|
||||
<!--
|
||||
This intent-filter accepts the application-specific EDIT_TITLE intent. It
|
||||
also accepts DEFAULT (required by non-MAIN activities). Because it is specified
|
||||
with ALTERNATIVE and SELECTED_ALTERNATIVE, the activity will appear as an
|
||||
alternative in a list of activities that work on the data type listed.
|
||||
This activity can only work on a single notes object.
|
||||
-->
|
||||
<intent-filter android:label="@string/resolve_title">
|
||||
<!-- This is the action we perform. It is a custom action we
|
||||
define for our application, not a generic VIEW or EDIT
|
||||
action since we are not a general note viewer/editor. -->
|
||||
<action android:name="com.android.notepad.action.EDIT_TITLE" />
|
||||
<!-- DEFAULT: execute if being directly invoked. -->
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<!-- ALTERNATIVE: show as an alternative action when the user is
|
||||
working with this type of data. -->
|
||||
<category android:name="android.intent.category.ALTERNATIVE" />
|
||||
<!-- SELECTED_ALTERNATIVE: show as an alternative action the user
|
||||
can perform when selecting this type of data. -->
|
||||
<category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
|
||||
<!-- This is the data type we operate on. -->
|
||||
<data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="NotesLiveFolder" android:label="@string/live_folder_name"
|
||||
<!--
|
||||
This activity handles live folder views of notes. The CREATE_LIVE_FOLDER filter
|
||||
tells the Launcher that this activity can handle live folders.
|
||||
-->
|
||||
<activity
|
||||
android:name="NotesLiveFolder"
|
||||
android:label="@string/live_folder_name"
|
||||
android:icon="@drawable/live_folder_notes">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_LIVE_FOLDER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!--
|
||||
This declares version information for the application:
|
||||
targetSdkVersion: Indicates that the application was tested with API level 4.
|
||||
Android will turn off compatibility features for APIs greater than 4, since the
|
||||
application does not require them. Also, if the application were to be uploaded to
|
||||
Android Market, then it would not be visible to devices running an API level below 4.
|
||||
minSdkVersion: Indicates that Android will not install this application to a
|
||||
device that is running an API level less than 4.
|
||||
-->
|
||||
<uses-sdk android:targetSdkVersion="3" android:minSdkVersion="3"/>
|
||||
</application>
|
||||
|
||||
<uses-sdk android:targetSdkVersion="4" android:minSdkVersion="3"/>
|
||||
</manifest>
|
||||
|
||||
|
||||
@@ -1,20 +1,62 @@
|
||||
<p>A simple note pad application.
|
||||
It demonstrates:</p>
|
||||
<h2>NotePad Documentation</h2>
|
||||
<p>
|
||||
NotePad is a simple application for entering and saving character-only notes.
|
||||
It demonstrates the following Android concepts:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Using views</li>
|
||||
<li>Accessing a database</li>
|
||||
<li>Using an intent to open a new window</li>
|
||||
<li>Managing activity lifecycle</li>
|
||||
<li>Creating <a href="../../../reference/android/provider/LiveFolders.html">Live Folders</a></li>
|
||||
<li>And more...</li>
|
||||
<li>ListView objects with Cursor-based adapters.</li>
|
||||
<li>Data storage and access with a ContentProvider.</li>
|
||||
<li>Intent filters that handle multiple actions.</li>
|
||||
<li>Activity objects that handle more than one Intent.</li>
|
||||
<li>Sending an Intent by selecting an item from a ListView.</li>
|
||||
<li>Sending data in an Intent.</li>
|
||||
<li>Dynamic construction of menus.</li>
|
||||
<li>Context menus.</li>
|
||||
<li>Providing other applications as alternative actions for a data item.</li>
|
||||
<li>Creating <a href="../../../reference/android/provider/LiveFolders.html">
|
||||
Live Folders</a></li>
|
||||
<li>Setting up an application that does not use "save" or "exit". The application
|
||||
saves the user's work whenever focus moves to another task. It remains available until
|
||||
the system destroys it.</li>
|
||||
</ul>
|
||||
|
||||
<p class="note">Please notice that this is not the same
|
||||
notepad code that's used for the <a href="../../../guide/tutorials/notepad/index.html">Notepad Tutorial</a>.
|
||||
They are similar in nature, but there are several differences in implementation — the tutorial
|
||||
is slightly more simple. If you're new to
|
||||
Android development, we suggest you start with the tutorial, then visit this
|
||||
code later to see more sample code.</p>
|
||||
|
||||
<h4>Functions</h4>
|
||||
<p>
|
||||
By default, NotePad displays a list of existing notes,
|
||||
and provides a menu that allows you to insert a new note. If you click an existing note, you
|
||||
can edit it, or select the menu to delete it or edit its title. If you context-click an
|
||||
existing note, you can also delete it. If you navigate away from a new note once you've entered
|
||||
text, NotePad automatically saves what you've entered.
|
||||
</p>
|
||||
<h4>Limitations</h4>
|
||||
<p>
|
||||
NotePad has limited functionality. It does not provide search/replace. It does not support
|
||||
HTML or any other type of markup. However, it does have copy/paste if it is running on a
|
||||
version of the Android platform that supports that functionality.
|
||||
</p>
|
||||
<h4>Provider</h4>
|
||||
<p>
|
||||
Other applications can use NotePad's content provider to store notes. The "contract" for
|
||||
the NotePad provider is documented in the Appendix.
|
||||
</p>
|
||||
<h4>Alternative actions</h4>
|
||||
<p>
|
||||
Other applications can also register themselves to handle NotePad notes. To do this, they must
|
||||
provide the necessary specifications in their manifest file. The specifications are listed in
|
||||
the Appendix.
|
||||
</p>
|
||||
<h4>Test package</h4>
|
||||
<p>
|
||||
This sample also contains a test package that demonstrates how to test a ContentProvider and
|
||||
an Activity. To learn more about this test package, please read
|
||||
<a href="tests/index.html">NotePadTest Documentation</a>
|
||||
</p>
|
||||
<p class="note">
|
||||
Please notice that this is not the same code that's used for the
|
||||
<a href="../../../guide/tutorials/notepad/index.html">Notepad Tutorial</a>. This code has been
|
||||
updated more recently, and the tutorial is a more simple implementation. If you're new to
|
||||
Android development, you should start with the tutorial, then return to this code.
|
||||
</p>
|
||||
<h4>Appendix A: Provider contract</h4>
|
||||
<h4>Appendix B: Registering an application</h4>
|
||||
<img alt="" src="../images/sample_notepad.png" />
|
||||
<img alt="" src="../images/sample_note.png" />
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<!--
|
||||
android:layout_width and android:layout_height use "fill_parent" to be compatible with API
|
||||
level 4. The setting was renamed "match_parent" in API level 8.
|
||||
-->
|
||||
<view xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
class="com.example.android.notepad.NoteEditor$LinedEditText"
|
||||
android:id="@+id/note"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="5dip"
|
||||
android:scrollbars="vertical"
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<!--
|
||||
android:layout_width and android:layout_height use "fill_parent" to be compatible with API
|
||||
level 4. The setting was renamed "match_parent" in API level 8.
|
||||
-->
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="center_vertical"
|
||||
|
||||
@@ -16,13 +16,10 @@
|
||||
|
||||
package com.example.android.notepad;
|
||||
|
||||
import com.example.android.notepad.NotePad.Notes;
|
||||
import com.example.android.notepad.NotePad;
|
||||
|
||||
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;
|
||||
@@ -39,27 +36,30 @@ import android.view.MenuItem;
|
||||
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 empty note
|
||||
* {@link Intent#ACTION_INSERT}, or create a new note from the current contents
|
||||
* of the clipboard {@link Intent#ACTION_PASTE}.
|
||||
* 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}, or create a note {@link Intent#ACTION_INSERT}.
|
||||
*
|
||||
* 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 = "Notes";
|
||||
|
||||
/**
|
||||
* Standard projection for the interesting columns of a normal note.
|
||||
/*
|
||||
* Creates a projection that returns the note ID and the note contents.
|
||||
*/
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
Notes._ID, // 0
|
||||
Notes.NOTE, // 1
|
||||
Notes.TITLE, // 2
|
||||
private static final String[] PROJECTION
|
||||
= new String[] {
|
||||
NotePad.Notes._ID,
|
||||
NotePad.Notes.COLUMN_NAME_NOTE
|
||||
};
|
||||
|
||||
/** 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";
|
||||
@@ -72,73 +72,111 @@ 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;
|
||||
|
||||
// Global variables
|
||||
private int mState;
|
||||
private boolean mNoteOnly = false;
|
||||
private Uri mUri;
|
||||
private Cursor mCursor;
|
||||
private EditText mText;
|
||||
private String mOriginalContent;
|
||||
|
||||
/**
|
||||
* A custom EditText that draws lines between each line of text that is displayed.
|
||||
* 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;
|
||||
|
||||
// we need this constructor for LayoutInflater
|
||||
// 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();
|
||||
|
||||
// Do some setup based on the action being performed.
|
||||
/*
|
||||
* 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)) {
|
||||
// Requested to edit: set that state, and the data being edited.
|
||||
|
||||
// Sets the Activity state to EDIT, and gets the URI for the data to be edited.
|
||||
mState = STATE_EDIT;
|
||||
mUri = intent.getData();
|
||||
} 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.
|
||||
|
||||
// For an insert action:
|
||||
} else if (Intent.ACTION_INSERT.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 we were unable to create a new note, then just finish
|
||||
// this activity. A RESULT_CANCELED will be sent back to the
|
||||
// original activity if they requested a result.
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -147,266 +185,425 @@ 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;
|
||||
}
|
||||
|
||||
// If the action was other than EDIT or INSERT:
|
||||
} else {
|
||||
// Whoops, unknown action! Bail.
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Set the layout for this activity. You can find it in res/layout/note_editor.xml
|
||||
// Sets the layout for this Activity. See res/layout/note_editor.xml
|
||||
setContentView(R.layout.note_editor);
|
||||
|
||||
// The text view for our note, identified by its ID in the XML file.
|
||||
// Gets a handle to the EditText in the the layout.
|
||||
mText = (EditText) findViewById(R.id.note);
|
||||
|
||||
// Get the note!
|
||||
mCursor = managedQuery(mUri, PROJECTION, null, null, null);
|
||||
/*
|
||||
* 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)
|
||||
);
|
||||
|
||||
// If an instance of this activity had previously stopped, we can
|
||||
// get the original text it started with.
|
||||
/*
|
||||
* 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();
|
||||
|
||||
// If we didn't have any trouble retrieving the data, it is now
|
||||
// time to get at the stuff.
|
||||
/*
|
||||
* 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) {
|
||||
// Make sure we are at the one and only row in the cursor.
|
||||
|
||||
/* 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();
|
||||
|
||||
// Modify our overall title depending on the mode we are running in.
|
||||
// Modifies the window title for the Activity according to the current Activity state.
|
||||
if (mState == STATE_EDIT) {
|
||||
|
||||
// Sets the title to "edit"
|
||||
setTitle(getText(R.string.title_edit));
|
||||
} else if (mState == STATE_INSERT || mState == STATE_PASTE) {
|
||||
} else if (mState == STATE_INSERT) {
|
||||
|
||||
// Sets the title to "create"
|
||||
setTitle(getText(R.string.title_create));
|
||||
}
|
||||
|
||||
// This is a little tricky: we may be resumed after previously being
|
||||
// paused/stopped. We want to put the new text in the text view,
|
||||
// but leave the user where they were (retain the cursor position
|
||||
// etc). This version of setText does that for us.
|
||||
/*
|
||||
* 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.
|
||||
String note = mCursor.getString(COLUMN_INDEX_NOTE);
|
||||
mText.setTextKeepState(note);
|
||||
|
||||
// If we hadn't previously retrieved the original text, do so
|
||||
// now. This allows the user to revert their changes.
|
||||
// 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
|
||||
// Saves 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();
|
||||
|
||||
// The user is going somewhere else, so make sure their current
|
||||
// changes are safely saved away in the provider. We don't need
|
||||
// to do this if only editing.
|
||||
/*
|
||||
* 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) {
|
||||
String text = mText.getText().toString();
|
||||
int length = text.length();
|
||||
|
||||
// If this activity is finished, and there is no text, then we
|
||||
// do something a little special: simply delete the note entry.
|
||||
// Note that we do this both for editing and inserting... it
|
||||
// would be reasonable to only do it when inserting.
|
||||
if (isFinishing() && (length == 0) && !mNoteOnly) {
|
||||
// Get the current note text.
|
||||
String text = mText.getText().toString();
|
||||
int note_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() && (note_length == 0)) {
|
||||
setResult(RESULT_CANCELED);
|
||||
deleteNote();
|
||||
|
||||
// Get out updates into the provider.
|
||||
} else {
|
||||
updateNote(text, null, !mNoteOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
// Build the menus that are shown when editing.
|
||||
if (mState == STATE_EDIT) {
|
||||
menu.add(0, REVERT_ID, 0, R.string.menu_revert)
|
||||
.setShortcut('0', 'r')
|
||||
.setIcon(android.R.drawable.ic_menu_revert);
|
||||
if (!mNoteOnly) {
|
||||
menu.add(0, DELETE_ID, 0, R.string.menu_delete)
|
||||
.setShortcut('1', 'd')
|
||||
.setIcon(android.R.drawable.ic_menu_delete);
|
||||
}
|
||||
|
||||
// Build the menus that are shown when inserting.
|
||||
} else {
|
||||
menu.add(0, DISCARD_ID, 0, R.string.menu_discard)
|
||||
.setShortcut('0', 'd')
|
||||
.setIcon(android.R.drawable.ic_menu_delete);
|
||||
}
|
||||
|
||||
// If we are working on a full note, then 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.
|
||||
if (!mNoteOnly) {
|
||||
Intent intent = new Intent(null, getIntent().getData());
|
||||
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
|
||||
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
|
||||
new ComponentName(this, NoteEditor.class), null, intent, 0, null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle all of the possible menu actions.
|
||||
switch (item.getItemId()) {
|
||||
case DELETE_ID:
|
||||
deleteNote();
|
||||
finish();
|
||||
break;
|
||||
case DISCARD_ID:
|
||||
cancelNote();
|
||||
break;
|
||||
case REVERT_ID:
|
||||
cancelNote();
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
//BEGIN_INCLUDE(paste)
|
||||
/**
|
||||
* Replace the note's data with the current contents of the clipboard.
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
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) {
|
||||
} else {
|
||||
// Creates a map to contain the new values for the columns
|
||||
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());
|
||||
// In the values map, sets the modification date column to the current time.
|
||||
values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, System.currentTimeMillis());
|
||||
|
||||
// If we are creating a new note, then we want to also create
|
||||
// an initial title for it.
|
||||
// Creates a title for a newly-inserted note.
|
||||
if (mState == STATE_INSERT) {
|
||||
if (title == null) {
|
||||
int length = text.length();
|
||||
title = text.substring(0, Math.min(30, length));
|
||||
if (length > 30) {
|
||||
|
||||
// Gets the first 30 characters of the note, or the entire note if it's
|
||||
// less than 30 characters long.
|
||||
String title = text.substring(0, Math.min(30, note_length));
|
||||
|
||||
// If the note's entire length is greater than 30, then the title is 30
|
||||
// characters long. Finds the last occurrence of blank in the title, and
|
||||
// removes all characters to the right of it from the title string.
|
||||
if (note_length > 30) {
|
||||
int lastSpace = title.lastIndexOf(' ');
|
||||
if (lastSpace > 0) {
|
||||
title = title.substring(0, lastSpace);
|
||||
}
|
||||
}
|
||||
}
|
||||
values.put(Notes.TITLE, title);
|
||||
}
|
||||
// In the values map, set the title column to the new title.
|
||||
values.put(NotePad.Notes.COLUMN_NAME_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);
|
||||
// In the values map, sets the note text column to the text in the View.
|
||||
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.
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take care of canceling work on a note. Deletes the note if we
|
||||
* 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) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
// Builds the menus that are shown when editing. These are 'revert' to undo changes, and
|
||||
// 'delete' to delete the note.
|
||||
if (mState == STATE_EDIT) {
|
||||
|
||||
// Adds the 'revert' menu item, and sets its shortcut to numeric 0, letter 'r' and its
|
||||
// icon to the Android standard revert icon.
|
||||
menu.add(0, REVERT_ID, 0, R.string.menu_revert)
|
||||
.setShortcut('0', 'r')
|
||||
.setIcon(android.R.drawable.ic_menu_revert);
|
||||
|
||||
// Adds the 'delete' menu item, and sets its shortcut to numeric 1, letter 'd' and its
|
||||
// icon to the Android standard delete icon
|
||||
menu.add(0, DELETE_ID, 0, R.string.menu_delete)
|
||||
.setShortcut('1', 'd')
|
||||
.setIcon(android.R.drawable.ic_menu_delete);
|
||||
|
||||
// Builds the menus that are shown when inserting. The only option is 'Discard' to throw
|
||||
// away the new note.
|
||||
} else {
|
||||
|
||||
// Adds the 'discard' menu item, using the 'delete' shortcuts and icon.
|
||||
menu.add(0, DISCARD_ID, 0, R.string.menu_discard)
|
||||
.setShortcut('0', 'd')
|
||||
.setIcon(android.R.drawable.ic_menu_delete);
|
||||
}
|
||||
|
||||
/*
|
||||
* Appends menu items for any Activity declarations that implement an alternative action
|
||||
* for this Activity's MIME type, one menu item for each Activity.
|
||||
*/
|
||||
// Makes a new Intent with the URI data passed to this Activity
|
||||
Intent intent = new Intent(null, getIntent().getData());
|
||||
|
||||
// Adds the ALTERNATIVE category to the Intent.
|
||||
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
|
||||
|
||||
/*
|
||||
* Constructs a new ComponentName object that represents the current Activity.
|
||||
*/
|
||||
ComponentName component = new ComponentName(
|
||||
this,
|
||||
NoteEditor.class);
|
||||
|
||||
/*
|
||||
* In the ALTERNATIVE menu group, adds an option for each Activity that is registered to
|
||||
* handle this Activity's MIME type. The Intent describes what type of items should be
|
||||
* added to the menu; in this case, Activity declarations with category ALTERNATIVE.
|
||||
*/
|
||||
menu.addIntentOptions(
|
||||
Menu.CATEGORY_ALTERNATIVE, // The menu group to add the items to.
|
||||
Menu.NONE, // No unique ID is needed.
|
||||
Menu.NONE, // No ordering is needed.
|
||||
component, // The current Activity object's component name
|
||||
null, // No specific items need to be placed first.
|
||||
intent, // The intent containing the type of items to add.
|
||||
Menu.NONE, // No flags are necessary.
|
||||
null // No need to generate an array of menu items.
|
||||
);
|
||||
|
||||
// The method returns TRUE, so that further menu processing is not done.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// Chooses the action to perform
|
||||
switch (item.getItemId()) {
|
||||
|
||||
// Deletes the note and close the Activity.
|
||||
case DELETE_ID:
|
||||
deleteNote();
|
||||
finish();
|
||||
break;
|
||||
|
||||
// Discards the new note.
|
||||
case DISCARD_ID:
|
||||
cancelNote();
|
||||
break;
|
||||
|
||||
// Discards any changes to an edited note.
|
||||
case REVERT_ID:
|
||||
cancelNote();
|
||||
break;
|
||||
}
|
||||
|
||||
// Continues with processing the menu item. In effect, if the item was an alternative
|
||||
// action, this invokes the Activity for that action.
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes care of canceling work on a note. Deletes the note if we
|
||||
* had created it, otherwise reverts to the original text.
|
||||
*/
|
||||
private final void cancelNote() {
|
||||
|
||||
/*
|
||||
* Tests to see that the original 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) {
|
||||
|
||||
/*
|
||||
* If the user is editing a note, and asked to discard or revert, this puts the
|
||||
* previous note contents back into the note.
|
||||
*/
|
||||
if (mState == STATE_EDIT) {
|
||||
// Put the original note text back into the database
|
||||
|
||||
// Closes the previous cursor prior to updating the provider
|
||||
mCursor.close();
|
||||
mCursor = null;
|
||||
|
||||
// Creates a new values map
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Notes.NOTE, mOriginalContent);
|
||||
getContentResolver().update(mUri, values, null, null);
|
||||
|
||||
// Puts the original notes content into the values map. The variable was set in
|
||||
// onResume().
|
||||
values.put(NotePad.Notes.COLUMN_NAME_NOTE, mOriginalContent);
|
||||
|
||||
/*
|
||||
* Update the provider with the reverted note content.
|
||||
*
|
||||
* 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 of the note or notes.
|
||||
values, // The reverted values to put into the provider.
|
||||
null, // No selection criteria, so no where columns are needed.
|
||||
null // No where columns are used, so no where values are needed.
|
||||
);
|
||||
|
||||
/*
|
||||
* If the user was inserting a note and decides to discard it, this deletes the note.
|
||||
*/
|
||||
} else if (mState == STATE_INSERT) {
|
||||
// We inserted an empty note, make sure to delete it
|
||||
// Deletes the note.
|
||||
deleteNote();
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a result of CANCELED to the calling Activity.
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
// Finishes the Activity. Once the user deletes or discards, nothing more can be done, so
|
||||
// return to the calling Activity, either NotesList or some other Activity.
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take care of deleting a note. Simply deletes the entry.
|
||||
* This method deletes a note from the provider.
|
||||
*/
|
||||
private final void deleteNote() {
|
||||
/*
|
||||
* Tests to see that the original 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) {
|
||||
|
||||
// Gets rid of all the Cursor's resources, and deactivates it.
|
||||
mCursor.close();
|
||||
mCursor = null;
|
||||
getContentResolver().delete(mUri, null, null);
|
||||
|
||||
/*
|
||||
* Deletes the note based on the ID in the URI.
|
||||
*
|
||||
* Note: This is being done on the UI thread. It will block the thread until the
|
||||
* delete 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 android.os.AsyncTask.
|
||||
*/
|
||||
|
||||
getContentResolver().delete(
|
||||
mUri, // The URI of the note to delete.
|
||||
null, // No selection criteria are specified, so no where columns are needed.
|
||||
null // No where columns are specified, so no where values are needed.
|
||||
);
|
||||
|
||||
// Throws away any text currently showing in the View.
|
||||
mText.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,25 +20,92 @@ import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
/**
|
||||
* Convenience definitions for NotePadProvider
|
||||
* 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() {}
|
||||
private NotePad() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes table
|
||||
* 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("content://" + AUTHORITY + "/notes");
|
||||
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.
|
||||
@@ -46,7 +113,8 @@ public final class NotePad {
|
||||
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.
|
||||
* 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";
|
||||
|
||||
@@ -55,28 +123,32 @@ public final class NotePad {
|
||||
*/
|
||||
public static final String DEFAULT_SORT_ORDER = "modified DESC";
|
||||
|
||||
/*
|
||||
* Column definitions
|
||||
*/
|
||||
|
||||
/**
|
||||
* The title of the note
|
||||
* Column name for the title of the note
|
||||
* <P>Type: TEXT</P>
|
||||
*/
|
||||
public static final String TITLE = "title";
|
||||
public static final String COLUMN_NAME_TITLE = "title";
|
||||
|
||||
/**
|
||||
* The note itself
|
||||
* Column name of the note content
|
||||
* <P>Type: TEXT</P>
|
||||
*/
|
||||
public static final String NOTE = "note";
|
||||
public static final String COLUMN_NAME_NOTE = "note";
|
||||
|
||||
/**
|
||||
* The timestamp for when the note was created
|
||||
* Column name for the creation timestamp
|
||||
* <P>Type: INTEGER (long from System.curentTimeMillis())</P>
|
||||
*/
|
||||
public static final String CREATED_DATE = "created";
|
||||
public static final String COLUMN_NAME_CREATE_DATE = "created";
|
||||
|
||||
/**
|
||||
* The timestamp for when the note was last modified
|
||||
* Column name for the modification timestamp
|
||||
* <P>Type: INTEGER (long from System.curentTimeMillis())</P>
|
||||
*/
|
||||
public static final String MODIFIED_DATE = "modified";
|
||||
public static final String COLUMN_NAME_MODIFICATION_DATE = "modified";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,11 @@
|
||||
|
||||
package com.example.android.notepad;
|
||||
|
||||
import com.example.android.notepad.NotePad.Notes;
|
||||
|
||||
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;
|
||||
@@ -32,340 +28,582 @@ 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.
|
||||
* This is a content provider for a table of notes. Each note has a title, the note
|
||||
* itself, a creation date and a modified data. The underlying data source is an SQLite
|
||||
* database.
|
||||
*
|
||||
* Notes:
|
||||
* SQLite database method signatures usually include a "where" argument and "whereArgs" argument.
|
||||
* "where" can either specify a full "where" clause in the format used by SQL "WHERE", or a
|
||||
* "where" clause in which the column names are followed by " = ?", or a combination of the two.
|
||||
* If the " = ?" form is present, then whereArgs must be non-null. It is a String array that
|
||||
* contains one element for each "?" present. In order, the "?" symbols are replace by elements
|
||||
* in whereArgs. This feature helps create selection criteria without needing to repeatedly
|
||||
* create a where clause from concatenations. In the comments, "where" is always annotated as
|
||||
* the where clause columns, and "whereArgs" as the where clause values, although "where" can
|
||||
* contain values and "whereArgs" can be null.
|
||||
*
|
||||
*/
|
||||
public class NotePadProvider extends ContentProvider implements PipeDataWriter<Cursor> {
|
||||
public class NotePadProvider extends ContentProvider {
|
||||
|
||||
// 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";
|
||||
private static final int DATABASE_VERSION = 2;
|
||||
private static final String NOTES_TABLE_NAME = "notes";
|
||||
|
||||
private static HashMap<String, String> sNotesProjectionMap;
|
||||
private static HashMap<String, String> sLiveFolderProjectionMap;
|
||||
|
||||
private static final int NOTES = 1;
|
||||
private static final int NOTE_ID = 2;
|
||||
private static final int LIVE_FOLDER_NOTES = 3;
|
||||
|
||||
private static final UriMatcher sUriMatcher;
|
||||
|
||||
/**
|
||||
* This class helps open, create, and upgrade the database file.
|
||||
* The database version
|
||||
*/
|
||||
private static class DatabaseHelper extends SQLiteOpenHelper {
|
||||
private static final int DATABASE_VERSION = 2;
|
||||
|
||||
/**
|
||||
* A projection map used to select columns from the database
|
||||
*/
|
||||
private static HashMap<String, String> sNotesProjectionMap;
|
||||
|
||||
/**
|
||||
* A projection map used to select columns from the database
|
||||
*/
|
||||
private static HashMap<String, String> sLiveFolderProjectionMap;
|
||||
|
||||
/*
|
||||
* 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<String, String>();
|
||||
|
||||
// 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<String, String>();
|
||||
|
||||
// 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 " + NOTES_TABLE_NAME + " ("
|
||||
+ Notes._ID + " INTEGER PRIMARY KEY,"
|
||||
+ Notes.TITLE + " TEXT,"
|
||||
+ Notes.NOTE + " TEXT,"
|
||||
+ Notes.CREATED_DATE + " INTEGER,"
|
||||
+ Notes.MODIFIED_DATE + " INTEGER"
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseHelper mOpenHelper;
|
||||
|
||||
/**
|
||||
*
|
||||
* 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) {
|
||||
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
||||
qb.setTables(NOTES_TABLE_NAME);
|
||||
|
||||
// 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 = <noteID>" to the where clause, so that
|
||||
* it selects that single note
|
||||
*/
|
||||
case NOTE_ID:
|
||||
qb.setProjectionMap(sNotesProjectionMap);
|
||||
qb.appendWhere(Notes._ID + "=" + uri.getPathSegments().get(1));
|
||||
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);
|
||||
}
|
||||
|
||||
// If no sort order is specified use the default
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Get the database and run the query
|
||||
// Opens the database object in "read" mode, since no writes need to be done.
|
||||
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
|
||||
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
|
||||
|
||||
// Tell the cursor what uri to watch, so it knows when its source data changes
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* @return The MIME type of the URI.
|
||||
* @throws IllegalArgumentException if the incoming URI pattern is invalid.
|
||||
*/
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
switch (sUriMatcher.match(uri)) {
|
||||
case NOTES:
|
||||
case LIVE_FOLDER_NOTES:
|
||||
return Notes.CONTENT_TYPE;
|
||||
|
||||
case NOTE_ID:
|
||||
return Notes.CONTENT_ITEM_TYPE;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
//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.
|
||||
* Chooses the MIME type based on the incoming URI pattern
|
||||
*/
|
||||
@Override
|
||||
public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
|
||||
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 null;
|
||||
return NotePad.Notes.CONTENT_TYPE;
|
||||
|
||||
// If the pattern is for note IDs, returns the note ID content type.
|
||||
case NOTE_ID:
|
||||
if (compareMimeTypes("text/plain", mimeTypeFilter)) {
|
||||
return new String[] { "text/plain" };
|
||||
}
|
||||
return null;
|
||||
return NotePad.Notes.CONTENT_ITEM_TYPE;
|
||||
|
||||
// If the pattern doesn't match any permitted patterns, throws an exception.
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard projection for the interesting columns of a normal note.
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
|
||||
// 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());
|
||||
|
||||
// Make sure that the fields are all set
|
||||
if (values.containsKey(NotePad.Notes.CREATED_DATE) == false) {
|
||||
values.put(NotePad.Notes.CREATED_DATE, now);
|
||||
// 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 (values.containsKey(NotePad.Notes.MODIFIED_DATE) == false) {
|
||||
values.put(NotePad.Notes.MODIFIED_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 (values.containsKey(NotePad.Notes.TITLE) == false) {
|
||||
// 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.TITLE, r.getString(android.R.string.untitled));
|
||||
values.put(NotePad.Notes.COLUMN_NAME_TITLE, r.getString(android.R.string.untitled));
|
||||
}
|
||||
|
||||
if (values.containsKey(NotePad.Notes.NOTE) == false) {
|
||||
values.put(NotePad.Notes.NOTE, "");
|
||||
// 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();
|
||||
long rowId = db.insert(NOTES_TABLE_NAME, Notes.NOTE, values);
|
||||
|
||||
// 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) {
|
||||
Uri noteUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_URI, rowId);
|
||||
// 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();
|
||||
|
||||
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(NOTES_TABLE_NAME, where, whereArgs);
|
||||
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:
|
||||
String noteId = uri.getPathSegments().get(1);
|
||||
count = db.delete(NOTES_TABLE_NAME, Notes._ID + "=" + noteId
|
||||
+ (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
|
||||
// From the incoming URI, get the note ID
|
||||
String noteId = uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION);
|
||||
|
||||
// If no where clause was passed in, uses the note ID column name
|
||||
// for a column and the note ID for a value.
|
||||
if (TextUtils.isEmpty(where)) {
|
||||
where = NotePad.Notes._ID + " = ?";
|
||||
whereArgs[0] = noteId;
|
||||
|
||||
} else {
|
||||
|
||||
/*
|
||||
* If where clause columns were passed in, appends the note ID column name to
|
||||
* the list of columns using a replaceable parameter. This works even if the
|
||||
* other columns have actual values.
|
||||
*/
|
||||
// Appends the note ID column name as an AND condition using a replaceable
|
||||
// parameter.
|
||||
where = where + " AND " + NotePad.Notes._ID + " = ?";
|
||||
|
||||
// Appends the note ID value to the end of the where clause values.
|
||||
whereArgs[whereArgs.length] = noteId;
|
||||
}
|
||||
|
||||
// Performs the delete.
|
||||
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 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#insert(Uri, ContentValues)}
|
||||
* 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.
|
||||
* @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;
|
||||
|
||||
// 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:
|
||||
count = db.update(NOTES_TABLE_NAME, values, where, whereArgs);
|
||||
|
||||
// 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:
|
||||
String noteId = uri.getPathSegments().get(1);
|
||||
count = db.update(NOTES_TABLE_NAME, values, Notes._ID + "=" + noteId
|
||||
+ (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
|
||||
break;
|
||||
// From the incoming URI, get the note ID
|
||||
String noteId = uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION);
|
||||
|
||||
// If no where clause was passed in, uses the note ID column name
|
||||
// for a column and the note ID for a value.
|
||||
if (TextUtils.isEmpty(where)) {
|
||||
where = NotePad.Notes._ID + " = ?";
|
||||
whereArgs[0] = noteId;
|
||||
|
||||
// If where clause columns were passed in, appends the note ID to the where
|
||||
// clause
|
||||
} else {
|
||||
|
||||
/*
|
||||
* Appends the note ID column name to the list of columns, with a replaceable
|
||||
* parameter. This will work even if the rest of the columns have been set with
|
||||
* actual values.
|
||||
*/
|
||||
// Appends the note ID column name as an AND condition with a replaceable
|
||||
// parameter.
|
||||
where = where + " AND " + NotePad.Notes._ID + " = ?";
|
||||
|
||||
// Appends the note ID value to the end of the where clause values.
|
||||
whereArgs[whereArgs.length] = noteId;
|
||||
}
|
||||
|
||||
// Does the update.
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
static {
|
||||
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
|
||||
sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
|
||||
sUriMatcher.addURI(NotePad.AUTHORITY, "live_folders/notes", LIVE_FOLDER_NOTES);
|
||||
|
||||
sNotesProjectionMap = new HashMap<String, String>();
|
||||
sNotesProjectionMap.put(Notes._ID, Notes._ID);
|
||||
sNotesProjectionMap.put(Notes.TITLE, Notes.TITLE);
|
||||
sNotesProjectionMap.put(Notes.NOTE, Notes.NOTE);
|
||||
sNotesProjectionMap.put(Notes.CREATED_DATE, Notes.CREATED_DATE);
|
||||
sNotesProjectionMap.put(Notes.MODIFIED_DATE, Notes.MODIFIED_DATE);
|
||||
|
||||
// Support for Live Folders.
|
||||
sLiveFolderProjectionMap = new HashMap<String, String>();
|
||||
sLiveFolderProjectionMap.put(LiveFolders._ID, Notes._ID + " AS " +
|
||||
LiveFolders._ID);
|
||||
sLiveFolderProjectionMap.put(LiveFolders.NAME, Notes.TITLE + " AS " +
|
||||
LiveFolders.NAME);
|
||||
// Add more columns here for more robust Live Folders.
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,9 @@
|
||||
|
||||
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;
|
||||
@@ -38,217 +33,464 @@ import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SimpleCursorAdapter;
|
||||
|
||||
import com.example.android.notepad.NotePad;
|
||||
|
||||
/**
|
||||
* Displays a list of notes. Will display notes from the {@link Uri}
|
||||
* provided in the intent if there is one, otherwise defaults to displaying the
|
||||
* contents of the {@link NotePadProvider}
|
||||
* 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";
|
||||
|
||||
// Menu item ids
|
||||
public static final int MENU_ITEM_DELETE = Menu.FIRST;
|
||||
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;
|
||||
public static final int MENU_ITEM_INSERT = Menu.FIRST + 1;
|
||||
|
||||
|
||||
/**
|
||||
* The columns we are interested in from the database
|
||||
* The columns needed by the cursor adapter
|
||||
*/
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
Notes._ID, // 0
|
||||
Notes.TITLE, // 1
|
||||
NotePad.Notes._ID, // 0
|
||||
NotePad.Notes.COLUMN_NAME_TITLE, // 1
|
||||
};
|
||||
|
||||
/** The index of the title column */
|
||||
private static final int COLUMN_INDEX_TITLE = 1;
|
||||
|
||||
private MenuItem mPasteItem;
|
||||
|
||||
/**
|
||||
* 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 was given in the intent (because we were started
|
||||
// as a MAIN activity), then use our default content provider.
|
||||
/* 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(Notes.CONTENT_URI);
|
||||
intent.setData(NotePad.Notes.CONTENT_URI);
|
||||
}
|
||||
|
||||
// Inform the list we provide context menus for items
|
||||
/*
|
||||
* 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);
|
||||
|
||||
// Perform a managed query. The Activity will handle closing and requerying the cursor
|
||||
// when needed.
|
||||
Cursor cursor = managedQuery(getIntent().getData(), PROJECTION, null, null,
|
||||
Notes.DEFAULT_SORT_ORDER);
|
||||
/* 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.
|
||||
);
|
||||
|
||||
// Used to map notes entries from the database to views
|
||||
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.noteslist_item, cursor,
|
||||
new String[] { Notes.TITLE }, new int[] { android.R.id.text1 });
|
||||
/*
|
||||
* 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) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
// This is our one standard application action -- inserting a
|
||||
// new note into the list.
|
||||
menu.add(0, MENU_ITEM_INSERT, 0, R.string.menu_insert)
|
||||
.setShortcut('3', 'a')
|
||||
.setIcon(android.R.drawable.ic_menu_add);
|
||||
// Adds an Insert menu item
|
||||
MenuItem menuItem = menu.add(
|
||||
Menu.NONE, // No menu group.
|
||||
MENU_ITEM_INSERT, // Unique ID for this item.
|
||||
Menu.NONE, // No order within the group.
|
||||
R.string.menu_insert // Displayed text for the menu item.
|
||||
);
|
||||
|
||||
// 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');
|
||||
// Sets keyboard shortcuts for the menu item, either "3" or "a";
|
||||
menuItem.setShortcut('3', 'a');
|
||||
|
||||
// Generate any additional actions that can be performed on the
|
||||
// Sets the icon for the menu item
|
||||
menuItem.setIcon(android.R.drawable.ic_menu_add);
|
||||
|
||||
// Generates 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.
|
||||
|
||||
/* Creates a new Intent with the same incoming data and no defined action.
|
||||
* It also sets its category to ALTERNATIVE. This prepares the Intent as a place
|
||||
* to group alternative options in the menu.
|
||||
*/
|
||||
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);
|
||||
|
||||
/* Creates a ComponentName from the current Context and this Activity object's
|
||||
* class object.
|
||||
*/
|
||||
ComponentName component = new ComponentName(this, NotesList.class);
|
||||
|
||||
/*
|
||||
* Adds any other activities that want to be alternatives for this view. In effect,
|
||||
* any application can add itself as an alternative on the options menu.
|
||||
*/
|
||||
menu.addIntentOptions(
|
||||
Menu.CATEGORY_ALTERNATIVE, // Add the options to the Alternatives group
|
||||
Menu.NONE, // Do not use a unique ID
|
||||
Menu.NONE, // No need to order the options
|
||||
component, // The ComponentName of the Activity making the request.
|
||||
// This Activity is excluded from the list of alternatives.
|
||||
null, // No specific items are listed first.
|
||||
intent, // The Intent to resolve to, in effect, an Intent listing
|
||||
// the alternatives
|
||||
Menu.NONE, // no flags are needed
|
||||
null // Since no specifics were used, so a menu item array is
|
||||
// not needed.
|
||||
);
|
||||
|
||||
// Returns true so that the menu is displayed.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks the device's Menu button any time after the first time for
|
||||
* this Activity. This modifies the standard options menu that was set up previously.
|
||||
* See onCreateOptionsMenu().
|
||||
* @param menu The existing Menu object for this Activity.
|
||||
* @return True, always. The menu should be displayed.
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
// Does the displayed list of notes have any items in it?
|
||||
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 there are notes in the list, assumes that one of them is selected.
|
||||
* Generates the menu options that can be performed on the current selection,
|
||||
* which includes options from this application plus actions from other
|
||||
* applications that "registered" themselves using an intent filter for the
|
||||
* NotePad.Notes.CONTENT_TYPE mimeType.
|
||||
*/
|
||||
|
||||
// Modifies the current menu
|
||||
if (haveItems) {
|
||||
// This is the selected item.
|
||||
|
||||
/*
|
||||
* Appends the selected item's row ID (note ID) to the the URI in the incoming Intent's
|
||||
* data area. The incoming Intent at this point could only be GET_CONTENT, which means
|
||||
* that the URI is for a single note, so this statement constructs a URI that will
|
||||
* select a single note from the provider.
|
||||
*/
|
||||
Uri uri = ContentUris.withAppendedId(getIntent().getData(), getSelectedItemId());
|
||||
|
||||
// Build menu... always starts with the EDIT action...
|
||||
/*
|
||||
* Builds the menu. The EDIT action is always the first action in the list, so
|
||||
* a specifics array is required, but only needs one element.
|
||||
*
|
||||
*/
|
||||
|
||||
// Creates an array of "specifics", Intents that must appear before all other options
|
||||
Intent[] specifics = new Intent[1];
|
||||
|
||||
// Sets the Intent of the specifics array to be EDIT on the note ID URI
|
||||
specifics[0] = new Intent(Intent.ACTION_EDIT, uri);
|
||||
|
||||
// Creates an array of menu items. The result of mapping specifics to Intents is put
|
||||
// into this array
|
||||
MenuItem[] items = new MenuItem[1];
|
||||
|
||||
// ... is followed by whatever other actions are available...
|
||||
// Creates an Intent with no defined action and the note ID URI as data
|
||||
Intent intent = new Intent(null, uri);
|
||||
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
|
||||
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, null, specifics, intent, 0,
|
||||
items);
|
||||
|
||||
// Give a shortcut to the edit action.
|
||||
/* 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) {
|
||||
|
||||
// Gets the ID of the selected item
|
||||
switch (item.getItemId()) {
|
||||
|
||||
// If the menu item is Insert
|
||||
case MENU_ITEM_INSERT:
|
||||
// Launch activity to insert a new item
|
||||
|
||||
/*
|
||||
* 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 MENU_ITEM_PASTE:
|
||||
// Launch activity to insert a new item
|
||||
startActivity(new Intent(Intent.ACTION_PASTE, getIntent().getData()));
|
||||
|
||||
// Returns true, so that no more processing is done.
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the menu item selected is not Insert, then this calls the regular processing to
|
||||
// handle the item.
|
||||
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 option is 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Setup the menu header
|
||||
menu.setHeaderTitle(cursor.getString(COLUMN_INDEX_TITLE));
|
||||
// Finds the index of the title column in the Cursor
|
||||
int titleIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE);
|
||||
|
||||
// Add a menu item to copy the note
|
||||
menu.add(0, MENU_ITEM_COPY, 0, R.string.menu_copy);
|
||||
// Sets the menu header to be the title of the selected note.
|
||||
menu.setHeaderTitle(cursor.getString(titleIndex));
|
||||
|
||||
// Add a menu item to delete the note
|
||||
menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_delete);
|
||||
// Adds a menu item to the context menu
|
||||
menu.add(
|
||||
Menu.NONE, // No grouping is needed for this menu
|
||||
MENU_ITEM_DELETE, // A unique ID for this menu item.
|
||||
Menu.NONE, // No ordering is necessary in this menu
|
||||
R.string.menu_delete // The resource ID for the string to display for this item.
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when the user selects an item from the context menu
|
||||
* (see onCreateContextMenu()). The only menu item that is actually handled is DELETE.
|
||||
* 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;
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the menu item's ID and compares it to known actions. The only action that is
|
||||
* implemented is DELETE (it is set in onCreateContextMenu()). The switch statement is
|
||||
* used to facilitate adding other actions in the future.
|
||||
*/
|
||||
switch (item.getItemId()) {
|
||||
case MENU_ITEM_DELETE: {
|
||||
// Delete the note that the context menu is for
|
||||
// Appends the selected note's ID to the URI sent with the incoming Intent.
|
||||
Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id);
|
||||
getContentResolver().delete(noteUri, null, null);
|
||||
|
||||
// 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;
|
||||
}
|
||||
//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)
|
||||
}
|
||||
|
||||
// For menu items other than DELETE, returns to the caller for further processing.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
// The caller is waiting for us to return a note selected by
|
||||
// the user. The have clicked on one, so return it now.
|
||||
|
||||
// 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 {
|
||||
// Launch activity to view/edit the currently selected item
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,48 +18,91 @@ package com.example.android.notepad;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
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 {
|
||||
/**
|
||||
* The URI for the Notes Live Folder content provider.
|
||||
* 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).
|
||||
*/
|
||||
public static final Uri CONTENT_URI = Uri.parse("content://"
|
||||
+ NotePad.AUTHORITY + "/live_folders/notes");
|
||||
|
||||
public static final Uri NOTE_URI = Uri.parse("content://"
|
||||
+ NotePad.AUTHORITY + "/notes/#");
|
||||
|
||||
@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)) {
|
||||
// Build the live folder intent.
|
||||
// Create a new Intent.
|
||||
final Intent liveFolderIntent = new Intent();
|
||||
|
||||
liveFolderIntent.setData(CONTENT_URI);
|
||||
liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME,
|
||||
getString(R.string.live_folder_name));
|
||||
liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON,
|
||||
Intent.ShortcutIconResource.fromContext(this,
|
||||
R.drawable.live_folder_notes));
|
||||
liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE,
|
||||
LiveFolders.DISPLAY_MODE_LIST);
|
||||
liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT,
|
||||
new Intent(Intent.ACTION_EDIT, NOTE_URI));
|
||||
/*
|
||||
* 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);
|
||||
|
||||
// The result of this activity should be a live folder intent.
|
||||
// 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();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,88 +28,154 @@ import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
/**
|
||||
* An activity that will edit the title of a note. Displays a floating
|
||||
* window with a text field.
|
||||
* 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 implements View.OnClickListener {
|
||||
|
||||
/**
|
||||
* This is a special intent action that means "edit the title of a note".
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* An array of the columns we are interested in.
|
||||
*/
|
||||
// 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.TITLE, // 1
|
||||
NotePad.Notes._ID,
|
||||
NotePad.Notes.COLUMN_NAME_TITLE,
|
||||
};
|
||||
/** Index of the title column */
|
||||
// The position of the title column in a Cursor returned by the provider.
|
||||
private static final int COLUMN_INDEX_TITLE = 1;
|
||||
|
||||
/**
|
||||
* Cursor which will provide access to the note whose title we are editing.
|
||||
*/
|
||||
// A Cursor object that will contain the results of querying the provider for a note.
|
||||
private Cursor mCursor;
|
||||
|
||||
/**
|
||||
* The EditText field from our UI. Keep track of this so we can extract the
|
||||
* text when we are finished.
|
||||
*/
|
||||
// An EditText object for preserving the edited title.
|
||||
private EditText mText;
|
||||
|
||||
/**
|
||||
* The content URI to the note that's being edited.
|
||||
*/
|
||||
// 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 uri of the note whose title we want to edit
|
||||
// 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();
|
||||
|
||||
// Get a cursor to access the note
|
||||
mCursor = managedQuery(mUri, PROJECTION, null, null, null);
|
||||
/*
|
||||
* 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.
|
||||
);
|
||||
|
||||
// Set up click handlers for the text field and button
|
||||
// Sets up a listener for the EditText. Gets the EditText by its ID, then sets its
|
||||
// onClickListener to this Activity.
|
||||
mText = (EditText) this.findViewById(R.id.title);
|
||||
mText.setOnClickListener(this);
|
||||
|
||||
// Sets up a listener for the OK button. Gets the Button by its ID, then sets its
|
||||
// onClickListener to this Activity.
|
||||
Button b = (Button) findViewById(R.id.ok);
|
||||
b.setOnClickListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
// Initialize the text with the title column from the cursor
|
||||
// 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) {
|
||||
// Write the title back to the note
|
||||
|
||||
// Creates a values map for updating the provider.
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Notes.TITLE, mText.getText().toString());
|
||||
getContentResolver().update(mUri, values, null, null);
|
||||
|
||||
// In the values map, sets the title to the current contents of the edit box.
|
||||
values.put(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.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when the user clicks anywhere in the title text box.
|
||||
*
|
||||
* It calls finish(), which immediately triggers the onPause() method in this Activity. In
|
||||
* turn, onPause() saves the text currently in the title text box to the note.
|
||||
*/
|
||||
public void onClick(View v) {
|
||||
// When the user clicks, just finish this activity.
|
||||
// onPause will be called, and we save our data there.
|
||||
// Calls finish to force the Activity to shut down. In the lifecycle, this forces a call to
|
||||
// onPause(), which saves the work the user has done.
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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.google.provider;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
/**
|
||||
* Convenience definitions for NotePadProvider
|
||||
*/
|
||||
public final class NotePad {
|
||||
/**
|
||||
* Notes table
|
||||
*/
|
||||
public static final class Notes implements BaseColumns {
|
||||
/**
|
||||
* The content:// style URL for this table
|
||||
*/
|
||||
public static final Uri CONTENT_URI
|
||||
= Uri.parse("content://com.google.provider.NotePad/notes");
|
||||
|
||||
/**
|
||||
* The default sort order for this table
|
||||
*/
|
||||
public static final String DEFAULT_SORT_ORDER = "modified DESC";
|
||||
|
||||
/**
|
||||
* The title of the note
|
||||
* <P>Type: TEXT</P>
|
||||
*/
|
||||
public static final String TITLE = "title";
|
||||
|
||||
/**
|
||||
* The note itself
|
||||
* <P>Type: TEXT</P>
|
||||
*/
|
||||
public static final String NOTE = "note";
|
||||
|
||||
/**
|
||||
* The timestamp for when the note was created
|
||||
* <P>Type: INTEGER (long)</P>
|
||||
*/
|
||||
public static final String CREATED_DATE = "created";
|
||||
|
||||
/**
|
||||
* The timestamp for when the note was last modified
|
||||
* <P>Type: INTEGER (long)</P>
|
||||
*/
|
||||
public static final String MODIFIED_DATE = "modified";
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ LOCAL_SRC_FILES := $(call all-subdir-java-files)
|
||||
|
||||
LOCAL_JAVA_LIBRARIES := android.test.runner
|
||||
|
||||
LOCAL_PACKAGE_NAME := NotePadTests
|
||||
LOCAL_PACKAGE_NAME := NotePadTest
|
||||
|
||||
LOCAL_MODULE_TAGS := tests
|
||||
|
||||
|
||||
@@ -13,20 +13,42 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<!--
|
||||
If you use this file as a template in your own test package, you must change
|
||||
the package name from "com.android.example.notepad.test" to one that you own or have
|
||||
control over.
|
||||
Notice that the package name is not necessarily related to any of the Java package
|
||||
identifiers used in this application.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.notepad.tests">
|
||||
package="com.example.android.notepad.test"
|
||||
android:versionCode="2"
|
||||
android:versionName="2.0">
|
||||
|
||||
<!-- We add an application tag here just so that we can indicate that
|
||||
this package needs to link against the android.test library,
|
||||
which is needed when building test cases. -->
|
||||
<!--
|
||||
Defines an application element. The XML DTD for Android requires
|
||||
the <uses-library> element to appear within an <application> element.
|
||||
In all other respects, this manifest declares a test package rather than a
|
||||
regular Android application.
|
||||
-->
|
||||
<application>
|
||||
<!--
|
||||
Specifies a code library that Android must load along with this package.
|
||||
The library contains the instrumented test runner.
|
||||
-->
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="com.example.android.notepad"
|
||||
android:label="NotePad sample tests">
|
||||
<!--
|
||||
Specifies the test runner that will run the test cases in this package.
|
||||
The android:name attribute specifies the test runner. The android:targetPackage specifies
|
||||
the Android package name of the application under test.
|
||||
In general, Android handles an instrumentation element by loading the class in the
|
||||
android:name attribute as an Instrumentation object before loading the package in which the
|
||||
instrumentation element occurs and before the package specified in android:targetPackage.
|
||||
-->
|
||||
<instrumentation
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="com.example.android.notepad">
|
||||
</instrumentation>
|
||||
|
||||
</manifest>
|
||||
|
||||
97
samples/NotePad/tests/_index.html
Normal file
97
samples/NotePad/tests/_index.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<p>
|
||||
This sample contains a test package that demonstrates
|
||||
<a href="../../../reference/android/content/ContentProvider.html">ContentProvider</a> testing
|
||||
and <a href="../../../reference/android/app/Activity.html">Activity</a> testing. It is a test
|
||||
of the <a href="../NotePad/index.html">Note Pad</a> sample application.
|
||||
</p>
|
||||
<h2>Test Package</h2>
|
||||
<p>
|
||||
The test package project is located in the <code>tests/</code> subdirectory of the
|
||||
Note Pad project. It provides an example of the structure of a test package project,
|
||||
and it contains a example of a <a href="AndroidManifest.html">
|
||||
<code>AndroidManifest.xml</code></a> file for a test package.
|
||||
The package uses <a href="../../../reference/android/test/InstrumentationTestRunner.html">
|
||||
<code>InstrumentationTestRunner</code></a> as its test runner.
|
||||
</p>
|
||||
<p>
|
||||
The <a href="AndroidManifest.html">manifest</a> for this test package contains an
|
||||
<code><instrumentation></code> element that links the test package
|
||||
with the application under test. Specifically, the element's <code>android:name</code>
|
||||
attribute specifies that Android should use <code>InstrumentationTestRunner</code> to
|
||||
control the classes in this package. The <code>android:targetPackage</code> attribute
|
||||
targets <code>com.android.example.notepad</code> as the Android package that contains the
|
||||
application under test.
|
||||
</p>
|
||||
<h2>ContentProvider Test</h2>
|
||||
<p>
|
||||
The ContentProvider test suite is contained in the single test case class
|
||||
<a href="src/com/example/android/notepad/NotePadProviderTest.html">
|
||||
<code>NotePadProviderTest</code></a>. This class extends
|
||||
<a href="../../../reference/android/test/ProviderTestCase2.html"><code>ProviderTestCase2</code>
|
||||
</a>, which extends the JUnit
|
||||
<a href="../../../reference/junit/framework/TestCase.html"><code>TestCase</code></a> class and
|
||||
also provides methods and objects that facilitate ContentProvider testing.
|
||||
</p>
|
||||
<p>
|
||||
Although the sample test suite for ContentProvider testing uses a single test case class,
|
||||
you may use as many test case classes as you wish, and each class can contain as many
|
||||
test methods as you wish.
|
||||
</p>
|
||||
<h3>ContentProvider test methods</h3>
|
||||
<p>
|
||||
<code>NotePadProviderTest</code></a> contains the following methods:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Test setup: The <code>setUp()</code> method re-initializes the test environment
|
||||
(the test fixutre) before each test is run. The call to <code>super.setUp()</code>
|
||||
creates a test context and test resolver, and creates a local instance of the
|
||||
provider under test. By creating the provider, the super constructor also tests the
|
||||
provider's <code>onCreate()</code> method, which is called during the provider's
|
||||
initialization.
|
||||
<p>
|
||||
The <code>setUp()</code> method then gets a handle to the local provider's
|
||||
underlying database. This is used to set up test data in succeeding tests.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
Test tear down: The <code>tearDown()</code> method calls <code>super.tearDown()</code>,
|
||||
which cleans up left-over object references to prevent memory leaks.
|
||||
</li>
|
||||
<li>
|
||||
Test database creation: The <code>insertData()</code> helper method adds test data
|
||||
to the local provider. Individual test methods call <code>insertData()</code>
|
||||
when they need to set up a test fixture that contains valid data. This data is not
|
||||
inserted during <code>setUp()</code>, because some tests expect an empty database.
|
||||
</li>
|
||||
<li>
|
||||
Content provider unit tests: The test package shows how to unit
|
||||
test a content provider's getType, query, insert, delete, and update
|
||||
methods against all the content Uris that the provider offers.
|
||||
Only a subset of all the possible unit tests is included.
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Activity Test</h2>
|
||||
<p>
|
||||
The Activity test suite is contained in the single test case class
|
||||
<a href="src/com/example/android/notepad/NotePadActivityTest.html">
|
||||
<code>NotePadActivityTest</code></a>. This class extends
|
||||
<a href="../../../reference/android/test/ActivityInstrumentationTestCase2.html">
|
||||
<code>ActivityInstrumentationTestCase2</code>, which provides the JUnit test framework and
|
||||
also provides instrumentation methods that facilitate Activity testing.
|
||||
</p>
|
||||
<p>
|
||||
The <code>NotePadActivityTest</code> test class contains a single assertion,
|
||||
<code>testActivityTestCaseSetUpProperly()</code>. This assertion calls
|
||||
<a href="../../../reference/android/test/ActivityInstrumentationTestCase2.html#getActivity()">
|
||||
<code>Activity.getActivity()</code> and then fails if the result is <code>null</code>,
|
||||
indicating that the test was unable to start the Activity under test.
|
||||
</p>
|
||||
<img
|
||||
src="../images/sample_note.png"
|
||||
alt="The user interface for a single note in the Note Pad sample app"
|
||||
style="height:230px"/>
|
||||
<img
|
||||
src="../images/sample_notepadtest_junit.png"
|
||||
alt="Results of a successful run of the NotePadTest test package in Eclipse"
|
||||
style="height:230px"/>
|
||||
@@ -1 +0,0 @@
|
||||
tested.project.dir=..
|
||||
@@ -19,18 +19,22 @@ package com.example.android.notepad;
|
||||
import android.test.ActivityInstrumentationTestCase2;
|
||||
|
||||
import com.example.android.notepad.NotesList;
|
||||
|
||||
/**
|
||||
* Make sure that the main launcher activity opens up properly, which will be
|
||||
* verified by {@link #testActivityTestCaseSetUpProperly}.
|
||||
*
|
||||
* To learn how to run an entire test package or one of its classes, please see
|
||||
* "Testing in Eclipse, with ADT" or "Testing in Other IDEs" in the Developer Guide.
|
||||
*/
|
||||
public class NotePadTest extends ActivityInstrumentationTestCase2<NotesList> {
|
||||
public class NotePadActivityTest extends ActivityInstrumentationTestCase2<NotesList> {
|
||||
|
||||
/**
|
||||
* Creates an {@link ActivityInstrumentationTestCase2} for the {@link NotesList} activity.
|
||||
*/
|
||||
public NotePadTest() {
|
||||
super(NotesList.class);
|
||||
public NotePadActivityTest() {
|
||||
// This constructor was deprecated in API level 8 (SDK 2.2) but is
|
||||
// used here so that the package remains compatible with API level 3 (SDK 1.5)
|
||||
super("com.example.android.notepad", NotesList.class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,585 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.test.ProviderTestCase2;
|
||||
import android.test.mock.MockContentResolver;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
/*
|
||||
*/
|
||||
/**
|
||||
* This class tests the content provider for the Note Pad sample application.
|
||||
*
|
||||
* To learn how to run an entire test package or one of its classes, please see
|
||||
* "Testing in Eclipse, with ADT" or "Testing in Other IDEs" in the Developer Guide.
|
||||
*/
|
||||
public class NotePadProviderTest extends ProviderTestCase2<NotePadProvider> {
|
||||
|
||||
// A URI that the provider does not offer, for testing error handling.
|
||||
private static final Uri INVALID_URI =
|
||||
Uri.withAppendedPath(NotePad.Notes.CONTENT_URI, "invalid");
|
||||
|
||||
// Contains a reference to the mocked content resolver for the provider under test.
|
||||
private MockContentResolver mMockResolver;
|
||||
|
||||
// Contains an SQLite database, used as test data
|
||||
private SQLiteDatabase mDb;
|
||||
|
||||
// Contains the test data, as an array of NoteInfo instances.
|
||||
private final NoteInfo[] TEST_NOTES = {
|
||||
new NoteInfo("Note0", "This is note 0"),
|
||||
new NoteInfo("Note1", "This is note 1"),
|
||||
new NoteInfo("Note2", "This is note 2"),
|
||||
new NoteInfo("Note3", "This is note 3"),
|
||||
new NoteInfo("Note4", "This is note 4"),
|
||||
new NoteInfo("Note5", "This is note 5"),
|
||||
new NoteInfo("Note6", "This is note 6"),
|
||||
new NoteInfo("Note7", "This is note 7"),
|
||||
new NoteInfo("Note8", "This is note 8"),
|
||||
new NoteInfo("Note9", "This is note 9") };
|
||||
|
||||
// Number of milliseconds in one day (milliseconds * seconds * minutes * hours)
|
||||
private static final long ONE_DAY_MILLIS = 1000 * 60 * 60 * 24;
|
||||
|
||||
// Number of milliseconds in one week
|
||||
private static final long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7;
|
||||
|
||||
// Creates a calendar object equal to January 1, 2010 at 12 midnight
|
||||
private static final GregorianCalendar TEST_CALENDAR =
|
||||
new GregorianCalendar(2010, Calendar.JANUARY, 1, 0, 0, 0);
|
||||
|
||||
// Stores a timestamp value, set to an arbitrary starting point
|
||||
private final static long START_DATE = TEST_CALENDAR.getTimeInMillis();
|
||||
|
||||
/*
|
||||
* Constructor for the test case class.
|
||||
* Calls the super constructor with the class name of the provider under test and the
|
||||
* authority name of the provider.
|
||||
*/
|
||||
public NotePadProviderTest() {
|
||||
super(NotePadProvider.class, NotePad.AUTHORITY);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Sets up the test environment before each test method. Creates a mock content resolver,
|
||||
* gets the provider under test, and creates a new database for the provider.
|
||||
*/
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
// Calls the base class implementation of this method.
|
||||
super.setUp();
|
||||
|
||||
// Gets the resolver for this test.
|
||||
mMockResolver = getMockContentResolver();
|
||||
|
||||
/*
|
||||
* Gets a handle to the database underlying the provider. Gets the provider instance
|
||||
* created in super.setUp(), gets the DatabaseOpenHelper for the provider, and gets
|
||||
* a database object from the helper.
|
||||
*/
|
||||
mDb = getProvider().getOpenHelperForTest().getWritableDatabase();
|
||||
}
|
||||
|
||||
/*
|
||||
* This method is called after each test method, to clean up the current fixture. Since
|
||||
* this sample test case runs in an isolated context, no cleanup is necessary.
|
||||
*/
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets up test data.
|
||||
* The test data is in an SQL database. It is created in setUp() without any data,
|
||||
* and populated in insertData if necessary.
|
||||
*/
|
||||
private void insertData() {
|
||||
// Creates an instance of the ContentValues map type expected by database insertions
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
// Sets up test data
|
||||
for (int index = 0; index < TEST_NOTES.length; index++) {
|
||||
|
||||
// Set the creation and modification date for the note
|
||||
TEST_NOTES[index].setCreationDate(START_DATE + (index * ONE_DAY_MILLIS));
|
||||
TEST_NOTES[index].setModificationDate(START_DATE + (index * ONE_WEEK_MILLIS));
|
||||
|
||||
// Adds a record to the database.
|
||||
mDb.insertOrThrow(
|
||||
NotePad.Notes.TABLE_NAME, // the table name for the insert
|
||||
NotePad.Notes.COLUMN_NAME_TITLE, // column set to null if empty values map
|
||||
TEST_NOTES[index].getContentValues() // the values map to insert
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests the provider's publicly available URIs. If the URI is not one that the provider
|
||||
* understands, the provider should throw an exception. It also tests the provider's getType()
|
||||
* method for each URI, which should return the MIME type associated with the URI.
|
||||
*/
|
||||
public void testUriAndGetType() {
|
||||
// Tests the MIME type for the notes table URI.
|
||||
String mimeType = mMockResolver.getType(NotePad.Notes.CONTENT_URI);
|
||||
assertEquals(NotePad.Notes.CONTENT_TYPE, mimeType);
|
||||
|
||||
// Tests the MIME type for the live folder URI.
|
||||
mimeType = mMockResolver.getType(NotePad.Notes.LIVE_FOLDER_URI);
|
||||
assertEquals(NotePad.Notes.CONTENT_TYPE, mimeType);
|
||||
|
||||
// Creates a URI with a pattern for note ids. The id doesn't have to exist.
|
||||
Uri noteIdUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, 1);
|
||||
|
||||
// Gets the note ID URI MIME type.
|
||||
mimeType = mMockResolver.getType(noteIdUri);
|
||||
assertEquals(NotePad.Notes.CONTENT_ITEM_TYPE, mimeType);
|
||||
|
||||
// Tests an invalid URI. This should throw an IllegalArgumentException.
|
||||
mimeType = mMockResolver.getType(INVALID_URI);
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests the provider's public API for querying data in the table, using the URI for
|
||||
* a dataset of records.
|
||||
*/
|
||||
public void testQueriesOnNotesUri() {
|
||||
// Defines a projection of column names to return for a query
|
||||
final String[] TEST_PROJECTION = {
|
||||
NotePad.Notes.COLUMN_NAME_TITLE,
|
||||
NotePad.Notes.COLUMN_NAME_NOTE,
|
||||
NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE
|
||||
};
|
||||
|
||||
// Defines a selection column for the query. When the selection columns are passed
|
||||
// to the query, the selection arguments replace the placeholders.
|
||||
final String TITLE_SELECTION = NotePad.Notes.COLUMN_NAME_TITLE + " = " + "?";
|
||||
|
||||
// Defines the selection columns for a query.
|
||||
final String SELECTION_COLUMNS =
|
||||
TITLE_SELECTION + " OR " + TITLE_SELECTION + " OR " + TITLE_SELECTION;
|
||||
|
||||
// Defines the arguments for the selection columns.
|
||||
final String[] SELECTION_ARGS = { "Note0", "Note1", "Note5" };
|
||||
|
||||
// Defines a query sort order
|
||||
final String SORT_ORDER = NotePad.Notes.COLUMN_NAME_TITLE + " ASC";
|
||||
|
||||
// Query subtest 1.
|
||||
// If there are no records in the table, the returned cursor from a query should be empty.
|
||||
Cursor cursor = mMockResolver.query(
|
||||
NotePad.Notes.CONTENT_URI, // the URI for the main data table
|
||||
null, // no projection, get all columns
|
||||
null, // no selection criteria, get all records
|
||||
null, // no selection arguments
|
||||
null // use default sort order
|
||||
);
|
||||
|
||||
// Asserts that the returned cursor contains no records
|
||||
assertEquals(0, cursor.getCount());
|
||||
|
||||
// Query subtest 2.
|
||||
// If the table contains records, the returned cursor from a query should contain records.
|
||||
|
||||
// Inserts the test data into the provider's underlying data source
|
||||
insertData();
|
||||
|
||||
// Gets all the columns for all the rows in the table
|
||||
cursor = mMockResolver.query(
|
||||
NotePad.Notes.CONTENT_URI, // the URI for the main data table
|
||||
null, // no projection, get all columns
|
||||
null, // no selection criteria, get all records
|
||||
null, // no selection arguments
|
||||
null // use default sort order
|
||||
);
|
||||
|
||||
// Asserts that the returned cursor contains the same number of rows as the size of the
|
||||
// test data array.
|
||||
assertEquals(TEST_NOTES.length, cursor.getCount());
|
||||
|
||||
// Query subtest 3.
|
||||
// A query that uses a projection should return a cursor with the same number of columns
|
||||
// as the projection, with the same names, in the same order.
|
||||
Cursor projectionCursor = mMockResolver.query(
|
||||
NotePad.Notes.CONTENT_URI, // the URI for the main data table
|
||||
TEST_PROJECTION, // get the title, note, and mod date columns
|
||||
null, // no selection columns, get all the records
|
||||
null, // no selection criteria
|
||||
null // use default the sort order
|
||||
);
|
||||
|
||||
// Asserts that the number of columns in the cursor is the same as in the projection
|
||||
assertEquals(TEST_PROJECTION.length, projectionCursor.getColumnCount());
|
||||
|
||||
// Asserts that the names of the columns in the cursor and in the projection are the same.
|
||||
// This also verifies that the names are in the same order.
|
||||
assertEquals(TEST_PROJECTION[0], projectionCursor.getColumnName(0));
|
||||
assertEquals(TEST_PROJECTION[1], projectionCursor.getColumnName(1));
|
||||
assertEquals(TEST_PROJECTION[2], projectionCursor.getColumnName(2));
|
||||
|
||||
// Query subtest 4
|
||||
// A query that uses selection criteria should return only those rows that match the
|
||||
// criteria. Use a projection so that it's easy to get the data in a particular column.
|
||||
projectionCursor = mMockResolver.query(
|
||||
NotePad.Notes.CONTENT_URI, // the URI for the main data table
|
||||
TEST_PROJECTION, // get the title, note, and mod date columns
|
||||
SELECTION_COLUMNS, // select on the title column
|
||||
SELECTION_ARGS, // select titles "Note0", "Note1", or "Note5"
|
||||
SORT_ORDER // sort ascending on the title column
|
||||
);
|
||||
|
||||
// Asserts that the cursor has the same number of rows as the number of selection arguments
|
||||
assertEquals(SELECTION_ARGS.length, projectionCursor.getCount());
|
||||
|
||||
int index = 0;
|
||||
|
||||
while (projectionCursor.moveToNext()) {
|
||||
|
||||
// Asserts that the selection argument at the current index matches the value of
|
||||
// the title column (column 0) in the current record of the cursor
|
||||
assertEquals(SELECTION_ARGS[index], projectionCursor.getString(0));
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
// Asserts that the index pointer is now the same as the number of selection arguments, so
|
||||
// that the number of arguments tested is exactly the same as the number of rows returned.
|
||||
assertEquals(SELECTION_ARGS.length, index);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests queries against the provider, using the note id URI. This URI encodes a single
|
||||
* record ID. The provider should only return 0 or 1 record.
|
||||
*/
|
||||
public void testQueriesOnNoteIdUri() {
|
||||
// Defines the selection column for a query. The "?" is replaced by entries in the
|
||||
// selection argument array
|
||||
final String SELECTION_COLUMNS = NotePad.Notes.COLUMN_NAME_TITLE + " = " + "?";
|
||||
|
||||
// Defines the argument for the selection column.
|
||||
final String[] SELECTION_ARGS = { "Note1" };
|
||||
|
||||
// A sort order for the query.
|
||||
final String SORT_ORDER = NotePad.Notes.COLUMN_NAME_TITLE + " ASC";
|
||||
|
||||
// Creates a projection includes the note id column, so that note id can be retrieved.
|
||||
final String[] NOTE_ID_PROJECTION = {
|
||||
NotePad.Notes._ID, // The Notes class extends BaseColumns,
|
||||
// which includes _ID as the column name for the
|
||||
// record's id in the data model
|
||||
NotePad.Notes.COLUMN_NAME_TITLE}; // The note's title
|
||||
|
||||
// Query subtest 1.
|
||||
// Tests that a query against an empty table returns null.
|
||||
|
||||
// Constructs a URI that matches the provider's notes id URI pattern, using an arbitrary
|
||||
// value of 1 as the note ID.
|
||||
Uri noteIdUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, 1);
|
||||
|
||||
// Queries the table with the notes ID URI. This should return an empty cursor.
|
||||
Cursor cursor = mMockResolver.query(
|
||||
noteIdUri, // URI pointing to a single record
|
||||
null, // no projection, get all the columns for each record
|
||||
null, // no selection criteria, get all the records in the table
|
||||
null, // no need for selection arguments
|
||||
null // default sort, by ascending title
|
||||
);
|
||||
|
||||
// Asserts that the cursor is null.
|
||||
assertEquals(0,cursor.getCount());
|
||||
|
||||
// Query subtest 2.
|
||||
// Tests that a query against a table containing records returns a single record whose ID
|
||||
// is the one requested in the URI provided.
|
||||
|
||||
// Inserts the test data into the provider's underlying data source.
|
||||
insertData();
|
||||
|
||||
// Queries the table using the URI for the full table.
|
||||
cursor = mMockResolver.query(
|
||||
NotePad.Notes.CONTENT_URI, // the base URI for the table
|
||||
NOTE_ID_PROJECTION, // returns the ID and title columns of rows
|
||||
SELECTION_COLUMNS, // select based on the title column
|
||||
SELECTION_ARGS, // select title of "Note1"
|
||||
SORT_ORDER // sort order returned is by title, ascending
|
||||
);
|
||||
|
||||
// Asserts that the cursor contains only one row.
|
||||
assertEquals(1, cursor.getCount());
|
||||
|
||||
// Moves to the cursor's first row, and asserts that this did not fail.
|
||||
assertTrue(cursor.moveToFirst());
|
||||
|
||||
// Saves the record's note ID.
|
||||
int inputNoteId = cursor.getInt(0);
|
||||
|
||||
// Builds a URI based on the provider's content ID URI base and the saved note ID.
|
||||
noteIdUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, inputNoteId);
|
||||
|
||||
// Queries the table using the content ID URI, which returns a single record with the
|
||||
// specified note ID, matching the selection criteria provided.
|
||||
cursor = mMockResolver.query(noteIdUri, // the URI for a single note
|
||||
NOTE_ID_PROJECTION, // same projection, get ID and title columns
|
||||
SELECTION_COLUMNS, // same selection, based on title column
|
||||
SELECTION_ARGS, // same selection arguments, title = "Note1"
|
||||
SORT_ORDER // same sort order returned, by title, ascending
|
||||
);
|
||||
|
||||
// Asserts that the cursor contains only one row.
|
||||
assertEquals(1, cursor.getCount());
|
||||
|
||||
// Moves to the cursor's first row, and asserts that this did not fail.
|
||||
assertTrue(cursor.moveToFirst());
|
||||
|
||||
// Asserts that the note ID passed to the provider is the same as the note ID returned.
|
||||
assertEquals(inputNoteId, cursor.getInt(0));
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests inserts into the data model.
|
||||
*/
|
||||
public void testInserts() {
|
||||
// Creates a new note instance with ID of 30.
|
||||
NoteInfo note = new NoteInfo(
|
||||
"Note30", // the note's title
|
||||
"Test inserting a note" // the note's content
|
||||
);
|
||||
|
||||
// Sets the note's creation and modification times
|
||||
note.setCreationDate(START_DATE + (10 * ONE_DAY_MILLIS));
|
||||
note.setModificationDate(START_DATE + (2 * ONE_WEEK_MILLIS));
|
||||
|
||||
// Insert subtest 1.
|
||||
// Inserts a row using the new note instance.
|
||||
// No assertion will be done. The insert() method either works or throws an Exception
|
||||
Uri rowUri = mMockResolver.insert(
|
||||
NotePad.Notes.CONTENT_URI, // the main table URI
|
||||
note.getContentValues() // the map of values to insert as a new record
|
||||
);
|
||||
|
||||
// Parses the returned URI to get the note ID of the new note. The ID is used in subtest 2.
|
||||
long noteId = ContentUris.parseId(rowUri);
|
||||
|
||||
// Does a full query on the table. Since insertData() hasn't yet been called, the
|
||||
// table should only contain the record just inserted.
|
||||
Cursor cursor = mMockResolver.query(
|
||||
NotePad.Notes.CONTENT_URI, // the main table URI
|
||||
null, // no projection, return all the columns
|
||||
null, // no selection criteria, return all the rows in the model
|
||||
null, // no selection arguments
|
||||
null // default sort order
|
||||
);
|
||||
|
||||
// Asserts that there should be only 1 record.
|
||||
assertEquals(1, cursor.getCount());
|
||||
|
||||
// Moves to the first (and only) record in the cursor and asserts that this worked.
|
||||
assertTrue(cursor.moveToFirst());
|
||||
|
||||
// Since no projection was used, get the column indexes of the returned columns
|
||||
int titleIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE);
|
||||
int noteIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE);
|
||||
int crdateIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_CREATE_DATE);
|
||||
int moddateIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE);
|
||||
|
||||
// Tests each column in the returned cursor against the data that was inserted, comparing
|
||||
// the field in the NoteInfo object to the data at the column index in the cursor.
|
||||
assertEquals(note.title, cursor.getString(titleIndex));
|
||||
assertEquals(note.note, cursor.getString(noteIndex));
|
||||
assertEquals(note.createDate, cursor.getLong(crdateIndex));
|
||||
assertEquals(note.modDate, cursor.getLong(moddateIndex));
|
||||
|
||||
// Insert subtest 2.
|
||||
// Tests that we can't insert a record whose id value already exists.
|
||||
|
||||
// Defines a ContentValues object so that the test can add a note ID to it.
|
||||
ContentValues values = note.getContentValues();
|
||||
|
||||
// Adds the note ID retrieved in subtest 1 to the ContentValues object.
|
||||
values.put(NotePad.Notes._ID, (int) noteId);
|
||||
|
||||
// Tries to insert this record into the table. This should fail and drop into the
|
||||
// catch block. If it succeeds, issue a failure message.
|
||||
try {
|
||||
rowUri = mMockResolver.insert(NotePad.Notes.CONTENT_URI, values);
|
||||
fail("Expected insert failure for existing record but insert succeeded.");
|
||||
} catch (Exception e) {
|
||||
// succeeded, so do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests deletions from the data model.
|
||||
*/
|
||||
public void testDeletes() {
|
||||
// Subtest 1.
|
||||
// Tries to delete a record from a data model that is empty.
|
||||
|
||||
// Sets the selection column to "title"
|
||||
final String SELECTION_COLUMNS = NotePad.Notes.COLUMN_NAME_TITLE + " = " + "?";
|
||||
|
||||
// Sets the selection argument "Note0"
|
||||
final String[] SELECTION_ARGS = { "Note0" };
|
||||
|
||||
// Tries to delete rows matching the selection criteria from the data model.
|
||||
int rowsDeleted = mMockResolver.delete(
|
||||
NotePad.Notes.CONTENT_URI, // the base URI of the table
|
||||
SELECTION_COLUMNS, // select based on the title column
|
||||
SELECTION_ARGS // select title = "Note0"
|
||||
);
|
||||
|
||||
// Assert that the deletion did not work. The number of deleted rows should be zero.
|
||||
assertEquals(0, rowsDeleted);
|
||||
|
||||
// Subtest 2.
|
||||
// Tries to delete an existing record. Repeats the previous subtest, but inserts data first.
|
||||
|
||||
// Inserts data into the model.
|
||||
insertData();
|
||||
|
||||
// Uses the same parameters to try to delete the row with title "Note0"
|
||||
rowsDeleted = mMockResolver.delete(
|
||||
NotePad.Notes.CONTENT_URI, // the base URI of the table
|
||||
SELECTION_COLUMNS, // same selection column, "title"
|
||||
SELECTION_ARGS // same selection arguments, title = "Note0"
|
||||
);
|
||||
|
||||
// The number of deleted rows should be 1.
|
||||
assertEquals(1, rowsDeleted);
|
||||
|
||||
// Tests that the record no longer exists. Tries to get it from the table, and
|
||||
// asserts that nothing was returned.
|
||||
|
||||
// Queries the table with the same selection column and argument used to delete the row.
|
||||
Cursor cursor = mMockResolver.query(
|
||||
NotePad.Notes.CONTENT_URI, // the base URI of the table
|
||||
null, // no projection, return all columns
|
||||
SELECTION_COLUMNS, // select based on the title column
|
||||
SELECTION_ARGS, // select title = "Note0"
|
||||
null // use the default sort order
|
||||
);
|
||||
|
||||
// Asserts that the cursor is empty since the record had already been deleted.
|
||||
assertEquals(0, cursor.getCount());
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests updates to the data model.
|
||||
*/
|
||||
public void testUpdates() {
|
||||
// Selection column for identifying a record in the data model.
|
||||
final String SELECTION_COLUMNS = NotePad.Notes.COLUMN_NAME_TITLE + " = " + "?";
|
||||
|
||||
// Selection argument for the selection column.
|
||||
final String[] selectionArgs = { "Note1" };
|
||||
|
||||
// Defines a map of column names and values
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
// Subtest 1.
|
||||
// Tries to update a record in an empty table.
|
||||
|
||||
// Sets up the update by putting the "note" column and a value into the values map.
|
||||
values.put(NotePad.Notes.COLUMN_NAME_NOTE, "Testing an update with this string");
|
||||
|
||||
// Tries to update the table
|
||||
int rowsUpdated = mMockResolver.update(
|
||||
NotePad.Notes.CONTENT_URI, // the URI of the data table
|
||||
values, // a map of the updates to do (column title and value)
|
||||
SELECTION_COLUMNS, // select based on the title column
|
||||
selectionArgs // select "title = Note1"
|
||||
);
|
||||
|
||||
// Asserts that no rows were updated.
|
||||
assertEquals(0, rowsUpdated);
|
||||
|
||||
// Subtest 2.
|
||||
// Builds the table, and then tries the update again using the same arguments.
|
||||
|
||||
// Inserts data into the model.
|
||||
insertData();
|
||||
|
||||
// Does the update again, using the same arguments as in subtest 1.
|
||||
rowsUpdated = mMockResolver.update(
|
||||
NotePad.Notes.CONTENT_URI, // The URI of the data table
|
||||
values, // the same map of updates
|
||||
SELECTION_COLUMNS, // same selection, based on the title column
|
||||
selectionArgs // same selection argument, to select "title = Note1"
|
||||
);
|
||||
|
||||
// Asserts that only one row was updated. The selection criteria evaluated to
|
||||
// "title = Note1", and the test data should only contain one row that matches that.
|
||||
assertEquals(1, rowsUpdated);
|
||||
}
|
||||
|
||||
// A utility for converting note data to a ContentValues map.
|
||||
private static class NoteInfo {
|
||||
String title;
|
||||
String note;
|
||||
long createDate;
|
||||
long modDate;
|
||||
|
||||
/*
|
||||
* Constructor for a NoteInfo instance. This class helps create a note and
|
||||
* return its values in a ContentValues map expected by data model methods.
|
||||
* The note's id is created automatically when it is inserted into the data model.
|
||||
*/
|
||||
public NoteInfo(String t, String n) {
|
||||
title = t;
|
||||
note = n;
|
||||
createDate = 0;
|
||||
modDate = 0;
|
||||
}
|
||||
|
||||
// Sets the creation date for a test note
|
||||
public void setCreationDate(long c) {
|
||||
createDate = c;
|
||||
}
|
||||
|
||||
// Sets the modification date for a test note
|
||||
public void setModificationDate(long m) {
|
||||
modDate = m;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns a ContentValues instance (a map) for this NoteInfo instance. This is useful for
|
||||
* inserting a NoteInfo into a database.
|
||||
*/
|
||||
public ContentValues getContentValues() {
|
||||
// Gets a new ContentValues object
|
||||
ContentValues v = new ContentValues();
|
||||
|
||||
// Adds map entries for the user-controlled fields in the map
|
||||
v.put(NotePad.Notes.COLUMN_NAME_TITLE, title);
|
||||
v.put(NotePad.Notes.COLUMN_NAME_NOTE, note);
|
||||
v.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, createDate);
|
||||
v.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, modDate);
|
||||
return v;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user