package com.jbirdvegas.mgerrit.database;
/*
* Copyright (C) 2013 Android Open Kang Project (AOKP)
* Author: Evan Conway (P4R4N01D), 2013
*
* 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.
*/
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.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import com.jbirdvegas.mgerrit.Prefs;
import com.jbirdvegas.mgerrit.helpers.DBParams;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/** This class aims to manage and abstract which database is being accessed.
* This is a singleton class, we can have only one DBHelper (it is also a
* singleton) and we only need one instance of a wdb as we should only
* have one database file open at a time. */
public class DatabaseFactory extends ContentProvider {
private static DBHelper dbHelper;
private static SQLiteDatabase wdb;
// The Authority of the content provider
public static final String AUTHORITY = "com.jbirdvegas.provider.mgerrit";
// All URIs inherit from this URI
static final String BASE_URI = "content://" + AUTHORITY + "/";
// MIME type of a cursor containing a list of rows
static final String BASE_MIME_LIST = "vnd.android.cursor.dir/vnd" + AUTHORITY + ".";
// MIME type of a cursor containing a single row
static final String BASE_MIME_ITEM = "vnd.android.cursor.item/vnd" + AUTHORITY + ".";
// Utility class to aid in matching URIs in content providers.
private static final UriMatcher URI_MATCHER;
/**
* Store a list of the current content provider instances. These should be WeakReferences
* so as to not impact garbage collection. When the current Gerrit changes, we will want
* to notify ALL of these to refresh as we just changed the underlying database.
* We could make this class a singleton but being able to read and write from different content
* provider objects at once could have its advantages.
*/
private static List<WeakReference<DatabaseFactory>> mInstances;
// prepare the UriMatcher
static {
URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
for (Class<? extends DatabaseTable> table : DatabaseTable.tables) {
try {
Method method = table.getDeclaredMethod("addURIMatches", UriMatcher.class);
method.invoke(null, URI_MATCHER);
} catch (Exception e) {
throw new RuntimeException("Unable to add URI matches for class " + table.getSimpleName(), e);
}
}
}
private boolean mLocked;
public DatabaseFactory() {
super();
if (mInstances == null) mInstances = new ArrayList<>();
mInstances.add(new WeakReference<>(this));
}
// Not static as this ensures getDatabase was called
public SQLiteOpenHelper getDatabaseHelper() {
return dbHelper;
}
/**
* Close the current database. We can always check if the database is open by
* looking for one of the results of this method (e.g. dbHelper == null)
*/
public void closeDatabase() {
if (mLocked) {
dbHelper.shutdown();
wdb = null;
dbHelper = null;
}
}
/** Locking methods **/
private void lock() { mLocked = true; }
private synchronized void unlock() {
mLocked = false;
this.notify();
}
/**
* Checks if the database is currently in use (i.e. a CRUD operation is being undertaken)
* If this is the case, we want it to proceed before trying to change the database out
* from under it. Otherwise, we are free to switch the database.
*/
void waitUntilUnlocked() {
new Thread() {
@Override
public void run() {
while (mLocked) {
try {
this.wait();
} catch (InterruptedException e) {
// Interupted. Stop waiting and try one final time to close the database
break;
}
}
closeDatabase();
}
}.start();
}
/** This should be called when the Gerrit source changes to modify all database references to
* use the new database source.
*/
public static void changeGerrit(@NotNull Context context, String newGerrit) {
Log.d("DatabaseFactory", "Switching Gerrit instance to: " + newGerrit);
/* Currently all the members of this class are static so it is only relevant
* for the first instance */
for (WeakReference<DatabaseFactory> key : mInstances) {
DatabaseFactory instance = key.get();
if (instance == null) {
mInstances.remove(key);
continue;
}
// Close the database file
instance.waitUntilUnlocked();
}
// Reopen the new database for all the instances
DatabaseFactory.getDatabase(context, newGerrit);
}
public static void getDatabase(@NotNull Context context, @NotNull String gerrit) {
String dbName = DBHelper.getDatabaseName(gerrit);
DatabaseFactory.dbHelper = new DBHelper(context, dbName);
// Ensure the database is open and we have a reference to it before
// trying to perform any queries using it.
DatabaseFactory.wdb = dbHelper.getWritableDatabase();
// Notify ALL content providers that their data has changed. This should force a refresh
// of every loader's data
context.getContentResolver().notifyChange(Uri.parse(DatabaseFactory.BASE_URI), null);
}
/** This the actual constructor **/
@Override
public boolean onCreate() {
Context context = getContext();
String gerrit = Prefs.getCurrentGerrit(context);
getDatabase(context, gerrit);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (!isUriList(uri))
selection = handleID(uri, selection);
String table = getUriTable(uri);
Integer limit = DBParams.getLimitParameter(uri);
String sLimit = (limit == null ? null : limit.toString());
String groupby = DBParams.getGroupByCondition(uri);
lock();
Cursor c = wdb.query(table, projection, selection, selectionArgs,
groupby, null, sortOrder, sLimit);
unlock();
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
@Override @Contract("null -> fail")
public String getType(Uri uri) {
int result = URI_MATCHER.match(uri);
String retval = DatabaseTable.sContentTypeMap.get(result);
if (retval != null) return retval;
else throw new IllegalArgumentException("Unsupported URI: " + uri);
}
@Override @Contract("null -> fail")
public Uri insert(Uri uri, ContentValues values) {
long id;
if (!isUriList(uri))
throw new IllegalArgumentException("Unsupported URI for insertion: " + uri);
String table = getUriTable(uri);
Integer conflictAlgorithm = DBParams.getConflictParameter(uri);
lock();
if (conflictAlgorithm == null) id = wdb.insert(table, null, values);
else {
id = wdb.insertWithOnConflict(table, null, values, conflictAlgorithm);
}
unlock();
if (id > 0) {
// notify all listeners of changes and return itemUri:
Uri itemUri = ContentUris.withAppendedId(uri, id);
getContext().getContentResolver().notifyChange(itemUri, null);
return itemUri;
}
return null;
}
@Override @Contract("null -> fail")
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (!isUriList(uri))
selection = handleID(uri, selection);
String table = getUriTable(uri);
lock();
int rows = wdb.delete(table, selection, selectionArgs);
unlock();
if (rows > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return rows;
}
@Override @Contract("null -> fail")
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int updateCount = 0, result = URI_MATCHER.match(uri);
if (!isUriList(uri)) selection = handleID(uri, selection);
String tableName = getUriTable(uri);
lock();
updateCount = wdb.update(tableName, values, selection, selectionArgs);
unlock();
if (updateCount > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return updateCount;
}
@Override @Contract("null -> fail")
public int bulkInsert(Uri uri, @NotNull ContentValues[] values) {
String table = getUriTable(uri);
Integer conflictAlgorithm = DBParams.getConflictParameter(uri);
boolean update = DBParams.updateOnDuplicateInsertion(uri);
int numInserted = 0;
lock();
wdb.beginTransaction();
try {
for (ContentValues cv : values) {
numInserted = (insert(table, cv, conflictAlgorithm, update)) ?
numInserted + 1 : numInserted;
}
wdb.setTransactionSuccessful();
} finally {
wdb.endTransaction();
}
unlock();
getContext().getContentResolver().notifyChange(uri, null);
return numInserted;
}
/**
* Insert method where the table has already been defined.
* @param table the table to insert the row into
* @param values A set of column_name/value pairs to add to the database. This must not be null.
* @param conflictAlgorithm for insert conflict resolver
* @param updateOnDuplicate
* @return Whether the database table changed as a result of the insertion
*/
public boolean insert(String table, ContentValues values,
Integer conflictAlgorithm, boolean updateOnDuplicate) {
long id;
if (table == null) return false;
lock();
if (conflictAlgorithm == null) {
id = wdb.insert(table, null, values);
} else {
id = wdb.insertWithOnConflict(table, null, values, conflictAlgorithm);
}
// Check if any rows were added/modified
if (id >= 0) {
SQLiteStatement stmt = wdb.compileStatement("SELECT CHANGES()");
try {
id = stmt.simpleQueryForLong();
} finally {
stmt.close();
}
}
unlock();
return id > 0;
}
/**
* Get the internal database table name for a given URI
* @param uri Resource identifier of a database table
* @return The internal name of the table
* @throws IllegalArgumentException When the uri does not match a valid table
*/
@Contract("null -> fail")
private static String getUriTable(Uri uri) throws IllegalArgumentException {
int result = URI_MATCHER.match(uri);
String tableName = DatabaseTable.sTableMap.get(result);
if (tableName.equals(UserChanges.TABLE)) return Users.TABLE + ", " + Changes.TABLE;
else if (tableName.equals(UserMessage.TABLE)) return Users.TABLE + ", " + MessageInfo.TABLE;
else if (tableName.equals(FileChanges.TABLE)) return FileInfoTable.TABLE + ", " + Changes.TABLE;
else if (tableName.equals(UserReviewers.TABLE)) return Users.TABLE + ", " + Reviewers.TABLE;
else if (tableName != null) return tableName;
else {
throw new IllegalArgumentException("Could not resolve URI data location: " + uri);
}
}
private boolean isUriList(Uri uri) {
String type = getType(uri);
return isUriList(type);
}
private boolean isUriList(String type) {
return type != null && type.contains("vnd.android.cursor.dir/");
}
private String handleID(Uri _uri, String _selection) {
if (!TextUtils.isEmpty(_selection)) {
return _selection + " AND ROWID = " + _uri.getLastPathSegment();
}
else return "ROWID = " + _uri.getLastPathSegment();
}
}