blob: 9ecc3d61196bb8ce52a02ef3f8f8b3cee5911218 [file] [log] [blame]
/*
** Copyright 2006, 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.providers.subscribedfeeds;
import android.content.UriMatcher;
import android.content.*;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.provider.SubscribedFeeds;
import android.text.TextUtils;
import android.util.Config;
import android.util.Log;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
/**
* Manages a list of feeds for which this client is interested in receiving
* change notifications.
*/
public class SubscribedFeedsProvider extends AbstractSyncableContentProvider {
private static final String TAG = "SubscribedFeedsProvider";
private static final String DATABASE_NAME = "subscribedfeeds.db";
private static final int DATABASE_VERSION = 10;
private static final int FEEDS = 1;
private static final int FEED_ID = 2;
private static final int DELETED_FEEDS = 3;
private static final int ACCOUNTS = 4;
private static final Map<String, String> ACCOUNTS_PROJECTION_MAP;
private static final UriMatcher sURLMatcher =
new UriMatcher(UriMatcher.NO_MATCH);
private static String sFeedsTable = "feeds";
private static Uri sFeedsUrl =
Uri.parse("content://subscribedfeeds/feeds/");
private static String sDeletedFeedsTable = "_deleted_feeds";
private static Uri sDeletedFeedsUrl =
Uri.parse("content://subscribedfeeds/deleted_feeds/");
public SubscribedFeedsProvider() {
super(DATABASE_NAME, DATABASE_VERSION, sFeedsUrl);
}
static {
sURLMatcher.addURI("subscribedfeeds", "feeds", FEEDS);
sURLMatcher.addURI("subscribedfeeds", "feeds/#", FEED_ID);
sURLMatcher.addURI("subscribedfeeds", "deleted_feeds", DELETED_FEEDS);
sURLMatcher.addURI("subscribedfeeds", "accounts", ACCOUNTS);
}
@Override
protected boolean upgradeDatabase(SQLiteDatabase db,
int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion +
" to " + newVersion +
", which will destroy all old data");
db.execSQL("DROP TRIGGER IF EXISTS feed_cleanup");
db.execSQL("DROP TABLE IF EXISTS _deleted_feeds");
db.execSQL("DROP TABLE IF EXISTS feeds");
bootstrapDatabase(db);
return false; // this was lossy
}
@Override
protected void bootstrapDatabase(SQLiteDatabase db) {
super.bootstrapDatabase(db);
db.execSQL("CREATE TABLE feeds (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," + // From the sync source
"_sync_id TEXT," + // From the sync source
"_sync_time TEXT," + // From the sync source
"_sync_version TEXT," + // From the sync source
"_sync_local_id INTEGER," + // Used while syncing,
// never stored persistently
"_sync_dirty INTEGER," + // if syncable, set if the record
// has local, unsynced, changes
"_sync_mark INTEGER," + // Used to filter out new rows
"feed TEXT," +
"authority TEXT," +
"service TEXT" +
");");
// Trigger to completely remove feeds data when they're deleted
db.execSQL("CREATE TRIGGER feed_cleanup DELETE ON feeds " +
"WHEN old._sync_id is not null " +
"BEGIN " +
"INSERT INTO _deleted_feeds " +
"(_sync_id, _sync_account, _sync_version) " +
"VALUES (old._sync_id, old._sync_account, " +
"old._sync_version);" +
"END");
db.execSQL("CREATE TABLE _deleted_feeds (" +
"_sync_version TEXT," + // From the sync source
"_sync_id TEXT," +
(isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
"_sync_account TEXT," +
"_sync_mark INTEGER, " + // Used to filter out new rows
"UNIQUE(_sync_id))");
}
@Override
protected void onDatabaseOpened(SQLiteDatabase db) {
db.markTableSyncable("feeds", "_deleted_feeds");
}
@Override
protected Iterable<FeedMerger> getMergers() {
return Collections.singletonList(new FeedMerger());
}
@Override
public String getType(Uri url) {
int match = sURLMatcher.match(url);
switch (match) {
case FEEDS:
return SubscribedFeeds.Feeds.CONTENT_TYPE;
case FEED_ID:
return SubscribedFeeds.Feeds.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Unknown URL");
}
}
@Override
public Cursor queryInternal(Uri url, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
// Generate the body of the query
int match = sURLMatcher.match(url);
if (Config.LOGV) Log.v(TAG, "SubscribedFeedsProvider.query: url=" +
url + ", match is " + match);
switch (match) {
case FEEDS:
qb.setTables(sFeedsTable);
break;
case DELETED_FEEDS:
if (!isTemporary()) {
throw new UnsupportedOperationException();
}
qb.setTables(sDeletedFeedsTable);
break;
case ACCOUNTS:
qb.setTables(sFeedsTable);
qb.setDistinct(true);
qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP);
return qb.query(getDatabase(), projection, selection, selectionArgs,
SubscribedFeeds.Feeds._SYNC_ACCOUNT, null, sortOrder);
case FEED_ID:
qb.setTables(sFeedsTable);
qb.appendWhere(sFeedsTable + "._id=");
qb.appendWhere(url.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
// run the query
return qb.query(getDatabase(), projection, selection, selectionArgs,
null, null, sortOrder);
}
@Override
public Uri insertInternal(Uri url, ContentValues initialValues) {
final SQLiteDatabase db = getDatabase();
Uri resultUri = null;
long rowID;
int match = sURLMatcher.match(url);
switch (match) {
case FEEDS:
ContentValues values = new ContentValues(initialValues);
values.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
rowID = db.insert(sFeedsTable, "feed", values);
if (rowID > 0) {
resultUri = Uri.parse(
"content://subscribedfeeds/feeds/" + rowID);
}
break;
case DELETED_FEEDS:
if (!isTemporary()) {
throw new UnsupportedOperationException();
}
rowID = db.insert(sDeletedFeedsTable, "_sync_id",
initialValues);
if (rowID > 0) {
resultUri = Uri.parse(
"content://subscribedfeeds/deleted_feeds/" + rowID);
}
break;
default:
throw new UnsupportedOperationException(
"Cannot insert into URL: " + url);
}
return resultUri;
}
@Override
public int deleteInternal(Uri url, String userWhere, String[] whereArgs) {
final SQLiteDatabase db = getDatabase();
String changedItemId;
switch (sURLMatcher.match(url)) {
case FEEDS:
changedItemId = null;
break;
case FEED_ID:
changedItemId = url.getPathSegments().get(1);
break;
default:
throw new UnsupportedOperationException(
"Cannot delete that URL: " + url);
}
String where = addIdToWhereClause(changedItemId, userWhere);
return db.delete(sFeedsTable, where, whereArgs);
}
@Override
public int updateInternal(Uri url, ContentValues initialValues,
String userWhere, String[] whereArgs) {
final SQLiteDatabase db = getDatabase();
ContentValues values = new ContentValues(initialValues);
values.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
String changedItemId;
switch (sURLMatcher.match(url)) {
case FEEDS:
changedItemId = null;
break;
case FEED_ID:
changedItemId = url.getPathSegments().get(1);
break;
default:
throw new UnsupportedOperationException(
"Cannot update URL: " + url);
}
String where = addIdToWhereClause(changedItemId, userWhere);
return db.update(sFeedsTable, values, where, whereArgs);
}
private static String addIdToWhereClause(String id, String where) {
if (id != null) {
StringBuilder whereSb = new StringBuilder("_id=");
whereSb.append(id);
if (!TextUtils.isEmpty(where)) {
whereSb.append(" AND (");
whereSb.append(where);
whereSb.append(')');
}
return whereSb.toString();
} else {
return where;
}
}
private class FeedMerger extends AbstractTableMerger {
private ContentValues mValues = new ContentValues();
FeedMerger() {
super(getDatabase(), sFeedsTable, sFeedsUrl, sDeletedFeedsTable, sDeletedFeedsUrl);
}
@Override
protected void notifyChanges() {
getContext().getContentResolver().notifyChange(
sFeedsUrl, null /* data change observer */,
false /* do not sync to network */);
}
@Override
public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
final SQLiteDatabase db = getDatabase();
// We don't ever want to add entries from the server, instead
// we want to tell the server to delete any entries we receive
// from the server that aren't already known by the client.
mValues.clear();
DatabaseUtils.cursorStringToContentValues(diffsCursor,
SubscribedFeeds.Feeds._SYNC_ID, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor,
SubscribedFeeds.Feeds._SYNC_ACCOUNT, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor,
SubscribedFeeds.Feeds._SYNC_VERSION, mValues);
db.replace(mDeletedTable, SubscribedFeeds.Feeds._SYNC_ID, mValues);
}
@Override
public void updateRow(long localPersonID, ContentProvider diffs,
Cursor diffsCursor) {
updateOrResolveRow(localPersonID, null, diffs, diffsCursor, false);
}
@Override
public void resolveRow(long localPersonID, String syncID,
ContentProvider diffs, Cursor diffsCursor) {
updateOrResolveRow(localPersonID, syncID, diffs, diffsCursor, true);
}
protected void updateOrResolveRow(long localPersonID, String syncID,
ContentProvider diffs, Cursor diffsCursor, boolean conflicts) {
mValues.clear();
// only copy over the fields that the server owns
DatabaseUtils.cursorStringToContentValues(diffsCursor,
SubscribedFeeds.Feeds._SYNC_ID, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor,
SubscribedFeeds.Feeds._SYNC_TIME, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor,
SubscribedFeeds.Feeds._SYNC_VERSION, mValues);
mValues.put(SubscribedFeeds.Feeds._SYNC_DIRTY, conflicts ? 1 : 0);
final SQLiteDatabase db = getDatabase();
db.update(mTable, mValues,
SubscribedFeeds.Feeds._ID + '=' + localPersonID, null);
}
@Override
public void deleteRow(Cursor localCursor) {
// Since the client is the authority we don't actually delete
// the row when the server says it has been deleted. Instead
// we break the association with the server by clearing out
// the id, time, and version, then we mark it dirty so that
// it will be synced back to the server.
long localPersonId = localCursor.getLong(localCursor.getColumnIndex(
SubscribedFeeds.Feeds._ID));
mValues.clear();
mValues.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
mValues.put(SubscribedFeeds.Feeds._SYNC_ID, (String) null);
mValues.put(SubscribedFeeds.Feeds._SYNC_TIME, (Long) null);
mValues.put(SubscribedFeeds.Feeds._SYNC_VERSION, (String) null);
final SQLiteDatabase db = getDatabase();
db.update(mTable, mValues, SubscribedFeeds.Feeds._ID + '=' + localPersonId, null);
localCursor.moveToNext();
}
}
static {
Map<String, String> map;
map = new HashMap<String, String>();
ACCOUNTS_PROJECTION_MAP = map;
map.put(SubscribedFeeds.Accounts._COUNT, "COUNT(*) AS _count");
map.put(SubscribedFeeds.Accounts._SYNC_ACCOUNT, SubscribedFeeds.Accounts._SYNC_ACCOUNT);
}
}