blob: 9ecc3d61196bb8ce52a02ef3f8f8b3cee5911218 [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";
42 private static final int DATABASE_VERSION = 10;
43
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
91 "_sync_id TEXT," + // From the sync source
92 "_sync_time TEXT," + // From the sync source
93 "_sync_version TEXT," + // From the sync source
94 "_sync_local_id INTEGER," + // Used while syncing,
95 // never stored persistently
96 "_sync_dirty INTEGER," + // if syncable, set if the record
97 // has local, unsynced, changes
98 "_sync_mark INTEGER," + // Used to filter out new rows
99 "feed TEXT," +
100 "authority TEXT," +
101 "service TEXT" +
102 ");");
103
104 // Trigger to completely remove feeds data when they're deleted
105 db.execSQL("CREATE TRIGGER feed_cleanup DELETE ON feeds " +
106 "WHEN old._sync_id is not null " +
107 "BEGIN " +
108 "INSERT INTO _deleted_feeds " +
109 "(_sync_id, _sync_account, _sync_version) " +
110 "VALUES (old._sync_id, old._sync_account, " +
111 "old._sync_version);" +
112 "END");
113
114 db.execSQL("CREATE TABLE _deleted_feeds (" +
115 "_sync_version TEXT," + // From the sync source
116 "_sync_id TEXT," +
117 (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
118 "_sync_account TEXT," +
119 "_sync_mark INTEGER, " + // Used to filter out new rows
120 "UNIQUE(_sync_id))");
121 }
122
123 @Override
124 protected void onDatabaseOpened(SQLiteDatabase db) {
125 db.markTableSyncable("feeds", "_deleted_feeds");
126 }
127
128 @Override
129 protected Iterable<FeedMerger> getMergers() {
130 return Collections.singletonList(new FeedMerger());
131 }
132
133 @Override
134 public String getType(Uri url) {
135 int match = sURLMatcher.match(url);
136 switch (match) {
137 case FEEDS:
138 return SubscribedFeeds.Feeds.CONTENT_TYPE;
139 case FEED_ID:
140 return SubscribedFeeds.Feeds.CONTENT_ITEM_TYPE;
141 default:
142 throw new IllegalArgumentException("Unknown URL");
143 }
144 }
145
146 @Override
147 public Cursor queryInternal(Uri url, String[] projection,
148 String selection, String[] selectionArgs, String sortOrder) {
149 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
150
151
152 // Generate the body of the query
153 int match = sURLMatcher.match(url);
154
155 if (Config.LOGV) Log.v(TAG, "SubscribedFeedsProvider.query: url=" +
156 url + ", match is " + match);
157
158 switch (match) {
159 case FEEDS:
160 qb.setTables(sFeedsTable);
161 break;
162 case DELETED_FEEDS:
163 if (!isTemporary()) {
164 throw new UnsupportedOperationException();
165 }
166 qb.setTables(sDeletedFeedsTable);
167 break;
168 case ACCOUNTS:
169 qb.setTables(sFeedsTable);
170 qb.setDistinct(true);
171 qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP);
172 return qb.query(getDatabase(), projection, selection, selectionArgs,
173 SubscribedFeeds.Feeds._SYNC_ACCOUNT, null, sortOrder);
174 case FEED_ID:
175 qb.setTables(sFeedsTable);
176 qb.appendWhere(sFeedsTable + "._id=");
177 qb.appendWhere(url.getPathSegments().get(1));
178 break;
179 default:
180 throw new IllegalArgumentException("Unknown URL " + url);
181 }
182
183 // run the query
184 return qb.query(getDatabase(), projection, selection, selectionArgs,
185 null, null, sortOrder);
186 }
187
188 @Override
189 public Uri insertInternal(Uri url, ContentValues initialValues) {
190 final SQLiteDatabase db = getDatabase();
191 Uri resultUri = null;
192 long rowID;
193
194 int match = sURLMatcher.match(url);
195 switch (match) {
196 case FEEDS:
197 ContentValues values = new ContentValues(initialValues);
198 values.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
199 rowID = db.insert(sFeedsTable, "feed", values);
200 if (rowID > 0) {
201 resultUri = Uri.parse(
202 "content://subscribedfeeds/feeds/" + rowID);
203 }
204 break;
205
206 case DELETED_FEEDS:
207 if (!isTemporary()) {
208 throw new UnsupportedOperationException();
209 }
210 rowID = db.insert(sDeletedFeedsTable, "_sync_id",
211 initialValues);
212 if (rowID > 0) {
213 resultUri = Uri.parse(
214 "content://subscribedfeeds/deleted_feeds/" + rowID);
215 }
216 break;
217
218 default:
219 throw new UnsupportedOperationException(
220 "Cannot insert into URL: " + url);
221 }
222
223 return resultUri;
224 }
225
226 @Override
227 public int deleteInternal(Uri url, String userWhere, String[] whereArgs) {
228 final SQLiteDatabase db = getDatabase();
229 String changedItemId;
230
231 switch (sURLMatcher.match(url)) {
232 case FEEDS:
233 changedItemId = null;
234 break;
235 case FEED_ID:
236 changedItemId = url.getPathSegments().get(1);
237 break;
238 default:
239 throw new UnsupportedOperationException(
240 "Cannot delete that URL: " + url);
241 }
242
243 String where = addIdToWhereClause(changedItemId, userWhere);
244 return db.delete(sFeedsTable, where, whereArgs);
245 }
246
247 @Override
248 public int updateInternal(Uri url, ContentValues initialValues,
249 String userWhere, String[] whereArgs) {
250 final SQLiteDatabase db = getDatabase();
251 ContentValues values = new ContentValues(initialValues);
252 values.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
253
254 String changedItemId;
255 switch (sURLMatcher.match(url)) {
256 case FEEDS:
257 changedItemId = null;
258 break;
259
260 case FEED_ID:
261 changedItemId = url.getPathSegments().get(1);
262 break;
263
264 default:
265 throw new UnsupportedOperationException(
266 "Cannot update URL: " + url);
267 }
268
269 String where = addIdToWhereClause(changedItemId, userWhere);
270 return db.update(sFeedsTable, values, where, whereArgs);
271 }
272
273 private static String addIdToWhereClause(String id, String where) {
274 if (id != null) {
275 StringBuilder whereSb = new StringBuilder("_id=");
276 whereSb.append(id);
277 if (!TextUtils.isEmpty(where)) {
278 whereSb.append(" AND (");
279 whereSb.append(where);
280 whereSb.append(')');
281 }
282 return whereSb.toString();
283 } else {
284 return where;
285 }
286 }
287
288 private class FeedMerger extends AbstractTableMerger {
289 private ContentValues mValues = new ContentValues();
290 FeedMerger() {
291 super(getDatabase(), sFeedsTable, sFeedsUrl, sDeletedFeedsTable, sDeletedFeedsUrl);
292 }
293
294 @Override
295 protected void notifyChanges() {
296 getContext().getContentResolver().notifyChange(
297 sFeedsUrl, null /* data change observer */,
298 false /* do not sync to network */);
299 }
300
301 @Override
302 public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
303 final SQLiteDatabase db = getDatabase();
304 // We don't ever want to add entries from the server, instead
305 // we want to tell the server to delete any entries we receive
306 // from the server that aren't already known by the client.
307 mValues.clear();
308 DatabaseUtils.cursorStringToContentValues(diffsCursor,
309 SubscribedFeeds.Feeds._SYNC_ID, mValues);
310 DatabaseUtils.cursorStringToContentValues(diffsCursor,
311 SubscribedFeeds.Feeds._SYNC_ACCOUNT, mValues);
312 DatabaseUtils.cursorStringToContentValues(diffsCursor,
313 SubscribedFeeds.Feeds._SYNC_VERSION, mValues);
314 db.replace(mDeletedTable, SubscribedFeeds.Feeds._SYNC_ID, mValues);
315 }
316
317 @Override
318 public void updateRow(long localPersonID, ContentProvider diffs,
319 Cursor diffsCursor) {
320 updateOrResolveRow(localPersonID, null, diffs, diffsCursor, false);
321 }
322
323 @Override
324 public void resolveRow(long localPersonID, String syncID,
325 ContentProvider diffs, Cursor diffsCursor) {
326 updateOrResolveRow(localPersonID, syncID, diffs, diffsCursor, true);
327 }
328
329 protected void updateOrResolveRow(long localPersonID, String syncID,
330 ContentProvider diffs, Cursor diffsCursor, boolean conflicts) {
331 mValues.clear();
332 // only copy over the fields that the server owns
333 DatabaseUtils.cursorStringToContentValues(diffsCursor,
334 SubscribedFeeds.Feeds._SYNC_ID, mValues);
335 DatabaseUtils.cursorStringToContentValues(diffsCursor,
336 SubscribedFeeds.Feeds._SYNC_TIME, mValues);
337 DatabaseUtils.cursorStringToContentValues(diffsCursor,
338 SubscribedFeeds.Feeds._SYNC_VERSION, mValues);
339 mValues.put(SubscribedFeeds.Feeds._SYNC_DIRTY, conflicts ? 1 : 0);
340 final SQLiteDatabase db = getDatabase();
341 db.update(mTable, mValues,
342 SubscribedFeeds.Feeds._ID + '=' + localPersonID, null);
343 }
344
345 @Override
346 public void deleteRow(Cursor localCursor) {
347 // Since the client is the authority we don't actually delete
348 // the row when the server says it has been deleted. Instead
349 // we break the association with the server by clearing out
350 // the id, time, and version, then we mark it dirty so that
351 // it will be synced back to the server.
352 long localPersonId = localCursor.getLong(localCursor.getColumnIndex(
353 SubscribedFeeds.Feeds._ID));
354 mValues.clear();
355 mValues.put(SubscribedFeeds.Feeds._SYNC_DIRTY, 1);
356 mValues.put(SubscribedFeeds.Feeds._SYNC_ID, (String) null);
357 mValues.put(SubscribedFeeds.Feeds._SYNC_TIME, (Long) null);
358 mValues.put(SubscribedFeeds.Feeds._SYNC_VERSION, (String) null);
359 final SQLiteDatabase db = getDatabase();
360 db.update(mTable, mValues, SubscribedFeeds.Feeds._ID + '=' + localPersonId, null);
361 localCursor.moveToNext();
362 }
363 }
364
365 static {
366 Map<String, String> map;
367
368 map = new HashMap<String, String>();
369 ACCOUNTS_PROJECTION_MAP = map;
370 map.put(SubscribedFeeds.Accounts._COUNT, "COUNT(*) AS _count");
371 map.put(SubscribedFeeds.Accounts._SYNC_ACCOUNT, SubscribedFeeds.Accounts._SYNC_ACCOUNT);
372 }
373}