ContentProvider

ええと、リファクタリングしてみました。NotesColumns というクラスを作成。
以下です。

package com.android.demo.notepad3;

import android.net.Uri;
import android.provider.BaseColumns;

public interface NotesColumns extends BaseColumns {
	public static final String NOTES_TABLE = "notes";
	
	public static final Uri CONTENT_URI =
		Uri.parse("content://" + NotesProvider.AUTHORITY + NOTES_TABLE);
	public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.note";
	public static final String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.google.note";
	
	public static final String KEY_TITLE = "title";
	public static final String KEY_BODY = "body";
}

android.provider.BaseColumns というソレですが、_id と _count という static な定数が定義されている模様。あと CONTENT_URI の定義の方法も若干どうなんだろな的ソレです。
MyTracks の TracksColumns というクラスを参考にしてみたのですが、なんとも言えない感満点。

上記を元に

NotesProvider を以下に修正しております。

package com.android.demo.notepad3;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;

public class NotesProvider extends ContentProvider {
    public static final String AUTHORITY = "com.android.demo.notepad3";
    private static final String DATABASE_NAME = "Notepad.db";
    private static final int DATABASE_VERSION = 1;

    private static final String TAG = "NotesProvider";
	  
    /**
     * Database creation sql statement
     */
    private static final String DATABASE_CREATE =
        "create table notes (_id integer primary key autoincrement, "
        + "title text not null, body text not null);";

    private static class DatabaseHelper extends SQLiteOpenHelper {

        public DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        public void onCreate(SQLiteDatabase db) {
            db.execSQL(DATABASE_CREATE);
        }

        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS notes");
            onCreate(db);
        }
    }
	  
    private UriMatcher urlMatcher; 
    private static final int NOTES = 1;
    private static final int NOTES_ID = 2;
	  
    private SQLiteDatabase db;

    public NotesProvider() {
        urlMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        urlMatcher.addURI(AUTHORITY, "notes", NOTES);
        urlMatcher.addURI(AUTHORITY, "notes/#", NOTES_ID);
    }
	  
    @Override
    public boolean onCreate() {
        DatabaseHelper dbHelper = new DatabaseHelper(getContext());
        db = dbHelper.getWritableDatabase();
        return db != null;
    }

    @Override
    public int delete(Uri url, String where, String[] selectionArgs) {
        int count = db.delete(NotesColumns.NOTES_TABLE, where, selectionArgs);
        getContext().getContentResolver().notifyChange(url, null, true);
        return count;
    }

    @Override
    public String getType(Uri url) {
        switch (urlMatcher.match(url)) {
        case NOTES:
            return NotesColumns.CONTENT_TYPE;
        case NOTES_ID:
            return NotesColumns.CONTENT_ITEMTYPE;
        default:
            throw new IllegalArgumentException("Unknown URL " + url);
        }
    }

    @Override
    public Uri insert(Uri url, ContentValues initialValues) {
        ContentValues values;
        if (initialValues != null) {
            values = initialValues;
        } else {
            values = new ContentValues();
        }
        long rowId = db.insert(NotesColumns.NOTES_TABLE, null, values);
        if (rowId >= 0) {
            Uri uri = ContentUris.appendId(NotesColumns.CONTENT_URI.buildUpon(), rowId).build();
            getContext().getContentResolver().notifyChange(url, null, true);
            return uri;
        }
        throw new SQLiteException("Failed to insert row into " + url);
    }

    @Override
    public Cursor query(
            Uri url, String[] projection, String selection, String[] selectionArgs,
            String sort) {
        Cursor c = db.query(true, NotesColumns.NOTES_TABLE, 
                new String[] {NotesColumns._ID, NotesColumns.KEY_TITLE,
                NotesColumns.KEY_BODY}, selection, null,
                null, null, null, null);
        c.setNotificationUri(getContext().getContentResolver(), url);
        return c;
    }

    @Override
    public int update(Uri url, ContentValues values, String where,
            String[] selectionArgs) {
        int count = db.update(NotesColumns.NOTES_TABLE, values, where, null);
        getContext().getContentResolver().notifyChange(url, null, true);
        return count;
    }
}

なるべくマジックナンバーとか文字列リテラルの定義元を集約させる方向で云々。ただ、上記のサンプルは UriMatcher とか getType とかの動作を確認できてる訳ではないので若干微妙。
このクラスでは SQLiteDatabase なオブジェクトを使ってデータ更新なメソッド呼び出しをしております。以前の NotesDBAdapter クラスがしていた事をこちらがやってるイメージですね。あ、Cursor#setNotificationUri メソドについて確認必要かな。あるいは ContentResolver#notifyChange とかも。

確認してみました。

Android Developers の Cursor クラスの setNotificationUri メソドの説明には以下の記述がある。

Register to watch a content URI for changes.

これは notifyChange なソレを watch する、という事なのでしょうね。ちなみに ContentResolver#notifyChange の記述が以下。

Notify registered observers that a row was updated.

で、NotesDBAdapter クラスの記述が以下になりました。

/*
 * Copyright (C) 2008 Google Inc.
 * 
 * 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.android.demo.notepad3;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.net.Uri;

/**
 * Simple notes database access helper class. Defines the basic CRUD operations
 * for the notepad example, and gives the ability to list all notes as well as
 * retrieve or modify a specific note.
 * 
 * This has been improved from the first version of this tutorial through the
 * addition of better error handling and also using returning a Cursor instead
 * of using a collection of inner classes (which is less scalable and not
 * recommended).
 */
public class NotesDbAdapter {
    private final Context mCtx;

    /**
     * Constructor - takes the context to allow the database to be
     * opened/created
     * 
     * @param ctx the Context within which to work
     */
    public NotesDbAdapter(Context ctx) {
        this.mCtx = ctx;
    }

    /**
     * Create a new note using the title and body provided. If the note is
     * successfully created return the new rowId for that note, otherwise return
     * a -1 to indicate failure.
     * 
     * @param title the title of the note
     * @param body the body of the note
     * @return rowId or -1 if failed
     */
    public long createNote(String title, String body) {
        ContentValues initialValues = new ContentValues();
        initialValues.put(NotesColumns.KEY_TITLE, title);
        initialValues.put(NotesColumns.KEY_BODY, body);

        Uri tmp = mCtx.getContentResolver().insert(NotesColumns.CONTENT_URI, initialValues);
        return Long.parseLong(tmp.getLastPathSegment());
    }

    /**
     * Delete the note with the given rowId
     * 
     * @param rowId id of note to delete
     * @return true if deleted, false otherwise
     */
    public boolean deleteNote(long rowId) {
        return mCtx.getContentResolver().delete(NotesColumns.CONTENT_URI, NotesColumns._ID + " = " + rowId, null) > 0;
    }

    /**
     * Return a Cursor over the list of all notes in the database
     * 
     * @return Cursor over all notes
     */
    public Cursor fetchAllNotes() {
    	return mCtx.getContentResolver().query(NotesColumns.CONTENT_URI, null, null, null, "_id DESC");
    }

    /**
     * Return a Cursor positioned at the note that matches the given rowId
     * 
     * @param rowId id of note to retrieve
     * @return Cursor positioned to matching note, if found
     * @throws SQLException if note could not be found/retrieved
     */
    public Cursor fetchNote(long rowId) throws SQLException {

        Cursor mCursor =
        	mCtx.getContentResolver().query(NotesColumns.CONTENT_URI, null, NotesColumns._ID + " = " + rowId, null, null);
        if (mCursor != null) {
            mCursor.moveToFirst();
        }
        return mCursor;

    }

    /**
     * Update the note using the details provided. The note to be updated is
     * specified using the rowId, and it is altered to use the title and body
     * values passed in
     * 
     * @param rowId id of note to update
     * @param title value to set note title to
     * @param body value to set note body to
     * @return true if the note was successfully updated, false otherwise
     */
    public boolean updateNote(long rowId, String title, String body) {
        ContentValues args = new ContentValues();
        args.put(NotesColumns.KEY_TITLE, title);
        args.put(NotesColumns.KEY_BODY, body);

        return mCtx.getContentResolver().update(NotesColumns.CONTENT_URI, args, NotesColumns._ID + " = " + rowId, null) > 0;
    }
}

こちらは非常に簡易にというか抽象化された格好になっております。あと Manifest も以下に引用しておきます。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.demo.notepad3">
    <application android:icon="@drawable/icon">
        <activity android:name=".Notepadv3" android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".NoteEdit"/>
        <provider android:name="com.android.demo.notepad3.NotesProvider"
          android:multiprocess="true"
          android:authorities="com.android.demo.notepad3" />
    </application>
</manifest> 

provider の name 属性が完全修飾なクラス名になってて、authrities は NotesProvider で定義されてる文字列と同一となってます。
あと multiprocess 属性ですが、ここによると

Whether or not an instance of the content provider can be created in every client process - "true" if instances can run in multiple processes, and "false" if not. The default value is "false".
Normally, a content provider is instantiated in the process of the application that defined it. However, if this flag is set to "true", the system can create an instance in every process where there's a client that wants to interact with it, thus avoiding the overhead of interprocess communication.

との事。ちょい理解が微妙かも。
あと、プロジェクト全体を zipped したのを ココ に投入しました。ご参考まで。

追記

画面遷移部分、暗黙な intent で画面遷移ができそうな気がしてます。
例えば以下な intent-filter を書いておいて

  <activity android:name="NoteEdit"
    <intent-filter>
      <action android:name="android.intent.action.INSERT">
      <action android:name="android.intent.action.EDIT">
      <data android:scheme="content">
    </intent-filter>
  </activity>

あるいは一覧画面からの画面遷移が例えば以下で

  Uri uri = Uri.parse(NotesColumns.CONTENT_URI + "/" + rowId);
  Intent i = new Intent();
  i.setAction(Intent.ACTION_EDIT);
  i.setData(uri);
  startActivity(i);

以下な形で切り分ければ良いのか。

    if (mRowId == null) {
        if (getIntent().getAction() == Intent.ACTION_EDIT) {
            mRowId = Long.parseLong(getIntent().getData().getLastPathSegment());

この件、model なソレが GAE になるとどうなるか、を想定しつつ検討してるんですが面白いスね (何