Implement a basic content provider for tags.
Hookup content change notifications so the tag
list updates when you delete a tag.
A few small optimizations in TagList.
Change-Id: I342ba98c77705a393ca9d84f5b2ff14437fb1d0b
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4349180..7c55121 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -50,5 +50,12 @@
<service android:name="TagService" />
+ <provider android:name=".provider.TagProvider"
+ android:authorities="com.android.apps.tag"
+ android:syncable="false"
+ android:multiprocess="false"
+ android:exported="false"
+ />
+
</application>
</manifest>
diff --git a/src/com/android/apps/tag/TagAdapter.java b/src/com/android/apps/tag/TagAdapter.java
deleted file mode 100644
index 8fbf064..0000000
--- a/src/com/android/apps/tag/TagAdapter.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.android.apps.tag;
-
-import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.text.format.DateUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Adapter;
-import android.widget.CursorAdapter;
-import android.widget.TextView;
-
-/**
- * A custom {@link Adapter} that renders tag entries for a list.
- */
-public class TagAdapter extends CursorAdapter {
-
- private final LayoutInflater mInflater;
-
- public TagAdapter(Context context) {
- super(context, null, false);
- mInflater = LayoutInflater.from(context);
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- TextView mainLine = (TextView) view.findViewById(R.id.title);
- TextView dateLine = (TextView) view.findViewById(R.id.date);
-
- mainLine.setText(cursor.getString(cursor.getColumnIndex(NdefMessagesTable.TITLE)));
- dateLine.setText(DateUtils.getRelativeTimeSpanString(
- context, cursor.getLong(cursor.getColumnIndex(NdefMessagesTable.DATE))));
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- return mInflater.inflate(R.layout.tag_list_item, null);
- }
-}
diff --git a/src/com/android/apps/tag/TagDBHelper.java b/src/com/android/apps/tag/TagDBHelper.java
deleted file mode 100644
index 9c91afb..0000000
--- a/src/com/android/apps/tag/TagDBHelper.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * 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.android.apps.tag;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteStatement;
-import android.net.Uri;
-import android.nfc.FormatException;
-import android.nfc.NdefMessage;
-import android.nfc.NdefRecord;
-
-import java.util.Locale;
-
-import com.android.apps.tag.message.NdefMessageParser;
-import com.android.apps.tag.message.ParsedNdefMessage;
-import com.google.common.annotations.VisibleForTesting;
-
-/**
- * Database utilities for the saved tags.
- */
-public class TagDBHelper extends SQLiteOpenHelper {
-
- private static final String DATABASE_NAME = "tags.db";
- private static final int DATABASE_VERSION = 5;
-
- public interface NdefMessagesTable {
- public static final String TABLE_NAME = "nedf_msg";
-
- public static final String _ID = "_id";
- public static final String TITLE = "title";
- public static final String BYTES = "bytes";
- public static final String DATE = "date";
- public static final String STARRED = "starred";
- }
-
- private static TagDBHelper sInstance;
-
- private Context mContext;
-
- public static synchronized TagDBHelper getInstance(Context context) {
- if (sInstance == null) {
- sInstance = new TagDBHelper(context.getApplicationContext());
- }
- return sInstance;
- }
-
- private TagDBHelper(Context context) {
- this(context, DATABASE_NAME);
- mContext = context;
- }
-
- @VisibleForTesting
- TagDBHelper(Context context, String dbFile) {
- super(context, dbFile, null, DATABASE_VERSION);
- mContext = context;
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + NdefMessagesTable.TABLE_NAME + " (" +
- NdefMessagesTable._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- NdefMessagesTable.TITLE + " TEXT NOT NULL DEFAULT ''," +
- NdefMessagesTable.BYTES + " BLOB NOT NULL, " +
- NdefMessagesTable.DATE + " INTEGER NOT NULL, " +
- NdefMessagesTable.STARRED + " INTEGER NOT NULL DEFAULT 0" + // boolean
- ");");
-
- db.execSQL("CREATE INDEX msgIndex ON " + NdefMessagesTable.TABLE_NAME + " (" +
- NdefMessagesTable.DATE + " DESC, " +
- NdefMessagesTable.STARRED + " ASC" +
- ")");
-
- addTestData(db);
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- // Drop everything and recreate it for now
- db.execSQL("DROP TABLE IF EXISTS " + NdefMessagesTable.TABLE_NAME);
- onCreate(db);
- }
-
- private void addTestData(SQLiteDatabase db) {
- // A fake message containing 1 URL
- NdefMessage msg1 = new NdefMessage(new NdefRecord[] {
- NdefUtil.toUriRecord(Uri.parse("http://www.google.com"))
- });
-
- // A fake message containing 2 URLs
- NdefMessage msg2 = new NdefMessage(new NdefRecord[] {
- NdefUtil.toUriRecord(Uri.parse("http://www.youtube.com")),
- NdefUtil.toUriRecord(Uri.parse("http://www.android.com"))
- });
-
- insertNdefMessage(db, msg1, false);
- insertNdefMessage(db, msg2, true);
-
- try {
- // insert some real messages we found in the field.
- for (byte[] msg : MockNdefMessages.ALL_MOCK_MESSAGES) {
- NdefMessage msg3 = new NdefMessage(msg);
- insertNdefMessage(db, msg3, false);
- }
- } catch (FormatException e) {
- throw new RuntimeException(e);
- }
- }
-
- public void insertNdefMessage(SQLiteDatabase db, NdefMessage msg, boolean isStarred) {
- ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msg);
- SQLiteStatement stmt = null;
- try {
- stmt = db.compileStatement("INSERT INTO " + NdefMessagesTable.TABLE_NAME +
- "(" + NdefMessagesTable.BYTES + ", " + NdefMessagesTable.DATE + ", " +
- NdefMessagesTable.STARRED + "," + NdefMessagesTable.TITLE + ") " +
- "values (?, ?, ?, ?)");
- stmt.bindBlob(1, msg.toByteArray());
- stmt.bindLong(2, System.currentTimeMillis());
- stmt.bindLong(3, isStarred ? 1 : 0);
- stmt.bindString(4, parsedMsg.getSnippet(mContext, Locale.getDefault()));
- stmt.executeInsert();
- } finally {
- if (stmt != null) stmt.close();
- }
- }
-}
diff --git a/src/com/android/apps/tag/TagList.java b/src/com/android/apps/tag/TagList.java
index 71e3097..f7eef94 100644
--- a/src/com/android/apps/tag/TagList.java
+++ b/src/com/android/apps/tag/TagList.java
@@ -16,69 +16,52 @@
package com.android.apps.tag;
+import com.android.apps.tag.provider.TagContract;
+import com.android.apps.tag.provider.TagContract.NdefMessages;
+
import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
import android.app.ListActivity;
-import android.content.DialogInterface;
+import android.content.Context;
import android.content.Intent;
+import android.database.CharArrayBuffer;
import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.text.format.DateUtils;
import android.util.Log;
-import android.view.Menu;
+import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
import android.widget.ListView;
-
-import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
+import android.widget.TextView;
/**
* An {@link Activity} that displays a flat list of tags that can be "opened".
*/
-public class TagList extends ListActivity implements DialogInterface.OnClickListener {
+public class TagList extends ListActivity {
static final String TAG = "TagList";
static final String EXTRA_SHOW_STARRED_ONLY = "show_starred_only";
- SQLiteDatabase mDatabase;
TagAdapter mAdapter;
+ boolean mShowStarredOnly;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- boolean showStarredOnly = getIntent().getBooleanExtra(EXTRA_SHOW_STARRED_ONLY, false);
- mDatabase = TagDBHelper.getInstance(this).getReadableDatabase();
- String selection = showStarredOnly ? NdefMessagesTable.STARRED + "=1" : null;
+ mShowStarredOnly = getIntent().getBooleanExtra(EXTRA_SHOW_STARRED_ONLY, false);
- new TagLoaderTask().execute(selection);
+ new TagLoaderTask().execute((Void[]) null);
mAdapter = new TagAdapter(this);
setListAdapter(mAdapter);
registerForContextMenu(getListView());
}
@Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
- menu.add("hello world");
- return true;
- }
-
- @Override
- protected Dialog onCreateDialog(int id, Bundle args) {
- String[] stuff = new String[] { "a", "b" };
- return new AlertDialog.Builder(this)
- .setTitle("blah")
- .setItems(stuff, this)
- .setPositiveButton("Delete", null)
- .setNegativeButton("Cancel", null)
- .create();
- }
-
- @Override
protected void onDestroy() {
if (mAdapter != null) {
mAdapter.changeCursor(null);
@@ -90,7 +73,7 @@
protected void onListItemClick(ListView l, View v, int position, long id) {
Cursor cursor = mAdapter.getCursor();
cursor.moveToPosition(position);
- byte[] tagBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(NdefMessagesTable.BYTES));
+ byte[] tagBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(TagContract.NdefMessages.BYTES));
try {
NdefMessage msg = new NdefMessage(tagBytes);
Intent intent = new Intent(this, TagViewer.class);
@@ -103,23 +86,31 @@
}
}
- @Override
- public void onClick(DialogInterface dialog, int which) {
+ interface TagQuery {
+ static final String[] PROJECTION = new String[] {
+ NdefMessages._ID, // 0
+ NdefMessages.DATE, // 1
+ NdefMessages.TITLE, // 2
+ };
+
+ static final int COLUMN_ID = 0;
+ static final int COLUMN_DATE = 1;
+ static final int COLUMN_TITLE = 2;
}
- final class TagLoaderTask extends AsyncTask<String, Void, Cursor> {
+ /**
+ * Asynchronously loads the tag info from the database.
+ */
+ final class TagLoaderTask extends AsyncTask<Void, Void, Cursor> {
@Override
- public Cursor doInBackground(String... args) {
- String selection = args[0];
- Cursor cursor = mDatabase.query(
- NdefMessagesTable.TABLE_NAME,
- new String[] {
- NdefMessagesTable._ID,
- NdefMessagesTable.BYTES,
- NdefMessagesTable.DATE,
- NdefMessagesTable.TITLE },
+ public Cursor doInBackground(Void... args) {
+ String selection = mShowStarredOnly ? NdefMessages.STARRED + "=1" : null;
+ Cursor cursor = getContentResolver().query(
+ NdefMessages.CONTENT_URI,
+ TagQuery.PROJECTION,
selection,
- null, null, null, NdefMessagesTable.DATE + " DESC");
+ null, NdefMessages.DATE + " DESC");
+ if (cursor != null)
cursor.getCount();
return cursor;
}
@@ -129,4 +120,57 @@
mAdapter.changeCursor(cursor);
}
}
+
+ /**
+ * Struct to hold pointers to views in the list items to save time at view binding time.
+ */
+ static final class ViewHolder {
+ public CharArrayBuffer titleBuffer;
+ public TextView mainLine;
+ public TextView dateLine;
+ }
+
+ /**
+ * Adapter to display the tag entries.
+ */
+ public class TagAdapter extends CursorAdapter {
+ private final LayoutInflater mInflater;
+
+ public TagAdapter(Context context) {
+ super(context, null, false);
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ ViewHolder holder = (ViewHolder) view.getTag();
+
+ CharArrayBuffer buf = holder.titleBuffer;
+ cursor.copyStringToBuffer(TagQuery.COLUMN_TITLE, buf);
+ holder.mainLine.setText(buf.data, 0, buf.sizeCopied);
+
+ holder.dateLine.setText(DateUtils.getRelativeTimeSpanString(
+ context, cursor.getLong(TagQuery.COLUMN_DATE)));
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.tag_list_item, null);
+
+ // Cache items for the view
+ ViewHolder holder = new ViewHolder();
+ holder.titleBuffer = new CharArrayBuffer(64);
+ holder.mainLine = (TextView) view.findViewById(R.id.title);
+ holder.dateLine = (TextView) view.findViewById(R.id.date);
+ view.setTag(holder);
+
+ return view;
+ }
+
+ @Override
+ public void onContentChanged() {
+ // Kick off an async query to refresh the list
+ new TagLoaderTask().execute((Void[]) null);
+ }
+ }
}
diff --git a/src/com/android/apps/tag/TagService.java b/src/com/android/apps/tag/TagService.java
index 8748a55..c43142a 100644
--- a/src/com/android/apps/tag/TagService.java
+++ b/src/com/android/apps/tag/TagService.java
@@ -16,15 +16,25 @@
package com.android.apps.tag;
-import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
+import com.android.apps.tag.provider.TagContract;
+import com.android.apps.tag.provider.TagContract.NdefMessages;
import android.app.IntentService;
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Intent;
-import android.database.sqlite.SQLiteDatabase;
+import android.content.OperationApplicationException;
import android.nfc.NdefMessage;
import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
public class TagService extends IntentService {
+ private static final String TAG = "TagService";
+
public static final String EXTRA_SAVE_MSGS = "msgs";
public static final String EXTRA_DELETE_ID = "delete";
@@ -34,24 +44,27 @@
@Override
public void onHandleIntent(Intent intent) {
- TagDBHelper helper = TagDBHelper.getInstance(this);
- SQLiteDatabase db = helper.getWritableDatabase();
if (intent.hasExtra(EXTRA_SAVE_MSGS)) {
Parcelable[] parcels = intent.getParcelableArrayExtra(EXTRA_SAVE_MSGS);
- db.beginTransaction();
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ for (Parcelable parcel : parcels) {
+ ContentValues values = NdefMessages.ndefMessageToValues(this, (NdefMessage) parcel
+ , false);
+ ops.add(ContentProviderOperation.newInsert(NdefMessages.CONTENT_URI)
+ .withValues(values).build());
+ }
try {
- for (Parcelable parcel : parcels) {
- helper.insertNdefMessage(db, (NdefMessage) parcel, false);
- }
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
+ getContentResolver().applyBatch(TagContract.AUTHORITY, ops);
+ } catch (OperationApplicationException e) {
+ Log.e(TAG, "Failed to save messages", e);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to save messages", e);
}
return;
} else if (intent.hasExtra(EXTRA_DELETE_ID)) {
long id = intent.getLongExtra(EXTRA_DELETE_ID, 0);
- db.delete(NdefMessagesTable.TABLE_NAME, NdefMessagesTable._ID + "=?",
- new String[] { Long.toString(id) });
+ getContentResolver().delete(ContentUris.withAppendedId(NdefMessages.CONTENT_URI, id),
+ null, null);
return;
}
}
diff --git a/src/com/android/apps/tag/provider/SQLiteContentProvider.java b/src/com/android/apps/tag/provider/SQLiteContentProvider.java
new file mode 100644
index 0000000..ade9eaf
--- /dev/null
+++ b/src/com/android/apps/tag/provider/SQLiteContentProvider.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.apps.tag.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.net.Uri;
+
+import java.util.ArrayList;
+
+/**
+ * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage.
+ */
+public abstract class SQLiteContentProvider extends ContentProvider
+ implements SQLiteTransactionListener {
+
+ private static final String TAG = "SQLiteContentProvider";
+
+ private SQLiteOpenHelper mOpenHelper;
+ private volatile boolean mNotifyChange;
+ protected SQLiteDatabase mDb;
+
+ private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
+ private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
+
+ /**
+ * Maximum number of operations allowed in a batch between yield points.
+ */
+ private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
+
+ @Override
+ public boolean onCreate() {
+ Context context = getContext();
+ mOpenHelper = getDatabaseHelper(context);
+ return true;
+ }
+
+ protected abstract SQLiteOpenHelper getDatabaseHelper(Context context);
+
+ /**
+ * The equivalent of the {@link #insert} method, but invoked within a transaction.
+ */
+ protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
+
+ /**
+ * The equivalent of the {@link #update} method, but invoked within a transaction.
+ */
+ protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs);
+
+ /**
+ * The equivalent of the {@link #delete} method, but invoked within a transaction.
+ */
+ protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
+
+ protected abstract void notifyChange();
+
+ protected SQLiteOpenHelper getDatabaseHelper() {
+ return mOpenHelper;
+ }
+
+ private boolean applyingBatch() {
+ return mApplyingBatch.get() != null && mApplyingBatch.get();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ Uri result = null;
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ result = insertInTransaction(uri, values);
+ if (result != null) {
+ mNotifyChange = true;
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction();
+ } else {
+ result = insertInTransaction(uri, values);
+ if (result != null) {
+ mNotifyChange = true;
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ int numValues = values.length;
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ for (int i = 0; i < numValues; i++) {
+ Uri result = insertInTransaction(uri, values[i]);
+ if (result != null) {
+ mNotifyChange = true;
+ }
+ mDb.yieldIfContendedSafely();
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction();
+ return numValues;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ int count = 0;
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ count = updateInTransaction(uri, values, selection, selectionArgs);
+ if (count > 0) {
+ mNotifyChange = true;
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction();
+ } else {
+ count = updateInTransaction(uri, values, selection, selectionArgs);
+ if (count > 0) {
+ mNotifyChange = true;
+ }
+ }
+
+ return count;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ int count = 0;
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ count = deleteInTransaction(uri, selection, selectionArgs);
+ if (count > 0) {
+ mNotifyChange = true;
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction();
+ } else {
+ count = deleteInTransaction(uri, selection, selectionArgs);
+ if (count > 0) {
+ mNotifyChange = true;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ int ypCount = 0;
+ int opCount = 0;
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ mApplyingBatch.set(true);
+ final int numOperations = operations.size();
+ final ContentProviderResult[] results = new ContentProviderResult[numOperations];
+ for (int i = 0; i < numOperations; i++) {
+ if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
+ throw new OperationApplicationException(
+ "Too many content provider operations between yield points. "
+ + "The maximum number of operations per yield point is "
+ + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
+ }
+ final ContentProviderOperation operation = operations.get(i);
+ if (i > 0 && operation.isYieldAllowed()) {
+ opCount = 0;
+ if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
+ ypCount++;
+ }
+ }
+ results[i] = operation.apply(this, results, i);
+ }
+ mDb.setTransactionSuccessful();
+ return results;
+ } finally {
+ mApplyingBatch.set(false);
+ mDb.endTransaction();
+ onEndTransaction();
+ }
+ }
+
+ public void onBegin() {
+ onBeginTransaction();
+ }
+
+ public void onCommit() {
+ beforeTransactionCommit();
+ }
+
+ public void onRollback() {
+ // not used
+ }
+
+ protected void onBeginTransaction() {
+ }
+
+ protected void beforeTransactionCommit() {
+ }
+
+ protected void onEndTransaction() {
+ if (mNotifyChange) {
+ mNotifyChange = false;
+ notifyChange();
+ }
+ }
+}
diff --git a/src/com/android/apps/tag/provider/TagContract.java b/src/com/android/apps/tag/provider/TagContract.java
new file mode 100644
index 0000000..2eaa653
--- /dev/null
+++ b/src/com/android/apps/tag/provider/TagContract.java
@@ -0,0 +1,77 @@
+/*
+ * 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.android.apps.tag.provider;
+
+import com.android.apps.tag.message.NdefMessageParser;
+import com.android.apps.tag.message.ParsedNdefMessage;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+
+import java.util.Locale;
+
+public class TagContract {
+ public static final String AUTHORITY = "com.android.apps.tag";
+ public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+ public static final class NdefMessages {
+ /**
+ * Utility class, cannot be instantiated.
+ */
+ private NdefMessages() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI = AUTHORITY_URI.buildUpon().appendPath("ndef").build();
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * NDEF messages.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/ndef_msg";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * NDEF message.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/ndef_msg";
+
+ // columns
+ public static final String _ID = "_id";
+ public static final String TITLE = "title";
+ public static final String BYTES = "bytes";
+ public static final String DATE = "date";
+ public static final String STARRED = "starred";
+
+ /**
+ * Converts an NdefMessage to ContentValues that can be insrted into this table.
+ */
+ public static ContentValues ndefMessageToValues(Context context, NdefMessage msg,
+ boolean isStarred) {
+ ParsedNdefMessage parsedMsg = NdefMessageParser.parse(msg);
+ ContentValues values = new ContentValues();
+ values.put(BYTES, msg.toByteArray());
+ values.put(DATE, System.currentTimeMillis());
+ values.put(STARRED, isStarred ? 1 : 0);
+ values.put(TITLE, parsedMsg.getSnippet(context, Locale.getDefault()));
+ return values;
+ }
+ }
+}
diff --git a/src/com/android/apps/tag/provider/TagDBHelper.java b/src/com/android/apps/tag/provider/TagDBHelper.java
new file mode 100644
index 0000000..a54c8e1
--- /dev/null
+++ b/src/com/android/apps/tag/provider/TagDBHelper.java
@@ -0,0 +1,67 @@
+/*
+ * 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.android.apps.tag.provider;
+
+import com.android.apps.tag.provider.TagContract.NdefMessages;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * Database utilities for the saved tags.
+ */
+public class TagDBHelper extends SQLiteOpenHelper {
+
+ private static final String DATABASE_NAME = "tags.db";
+ private static final int DATABASE_VERSION = 5;
+
+ public static final String TABLE_NAME_NDEF_MESSAGES = "nedf_msg";
+
+ TagDBHelper(Context context) {
+ this(context, DATABASE_NAME);
+ }
+
+ @VisibleForTesting
+ TagDBHelper(Context context, String dbFile) {
+ super(context, dbFile, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_NAME_NDEF_MESSAGES + " (" +
+ NdefMessages._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ NdefMessages.TITLE + " TEXT NOT NULL DEFAULT ''," +
+ NdefMessages.BYTES + " BLOB NOT NULL, " +
+ NdefMessages.DATE + " INTEGER NOT NULL, " +
+ NdefMessages.STARRED + " INTEGER NOT NULL DEFAULT 0" + // boolean
+ ");");
+
+ db.execSQL("CREATE INDEX msgIndex ON " + TABLE_NAME_NDEF_MESSAGES + " (" +
+ TagContract.NdefMessages.DATE + " DESC, " +
+ TagContract.NdefMessages.STARRED + " ASC" +
+ ")");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // Drop everything and recreate it for now
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME_NDEF_MESSAGES);
+ onCreate(db);
+ }
+}
diff --git a/src/com/android/apps/tag/provider/TagProvider.java b/src/com/android/apps/tag/provider/TagProvider.java
new file mode 100644
index 0000000..89dd974
--- /dev/null
+++ b/src/com/android/apps/tag/provider/TagProvider.java
@@ -0,0 +1,203 @@
+/*
+ * 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.android.apps.tag.provider;
+
+import com.android.apps.tag.provider.TagContract.NdefMessages;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.util.Log;
+
+import java.util.HashMap;
+
+public class TagProvider extends SQLiteContentProvider {
+
+ private static final int NDEF_MESSAGES = 1000;
+ private static final int NDEF_MESSAGES_ID = 1001;
+ private static final UriMatcher sMatcher;
+
+ private static final HashMap<String, String> sNdefMessagesProjectionMap;
+
+ static {
+ sMatcher = new UriMatcher(0);
+ String auth = TagContract.AUTHORITY;
+ sMatcher.addURI(auth, "ndef", NDEF_MESSAGES);
+ sMatcher.addURI(auth, "ndef/#", NDEF_MESSAGES_ID);
+
+ HashMap<String, String> map = new HashMap<String, String>();
+ map.put(NdefMessages._ID, NdefMessages._ID);
+ map.put(NdefMessages.TITLE, NdefMessages.TITLE);
+ map.put(NdefMessages.BYTES, NdefMessages.BYTES);
+ map.put(NdefMessages.DATE, NdefMessages.DATE);
+ map.put(NdefMessages.STARRED, NdefMessages.STARRED);
+ sNdefMessagesProjectionMap = map;
+ }
+
+ @Override
+ protected SQLiteOpenHelper getDatabaseHelper(Context context) {
+ return new TagDBHelper(context);
+ }
+
+ /**
+ * Appends one set of selection args to another. This is useful when adding a selection
+ * argument to a user provided set.
+ */
+ public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
+ if (originalValues == null || originalValues.length == 0) {
+ return newValues;
+ }
+ String[] result = new String[originalValues.length + newValues.length ];
+ System.arraycopy(originalValues, 0, result, 0, originalValues.length);
+ System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
+ return result;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ int match = sMatcher.match(uri);
+ switch (match) {
+ case NDEF_MESSAGES_ID: {
+ selection = DatabaseUtils.concatenateWhere(selection,
+ TagDBHelper.TABLE_NAME_NDEF_MESSAGES + "._id=?");
+ selectionArgs = appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ }
+ case NDEF_MESSAGES: {
+ qb.setTables(TagDBHelper.TABLE_NAME_NDEF_MESSAGES);
+ qb.setProjectionMap(sNdefMessagesProjectionMap);
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException("unkown uri " + uri);
+ }
+ }
+
+ Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ if (cursor != null) {
+ cursor.setNotificationUri(getContext().getContentResolver(), TagContract.AUTHORITY_URI);
+ }
+ return cursor;
+ }
+
+ @Override
+ protected Uri insertInTransaction(Uri uri, ContentValues values) {
+ SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+ int match = sMatcher.match(uri);
+ long id = -1;
+ switch (match) {
+ case NDEF_MESSAGES: {
+ id = db.insert(TagDBHelper.TABLE_NAME_NDEF_MESSAGES, NdefMessages.TITLE, values);
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException("unkown uri " + uri);
+ }
+ }
+
+ if (id >= 0) {
+ return ContentUris.withAppendedId(uri, id);
+ }
+ return null;
+ }
+
+ @Override
+ protected int updateInTransaction(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+ int match = sMatcher.match(uri);
+ int count = 0;
+ switch (match) {
+ case NDEF_MESSAGES_ID: {
+ selection = DatabaseUtils.concatenateWhere(selection,
+ TagDBHelper.TABLE_NAME_NDEF_MESSAGES + "._id=?");
+ selectionArgs = appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ }
+ case NDEF_MESSAGES: {
+ count = db.update(TagDBHelper.TABLE_NAME_NDEF_MESSAGES, values, selection,
+ selectionArgs);
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException("unkown uri " + uri);
+ }
+ }
+
+ return count;
+ }
+
+ @Override
+ protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+ int match = sMatcher.match(uri);
+ int count = 0;
+ switch (match) {
+ case NDEF_MESSAGES_ID: {
+ selection = DatabaseUtils.concatenateWhere(selection,
+ TagDBHelper.TABLE_NAME_NDEF_MESSAGES + "._id=?");
+ selectionArgs = appendSelectionArgs(selectionArgs,
+ new String[] { Long.toString(ContentUris.parseId(uri)) });
+ // fall through
+ }
+ case NDEF_MESSAGES: {
+ count = db.delete(TagDBHelper.TABLE_NAME_NDEF_MESSAGES, selection, selectionArgs);
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException("unkown uri " + uri);
+ }
+ }
+
+ return count;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ int match = sMatcher.match(uri);
+ switch (match) {
+ case NDEF_MESSAGES_ID: {
+ return NdefMessages.CONTENT_ITEM_TYPE;
+ }
+ case NDEF_MESSAGES: {
+ return NdefMessages.CONTENT_TYPE;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void notifyChange() {
+ getContext().getContentResolver().notifyChange(TagContract.AUTHORITY_URI, null, false);
+ }
+}