OSDN Git Service

Initial commit of 3D gallery source code to project.
[android-x86/packages-apps-Gallery2.git] / src / com / cooliris / picasa / PicasaContentProvider.java
1 package com.cooliris.picasa;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.List;
6
7 import android.accounts.Account;
8 import android.accounts.AccountManager;
9 import android.content.ContentResolver;
10 import android.content.Context;
11 import android.content.SyncResult;
12 import android.content.pm.ProviderInfo;
13 import android.database.Cursor;
14 import android.database.sqlite.SQLiteDatabase;
15 import android.database.sqlite.SQLiteOpenHelper;
16 import android.net.Uri;
17 import android.util.Log;
18
19 public final class PicasaContentProvider extends TableContentProvider {
20     public static final String AUTHORITY = "com.cooliris.picasa.contentprovider";
21     public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
22     public static final Uri PHOTOS_URI = Uri.withAppendedPath(BASE_URI, "photos");
23     public static final Uri ALBUMS_URI = Uri.withAppendedPath(BASE_URI, "albums");
24
25     private static final String TAG = "PicasaContentProvider";
26     private static final String[] ID_EDITED_PROJECTION = { "_id", "date_edited" };
27     private static final String[] ID_EDITED_INDEX_PROJECTION = { "_id", "date_edited", "display_index" };
28     private static final String WHERE_ACCOUNT = "sync_account=?";
29     private static final String WHERE_ALBUM_ID = "album_id=?";
30
31     private final PhotoEntry mPhotoInstance = new PhotoEntry();
32     private final AlbumEntry mAlbumInstance = new AlbumEntry();
33     private SyncContext mSyncContext = null;
34     private Account mActiveAccount;
35     
36     @Override
37     public void attachInfo(Context context, ProviderInfo info) {
38         // Initialize the provider and set the database.
39         super.attachInfo(context, info);
40         setDatabase(new Database(context));
41
42         // Add mappings for each of the exposed tables.
43         addMapping(AUTHORITY, "photos", "vnd.cooliris.picasa.photo", PhotoEntry.SCHEMA);
44         addMapping(AUTHORITY, "albums", "vnd.cooliris.picasa.album", AlbumEntry.SCHEMA);
45
46         // Create the sync context.
47         mSyncContext = new SyncContext();
48     }
49
50     public static final class Database extends SQLiteOpenHelper {
51         public static final String DATABASE_NAME = "picasa.db";
52         public static final int DATABASE_VERSION = 83;
53         public Database(Context context) {
54             super(context, DATABASE_NAME, null, DATABASE_VERSION);
55         }
56
57         @Override
58         public void onCreate(SQLiteDatabase db) {
59             PhotoEntry.SCHEMA.createTables(db);
60             AlbumEntry.SCHEMA.createTables(db);
61             UserEntry.SCHEMA.createTables(db);
62         }
63
64         @Override
65         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
66             // No new versions yet, if we are asked to upgrade we just reset everything.
67             PhotoEntry.SCHEMA.dropTables(db);
68             AlbumEntry.SCHEMA.dropTables(db);
69             UserEntry.SCHEMA.dropTables(db);
70             onCreate(db);
71         }
72     }
73
74     @Override
75     public int delete(Uri uri, String selection, String[] selectionArgs) {
76         // Ensure that the URI is well-formed. We currently do not allow WHERE clauses.
77         List<String> path = uri.getPathSegments();
78         if (path.size() != 2 || !uri.getAuthority().equals(AUTHORITY) || selection != null) {
79             return 0;
80         }
81
82         // Get the sync context.
83         SyncContext context = mSyncContext;
84
85         // Determine if the URI refers to an album or photo.
86         String type = path.get(0);
87         long id = Long.parseLong(path.get(1));
88         SQLiteDatabase db = context.db;
89         if (type.equals("photos")) {
90             // Retrieve the photo from the database to get the edit URI.
91             PhotoEntry photo = mPhotoInstance;
92             if (PhotoEntry.SCHEMA.queryWithId(db, id, photo)) {
93                 // Send a DELETE request to the API.
94                 if (context.login(photo.syncAccount)) {
95                     if (context.api.deleteEntry(photo.editUri) == PicasaApi.RESULT_OK) {
96                         deletePhoto(db, id);
97                         context.photosChanged = true;
98                         return 1;
99                     }
100                 }
101             }
102         } else if (type.equals("albums")) {
103             // Retrieve the album from the database to get the edit URI.
104             AlbumEntry album = mAlbumInstance;
105             if (AlbumEntry.SCHEMA.queryWithId(db, id, album)) {
106                 // Send a DELETE request to the API.
107                 if (context.login(album.syncAccount)) {
108                     if (context.api.deleteEntry(album.editUri) == PicasaApi.RESULT_OK) {
109                         deleteAlbum(db, id);
110                         context.albumsChanged = true;
111                         return 1;
112                     }
113                 }
114             }
115         }
116         context.finish();
117         return 0;
118     }
119     
120     public void reloadAccounts() {
121         mSyncContext.reloadAccounts();
122     }
123  
124     public void setActiveSyncAccount(Account account) {
125         mActiveAccount = account;
126     }
127     
128     public void syncUsers(SyncResult syncResult) {
129         syncUsers(mSyncContext, syncResult);
130     }
131
132     public void syncUsersAndAlbums(final boolean syncAlbumPhotos, SyncResult syncResult) {
133         SyncContext context = mSyncContext;
134         
135         // Synchronize users authenticated on the device.
136         UserEntry[] users = syncUsers(context, syncResult);
137         
138         // Synchronize albums for each user.
139         String activeUsername = null;
140         if (mActiveAccount != null) {
141             String username = mActiveAccount.name;
142             if (username.contains("@gmail.")) {
143                 username = username.substring(0, username.indexOf('@'));
144             }
145             activeUsername = username;
146         }
147         boolean didSyncActiveUserName = false;
148         for (int i = 0, numUsers = users.length; i != numUsers; ++i) {
149             if (activeUsername != null && !context.accounts[i].user.equals(activeUsername))
150                 continue;
151             if (!ContentResolver.getSyncAutomatically(context.accounts[i].account, AUTHORITY)) continue;
152             didSyncActiveUserName = true;
153             context.api.setAuth(context.accounts[i]);
154             syncUserAlbums(context, users[i], syncResult);
155             if (syncAlbumPhotos) {
156                 syncUserPhotos(context, users[i].account, syncResult);
157             } else {
158                 // // Always sync added albums.
159                 // for (Long albumId : context.albumsAdded) {
160                 // syncAlbumPhotos(albumId, false);
161                 // }
162             }
163         }
164         if (!didSyncActiveUserName) {
165             ++syncResult.stats.numAuthExceptions;
166         }
167         context.finish();
168     }
169
170     public void syncAlbumPhotos(final long albumId, final boolean forceRefresh, SyncResult syncResult) {
171         SyncContext context = mSyncContext;
172         AlbumEntry album = new AlbumEntry();
173         if (AlbumEntry.SCHEMA.queryWithId(context.db, albumId, album)) {
174             if ((album.photosDirty || forceRefresh) && context.login(album.syncAccount)) {
175                 if (isSyncEnabled(album.syncAccount, context)) {
176                     syncAlbumPhotos(context, album.syncAccount, album, syncResult);
177                 }
178             }
179         }
180         context.finish();
181     }
182     
183     public static boolean isSyncEnabled(String accountName, SyncContext context) {
184         PicasaApi.AuthAccount[] accounts = context.accounts;
185         int numAccounts = accounts.length;
186         for (int i = 0; i < numAccounts; ++i) {
187             PicasaApi.AuthAccount account = accounts[i];
188             if (account.user.equals(accountName)) {
189                 return ContentResolver.getSyncAutomatically(account.account, AUTHORITY);
190             }
191         }
192         return true;
193     }
194
195     private UserEntry[] syncUsers(SyncContext context, SyncResult syncResult) {
196         Log.i(TAG, "syncUsers");
197
198         // Get authorized accounts.
199         context.reloadAccounts();
200         PicasaApi.AuthAccount[] accounts = context.accounts;
201         int numUsers = accounts.length;
202         UserEntry[] users = new UserEntry[numUsers];
203
204         // Scan existing accounts.
205         EntrySchema schema = UserEntry.SCHEMA;
206         SQLiteDatabase db = context.db;
207         Cursor cursor = schema.queryAll(db);
208         Log.i(TAG, "#users: " + cursor.getCount());
209         if (cursor.moveToFirst()) {
210             do {
211                 // Read the current account.
212                 UserEntry entry = new UserEntry();
213                 schema.cursorToObject(cursor, entry);
214
215                 // Find the corresponding account, or delete the row if it does not exist.
216                 int i;
217                 for (i = 0; i != numUsers; ++i) {
218                     Log.i(TAG, "Check " + accounts[i].user + " == " + entry.account);
219                     if (accounts[i].user.equals(entry.account)) {
220                         users[i] = entry;
221                         Log.e(TAG, "Updating user " + entry.account);
222                         break;
223                     }
224                 }
225                 if (i == numUsers) {
226                     Log.e(TAG, "Deleting user " + entry.account);
227                     entry.albumsEtag = null;
228                     deleteUser(db, entry.account);
229                 }
230             } while (cursor.moveToNext());
231         } else {
232             // Log.i(TAG, "No users in database yet");
233         }
234         cursor.close();
235
236         // Add new accounts and synchronize user albums if recursive.
237         for (int i = 0; i != numUsers; ++i) {
238             UserEntry entry = users[i];
239             PicasaApi.AuthAccount account = accounts[i];
240             if (entry == null) {
241                 entry = new UserEntry();
242                 entry.account = account.user;
243                 Log.d(TAG, "insert/replace: " + schema.insertOrReplace(db, entry));
244                 users[i] = entry;
245                 Log.e(TAG, "Inserting user " + entry.account);
246             }
247         }
248         return users;
249     }
250
251     private void syncUserAlbums(final SyncContext context, final UserEntry user, final SyncResult syncResult) {
252         // Query existing album entry (id, dateEdited) sorted by ID.
253         final SQLiteDatabase db = context.db;
254         Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), ID_EDITED_PROJECTION, WHERE_ACCOUNT,
255                 new String[] { user.account }, null, null, AlbumEntry.Columns.DATE_EDITED);
256         int localCount = cursor.getCount();
257
258         // Build a sorted index with existing entry timestamps.
259         final EntryMetadata local[] = new EntryMetadata[localCount];
260         for (int i = 0; i != localCount; ++i) {
261             cursor.moveToPosition(i); // TODO: throw exception here if returns false?
262             local[i] = new EntryMetadata(cursor.getLong(0), cursor.getLong(1), 0);
263         }
264         cursor.close();
265         Arrays.sort(local);
266
267         // Merge the truth from the API into the local database.
268         final EntrySchema albumSchema = AlbumEntry.SCHEMA;
269         final EntryMetadata key = new EntryMetadata();
270         final AccountManager accountManager = AccountManager.get(getContext());
271         int result = context.api.getAlbums(accountManager, syncResult, user, new GDataParser.EntryHandler() {
272             public void handleEntry(Entry entry) {
273                 AlbumEntry album = (AlbumEntry) entry;
274                 long albumId = album.id;
275                 key.id = albumId;
276                 int index = Arrays.binarySearch(local, key);
277                 EntryMetadata metadata = index >= 0 ? local[index] : null;
278                 if (metadata == null || metadata.dateEdited < album.dateEdited) {
279                     // Insert / update.
280                     Log.i(TAG, "insert / update album " + album.title);
281                     album.syncAccount = user.account;
282                     album.photosDirty = true;
283                     albumSchema.insertOrReplace(db, album);
284                     if (metadata == null) {
285                         context.albumsAdded.add(albumId);
286                     }
287                     ++syncResult.stats.numUpdates;
288                 } else {
289                     // Up-to-date.
290                     // Log.i(TAG, "up-to-date album " + album.title);
291                 }
292
293                 // Mark item as surviving so it is not deleted.
294                 if (metadata != null) {
295                     metadata.survived = true;
296                 }
297             }
298         });
299
300         // Return if not modified or on error.
301         switch (result) {
302         case PicasaApi.RESULT_ERROR:
303             ++syncResult.stats.numParseExceptions;
304         case PicasaApi.RESULT_NOT_MODIFIED:
305             return;
306         }
307
308         // Update the user entry with the new ETag.
309         UserEntry.SCHEMA.insertOrReplace(db, user);
310
311         // Delete all entries not present in the API response.
312         for (int i = 0; i != localCount; ++i) {
313             EntryMetadata metadata = local[i];
314             if (!metadata.survived) {
315                 deleteAlbum(db, metadata.id);
316                 ++syncResult.stats.numDeletes;
317                 Log.i(TAG, "delete album " + metadata.id);
318             }
319         }
320
321         // Note that albums changed.
322         context.albumsChanged = true;
323     }
324
325     private void syncUserPhotos(SyncContext context, String account, SyncResult syncResult) {
326         // Synchronize albums with out-of-date photos.
327         SQLiteDatabase db = context.db;
328         Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), Entry.ID_PROJECTION, "sync_account=? AND photos_dirty=1",
329                 new String[] { account }, null, null, null);
330         AlbumEntry album = new AlbumEntry();
331         for (int i = 0, count = cursor.getCount(); i != count; ++i) {
332             cursor.moveToPosition(i);
333             if (AlbumEntry.SCHEMA.queryWithId(db, cursor.getLong(0), album)) {
334                 syncAlbumPhotos(context, account, album, syncResult);
335             }
336             
337             // Abort if interrupted.
338             if (Thread.interrupted()) {
339                 ++syncResult.stats.numIoExceptions;
340                 Log.e(TAG, "syncUserPhotos interrupted");
341             }
342         }
343         cursor.close();
344     }
345
346     private void syncAlbumPhotos(SyncContext context, final String account, AlbumEntry album, final SyncResult syncResult) {
347         Log.i(TAG, "Syncing Picasa album: " + album.title);
348         
349         // Query existing album entry (id, dateEdited) sorted by ID.
350         final SQLiteDatabase db = context.db;
351         long albumId = album.id;
352         String[] albumIdArgs = { Long.toString(albumId) };
353         Cursor cursor = db.query(PhotoEntry.SCHEMA.getTableName(), ID_EDITED_INDEX_PROJECTION, WHERE_ALBUM_ID, albumIdArgs, null,
354                 null, "date_edited");
355         int localCount = cursor.getCount();
356
357         // Build a sorted index with existing entry timestamps and display indexes.
358         final EntryMetadata local[] = new EntryMetadata[localCount];
359         final EntryMetadata key = new EntryMetadata();
360         for (int i = 0; i != localCount; ++i) {
361             cursor.moveToPosition(i); // TODO: throw exception here if returns false?
362             local[i] = new EntryMetadata(cursor.getLong(0), cursor.getLong(1), cursor.getInt(2));
363         }
364         cursor.close();
365         Arrays.sort(local);
366
367         // Merge the truth from the API into the local database.
368         final EntrySchema photoSchema = PhotoEntry.SCHEMA;
369         final int[] displayIndex = { 0 };
370         final AccountManager accountManager = AccountManager.get(getContext());
371         int result = context.api.getAlbumPhotos(accountManager, syncResult, album, new GDataParser.EntryHandler() {
372             public void handleEntry(Entry entry) {
373                 PhotoEntry photo = (PhotoEntry) entry;
374                 long photoId = photo.id;
375                 int newDisplayIndex = displayIndex[0];
376                 key.id = photoId;
377                 int index = Arrays.binarySearch(local, key);
378                 EntryMetadata metadata = index >= 0 ? local[index] : null;
379                 if (metadata == null || metadata.dateEdited < photo.dateEdited || metadata.displayIndex != newDisplayIndex) {
380
381                     // Insert / update.
382                     // Log.i(TAG, "insert / update photo " + photo.title);
383                     photo.syncAccount = account;
384                     photo.displayIndex = newDisplayIndex;
385                     photoSchema.insertOrReplace(db, photo);
386                     ++syncResult.stats.numUpdates;
387                 } else {
388                     // Up-to-date.
389                     // Log.i(TAG, "up-to-date photo " + photo.title);
390                 }
391
392                 // Mark item as surviving so it is not deleted.
393                 if (metadata != null) {
394                     metadata.survived = true;
395                 }
396
397                 // Increment the display index.
398                 displayIndex[0] = newDisplayIndex + 1;
399             }
400         });
401
402         // Return if not modified or on error.
403         switch (result) {
404         case PicasaApi.RESULT_ERROR:
405             ++syncResult.stats.numParseExceptions;
406             Log.e(TAG, "syncAlbumPhotos error");
407         case PicasaApi.RESULT_NOT_MODIFIED:
408             // Log.e(TAG, "result not modified");
409             return;
410         }
411
412         // Delete all entries not present in the API response.
413         for (int i = 0; i != localCount; ++i) {
414             EntryMetadata metadata = local[i];
415             if (!metadata.survived) {
416                 deletePhoto(db, metadata.id);
417                 ++syncResult.stats.numDeletes;
418                 // Log.i(TAG, "delete photo " + metadata.id);
419             }
420         }
421
422         // Mark album as no longer dirty and store the new ETag.
423         album.photosDirty = false;
424         AlbumEntry.SCHEMA.insertOrReplace(db, album);
425         // Log.i(TAG, "Clearing dirty bit on album " + albumId);
426
427         // Mark that photos changed.
428         // context.photosChanged = true;
429         getContext().getContentResolver().notifyChange(ALBUMS_URI, null);
430         getContext().getContentResolver().notifyChange(PHOTOS_URI, null);
431     }
432
433     private void deleteUser(SQLiteDatabase db, String account) {
434         Log.w(TAG, "deleteUser(" + account + ")");
435
436         // Select albums owned by the user.
437         String albumTableName = AlbumEntry.SCHEMA.getTableName();
438         String[] whereArgs = { account };
439         Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), Entry.ID_PROJECTION, WHERE_ACCOUNT, whereArgs, null, null, null);
440
441         // Delete contained photos for each album.
442         if (cursor.moveToFirst()) {
443             do {
444                 deleteAlbumPhotos(db, cursor.getLong(0));
445             } while (cursor.moveToNext());
446         }
447         cursor.close();
448
449         // Delete all albums.
450         db.delete(albumTableName, WHERE_ACCOUNT, whereArgs);
451         
452         // Delete the user entry.
453         db.delete(UserEntry.SCHEMA.getTableName(), "account=?", whereArgs);
454     }
455
456     private void deleteAlbum(SQLiteDatabase db, long albumId) {
457         // Delete contained photos.
458         deleteAlbumPhotos(db, albumId);
459
460         // Delete the album.
461         AlbumEntry.SCHEMA.deleteWithId(db, albumId);
462     }
463
464     private void deleteAlbumPhotos(SQLiteDatabase db, long albumId) {
465         Log.w(TAG, "deleteAlbumPhotos(" + albumId + ")");
466         String photoTableName = PhotoEntry.SCHEMA.getTableName();
467         String[] whereArgs = { Long.toString(albumId) };
468         Cursor cursor = db.query(photoTableName, Entry.ID_PROJECTION, WHERE_ALBUM_ID, whereArgs, null, null, null);
469
470         // Delete cache entry for each photo.
471         if (cursor.moveToFirst()) {
472             do {
473                 deletePhotoCache(cursor.getLong(0));
474             } while (cursor.moveToNext());
475         }
476         cursor.close();
477
478         // Delete all photos.
479         db.delete(photoTableName, WHERE_ALBUM_ID, whereArgs);
480     }
481
482     private void deletePhoto(SQLiteDatabase db, long photoId) {
483         PhotoEntry.SCHEMA.deleteWithId(db, photoId);
484         deletePhotoCache(photoId);
485     }
486
487     private void deletePhotoCache(long photoId) {
488         // TODO
489         Log.w(TAG, "deletePhotoCache(" + photoId + ")");
490     }
491
492     private final class SyncContext {
493         // List of all authenticated user accounts.
494         public PicasaApi.AuthAccount[] accounts;
495
496         // A connection to the Picasa API for a specific user account. Initially null.
497         public PicasaApi api = new PicasaApi();
498
499         // A handle to the Picasa databse.
500         public SQLiteDatabase db;
501
502         // List of album IDs that were added during the sync.
503         public final ArrayList<Long> albumsAdded = new ArrayList<Long>();
504
505         // Set to true if albums were changed.
506         public boolean albumsChanged = false;
507
508         // Set to true if photos were changed.
509         public boolean photosChanged = false;
510
511         public SyncContext() {
512             db = mDatabase.getWritableDatabase();
513             reloadAccounts();
514         }
515         
516         public void reloadAccounts() {
517                 accounts = PicasaApi.getAuthenticatedAccounts(getContext());
518         }
519         
520         public void finish() {
521             // Send notifications if needed and reset state.
522             ContentResolver cr = getContext().getContentResolver();
523             if (albumsChanged) {
524                 cr.notifyChange(ALBUMS_URI, null);
525             }
526             if (photosChanged) {
527                 cr.notifyChange(PHOTOS_URI, null);
528             }
529             albumsChanged = false;
530             photosChanged = false;
531         }
532
533         public boolean login(String user) {
534             for (PicasaApi.AuthAccount auth : accounts) {
535                 if (auth.user.equals(user)) {
536                     api.setAuth(auth);
537                     return true;
538                 }
539             }
540             return false;
541         }
542     }
543
544     /**
545      * Minimal metadata gathered during sync.
546      */
547     private static final class EntryMetadata implements Comparable<EntryMetadata> {
548         public long id;
549         public long dateEdited;
550         public int displayIndex;
551         public boolean survived = false;
552
553         public EntryMetadata() {
554         }
555
556         public EntryMetadata(long id, long dateEdited, int displayIndex) {
557             this.id = id;
558             this.dateEdited = dateEdited;
559             this.displayIndex = displayIndex;
560         }
561
562         public int compareTo(EntryMetadata other) {
563             return Long.signum(id - other.id);
564         }
565
566     }
567 }