/*
* Copyright 2014 sonaive.com. All rights reserved.
*
* 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.sonaive.v2ex.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.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import com.sonaive.v2ex.provider.V2exContract.*;
import com.sonaive.v2ex.util.SelectionBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import com.sonaive.v2ex.provider.V2exDatabase.*;
import static com.sonaive.v2ex.util.LogUtils.LOGD;
import static com.sonaive.v2ex.util.LogUtils.LOGV;
import static com.sonaive.v2ex.util.LogUtils.makeLogTag;
/**
* Created by liutao on 12/6/14.
*/
public class V2exProvider extends ContentProvider {
private static final String TAG = makeLogTag(V2exProvider.class);
private static final int MEMBERS = 100;
private static final int MEMBERS_USERNAME = 101;
private static final int PICASAS = 200;
private static final int PICASAS_ID = 201;
private static final int DATE = 300;
private static final int FEEDS = 400;
private static final int FEEDS_ID = 401;
private static final int NODES = 500;
private static final int NODES_ID = 501;
private static final int REVIEWS = 600;
private static final int REVIEWS_TOPIC_ID = 601;
private static final int SEARCH = 700;
private static final int SEARCH_ID = 701;
private V2exDatabase mOpenHelper;
private static final UriMatcher sUriMatcher = buildUriMatcher();
/**
* Build and return a {@link UriMatcher} that catches all {@link Uri}
* variations supported by this {@link ContentProvider}.
*/
private static UriMatcher buildUriMatcher() {
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = V2exContract.CONTENT_AUTHORITY;
matcher.addURI(authority, "members", MEMBERS);
matcher.addURI(authority, "members/*", MEMBERS_USERNAME);
matcher.addURI(authority, "feeds", FEEDS);
matcher.addURI(authority, "feeds/*", FEEDS_ID);
matcher.addURI(authority, "nodes", NODES);
matcher.addURI(authority, "nodes/*", NODES_ID);
matcher.addURI(authority, "reviews", REVIEWS);
matcher.addURI(authority, "reviews/*", REVIEWS_TOPIC_ID);
matcher.addURI(authority, "search", SEARCH);
matcher.addURI(authority, "search/*", SEARCH_ID);
matcher.addURI(authority, "picasas", PICASAS);
matcher.addURI(authority, "picasas/*", PICASAS_ID);
matcher.addURI(authority, "dates", DATE);
return matcher;
}
private void deleteDatabase() {
// TODO: wait for content provider operations to finish, then tear down
mOpenHelper.close();
Context context = getContext();
V2exDatabase.deleteDatabase(context);
mOpenHelper = new V2exDatabase(getContext());
}
@Override
public boolean onCreate() {
mOpenHelper = new V2exDatabase(getContext());
return true;
}
@Override
public String getType(Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match) {
case MEMBERS: {
return Members.CONTENT_TYPE;
}
case MEMBERS_USERNAME: {
return Members.CONTENT_ITEM_TYPE;
}
case FEEDS: {
return Feeds.CONTENT_TYPE;
}
case FEEDS_ID: {
return Feeds.CONTENT_ITEM_TYPE;
}
case NODES: {
return Nodes.CONTENT_TYPE;
}
case NODES_ID: {
return Nodes.CONTENT_ITEM_TYPE;
}
case REVIEWS: {
return Reviews.CONTENT_TYPE;
}
case REVIEWS_TOPIC_ID: {
return Reviews.CONTENT_ITEM_TYPE;
}
case SEARCH: {
return Search.CONTENT_TYPE;
}
case SEARCH_ID: {
return Search.CONTENT_ITEM_TYPE;
}
case PICASAS: {
return PicasaImages.CONTENT_TYPE;
}
case PICASAS_ID: {
return PicasaImages.CONTENT_ITEM_TYPE;
}
case DATE: {
return ModiDates.CONTENT_TYPE;
}
default: {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
final int match = sUriMatcher.match(uri);
// avoid the expensive string concatenation below if not loggable
if (Log.isLoggable(TAG, Log.VERBOSE)) {
LOGV(TAG, "uri=" + uri + " match=" + match + " proj=" + Arrays.toString(projection) +
" selection=" + selection + " args=" + Arrays.toString(selectionArgs) + ")");
}
switch (match) {
default:
// Most cases are handled with simple SelectionBuilder
final SelectionBuilder builder = buildExpandedSelection(uri, match);
boolean distinct = !TextUtils.isEmpty(
uri.getQueryParameter(V2exContract.QUERY_PARAMETER_DISTINCT));
String limit = uri.getQueryParameter(V2exContract.QUERY_PARAMETER_LIMIT);
String offset = uri.getQueryParameter(V2exContract.QUERY_PARAMETER_OFFSET);
String limitClause = null;
if (limit != null) {
if (offset == null) {
limitClause = limit;
} else {
limitClause = offset + "," + limit;
}
}
LOGD(TAG, "limit clause is: " + limitClause);
Cursor cursor = builder
.where(selection, selectionArgs)
.query(db, distinct, projection, sortOrder, limitClause);
Context context = getContext();
if (null != context) {
cursor.setNotificationUri(context.getContentResolver(), uri);
}
return cursor;
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
LOGV(TAG, "insert(uri=" + uri + ", values=" + values.toString());
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
switch (match) {
// For the modification date table
case DATE: {
// Inserts the row into the table and returns the new row's _id value
long id = db.insert(
Tables.MODI_DATE,
ModiDates.MODI_DATE,
values
);
// If the insert succeeded, notify a change and return the new row's content URI.
if (-1 != id) {
notifyChange(uri);
return Uri.withAppendedPath(uri, Long.toString(id));
} else {
throw new SQLiteException("Insert error:" + uri);
}
}
case MEMBERS_USERNAME: {
db.insertOrThrow(Tables.MEMBERS, null, values);
notifyChange(uri);
return Members.buildMemberUsernameUri(values.getAsString(Members.MEMBER_USERNAME));
}
case FEEDS: {
db.insertOrThrow(Tables.FEEDS, null, values);
notifyChange(uri);
return Feeds.buildFeedUri(values.getAsString(Feeds.FEED_ID));
}
case NODES: {
db.insertOrThrow(Tables.NODES, null, values);
notifyChange(uri);
return Nodes.buildNodeUri(values.getAsString(Nodes.NODE_ID));
}
case REVIEWS: {
db.insertOrThrow(Tables.REVIEWS, null, values);
notifyChange(uri);
return Reviews.buildReviewTopicUri(values.getAsString(Reviews.REVIEW_ID));
}
case SEARCH: {
db.insertOrThrow(Tables.SEARCH, null, values);
notifyChange(uri);
return Search.buildSearchUri(values.getAsString(Search._ID));
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
/** {@inheritDoc} */
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
LOGV(TAG, "delete(uri=" + uri);
if (uri == V2exContract.BASE_CONTENT_URI) {
// Handle whole database deletes (e.g. when signing out)
deleteDatabase();
notifyChange(uri);
return 1;
}
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final SelectionBuilder builder = buildSimpleSelection(uri);
int retVal = builder.where(selection, selectionArgs).delete(db);
notifyChange(uri);
return retVal;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
LOGV(TAG, "update(uri=" + uri + ", values=" + values.toString());
// Decodes the content URI and choose which insert to use
switch (match) {
default: {
final SelectionBuilder builder = buildSimpleSelection(uri);
int retVal = builder.where(selection, selectionArgs).update(db, values);
notifyChange(uri);
return retVal;
}
// A picture URL content URI
case DATE: {
// Updates the table
int rows = db.update(
Tables.MODI_DATE,
values,
selection,
selectionArgs);
// If the update succeeded, notify a change and return the number of updated rows.
if (0 != rows) {
getContext().getContentResolver().notifyChange(uri, null);
return rows;
} else {
throw new SQLiteException("Update error:" + uri);
}
}
case PICASAS: {
throw new UnsupportedOperationException("Update: Invalid URI: " + uri);
}
}
}
/**
* Implements bulk row insertion using
* {@link android.database.sqlite.SQLiteDatabase#insert(String, String, android.content.ContentValues) SQLiteDatabase.insert()}
* and SQLite transactions. The method also notifies the current
* {@link android.content.ContentResolver} that the {@link android.content.ContentProvider} has
* been changed.
* @see android.content.ContentProvider#bulkInsert(android.net.Uri, android.content.ContentValues[])
* @param uri The content URI for the insertion
* @param insertValuesArray A {@link android.content.ContentValues} array containing the row to
* insert
* @return The number of rows inserted.
*/
@Override
public int bulkInsert(Uri uri, ContentValues[] insertValuesArray) {
// Gets a writeable database instance if one is not already cached
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
switch (match) {
// picture URLs table
case PICASAS:
/*
* Begins a transaction in "exclusive" mode. No other mutations can occur on the
* db until this transaction finishes.
*/
db.beginTransaction();
// Deletes all the existing rows in the table
db.delete(Tables.PICASA_IMAGES, null, null);
// Gets the size of the bulk insert
int numImages = insertValuesArray.length;
// Inserts each ContentValues entry in the array as a row in the database
for (int i = 0; i < numImages; i++) {
db.insert(Tables.PICASA_IMAGES,
PicasaImages.PICASA_IMAGE_URL, insertValuesArray[i]);
}
// Reports that the transaction was successful and should not be backed out.
db.setTransactionSuccessful();
// Ends the transaction and closes the current db instances
db.endTransaction();
db.close();
/*
* Notifies the current ContentResolver that the data associated with "uri" has
* changed.
*/
getContext().getContentResolver().notifyChange(uri, null);
// The semantics of bulkInsert is to return the number of rows inserted.
return numImages;
default:
throw new IllegalArgumentException("Bulk insert -- Invalid URI:" + uri);
}
}
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
db.beginTransaction();
try {
final int numOperations = operations.size();
final ContentProviderResult[] results = new ContentProviderResult[numOperations];
for (int i = 0; i < numOperations; i++) {
results[i] = operations.get(i).apply(this, results, i);
}
db.setTransactionSuccessful();
return results;
} finally {
db.endTransaction();
}
}
private void notifyChange(Uri uri) {
// We only notify changes if the caller is not the sync adapter.
// The sync adapter has the responsibility of notifying changes (it can do so
// more intelligently than we can -- for example, doing it only once at the end
// of the sync instead of issuing thousands of notifications for each record).
if (!V2exContract.hasCallerIsSyncAdapterParameter(uri)) {
Context context = getContext();
context.getContentResolver().notifyChange(uri, null);
}
}
/**
* Build a simple {@link SelectionBuilder} to match the requested
* {@link Uri}. This is usually enough to support {@link #insert},
* {@link #update}, and {@link #delete} operations.
*/
private SelectionBuilder buildSimpleSelection(Uri uri) {
final SelectionBuilder builder = new SelectionBuilder();
final int match = sUriMatcher.match(uri);
switch (match) {
case MEMBERS:
return builder.table(Tables.MEMBERS);
case MEMBERS_USERNAME: {
final String username = Members.getMemberUsername(uri);
return builder.table(Tables.MEMBERS).where(Members.MEMBER_USERNAME + "=?", username);
}
case FEEDS: {
return builder.table(Tables.FEEDS);
}
case FEEDS_ID: {
final String feedId = Feeds.getFeedId(uri);
return builder.table(Tables.FEEDS).where(Feeds.FEED_ID + "=?", feedId);
}
case NODES: {
return builder.table(Tables.NODES);
}
case NODES_ID: {
final String nodeId = Nodes.getNodeId(uri);
return builder.table(Tables.NODES).where(Nodes.NODE_ID + "=?", nodeId);
}
case REVIEWS: {
return builder.table(Tables.REVIEWS);
}
case REVIEWS_TOPIC_ID: {
final String reviewId = Reviews.getReviewTopicId(uri);
return builder.table(Tables.REVIEWS).where(Reviews.REVIEW_ID + "=?", reviewId);
}
case SEARCH: {
return builder.table(Tables.SEARCH);
}
case SEARCH_ID: {
final String searchId = Search.getSearchId(uri);
return builder.table(Tables.SEARCH).where(Search._ID + "=?", searchId);
}
case PICASAS: {
return builder.table(Tables.PICASA_IMAGES);
}
case DATE: {
return builder.table(Tables.MODI_DATE);
}
default: {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
}
/**
* Build an advanced {@link SelectionBuilder} to match the requested
* {@link Uri}. This is usually only used by {@link #query}, since it
* performs table joins useful for {@link Cursor} data.
*/
private SelectionBuilder buildExpandedSelection(Uri uri, int match) {
final SelectionBuilder builder = new SelectionBuilder();
switch (match) {
case MEMBERS: {
return builder.table(Tables.MEMBERS);
}
case MEMBERS_USERNAME: {
final String username = Members.getMemberUsername(uri);
return builder.table(Tables.MEMBERS).where(Members.MEMBER_USERNAME + "=?", username);
}
case FEEDS: {
return builder.table(Tables.FEEDS);
}
case FEEDS_ID: {
final String feedId = Feeds.getFeedId(uri);
return builder.table(Tables.FEEDS).where(Feeds.FEED_ID + "=?", feedId);
}
case NODES: {
return builder.table(Tables.NODES);
}
case NODES_ID: {
final String nodeId = Nodes.getNodeId(uri);
return builder.table(Tables.NODES).where(Nodes.NODE_ID + "=?", nodeId);
}
case REVIEWS: {
return builder.table(Tables.REVIEWS);
}
case REVIEWS_TOPIC_ID: {
final String topicId = Reviews.getReviewTopicId(uri);
return builder.table(Tables.REVIEWS).where(Reviews.REVIEW_TOPIC_ID + "=?", topicId);
}
case SEARCH: {
return builder.table(Tables.SEARCH);
}
case SEARCH_ID: {
final String searchId = Search.getSearchId(uri);
return builder.table(Tables.SEARCH).where(Search._ID + "=?", searchId);
}
case PICASAS: {
return builder.table(Tables.PICASA_IMAGES);
}
case DATE: {
return builder.table(Tables.MODI_DATE);
}
default: {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}
}
}