blob: d87f5e72ef141284e828ea56c67279c0721a1e3e [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2** Copyright 2006, The Android Open Source Project
3**
4** Licensed under the Apache License, Version 2.0 (the "License");
5** you may not use this file except in compliance with the License.
6** You may obtain a copy of the License at
7**
8** http://www.apache.org/licenses/LICENSE-2.0
9**
10** Unless required by applicable law or agreed to in writing, software
11** distributed under the License is distributed on an "AS IS" BASIS,
12** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13** See the License for the specific language governing permissions and
14** limitations under the License.
15*/
16
17package com.android.providers.subscribedfeeds;
18
19import android.content.UriMatcher;
20import android.content.*;
21import android.database.Cursor;
22import android.database.DatabaseUtils;
23import android.database.sqlite.SQLiteDatabase;
24import android.database.sqlite.SQLiteQueryBuilder;
25import android.net.Uri;
26import android.provider.SubscribedFeeds;
27import android.text.TextUtils;
28import android.util.Config;
29import android.util.Log;
30
31import java.util.Collections;
32import java.util.Map;
33import java.util.HashMap;
34
35/**
36 * Manages a list of feeds for which this client is interested in receiving
37 * change notifications.
38 */
39public class SubscribedFeedsProvider extends AbstractSyncableContentProvider {
40 private static final String TAG = "SubscribedFeedsProvider";
41 private static final String DATABASE_NAME = "subscribedfeeds.db";
Fred Quintanad9d2f112009-04-23 13:36:27 -070042 private static final int DATABASE_VERSION = 11;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080043
44 private static final int FEEDS = 1;
45 private static final int FEED_ID = 2;
46 private static final int DELETED_FEEDS = 3;
47 private static final int ACCOUNTS = 4;
48
49 private static final Map<String, String> ACCOUNTS_PROJECTION_MAP;
50
51 private static final UriMatcher sURLMatcher =
52 new UriMatcher(UriMatcher.NO_MATCH);
53
54 private static String sFeedsTable = "feeds";
55 private static Uri sFeedsUrl =
56 Uri.parse("content://subscribedfeeds/feeds/");
57 private static String sDeletedFeedsTable = "_deleted_feeds";
58 private static Uri sDeletedFeedsUrl =
59 Uri.parse("content://subscribedfeeds/deleted_feeds/");
60
61 public SubscribedFeedsProvider() {
62 super(DATABASE_NAME, DATABASE_VERSION, sFeedsUrl);
63 }
64
65 static {
66 sURLMatcher.addURI("subscribedfeeds", "feeds", FEEDS);
67 sURLMatcher.addURI("subscribedfeeds", "feeds/#", FEED_ID);
68 sURLMatcher.addURI("subscribedfeeds", "deleted_feeds", DELETED_FEEDS);
69 sURLMatcher.addURI("subscribedfeeds", "accounts", ACCOUNTS);
70 }
71
72 @Override
73 protected boolean upgradeDatabase(SQLiteDatabase db,
74 int oldVersion, int newVersion) {
75 Log.w(TAG, "Upgrading database from version " + oldVersion +
76 " to " + newVersion +
77 ", which will destroy all old data");
78 db.execSQL("DROP TRIGGER IF EXISTS feed_cleanup");
79 db.execSQL("DROP TABLE IF EXISTS _deleted_feeds");
80 db.execSQL("DROP TABLE IF EXISTS feeds");
81 bootstrapDatabase(db);
82 return false; // this was lossy
83 }
84
85 @Override
86 protected void bootstrapDatabase(SQLiteDatabase db) {
87 super.bootstrapDatabase(db);
88 db.execSQL("CREATE TABLE feeds (" +
89 "_id INTEGER PRIMARY KEY," +
90 "_sync_account TEXT," + // From the sync source
Fred Quintanad9d2f112009-04-23 13:36:27 -070091 "_sync_account_type TEXT," + // From the sync source
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080092 "_sync_id TEXT," + // From the sync source
93 "_sync_time TEXT," + // From the sync source
94 "_sync_version TEXT," + // From the sync source
95 "_sync_local_id INTEGER," + // Used while syncing,
96 // never stored persistently
97 "_sync_dirty INTEGER," + // if syncable, set if the record
98 // has local, unsynced, changes
99 "_sync_mark INTEGER," + // Used to filter out new rows
100 "feed TEXT," +
101 "authority TEXT," +
102 "service TEXT" +
103 ");");
104
105 // Trigger to completely remove feeds data when they're deleted
106 db.execSQL("CREATE TRIGGER feed_cleanup DELETE ON feeds " +
107 "WHEN old._sync_id is not null " +
108 "BEGIN " +
109 "INSERT INTO _deleted_feeds " +
Fred Quintanad9d2f112009-04-23 13:36:27 -0700110 "(_sync_id, _sync_account, _sync_account_type, _sync_version) " +
111 "VALUES (old._sync_id, old._sync_account, old._sync_account_type, " +
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800112 "old._sync_version);" +
113 "END");
114
115 db.execSQL("CREATE TABLE _deleted_feeds (" +
116 "_sync_version TEXT," + // From the sync source
117 "_sync_id TEXT," +
118 (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
119 "_sync_account TEXT," +
Fred Quintanad9d2f112009-04-23 13:36:27 -0700120 "_sync_account_type TEXT," +
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800121 "_sync_mark INTEGER, " + // Used to filter out new rows
122 "UNIQUE(_sync_id))");
123 }
124
125 @Override
126 protected void onDatabaseOpened(SQLiteDatabase db) {
127 db.markTableSyncable("feeds", "_deleted_feeds");
128 }
129
130 @Override
131 protected Iterable<FeedMerger> getMergers() {
132 return Collections.singletonList(new FeedMerger());
133 }
134
135 @Override
136 public String getType(Uri url) {
137 int match = sURLMatcher.match(url);
138 switch (match) {
139 case FEEDS:
140 return SubscribedFeeds.Feeds.CONTENT_TYPE;
141 case FEED_ID:
142 return SubscribedFeeds.Feeds.CONTENT_ITEM_TYPE;
143 default:
144 throw new IllegalArgumentException("Unknown URL");
145 }
146 }
147
148 @Override
149 public Cursor queryInternal(Uri url, String[] projection,
150 String selection, String[] selectionArgs, String sortOrder) {
151 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
152
153
154 // Generate the body of the query
155 int match = sURLMatcher.match(url);
156
157 if (Config.LOGV) Log.v(TAG, "SubscribedFeedsProvider.query: url=" +
158 url + ", match is " + match);
159
160 switch (match) {
161 case FEEDS:
162 qb.setTables(sFeedsTable);
163 break;
164 case DELETED_FEEDS:
165 if (!isTemporary()) {
166 throw new UnsupportedOperationException();
167 }
168 qb.setTables(sDeletedFeedsTable);
169 break;
170 case ACCOUNTS:
171 qb.setTables(sFeedsTable);
172 qb.setDistinct(true);
173 qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP);
174 return qb.query(getDatabase(), projection, selection, selectionArgs,
Fred Quintanad9d2f112009-04-23 13:36:27 -0700175 SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE + ","
176 + SubscribedFeeds.Feeds._SYNC_ACCOUNT, null, sortOrder);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800177 case FEED_ID:
178 qb.setTables(sFeedsTable);
179 qb.appendWhere(sFeedsTable + "._id=");
180 qb.appendWhere(url.getPathSegments().get(1));
181 break;
182 default:
183 throw new IllegalArgumentException("Unknown URL " + url);
184 }
185
186 // run the query
187 return qb.query(getDatabase(), projection, selection, selectionArgs,
188 null, null, sortOrder);
189 }
190
191 @Override
192 public Uri insertInternal(Uri url, ContentValues initialValues) {
193 final SQLiteDatabase db = getDatabase();
194 Uri resultUri = null;
195 long rowID;
196
197 int match = sURLMatcher.match(url);
198 switch (match) {
199 case FEEDS:
200 ContentValues values = new ContentValues(initialValues);
201 values.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
202 rowID = db.insert(sFeedsTable, "feed", values);
203 if (rowID > 0) {
204 resultUri = Uri.parse(
205 "content://subscribedfeeds/feeds/" + rowID);
206 }
207 break;
208
209 case DELETED_FEEDS:
210 if (!isTemporary()) {
211 throw new UnsupportedOperationException();
212 }
213 rowID = db.insert(sDeletedFeedsTable, "_sync_id",
214 initialValues);
215 if (rowID > 0) {
216 resultUri = Uri.parse(
217 "content://subscribedfeeds/deleted_feeds/" + rowID);
218 }
219 break;
220
221 default:
222 throw new UnsupportedOperationException(
223 "Cannot insert into URL: " + url);
224 }
225
226 return resultUri;
227 }
228
229 @Override
230 public int deleteInternal(Uri url, String userWhere, String[] whereArgs) {
231 final SQLiteDatabase db = getDatabase();
232 String changedItemId;
233
234 switch (sURLMatcher.match(url)) {
235 case FEEDS:
236 changedItemId = null;
237 break;
238 case FEED_ID:
239 changedItemId = url.getPathSegments().get(1);
240 break;
241 default:
242 throw new UnsupportedOperationException(
243 "Cannot delete that URL: " + url);
244 }
245
246 String where = addIdToWhereClause(changedItemId, userWhere);
247 return db.delete(sFeedsTable, where, whereArgs);
248 }
249
250 @Override
251 public int updateInternal(Uri url, ContentValues initialValues,
252 String userWhere, String[] whereArgs) {
253 final SQLiteDatabase db = getDatabase();
254 ContentValues values = new ContentValues(initialValues);
255 values.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
256
257 String changedItemId;
258 switch (sURLMatcher.match(url)) {
259 case FEEDS:
260 changedItemId = null;
261 break;
262
263 case FEED_ID:
264 changedItemId = url.getPathSegments().get(1);
265 break;
266
267 default:
268 throw new UnsupportedOperationException(
269 "Cannot update URL: " + url);
270 }
271
272 String where = addIdToWhereClause(changedItemId, userWhere);
273 return db.update(sFeedsTable, values, where, whereArgs);
274 }
275
276 private static String addIdToWhereClause(String id, String where) {
277 if (id != null) {
278 StringBuilder whereSb = new StringBuilder("_id=");
279 whereSb.append(id);
280 if (!TextUtils.isEmpty(where)) {
281 whereSb.append(" AND (");
282 whereSb.append(where);
283 whereSb.append(')');
284 }
285 return whereSb.toString();
286 } else {
287 return where;
288 }
289 }
290
291 private class FeedMerger extends AbstractTableMerger {
292 private ContentValues mValues = new ContentValues();
293 FeedMerger() {
294 super(getDatabase(), sFeedsTable, sFeedsUrl, sDeletedFeedsTable, sDeletedFeedsUrl);
295 }
296
297 @Override
298 protected void notifyChanges() {
299 getContext().getContentResolver().notifyChange(
300 sFeedsUrl, null /* data change observer */,
301 false /* do not sync to network */);
302 }
303
304 @Override
305 public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
306 final SQLiteDatabase db = getDatabase();
307 // We don't ever want to add entries from the server, instead
308 // we want to tell the server to delete any entries we receive
309 // from the server that aren't already known by the client.
310 mValues.clear();
311 DatabaseUtils.cursorStringToContentValues(diffsCursor,
312 SubscribedFeeds.Feeds._SYNC_ID, mValues);
313 DatabaseUtils.cursorStringToContentValues(diffsCursor,
314 SubscribedFeeds.Feeds._SYNC_ACCOUNT, mValues);
315 DatabaseUtils.cursorStringToContentValues(diffsCursor,
Fred Quintanad9d2f112009-04-23 13:36:27 -0700316 SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE, mValues);
317 DatabaseUtils.cursorStringToContentValues(diffsCursor,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800318 SubscribedFeeds.Feeds._SYNC_VERSION, mValues);
319 db.replace(mDeletedTable, SubscribedFeeds.Feeds._SYNC_ID, mValues);
320 }
321
322 @Override
323 public void updateRow(long localPersonID, ContentProvider diffs,
324 Cursor diffsCursor) {
325 updateOrResolveRow(localPersonID, null, diffs, diffsCursor, false);
326 }
327
328 @Override
329 public void resolveRow(long localPersonID, String syncID,
330 ContentProvider diffs, Cursor diffsCursor) {
331 updateOrResolveRow(localPersonID, syncID, diffs, diffsCursor, true);
332 }
333
334 protected void updateOrResolveRow(long localPersonID, String syncID,
335 ContentProvider diffs, Cursor diffsCursor, boolean conflicts) {
336 mValues.clear();
337 // only copy over the fields that the server owns
338 DatabaseUtils.cursorStringToContentValues(diffsCursor,
339 SubscribedFeeds.Feeds._SYNC_ID, mValues);
340 DatabaseUtils.cursorStringToContentValues(diffsCursor,
341 SubscribedFeeds.Feeds._SYNC_TIME, mValues);
342 DatabaseUtils.cursorStringToContentValues(diffsCursor,
343 SubscribedFeeds.Feeds._SYNC_VERSION, mValues);
344 mValues.put(SubscribedFeeds.Feeds._SYNC_DIRTY, conflicts ? 1 : 0);
345 final SQLiteDatabase db = getDatabase();
346 db.update(mTable, mValues,
347 SubscribedFeeds.Feeds._ID + '=' + localPersonID, null);
348 }
349
350 @Override
351 public void deleteRow(Cursor localCursor) {
352 // Since the client is the authority we don't actually delete
353 // the row when the server says it has been deleted. Instead
354 // we break the association with the server by clearing out
355 // the id, time, and version, then we mark it dirty so that
356 // it will be synced back to the server.
357 long localPersonId = localCursor.getLong(localCursor.getColumnIndex(
358 SubscribedFeeds.Feeds._ID));
359 mValues.clear();
360 mValues.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
361 mValues.put(SubscribedFeeds.Feeds._SYNC_ID, (String) null);
362 mValues.put(SubscribedFeeds.Feeds._SYNC_TIME, (Long) null);
363 mValues.put(SubscribedFeeds.Feeds._SYNC_VERSION, (String) null);
364 final SQLiteDatabase db = getDatabase();
365 db.update(mTable, mValues, SubscribedFeeds.Feeds._ID + '=' + localPersonId, null);
366 localCursor.moveToNext();
367 }
368 }
369
370 static {
371 Map<String, String> map;
372
373 map = new HashMap<String, String>();
374 ACCOUNTS_PROJECTION_MAP = map;
375 map.put(SubscribedFeeds.Accounts._COUNT, "COUNT(*) AS _count");
376 map.put(SubscribedFeeds.Accounts._SYNC_ACCOUNT, SubscribedFeeds.Accounts._SYNC_ACCOUNT);
Fred Quintanad9d2f112009-04-23 13:36:27 -0700377 map.put(SubscribedFeeds.Accounts._SYNC_ACCOUNT_TYPE,
378 SubscribedFeeds.Accounts._SYNC_ACCOUNT_TYPE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800379 }
380}