/*
* 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.justeat.mickeydb;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.justeat.mickeydb.util.Uris;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
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.net.Uri;
import android.text.TextUtils;
import android.util.LruCache;
/**
* <p>Base for all generated mickey content providers</p>
*
*/
public abstract class MickeyContentProvider extends ContentProvider {
public static final String PARAM_NOTIFY = "mickey_notify";
public static final String PARAM_GROUP_BY = "mickey_group_by";
public static final String PARAM_LIMIT = "mickey_limit";
public static final String PARAM_OFFSET = "mickey_offset";
private MickeyOpenHelper mOpenHelper;
private UriMatcher mUriMatcher;
private String[] mContentTypes;
private LruCache<Integer, ContentProviderActions> mActionCache = new LruCache<>(10);
private boolean mDebug;
public MickeyContentProvider(boolean debug) {
mDebug = debug;
}
public MickeyContentProvider() {
this(false);
}
public MickeyOpenHelper getOpenHelper() {
return mOpenHelper;
}
protected int matchUri(Uri uri) {
return mUriMatcher.match(uri);
}
@Override
public boolean onCreate() {
final Context context = getContext();
mUriMatcher = createUriMatcher();
mContentTypes = createContentTypes();
String databaseFileName = createDatabaseFilename(getDatabaseName(), getDatabaseVersion());
cleanupOldDatabaseFileVersions(databaseFileName);
mOpenHelper = createOpenHelper(context, databaseFileName);
return true;
}
protected String createDatabaseFilename(String databaseName, int databaseVersion) {
return databaseName + (databaseVersion <= 0 ? "" : "." + databaseVersion) + ".db";
}
protected void cleanupOldDatabaseFileVersions(String currentDatabaseFilename) {
File dbFile = getContext().getDatabasePath(currentDatabaseFilename);
if (!dbFile.exists()) {
if(mDebug) {
MickeyLogger.d(Mickey.TAG, "Create File", "%s", currentDatabaseFilename);
}
File[] files = dbFile.getParentFile().listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.matches(getDatabaseName() + ".*db");
}
});
if(files != null) {
for (File file : files) {
if(mDebug) {
MickeyLogger.d(Mickey.TAG, "Delete Old Version", "%s", file.getName());
}
file.delete();
}
}
}
}
protected abstract String getDatabaseName();
protected abstract int getDatabaseVersion();
protected abstract UriMatcher createUriMatcher();
protected abstract String[] createContentTypes();
protected abstract ContentProviderActions createActions(int id);
private ContentProviderActions getActions(int id) {
ContentProviderActions actions = mActionCache.get(id);
if(actions == null) {
actions = createActions(id);
mActionCache.put(id, actions);
}
return actions;
}
protected abstract MickeyOpenHelper createOpenHelper(Context context, String databaseFilename);
protected abstract Set<Uri> getRelatedUris(Uri uri);
/**
* Notifies a change (invokes {@link ContentResolver#notifyChange(Uri, android.database.ContentObserver)
* if {@link PARAM_NOTIFY} parameter is not present in the given Uri, or, if it
* is present and set to a value other than the string <em>true</em>.
* @param uri The uri to notify on.
*/
protected void tryNotifyChange(Uri uri) {
boolean notify = shouldNotify(uri);
if(notify) {
if(mDebug) {
MickeyLogger.d(Mickey.TAG, "Notify", "%s", Uris.getPathAndQueryAsString(uri));
}
getContext().getContentResolver().notifyChange(uri, null);
if(uri.getPathSegments().size() > 0) {
Uri key = new Uri.Builder()
.scheme(uri.getScheme())
.authority(uri.getAuthority())
.appendPath(uri.getPathSegments().get(0))
.build();
Set<Uri> relatedUris = getRelatedUris(key);
if(relatedUris != null) {
for(Uri relatedUri : relatedUris) {
if(mDebug) {
MickeyLogger.d(Mickey.TAG, "Notify","%s", Uris.getPathAndQueryAsString(relatedUri));
}
getContext().getContentResolver().notifyChange(relatedUri, null);
}
}
}
}
}
protected boolean shouldNotify(Uri uri) {
boolean notify = true;
String paramNotify = uri.getQueryParameter(PARAM_NOTIFY);
if(paramNotify != null) {
notify = Boolean.valueOf(paramNotify);
}
return notify;
}
/**
* Sets the Uri as the notification Uri on the given Cursor if the Uri is not null, and
* the {@link PARAM_NOTIFY} parameter is not present in the given Uri, or, if it is present
* and set to a value other than the string <em>true</em>
* @param cursor
* @param uri
*/
protected void trySetNotificationUri(Cursor cursor, Uri uri) {
if(cursor == null) {
return;
}
boolean notify = shouldNotify(uri);
if(notify) {
cursor.setNotificationUri(getContext().getContentResolver(), uri);
}
}
private void tryNotifyForActions(Uri uri, ContentProviderActions actions) {
if(shouldNotify(uri)) {
List<Uri> notifyUris = actions.getNotifyUris(this, uri);
if(notifyUris == null || notifyUris.size() == 0) {
tryNotifyChange(uri);
} else {
for(Uri actionNotifyUri : notifyUris) {
if(mDebug) {
MickeyLogger.d(Mickey.TAG, "Notify", "%s", Uris.getPathAndQueryAsString(actionNotifyUri));
}
getContext().getContentResolver().notifyChange(actionNotifyUri, null);
}
}
}
}
@Override
public String getType(Uri uri) {
final int match = matchUri(uri);
if(match == UriMatcher.NO_MATCH) {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
return mContentTypes[match];
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final int match = matchUri(uri);
if(match == UriMatcher.NO_MATCH) {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
ContentProviderActions actions = getActions(match);
int affected = actions.delete(this, uri, selection, selectionArgs);
if(mDebug) {
MickeyLogger.logAction(Mickey.TAG, "Delete", actions, uri);
if(!TextUtils.isEmpty(selection)) {
MickeyLogger.d(Mickey.TAG, "Delete", "%s", selection);
}
}
if(affected > 0) {
tryNotifyForActions(uri, actions);
}
return affected;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
final int match = matchUri(uri);
if(match == UriMatcher.NO_MATCH) {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
ContentProviderActions actions = getActions(match);
Uri newUri = actions.insert(this, uri, values);
if(mDebug) {
MickeyLogger.logAction(Mickey.TAG, "Insert", actions, uri);
MickeyLogger.d(Mickey.TAG, "Insert", "%s", values);
}
if(newUri != null) {
tryNotifyForActions(uri, actions);
}
return newUri;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
final int match = matchUri(uri);
if(match == UriMatcher.NO_MATCH) {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
ContentProviderActions actions = getActions(match);
int affected = actions.bulkInsert(this, uri, values);
if(mDebug) {
MickeyLogger.logAction(Mickey.TAG, "Bulk", actions, uri);
}
if(affected > 0) {
tryNotifyForActions(uri, actions);
}
return affected;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final int match = matchUri(uri);
if(match == UriMatcher.NO_MATCH) {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
ContentProviderActions actions = getActions(match);
Cursor cursor = actions.query(this, uri, projection, selection, selectionArgs, sortOrder);
trySetNotificationUri(cursor, uri);
if(mDebug) {
if(projection != null && projection.length > 0 && projection[0].equals(Query.COUNT_TOKEN)) {
MickeyLogger.logAction(Mickey.TAG, "Count", actions, uri);
if(!TextUtils.isEmpty(selection)) {
MickeyLogger.d(Mickey.TAG, "Count", "%s", selection);
}
} else {
MickeyLogger.logAction(Mickey.TAG, "Query", actions, uri);
if(!TextUtils.isEmpty(selection)) {
MickeyLogger.d(Mickey.TAG, "Query", "%s", selection);
}
}
}
return cursor;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final int match = matchUri(uri);
if(match == UriMatcher.NO_MATCH) {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
ContentProviderActions actions = getActions(match);
int affected = actions.update(this, uri, values, selection, selectionArgs);
if(mDebug) {
MickeyLogger.logAction(Mickey.TAG, "Update", actions, uri);
if(!TextUtils.isEmpty(selection)) {
MickeyLogger.d(Mickey.TAG, "Update", "%s", selection);
}
MickeyLogger.d(Mickey.TAG, "Update", "%s", values);
}
if(affected > 0) {
tryNotifyForActions(uri, actions);
}
return affected;
}
public <T extends ActiveRecord> List<T> selectRecords(Uri uri, Query sQuery, String sortOrder) {
final int match = matchUri(uri);
if(match == UriMatcher.NO_MATCH) {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
ContentProviderActions actions = getActions(match);
if(mDebug) {
MickeyLogger.logAction(Mickey.TAG, "Select", actions, uri);
if(!TextUtils.isEmpty(sQuery.toString())) {
MickeyLogger.d(Mickey.TAG, "Select", "%s", sQuery.toString());
}
}
return actions.selectRecords(this, uri, sQuery, sortOrder);
}
public <T extends ActiveRecord> Map<String, T> selectRecordMap(Uri uri, Query sQuery, String keyColumnName) {
final int match = matchUri(uri);
if(match == UriMatcher.NO_MATCH) {
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
ContentProviderActions actions = getActions(match);
if(mDebug) {
MickeyLogger.logAction(Mickey.TAG, "Select", actions, uri);
if(!TextUtils.isEmpty(sQuery.toString())) {
MickeyLogger.d(Mickey.TAG, "Select", "%s", sQuery.toString());
}
}
return actions.selectRecordMap(this, uri, sQuery, keyColumnName);
}
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
final SQLiteDatabase db = getOpenHelper().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();
}
}
}