package edu.mit.mobile.android.locast.data;
/*
* Copyright (C) 2010 MIT Mobile Experience Lab
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import junit.framework.Assert;
import android.accounts.Account;
import android.accounts.AccountManager;
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.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import edu.mit.mobile.android.content.DBHelper;
import edu.mit.mobile.android.content.DBHelperMapper;
import edu.mit.mobile.android.content.GenericDBHelper;
import edu.mit.mobile.android.content.ManyToMany;
import edu.mit.mobile.android.content.ProviderUtils;
import edu.mit.mobile.android.locast.accounts.AuthenticationService;
import edu.mit.mobile.android.locast.accounts.Authenticator;
import edu.mit.mobile.android.locast.sync.LocastSyncService;
import edu.mit.mobile.android.utils.ListUtils;
public class MediaProvider extends ContentProvider {
@SuppressWarnings("unused")
private final static String TAG = MediaProvider.class.getSimpleName();
public final static String NAMESPACE = "edu.mit.mobile.android.locast.ver2";
public final static String AUTHORITY = NAMESPACE + ".provider";
private static final String
CAST_TABLE_NAME = "casts",
CASTMEDIA_TABLE_NAME = "castmedia", // casts with multiple media objects
COMMENT_TABLE_NAME = "comments",
TAG_TABLE_NAME = "tags",
ITINERARY_TABLE_NAME = "itineraries",
EVENT_TABLE_NAME = "events";
public final static String
TYPE_CAST_ITEM = "vnd.android.cursor.item/vnd."+NAMESPACE+".casts",
TYPE_CAST_DIR = "vnd.android.cursor.dir/vnd."+NAMESPACE+".casts",
TYPE_CASTMEDIA_ITEM = "vnd.android.cursor.item/vnd."+NAMESPACE+".castmedia",
TYPE_CASTMEDIA_DIR = "vnd.android.cursor.dir/vnd."+NAMESPACE+".castmedia",
TYPE_COMMENT_ITEM = "vnd.android.cursor.item/vnd."+NAMESPACE+".comments",
TYPE_COMMENT_DIR = "vnd.android.cursor.dir/vnd."+NAMESPACE+".comments",
TYPE_TAG_DIR = "vnd.android.cursor.dir/vnd."+NAMESPACE+".tags",
TYPE_ITINERARY_DIR = "vnd.android.cursor.dir/vnd."+NAMESPACE+".itineraries",
TYPE_ITINERARY_ITEM = "vnd.android.cursor.item/vnd."+NAMESPACE+".itineraries",
TYPE_EVENT_DIR = "vnd.android.cursor.dir/vnd."+NAMESPACE+"."+EVENT_TABLE_NAME,
TYPE_EVENT_ITEM = "vnd.android.cursor.item/vnd."+NAMESPACE+"."+EVENT_TABLE_NAME
;
private static final JSONSyncableIdenticalChildFinder mChildFinder = new JSONSyncableIdenticalChildFinder();
private static final ManyToMany.M2MDBHelper
// ITINERARY_CASTS_DBHELPER = new ManyToMany.M2MDBHelper(ITINERARY_TABLE_NAME, CAST_TABLE_NAME, mChildFinder, Cast.CONTENT_URI),
ITINERARY_CASTS_DBHELPER = new ManyToMany.M2MDBHelper(ITINERARY_TABLE_NAME, CAST_TABLE_NAME, mChildFinder),
CASTS_CASTMEDIA_DBHELPER = new ManyToMany.M2MDBHelper(CAST_TABLE_NAME, CASTMEDIA_TABLE_NAME, mChildFinder);
private static final DBHelper
EVENT_DBHELPER = new GenericDBHelper(EVENT_TABLE_NAME, Event.CONTENT_URI);
private final static UriMatcher uriMatcher;
private final static DBHelperMapper mDBHelperMapper = new DBHelperMapper();
private static final int
MATCHER_CAST_DIR = 1,
MATCHER_CAST_ITEM = 2,
MATCHER_COMMENT_DIR = 5,
MATCHER_COMMENT_ITEM = 6,
MATCHER_EVENT_DIR = 7,
MATCHER_EVENT_ITEM = 8,
MATCHER_CHILD_COMMENT_DIR = 9,
MATCHER_CHILD_COMMENT_ITEM = 10,
MATCHER_TAG_DIR = 13,
MATCHER_ITEM_TAGS = 14,
MATCHER_ITINERARY_DIR = 21,
MATCHER_ITINERARY_ITEM = 22,
MATCHER_CHILD_CAST_DIR = 23,
MATCHER_CHILD_CAST_ITEM = 24,
MATCHER_ITINERARY_BY_TAGS = 27,
MATCHER_CHILD_CASTMEDIA_DIR = 28,
MATCHER_CHILD_CASTMEDIA_ITEM = 29;
;
private static class DatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "content.db";
private static final int DB_VER = 42;
public DatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VER);
}
private static final String JSON_SYNCABLE_ITEM_FIELDS =
JsonSyncableItem._ID + " INTEGER PRIMARY KEY,"
+ JsonSyncableItem._PUBLIC_URI + " TEXT UNIQUE,"
+ JsonSyncableItem._MODIFIED_DATE+ " INTEGER,"
+ JsonSyncableItem._SERVER_MODIFIED_DATE+ " INTEGER,"
+ JsonSyncableItem._CREATED_DATE + " INTEGER,";
private static final String JSON_COMMENTABLE_FIELDS =
Commentable.Columns._COMMENT_DIR_URI + " TEXT,";
private static final String LOCATABLE_FIELDS =
Locatable.Columns._GEOCELL + " TEXT,"
+ Locatable.Columns._LATITUDE + " REAL,"
+ Locatable.Columns._LONGITUDE + " REAL,";
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + CAST_TABLE_NAME + " ("
+ JSON_SYNCABLE_ITEM_FIELDS
+ JSON_COMMENTABLE_FIELDS
+ LOCATABLE_FIELDS
+ Cast._TITLE + " TEXT,"
+ Cast._AUTHOR + " TEXT,"
+ Cast._AUTHOR_URI + " TEXT,"
+ Cast._DESCRIPTION + " TEXT,"
+ Cast._MEDIA_PUBLIC_URI + " TEXT,"
+ Cast._PRIVACY + " TEXT,"
+ Cast._FAVORITED + " BOOLEAN,"
+ Cast._DRAFT + " BOOLEAN,"
+ Cast._OFFICIAL + " BOOLEAN,"
+ Cast._THUMBNAIL_URI+ " TEXT"
+ ");"
);
db.execSQL("CREATE TABLE " + COMMENT_TABLE_NAME + " ("
+ JSON_SYNCABLE_ITEM_FIELDS
+ Comment._AUTHOR + " TEXT,"
+ Comment._AUTHOR_ICON + " TEXT,"
+ Comment._PARENT_ID + " INTEGER,"
+ Comment._PARENT_CLASS + " TEXT,"
+ Comment._COMMENT_NUMBER+ " TEXT,"
+ Comment._DESCRIPTION + " TEXT"
+ ");"
);
db.execSQL("CREATE TABLE " + TAG_TABLE_NAME + " ("
+ Tag._ID + " INTEGER PRIMARY KEY,"
+ Tag._REF_ID + " INTEGER,"
+ Tag._REF_CLASS + " TEXT,"
+ Tag._NAME + " TEXT,"
// easiest way to prevent tag duplicates
+ "CONSTRAINT tag_unique UNIQUE ("+ Tag._REF_ID+ ","+Tag._REF_CLASS+","+Tag._NAME+") ON CONFLICT IGNORE"
+ ");"
);
db.execSQL("CREATE TABLE "+ CASTMEDIA_TABLE_NAME + " ("
+ JSON_SYNCABLE_ITEM_FIELDS
+ CastMedia._AUTHOR + " TEXT,"
+ CastMedia._AUTHOR_URI + " TEXT,"
+ CastMedia._TITLE + " TEXT,"
+ CastMedia._DESCRIPTION + " TEXT,"
+ CastMedia._LANGUAGE + " TEXT,"
+ CastMedia._MEDIA_URL + " TEXT,"
+ CastMedia._LOCAL_URI + " TEXT,"
+ CastMedia._MIME_TYPE + " TEXT,"
+ CastMedia._THUMBNAIL + " TEXT,"
+ CastMedia._THUMB_LOCAL + " TEXT,"
+ CastMedia._KEEP_OFFLINE + " BOOLEAN,"
+ CastMedia._DURATION + " INTEGER"
+ ")"
);
db.execSQL("CREATE TABLE " + ITINERARY_TABLE_NAME + " ("
+ JSON_SYNCABLE_ITEM_FIELDS
+ Itinerary._TITLE + " TEXT,"
+ Itinerary._AUTHOR + " TEXT,"
+ Itinerary._AUTHOR_URI + " TEXT,"
+ Itinerary._DESCRIPTION + " TEXT,"
+ Itinerary._PRIVACY + " TEXT,"
+ Itinerary._CASTS_URI + " TEXT,"
+ Itinerary._PATH + " TEXT,"
+ Itinerary._CASTS_COUNT + " INTEGER,"
+ Itinerary._FAVORITES_COUNT + " INTEGER,"
+ Itinerary._FAVORITED + " BOOLEAN,"
+ Itinerary._THUMBNAIL + " TEXT,"
+ Itinerary._DRAFT + " BOOLEAN"
+ ");"
);
db.execSQL("CREATE TABLE " + EVENT_TABLE_NAME + " ("
+ JSON_SYNCABLE_ITEM_FIELDS
+ LOCATABLE_FIELDS
+ Event._TITLE + " TEXT,"
+ Event._AUTHOR + " TEXT,"
+ Event._AUTHOR_URI + " TEXT,"
+ Event._DESCRIPTION + " TEXT,"
+ Event._START_DATE + " INTEGER,"
+ Event._END_DATE + " INTEGER,"
+ Event._DRAFT + " BOOLEAN,"
+ Event._THUMBNAIL_URI+ " TEXT"
+ ");"
);
ITINERARY_CASTS_DBHELPER.createJoinTable(db);
CASTS_CASTMEDIA_DBHELPER.createJoinTable(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// If it's a step greater than 1, step through all older versions and incrementally upgrade to the latest
for (; oldVersion < newVersion - 1; oldVersion++){
onUpgrade(db, oldVersion, oldVersion + 1);
}
db.execSQL("DROP TABLE IF EXISTS " + CAST_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + COMMENT_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + TAG_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + CASTMEDIA_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + CASTMEDIA_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + ITINERARY_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + EVENT_TABLE_NAME);
ITINERARY_CASTS_DBHELPER.deleteJoinTable(db);
CASTS_CASTMEDIA_DBHELPER.deleteJoinTable(db);
onCreate(db);
}
}
private DatabaseHelper dbHelper;
@Override
public boolean onCreate() {
dbHelper = new DatabaseHelper(getContext());
return true;
}
/**
* @param uri
* @return true if the matching URI can sync.
*/
public static boolean canSync(Uri uri){
switch (uriMatcher.match(uri)){
case MATCHER_ITEM_TAGS:
case MATCHER_TAG_DIR:
case MATCHER_COMMENT_ITEM:
case MATCHER_CHILD_COMMENT_ITEM:
return false;
case MATCHER_CHILD_COMMENT_DIR:
case MATCHER_COMMENT_DIR:
case MATCHER_CAST_DIR:
case MATCHER_CAST_ITEM:
case MATCHER_CHILD_CASTMEDIA_DIR:
case MATCHER_CHILD_CASTMEDIA_ITEM:
case MATCHER_CHILD_CAST_DIR:
case MATCHER_CHILD_CAST_ITEM:
case MATCHER_ITINERARY_DIR:
case MATCHER_ITINERARY_ITEM:
case MATCHER_EVENT_DIR:
case MATCHER_EVENT_ITEM:
return true;
default:
throw new IllegalArgumentException("Cannot get syncability for URI "+uri);
}
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)){
case MATCHER_CAST_DIR:
return TYPE_CAST_DIR;
case MATCHER_CAST_ITEM:
return TYPE_CAST_ITEM;
case MATCHER_CHILD_CASTMEDIA_DIR:
return TYPE_CASTMEDIA_DIR;
case MATCHER_CHILD_CASTMEDIA_ITEM:
return TYPE_CASTMEDIA_ITEM;
case MATCHER_COMMENT_DIR:
case MATCHER_CHILD_COMMENT_DIR:
return TYPE_COMMENT_DIR;
case MATCHER_COMMENT_ITEM:
case MATCHER_CHILD_COMMENT_ITEM:
return TYPE_COMMENT_ITEM;
// tags
case MATCHER_TAG_DIR:
case MATCHER_ITEM_TAGS:
return TYPE_TAG_DIR;
//////////////// itineraries
case MATCHER_CHILD_CAST_DIR:
return TYPE_CAST_DIR;
case MATCHER_CHILD_CAST_ITEM:
return TYPE_CAST_ITEM;
case MATCHER_ITINERARY_DIR:
return TYPE_ITINERARY_DIR;
case MATCHER_ITINERARY_ITEM:
return TYPE_ITINERARY_ITEM;
case MATCHER_EVENT_DIR:
return TYPE_EVENT_DIR;
case MATCHER_EVENT_ITEM:
return TYPE_EVENT_ITEM;
default:
throw new IllegalArgumentException("Cannot get type for URI "+uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = dbHelper.getWritableDatabase();
final Context context = getContext();
long rowid;
final boolean syncable = canSync(uri);
if (syncable && !values.containsKey(JsonSyncableItem._MODIFIED_DATE)){
values.put(JsonSyncableItem._MODIFIED_DATE, new Date().getTime());
}
values.remove(JsonSyncableItem._ID);
Uri newItem = null;
boolean isDraft = false;
final int code = uriMatcher.match(uri);
switch (code){
//////////////////////////////////////////////////////////////////////////////////
case MATCHER_CAST_DIR:{
Assert.assertNotNull(values);
final ContentValues cvTags = ProviderUtils.extractContentValueItem(values, Tag.PATH);
rowid = db.insert(CAST_TABLE_NAME, null, values);
if (rowid > 0){
newItem = ContentUris.withAppendedId(Cast.CONTENT_URI, rowid);
update(Uri.withAppendedPath(newItem, Tag.PATH), cvTags, null, null);
if (values.containsKey(Cast._DRAFT)){
isDraft = values.getAsBoolean(Cast._DRAFT);
}else{
isDraft = false;
}
}
break;
}
//////////////////////////////////////////////////////////////////////////////////
case MATCHER_COMMENT_DIR:{
Assert.assertNotNull(values);
rowid = db.insert(COMMENT_TABLE_NAME, null, values);
if (rowid > 0){
newItem = ContentUris.withAppendedId(Comment.CONTENT_URI, rowid);
}
}break;
//////////////////////////////////////////////////////////////////////////////////
case MATCHER_CHILD_COMMENT_DIR:{
final List<String> pathSegs = uri.getPathSegments();
values.put(Comment._PARENT_CLASS, pathSegs.get(pathSegs.size() - 3));
values.put(Comment._PARENT_ID, pathSegs.get(pathSegs.size() - 2));
rowid = db.insert(COMMENT_TABLE_NAME, null, values);
if (rowid > 0){
newItem = ContentUris.withAppendedId(uri, rowid);
}
}break;
//////////////////////////////////////////////////////////////////////////////////
case MATCHER_ITEM_TAGS:{
final List<String> pathSegments = uri.getPathSegments();
values.put(Tag._REF_CLASS, pathSegments.get(pathSegments.size() - 3));
values.put(Tag._REF_ID, pathSegments.get(pathSegments.size() - 2));
rowid = 0;
final ContentValues cv2 = new ContentValues(values);
cv2.remove(Tag.PATH);
try {
db.beginTransaction();
for (final String tag : TaggableItem.getList(values.getAsString(Tag.PATH))){
cv2.put(Tag._NAME, tag);
rowid = db.insert(TAG_TABLE_NAME, null, cv2);
}
db.setTransactionSuccessful();
}finally{
db.endTransaction();
}
newItem = ContentUris.withAppendedId(uri, rowid);
break;
}
//////////////////////////////////////////////////////////////////////////////////
case MATCHER_ITINERARY_BY_TAGS:
case MATCHER_ITINERARY_DIR:{
newItem = insertWithTags(values, db, ITINERARY_TABLE_NAME, Itinerary.CONTENT_URI);
if (newItem != null){
if (values.containsKey(Itinerary._DRAFT)){
isDraft = values.getAsBoolean(Itinerary._DRAFT);
}else{
isDraft = false;
}
}
} break;
//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
default:
if (mDBHelperMapper.canInsert(code)){
// XXX draft should probably be looked at better.
if (values.containsKey(TaggableItem._DRAFT)){
isDraft = values.getAsBoolean(TaggableItem._DRAFT);
}else{
isDraft = false;
}
newItem = mDBHelperMapper.insert(code, this, db, uri, values);
}else{
throw new IllegalArgumentException("Unknown URI: "+uri);
}
}
if (newItem != null){
context.getContentResolver().notifyChange(uri, null);
}else{
throw new SQLException("Failed to insert row into "+uri);
}
// XXX figure out sync
// if (syncable && !isDraft){
// context.startService(new Intent(Intent.ACTION_SYNC, uri));
// }
return newItem;
}
/**
* Performs an insert on the database, taking the tags parameter from the ContentValues,
* parsing it and inserting it into the appropriate tables.
*
* @param values standard ContentValues, but with a special Tag.PATH parameter.
* @param db
* @param table
* @param contentUri
* @return
*/
private Uri insertWithTags(ContentValues values, SQLiteDatabase db, String table, Uri contentUri){
Uri newItem = null;
final ContentValues cvTags = ProviderUtils.extractContentValueItem(values, Tag.PATH);
db.beginTransaction();
try {
final long rowid = db.insert(table, null, values);
if (rowid > 0){
newItem = ContentUris.withAppendedId(contentUri, rowid);
update(Uri.withAppendedPath(newItem, Tag.PATH), cvTags, null, null);
db.setTransactionSuccessful();
}
}finally{
db.endTransaction();
}
return newItem;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
final SQLiteDatabase db = dbHelper.getReadableDatabase();
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
final long id;
Cursor c;
final int code = uriMatcher.match(uri);
switch (code){
case MATCHER_CAST_DIR:{
qb.setTables(CAST_TABLE_NAME);
final String tags = uri.getQueryParameter(TaggableItem.SERVER_QUERY_PARAMETER);
final String dist = uri.getQueryParameter(Locatable.SERVER_QUERY_PARAMETER);
final Boolean favorited = Favoritable.decodeFavoritedUri(uri);
if (favorited != null){
selection = ProviderUtils.addExtraWhere(selection, Favoritable.Columns._FAVORITED + "=?");
selectionArgs = ProviderUtils.addExtraWhereArgs(selectionArgs, favorited ? "1" : "0");
}
if (tags != null){
c = queryByTags(qb, db, tags, CAST_TABLE_NAME, projection, selection, selectionArgs, sortOrder);
}else if (dist != null){
c = queryByLocation(qb, db, dist, CAST_TABLE_NAME, projection, selection, selectionArgs, sortOrder);
}else{
c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
}
}break;
case MATCHER_CAST_ITEM:
qb.setTables(CAST_TABLE_NAME);
id = ContentUris.parseId(uri);
qb.appendWhere(Cast._ID + "="+id);
c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
break;
case MATCHER_EVENT_DIR:{
qb.setTables(EVENT_TABLE_NAME);
final String tags = uri.getQueryParameter(TaggableItem.SERVER_QUERY_PARAMETER);
final String dist = uri.getQueryParameter(Locatable.SERVER_QUERY_PARAMETER);
if (tags != null){
c = queryByTags(qb, db, tags, EVENT_TABLE_NAME, projection, selection, selectionArgs, sortOrder);
}else if (dist != null){
c = queryByLocation(qb, db, dist, EVENT_TABLE_NAME, projection, selection, selectionArgs, sortOrder);
}else{
c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
}
}break;
case MATCHER_COMMENT_DIR:{
qb.setTables(COMMENT_TABLE_NAME);
c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
break;
}
case MATCHER_COMMENT_ITEM:{
qb.setTables(COMMENT_TABLE_NAME);
id = ContentUris.parseId(uri);
qb.appendWhere(Comment._ID + "="+id);
c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
break;
}
case MATCHER_CHILD_COMMENT_DIR:{
final List<String> pathSegs = uri.getPathSegments();
c = db.query(COMMENT_TABLE_NAME, projection,
ProviderUtils.addExtraWhere(selection, Comment._PARENT_ID+"=?", Comment._PARENT_CLASS+"=?"),
ProviderUtils.addExtraWhereArgs(selectionArgs, pathSegs.get(pathSegs.size() - 2), pathSegs.get(pathSegs.size() - 3)), null, null, sortOrder);
break;
}
case MATCHER_CHILD_COMMENT_ITEM:{
final List<String> pathSegs = uri.getPathSegments();
id = ContentUris.parseId(uri);
c = db.query(COMMENT_TABLE_NAME, projection,
ProviderUtils.addExtraWhere(selection, Comment._ID+"=?", Comment._PARENT_ID+"=?", Comment._PARENT_CLASS+"=?"),
ProviderUtils.addExtraWhereArgs(selectionArgs, String.valueOf(id), pathSegs.get(pathSegs.size() - 3), pathSegs.get(pathSegs.size() - 4))
, null, null, sortOrder);
break;
}
case MATCHER_ITEM_TAGS:{
qb.setTables(TAG_TABLE_NAME);
final List<String> pathSegments = uri.getPathSegments();
qb.appendWhere(Tag._REF_CLASS+"=\'"+pathSegments.get(pathSegments.size() - 3)+"\'");
qb.appendWhere(" AND " + Tag._REF_ID+"="+pathSegments.get(pathSegments.size() - 2));
c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
break;
}
case MATCHER_TAG_DIR:{
qb.setTables(TAG_TABLE_NAME);
c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
break;
}
case MATCHER_ITINERARY_DIR:{
if (sortOrder == null){
sortOrder = Itinerary.SORT_DEFAULT;
}
c = db.query(ITINERARY_TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
}break;
case MATCHER_ITINERARY_ITEM:{
final String itemId = uri.getLastPathSegment();
c = db.query(ITINERARY_TABLE_NAME,
projection,
ProviderUtils.addExtraWhere(selection, Itinerary._ID+"=?"),
ProviderUtils.addExtraWhereArgs(selectionArgs, itemId),
null, null, sortOrder);
}break;
default:
if (mDBHelperMapper.canQuery(code)){
c = mDBHelperMapper.query(code, this, db, uri, projection, selection, selectionArgs, sortOrder);
}else{
throw new IllegalArgumentException("unknown URI "+uri);
}
}
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
// TODO rework tag system to use m2m relationship or at least rework this to use the builder
private Cursor queryByTags(SQLiteQueryBuilder qb, SQLiteDatabase db, String tagString, String taggableItemTable, String[] projection, String selection, String[] selectionArgs, String sortOrder){
qb.setTables(taggableItemTable + " AS c, "+TAG_TABLE_NAME +" AS t");
final Set<String> tags = Tag.toSet(tagString.toLowerCase());
final List<String> tagFilterList = new ArrayList<String>(tags.size());
qb.appendWhere("t."+Tag._REF_ID+"=c."+TaggableItem._ID);
for (final String tag : tags){
tagFilterList.add(DatabaseUtils.sqlEscapeString(tag));
}
qb.appendWhere(" AND (t."+Tag._NAME+" IN ("+ListUtils.join(tagFilterList, ",")+"))");
// limit to only items of the given object class
qb.appendWhere(" AND t."+Tag._REF_CLASS + "=\'"+taggableItemTable+"\'");
// Modify the projection so that _ID explicitly refers to that of the objects being searched,
// not the tags. Without this, _ID is ambiguous and the query fails.
final String[] projection2 = ProviderUtils.addPrefixToProjection("c", projection);
return qb.query(db, projection2, selection, selectionArgs,
"c."+TaggableItem._ID,
"COUNT ("+"c."+TaggableItem._ID+")="+tags.size(),
sortOrder);
}
private static final Pattern LOC_STRING_REGEX = Pattern.compile("^([\\d\\.-]+),([\\d\\.-]+),([\\d\\.]+)");
private Cursor queryByLocation(SQLiteQueryBuilder qb, SQLiteDatabase db, String locString, String locatableItemTable, String[] projection, String selection, String[] selectionArgs, String sortOrder){
qb.setTables(locatableItemTable);
final Matcher m = LOC_STRING_REGEX.matcher(locString);
if (!m.matches()){
throw new IllegalArgumentException("bad location string '"+locString+"'");
}
final String lon = m.group(1);
final String lat = m.group(2);
final String dist = m.group(3);
//final GeocellQuery gq = new GeocellQuery();
//GeocellUtils.compute(new Point(Double.valueOf(lat), Double.valueOf(lon)), resolution);
//String extraWhere = "(lat - 2) > ? AND (lon - 2) > ? AND (lat + 2) < ? AND (lat + 2) < ?";
final String[] extraArgs = {lat, lon};
return qb.query(db, projection, ProviderUtils.addExtraWhere(selection, Locatable.SELECTION_LAT_LON), ProviderUtils.addExtraWhereArgs(selectionArgs, extraArgs), null, null, sortOrder);
}
/**
* Add this key to the values to tell update() to not mark the data as being dirty. This is useful
* for updating local-only information that will not be synchronized and avoid triggering synchronization.
*/
public static final String CV_FLAG_DO_NOT_MARK_DIRTY = "_CV_FLAG_DO_NOT_MARK_DIRTY";
@Override
public int update(Uri uri, ContentValues values, String where,
String[] whereArgs) {
final SQLiteDatabase db = dbHelper.getWritableDatabase();
int count;
final long id;
boolean needSync = false;
final boolean canSync = canSync(uri);
if (!values.containsKey(CV_FLAG_DO_NOT_MARK_DIRTY) &&
canSync && !values.containsKey(JsonSyncableItem._MODIFIED_DATE)){
values.put(JsonSyncableItem._MODIFIED_DATE, new Date().getTime());
needSync = true;
}
values.remove(CV_FLAG_DO_NOT_MARK_DIRTY);
final int code = uriMatcher.match(uri);
switch (code){
case MATCHER_CAST_DIR:
count = db.update(CAST_TABLE_NAME, values, where, whereArgs);
break;
case MATCHER_CHILD_CAST_ITEM:
case MATCHER_CAST_ITEM:{
id = ContentUris.parseId(uri);
if ( values.size() == 2 && values.containsKey(Favoritable.Columns._FAVORITED)){
values.put(JsonSyncableItem._MODIFIED_DATE, 0);
}
count = db.update(CAST_TABLE_NAME, values,
Cast._ID+"="+id+ (where != null && where.length() > 0 ? " AND ("+where+")":""),
whereArgs);
break;
}
case MATCHER_COMMENT_DIR:
count = db.update(COMMENT_TABLE_NAME, values, where, whereArgs);
break;
case MATCHER_COMMENT_ITEM:
id = ContentUris.parseId(uri);
count = db.update(COMMENT_TABLE_NAME, values,
Comment._ID+"="+id+ (where != null && where.length() > 0 ? " AND ("+where+")":""),
whereArgs);
break;
case MATCHER_CHILD_COMMENT_DIR:{
id = ContentUris.parseId(uri);
final List<String>pathSegs = uri.getPathSegments();
count = db.update(COMMENT_TABLE_NAME, values,
ProviderUtils.addExtraWhere(where, Comment._PARENT_ID+"=?", Comment._PARENT_CLASS+"=?"),
ProviderUtils.addExtraWhereArgs(whereArgs, pathSegs.get(pathSegs.size() - 2), pathSegs.get(pathSegs.size() - 3))
);
break;
}
case MATCHER_CHILD_COMMENT_ITEM:{
id = ContentUris.parseId(uri);
final List<String>pathSegs = uri.getPathSegments();
count = db.update(COMMENT_TABLE_NAME, values,
ProviderUtils.addExtraWhere(where, Comment._ID+"=?", Comment._PARENT_ID+"=?", Comment._PARENT_CLASS+"=?"),
ProviderUtils.addExtraWhereArgs(whereArgs, String.valueOf(id), pathSegs.get(pathSegs.size() - 3), pathSegs.get(pathSegs.size() - 4))
);
break;
}
case MATCHER_ITEM_TAGS:{
final String[] tag_projection = {Tag._REF_ID, Tag._REF_CLASS, Tag._NAME};
final Cursor tagC = query(uri, tag_projection, null, null, null);
final int tagIdx = tagC.getColumnIndex(Tag._NAME);
final Set<String> existingTags = new HashSet<String>(tagC.getCount());
for (tagC.moveToFirst(); !tagC.isAfterLast(); tagC.moveToNext()){
existingTags.add(tagC.getString(tagIdx));
}
tagC.close();
final Set<String> newTags = new HashSet<String>(TaggableItem.getList(values.getAsString(Tag.PATH)));
// If the CV_TAG_PREFIX key is present, only work with tags that have that prefix.
if (values.containsKey(TaggableItem.CV_TAG_PREFIX)){
final String prefix = values.getAsString(TaggableItem.CV_TAG_PREFIX);
TaggableItem.filterTagsInPlace(prefix, newTags);
TaggableItem.filterTagsInPlace(prefix, existingTags);
values.remove(TaggableItem.CV_TAG_PREFIX);
}
// tags that need to be removed.
final Set<String> toDelete = new HashSet<String>(existingTags);
toDelete.removeAll(newTags);
final List<String> toDeleteWhere = new ArrayList<String>(toDelete);
for (int i = 0; i < toDeleteWhere.size(); i++ ){
toDeleteWhere.set(i, Tag._NAME + "=\'" + toDeleteWhere.get(i) + "'");
}
if (toDeleteWhere.size() > 0){
final String delWhere = ListUtils.join(toDeleteWhere, " OR ");
delete(uri, delWhere, null);
}
// duplicates will be ignored.
final ContentValues cvAdd = new ContentValues();
cvAdd.put(Tag.PATH, TaggableItem.toListString(newTags));
insert(uri, cvAdd);
count = 0;
break;
}
case MATCHER_ITINERARY_DIR:{
count = db.update(ITINERARY_TABLE_NAME, values, where, whereArgs);
}break;
case MATCHER_ITINERARY_ITEM:{
final String itemId = uri.getLastPathSegment();
count = db.update(ITINERARY_TABLE_NAME, values,
ProviderUtils.addExtraWhere(where, Itinerary._ID+"=?"),
ProviderUtils.addExtraWhereArgs(whereArgs, itemId));
}break;
default:
if (mDBHelperMapper.canUpdate(code)){
count = mDBHelperMapper.update(code, this, db, uri, values, where, whereArgs);
}else{
throw new IllegalArgumentException("unknown URI "+uri);
}
}
getContext().getContentResolver().notifyChange(uri, null);
if (needSync && canSync){
LocastSyncService.startSync(getContext(), uri, false);
}
return count;
}
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
final SQLiteDatabase db = dbHelper.getWritableDatabase();
final long id;
int count;
final int code = uriMatcher.match(uri);
switch (code) {
case MATCHER_CAST_DIR:
count = db.delete(CAST_TABLE_NAME, where, whereArgs);
// special case to handle deletion of ALL THE THINGS
if (where == null) {
db.delete(CASTS_CASTMEDIA_DBHELPER.getJoinTableName(), null, null);
}
break;
case MATCHER_CAST_ITEM:
id = ContentUris.parseId(uri);
count = db.delete(CAST_TABLE_NAME,
Cast._ID
+ "="
+ id
+ (where != null && where.length() > 0 ? " AND (" + where + ")"
: ""), whereArgs);
break;
case MATCHER_COMMENT_DIR:
count = db.delete(COMMENT_TABLE_NAME, where, whereArgs);
break;
case MATCHER_COMMENT_ITEM: {
id = ContentUris.parseId(uri);
count = db.delete(COMMENT_TABLE_NAME,
ProviderUtils.addExtraWhere(where, Comment._ID + "=?"),
ProviderUtils.addExtraWhereArgs(whereArgs, String.valueOf(id)));
}
break;
case MATCHER_CHILD_COMMENT_ITEM: {
final List<String> pathSegs = uri.getPathSegments();
id = ContentUris.parseId(uri);
count = db.delete(
COMMENT_TABLE_NAME,
ProviderUtils.addExtraWhere(where, Comment._ID + "=?", Comment._PARENT_ID
+ "=?", Comment._PARENT_CLASS + "=?"),
ProviderUtils.addExtraWhereArgs(whereArgs, Long.toString(id),
pathSegs.get(pathSegs.size() - 3),
pathSegs.get(pathSegs.size() - 4)));
}
break;
case MATCHER_ITEM_TAGS: {
final List<String> pathSegments = uri.getPathSegments();
count = db.delete(
TAG_TABLE_NAME,
ProviderUtils.addExtraWhere(where, Tag._REF_CLASS + "=?", Tag._REF_ID
+ "=?"),
ProviderUtils.addExtraWhereArgs(whereArgs,
pathSegments.get(pathSegments.size() - 3),
pathSegments.get(pathSegments.size() - 2)));
break;
}
case MATCHER_TAG_DIR: {
count = db.delete(TAG_TABLE_NAME, where, whereArgs);
break;
}
case MATCHER_ITINERARY_DIR: {
count = db.delete(ITINERARY_TABLE_NAME, where, whereArgs);
// special case to handle deletion of ALL THE THINGS
if (where == null) {
db.delete(ITINERARY_CASTS_DBHELPER.getJoinTableName(), null, null);
}
}
break;
case MATCHER_ITINERARY_ITEM: {
final String itemId = uri.getLastPathSegment();
count = db.delete(ITINERARY_TABLE_NAME,
ProviderUtils.addExtraWhere(where, Itinerary._ID + "=?"),
ProviderUtils.addExtraWhereArgs(whereArgs, itemId));
}
break;
default:
if (mDBHelperMapper.canDelete(code)) {
count = mDBHelperMapper.delete(code, this, db, uri, where, whereArgs);
} else {
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
if (!db.inTransaction()) {
db.execSQL("VACUUM");
}
getContext().getContentResolver().notifyChange(uri, null, true);
return count;
}
/**
* @param cr
* @param uri
* @return The path that one should post to for the given content item. Should always point to an item, not a dir.
* @throws NoPublicPath
*/
public static String getPostPath(Context context, Uri uri) throws NoPublicPath{
return getPublicPath(context, uri, null, true);
}
public static String getPublicPath(Context context, Uri uri) throws NoPublicPath{
return getPublicPath(context, uri, null, false);
}
/**
* Returns a public ID to an item, given the parent URI and a public ID
*
* @param context
* @param uri URI of the parent item
* @param publicId public ID of the child item
* @return
* @throws NoPublicPath
*/
public static String getPublicPath(Context context, Uri uri, Long publicId) throws NoPublicPath{
return getPublicPath(context, uri, publicId, false);
}
/**
* @param context
* @param uri the URI of the item whose field should be queried
* @param field the string name of the field
* @return
*/
private static String getPathFromField(Context context, Uri uri, String field){
String path = null;
final String[] generalProjection = {JsonSyncableItem._ID, field};
final Cursor c = context.getContentResolver().query(uri, generalProjection, null, null, null);
try{
if (c.getCount() == 1 && c.moveToFirst()){
final String storedPath = c.getString(c.getColumnIndex(field));
if (storedPath != null){
path = storedPath;
}
}else{
throw new IllegalArgumentException("could not get path from field '"+field+"' in uri "+uri);
}
}finally{
c.close();
}
return path;
}
public static String getPublicPath(Context context, Uri uri, Long publicId, boolean parent) throws NoPublicPath{
String path;
final int match = uriMatcher.match(uri);
// first check to see if the path is stored already.
switch (match){
// these should be the only hard-coded paths in the system.
case MATCHER_EVENT_DIR:
path = internalToPublicQueryMap(context, Event.SERVER_PATH, uri);
break;
case MATCHER_CAST_DIR:{
path = internalToPublicQueryMap(context, Cast.SERVER_PATH, uri);
}break;
case MATCHER_ITINERARY_DIR:{
path = Itinerary.SERVER_PATH;
}break;
case MATCHER_COMMENT_DIR:
path = Comment.SERVER_PATH;
break;
case MATCHER_CHILD_COMMENT_DIR:
path = getPathFromField(context, ProviderUtils.removeLastPathSegment(uri), Commentable.Columns._COMMENT_DIR_URI);
break;
case MATCHER_CAST_ITEM:
case MATCHER_CHILD_COMMENT_ITEM:
case MATCHER_CHILD_CAST_ITEM:
case MATCHER_COMMENT_ITEM:
case MATCHER_ITINERARY_ITEM:
case MATCHER_CHILD_CASTMEDIA_ITEM:
{
if (parent || publicId != null){
path = getPublicPath(context, ProviderUtils.removeLastPathSegment(uri));
}else{
path = getPathFromField(context, uri, JsonSyncableItem._PUBLIC_URI);
}
}break;
case MATCHER_CHILD_CAST_DIR:
path = getPathFromField(context, ProviderUtils.removeLastPathSegment(uri), Itinerary._CASTS_URI);
break;
case MATCHER_CHILD_CASTMEDIA_DIR:
path = getPathFromField(context, ProviderUtils.removeLastPathSegment(uri), Cast._MEDIA_PUBLIC_URI);
break;
case MATCHER_ITINERARY_BY_TAGS:{
final Set<String> tags = TaggableItem.removePrefixesFromTags(Tag.toSet(uri.getQuery()));
path = getPublicPath(context, ProviderUtils.removeLastPathSegment(uri)) + "?tags=" + ListUtils.join(tags, ",");
}break;
default:
throw new IllegalArgumentException("Don't know how to get the public path for "+uri);
}
if (path == null){
throw new NoPublicPath("got null path for " + uri);
}
if (publicId != null){
path += publicId + "/";
}
path = path.replaceAll("//", "/"); // hack to get around a tedious problem
return path;
}
private static String internalToPublicQueryMap(Context context, String serverPath, Uri uri){
final String tags = uri.getQueryParameter(TaggableItem.SERVER_QUERY_PARAMETER);
final String dist = uri.getQueryParameter(Locatable.SERVER_QUERY_PARAMETER);
final Boolean favorite = Favoritable.decodeFavoritedUri(uri);
String query = null;
// TODO figure out a better way to do this without needing to hard-code this logic.
if (tags != null){
final Set<String> tagSet = TaggableItem.removePrefixesFromTags(Tag.toSet(tags));
query = TaggableItem.SERVER_QUERY_PARAMETER+"=" + ListUtils.join(tagSet, ",");
}
if (dist != null){
if (query != null){
query += "&";
}else{
query = "";
}
query += Locatable.SERVER_QUERY_PARAMETER+"=" + dist;
}
if (favorite != null){
final Account[] accounts = Authenticator.getAccounts(context);
if (accounts.length > 0){
final String id = AccountManager.get(context).getUserData(accounts[0], AuthenticationService.USERDATA_USERID);
if(id != null){
if (query != null){
query += "&";
}else{
query = "";
}
query += "favorited_by=" + id;
}
}
}
return serverPath + (query != null ? "?"+query : "");
}
public static UriMatcher getUriMatcher(){
return uriMatcher;
}
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, Cast.PATH, MATCHER_CAST_DIR);
uriMatcher.addURI(AUTHORITY, Cast.PATH+"/#", MATCHER_CAST_ITEM);
// /cast/1/media
uriMatcher.addURI(AUTHORITY, Cast.PATH+"/#/"+CastMedia.PATH, MATCHER_CHILD_CASTMEDIA_DIR);
uriMatcher.addURI(AUTHORITY, Cast.PATH+"/#/"+CastMedia.PATH+"/#", MATCHER_CHILD_CASTMEDIA_ITEM);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#/" + Cast.PATH + "/#/" + CastMedia.PATH, MATCHER_CHILD_CASTMEDIA_DIR);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#/" + Cast.PATH + "/#/" + CastMedia.PATH + "/#/", MATCHER_CHILD_CASTMEDIA_ITEM);
// /comments/1, etc.
uriMatcher.addURI(AUTHORITY, Comment.PATH, MATCHER_COMMENT_DIR);
uriMatcher.addURI(AUTHORITY, Comment.PATH + "/#", MATCHER_COMMENT_ITEM);
// project/1/comments, etc.
uriMatcher.addURI(AUTHORITY, Cast.PATH + "/#/" + Comment.PATH, MATCHER_CHILD_COMMENT_DIR);
uriMatcher.addURI(AUTHORITY, Cast.PATH + "/#/" + Comment.PATH + "/#", MATCHER_CHILD_COMMENT_ITEM);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#/" + Comment.PATH, MATCHER_CHILD_COMMENT_DIR);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#/" + Comment.PATH + "/#", MATCHER_CHILD_COMMENT_ITEM);
// /event
uriMatcher.addURI(AUTHORITY, Event.PATH, MATCHER_EVENT_DIR);
uriMatcher.addURI(AUTHORITY, Event.PATH + "/#", MATCHER_EVENT_ITEM);
// /content/1/tags
uriMatcher.addURI(AUTHORITY, Cast.PATH + "/#/"+Tag.PATH, MATCHER_ITEM_TAGS);
uriMatcher.addURI(AUTHORITY, Event.PATH + "/#/"+Tag.PATH, MATCHER_ITEM_TAGS);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#/"+Tag.PATH, MATCHER_ITEM_TAGS);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#/"+Cast.PATH + "/#/"+Tag.PATH, MATCHER_ITEM_TAGS);
// /content/tags?tag1,tag2
uriMatcher.addURI(AUTHORITY, Itinerary.PATH +'/'+Tag.PATH, MATCHER_ITINERARY_BY_TAGS);
// tag list
uriMatcher.addURI(AUTHORITY, Tag.PATH, MATCHER_TAG_DIR);
// Itineraries
uriMatcher.addURI(AUTHORITY, Itinerary.PATH, MATCHER_ITINERARY_DIR);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#", MATCHER_ITINERARY_ITEM);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#/" + Cast.PATH, MATCHER_CHILD_CAST_DIR);
uriMatcher.addURI(AUTHORITY, Itinerary.PATH + "/#/" + Cast.PATH + "/#", MATCHER_CHILD_CAST_ITEM);
mDBHelperMapper.addDirMapping(MATCHER_CHILD_CAST_DIR, ITINERARY_CASTS_DBHELPER, DBHelperMapper.TYPE_ALL);
mDBHelperMapper.addItemMapping(MATCHER_CHILD_CAST_ITEM, ITINERARY_CASTS_DBHELPER, DBHelperMapper.TYPE_ALL);
mDBHelperMapper.addDirMapping(MATCHER_CHILD_CASTMEDIA_DIR, CASTS_CASTMEDIA_DBHELPER, DBHelperMapper.TYPE_ALL);
mDBHelperMapper.addItemMapping(MATCHER_CHILD_CASTMEDIA_ITEM, CASTS_CASTMEDIA_DBHELPER, DBHelperMapper.TYPE_ALL);
mDBHelperMapper.addDirMapping(MATCHER_EVENT_DIR, EVENT_DBHELPER, DBHelperMapper.TYPE_ALL);
mDBHelperMapper.addItemMapping(MATCHER_EVENT_ITEM, EVENT_DBHELPER, DBHelperMapper.TYPE_ALL);
}
}