Use Tree Navigation
public static final class

PicasaContentProvider.Database

extends SQLiteOpenHelper
/*
 * Copyright (C) 2009 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.cooliris.picasa;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.net.Uri;
import android.util.Log;

public final class PicasaContentProvider extends TableContentProvider {
   
public static final String AUTHORITY = "com.cooliris.picasa.contentprovider";
   
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
   
public static final Uri PHOTOS_URI = Uri.withAppendedPath(BASE_URI, "photos");
   
public static final Uri ALBUMS_URI = Uri.withAppendedPath(BASE_URI, "albums");

   
private static final String TAG = "PicasaContentProvider";
   
private static final String[] ID_EDITED_PROJECTION = { "_id", "date_edited" };
   
private static final String[] ID_EDITED_INDEX_PROJECTION = { "_id", "date_edited", "display_index" };
   
private static final String WHERE_ACCOUNT = "sync_account=?";
   
private static final String WHERE_ALBUM_ID = "album_id=?";

   
private final PhotoEntry mPhotoInstance = new PhotoEntry();
   
private final AlbumEntry mAlbumInstance = new AlbumEntry();
   
private SyncContext mSyncContext = null;
   
private Account mActiveAccount;

   
@Override
   
public void attachInfo(Context context, ProviderInfo info) {
       
// Initialize the provider and set the database.
       
super.attachInfo(context, info);
        setDatabase
(new Database(context, Database.DATABASE_NAME));

       
// Add mappings for each of the exposed tables.
        addMapping
(AUTHORITY, "photos", "vnd.cooliris.picasa.photo", PhotoEntry.SCHEMA);
        addMapping
(AUTHORITY, "albums", "vnd.cooliris.picasa.album", AlbumEntry.SCHEMA);

       
// Create the sync context.
       
try {
            mSyncContext
= new SyncContext();
       
} catch (Exception e) {
           
// The database wasn't created successfully, we create a memory backed database.
            setDatabase
(new Database(context, null));
       
}
   
}

   
public static final class Database extends SQLiteOpenHelper {
       
public static final String DATABASE_NAME = "picasa.db";
       
public static final int DATABASE_VERSION = 83;

       
public Database(Context context, String name) {
           
super(context, name, null, DATABASE_VERSION);
       
}

       
@Override
       
public void onCreate(SQLiteDatabase db) {
           
PhotoEntry.SCHEMA.createTables(db);
           
AlbumEntry.SCHEMA.createTables(db);
           
UserEntry.SCHEMA.createTables(db);
       
}

       
@Override
       
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
           
// No new versions yet, if we are asked to upgrade we just reset
           
// everything.
           
PhotoEntry.SCHEMA.dropTables(db);
           
AlbumEntry.SCHEMA.dropTables(db);
           
UserEntry.SCHEMA.dropTables(db);
            onCreate
(db);
       
}
   
}

   
@Override
   
public int delete(Uri uri, String selection, String[] selectionArgs) {
       
// Ensure that the URI is well-formed. We currently do not allow WHERE
       
// clauses.
       
List<String> path = uri.getPathSegments();
       
if (path.size() != 2 || !uri.getAuthority().equals(AUTHORITY) || selection != null) {
           
return 0;
       
}

       
// Get the sync context.
       
SyncContext context = mSyncContext;

       
// Determine if the URI refers to an album or photo.
       
String type = path.get(0);
       
long id = Long.parseLong(path.get(1));
       
SQLiteDatabase db = context.db;
       
if (type.equals("photos")) {
           
// Retrieve the photo from the database to get the edit URI.
           
PhotoEntry photo = mPhotoInstance;
           
if (PhotoEntry.SCHEMA.queryWithId(db, id, photo)) {
               
// Send a DELETE request to the API.
               
if (context.login(photo.syncAccount)) {
                   
if (context.api.deleteEntry(photo.editUri) == PicasaApi.RESULT_OK) {
                        deletePhoto
(db, id);
                        context
.photosChanged = true;
                       
return 1;
                   
}
               
}
           
}
       
} else if (type.equals("albums")) {
           
// Retrieve the album from the database to get the edit URI.
           
AlbumEntry album = mAlbumInstance;
           
if (AlbumEntry.SCHEMA.queryWithId(db, id, album)) {
               
// Send a DELETE request to the API.
               
if (context.login(album.syncAccount)) {
                   
if (context.api.deleteEntry(album.editUri) == PicasaApi.RESULT_OK) {
                        deleteAlbum
(db, id);
                        context
.albumsChanged = true;
                       
return 1;
                   
}
               
}
           
}
       
}
        context
.finish();
       
return 0;
   
}

   
public void reloadAccounts() {
        mSyncContext
.reloadAccounts();
   
}

   
public void setActiveSyncAccount(Account account) {
        mActiveAccount
= account;
   
}

   
public void syncUsers(SyncResult syncResult) {
        syncUsers
(mSyncContext, syncResult);
   
}

   
public void syncUsersAndAlbums(final boolean syncAlbumPhotos, SyncResult syncResult) {
       
SyncContext context = mSyncContext;

       
// Synchronize users authenticated on the device.
       
UserEntry[] users = syncUsers(context, syncResult);

       
// Synchronize albums for each user.
       
String activeUsername = null;
       
if (mActiveAccount != null) {
            activeUsername
= PicasaApi.canonicalizeUsername(mActiveAccount.name);
       
}
       
boolean didSyncActiveUserName = false;
       
for (int i = 0, numUsers = users.length; i != numUsers; ++i) {
           
if (activeUsername != null && !context.accounts[i].user.equals(activeUsername))
               
continue;
           
if (!ContentResolver.getSyncAutomatically(context.accounts[i].account, AUTHORITY))
               
continue;
            didSyncActiveUserName
= true;
            context
.api.setAuth(context.accounts[i]);
            syncUserAlbums
(context, users[i], syncResult);
           
if (syncAlbumPhotos) {
                syncUserPhotos
(context, users[i].account, syncResult);
           
} else {
               
// // Always sync added albums.
               
// for (Long albumId : context.albumsAdded) {
               
// syncAlbumPhotos(albumId, false);
               
// }
           
}
       
}
       
if (!didSyncActiveUserName) {
           
++syncResult.stats.numAuthExceptions;
       
}
        context
.finish();
   
}

   
public void syncAlbumPhotos(final long albumId, final boolean forceRefresh, SyncResult syncResult) {
       
SyncContext context = mSyncContext;
       
AlbumEntry album = new AlbumEntry();
       
if (AlbumEntry.SCHEMA.queryWithId(context.db, albumId, album)) {
           
if ((album.photosDirty || forceRefresh) && context.login(album.syncAccount)) {
               
if (isSyncEnabled(album.syncAccount, context)) {
                    syncAlbumPhotos
(context, album.syncAccount, album, syncResult);
               
}
           
}
       
}
        context
.finish();
   
}

   
public static boolean isSyncEnabled(String accountName, SyncContext context) {
       
if (context.accounts == null) {
            context
.reloadAccounts();
       
}
       
PicasaApi.AuthAccount[] accounts = context.accounts;
       
int numAccounts = accounts.length;
       
for (int i = 0; i < numAccounts; ++i) {
           
PicasaApi.AuthAccount account = accounts[i];
           
if (account.user.equals(accountName)) {
               
return ContentResolver.getSyncAutomatically(account.account, AUTHORITY);
           
}
       
}
       
return true;
   
}

   
private UserEntry[] syncUsers(SyncContext context, SyncResult syncResult) {
       
// Get authorized accounts.
        context
.reloadAccounts();
       
PicasaApi.AuthAccount[] accounts = context.accounts;
       
int numUsers = accounts.length;
       
UserEntry[] users = new UserEntry[numUsers];

       
// Scan existing accounts.
       
EntrySchema schema = UserEntry.SCHEMA;
       
SQLiteDatabase db = context.db;
       
Cursor cursor = schema.queryAll(db);
       
if (cursor.moveToFirst()) {
           
do {
               
// Read the current account.
               
UserEntry entry = new UserEntry();
                schema
.cursorToObject(cursor, entry);

               
// Find the corresponding account, or delete the row if it does
               
// not exist.
               
int i;
               
for (i = 0; i != numUsers; ++i) {
                   
if (accounts[i].user.equals(entry.account)) {
                        users
[i] = entry;
                       
break;
                   
}
               
}
               
if (i == numUsers) {
                   
Log.e(TAG, "Deleting user " + entry.account);
                    entry
.albumsEtag = null;
                    deleteUser
(db, entry.account);
               
}
           
} while (cursor.moveToNext());
       
} else {
           
// Log.i(TAG, "No users in database yet");
       
}
        cursor
.close();

       
// Add new accounts and synchronize user albums if recursive.
       
for (int i = 0; i != numUsers; ++i) {
           
UserEntry entry = users[i];
           
PicasaApi.AuthAccount account = accounts[i];
           
if (entry == null) {
                entry
= new UserEntry();
                entry
.account = account.user;
                users
[i] = entry;
               
Log.e(TAG, "Inserting user " + entry.account);
           
}
       
}
       
return users;
   
}

   
private void syncUserAlbums(final SyncContext context, final UserEntry user, final SyncResult syncResult) {
       
// Query existing album entry (id, dateEdited) sorted by ID.
       
final SQLiteDatabase db = context.db;
       
Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), ID_EDITED_PROJECTION, WHERE_ACCOUNT,
               
new String[] { user.account }, null, null, AlbumEntry.Columns.DATE_EDITED);
       
int localCount = cursor.getCount();

       
// Build a sorted index with existing entry timestamps.
       
final EntryMetadata local[] = new EntryMetadata[localCount];
       
for (int i = 0; i != localCount; ++i) {
            cursor
.moveToPosition(i); // TODO: throw exception here if returns
                                     
// false?
           
local[i] = new EntryMetadata(cursor.getLong(0), cursor.getLong(1), 0);
       
}
        cursor
.close();
       
Arrays.sort(local);

       
// Merge the truth from the API into the local database.
       
final EntrySchema albumSchema = AlbumEntry.SCHEMA;
       
final EntryMetadata key = new EntryMetadata();
       
final AccountManager accountManager = AccountManager.get(getContext());
       
int result = context.api.getAlbums(accountManager, syncResult, user, new GDataParser.EntryHandler() {
           
public void handleEntry(Entry entry) {
               
AlbumEntry album = (AlbumEntry) entry;
               
long albumId = album.id;
                key
.id = albumId;
               
int index = Arrays.binarySearch(local, key);
               
EntryMetadata metadata = index >= 0 ? local[index] : null;
               
if (metadata == null || metadata.dateEdited < album.dateEdited) {
                   
// Insert / update.
                   
Log.i(TAG, "insert / update album " + album.title);
                    album
.syncAccount = user.account;
                    album
.photosDirty = true;
                    albumSchema
.insertOrReplace(db, album);
                   
if (metadata == null) {
                        context
.albumsAdded.add(albumId);
                   
}
                   
++syncResult.stats.numUpdates;
               
} else {
                   
// Up-to-date.
                   
// Log.i(TAG, "up-to-date album " + album.title);
               
}

               
// Mark item as surviving so it is not deleted.
               
if (metadata != null) {
                    metadata
.survived = true;
               
}
           
}
       
});

       
// Return if not modified or on error.
       
switch (result) {
       
case PicasaApi.RESULT_ERROR:
           
++syncResult.stats.numParseExceptions;
       
case PicasaApi.RESULT_NOT_MODIFIED:
           
return;
       
}

       
// Update the user entry with the new ETag.
       
UserEntry.SCHEMA.insertOrReplace(db, user);

       
// Delete all entries not present in the API response.
       
for (int i = 0; i != localCount; ++i) {
           
EntryMetadata metadata = local[i];
           
if (!metadata.survived) {
                deleteAlbum
(db, metadata.id);
               
++syncResult.stats.numDeletes;
               
Log.i(TAG, "delete album " + metadata.id);
           
}
       
}

       
// Note that albums changed.
        context
.albumsChanged = true;
   
}

   
private void syncUserPhotos(SyncContext context, String account, SyncResult syncResult) {
       
// Synchronize albums with out-of-date photos.
       
SQLiteDatabase db = context.db;
       
Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), Entry.ID_PROJECTION, "sync_account=? AND photos_dirty=1",
               
new String[] { account }, null, null, null);
       
AlbumEntry album = new AlbumEntry();
       
for (int i = 0, count = cursor.getCount(); i != count; ++i) {
            cursor
.moveToPosition(i);
           
if (AlbumEntry.SCHEMA.queryWithId(db, cursor.getLong(0), album)) {
                syncAlbumPhotos
(context, account, album, syncResult);
           
}

           
// Abort if interrupted.
           
if (Thread.interrupted()) {
               
++syncResult.stats.numIoExceptions;
               
Log.e(TAG, "syncUserPhotos interrupted");
           
}
       
}
        cursor
.close();
   
}

   
private void syncAlbumPhotos(SyncContext context, final String account, AlbumEntry album, final SyncResult syncResult) {
       
Log.i(TAG, "Syncing Picasa album: " + album.title);
       
// Query existing album entry (id, dateEdited) sorted by ID.
       
final SQLiteDatabase db = context.db;
       
long albumId = album.id;
       
String[] albumIdArgs = { Long.toString(albumId) };
       
Cursor cursor = db.query(PhotoEntry.SCHEMA.getTableName(), ID_EDITED_INDEX_PROJECTION, WHERE_ALBUM_ID, albumIdArgs, null,
               
null, "date_edited");
       
int localCount = cursor.getCount();

       
// Build a sorted index with existing entry timestamps and display
       
// indexes.
       
final EntryMetadata local[] = new EntryMetadata[localCount];
       
final EntryMetadata key = new EntryMetadata();
       
for (int i = 0; i != localCount; ++i) {
            cursor
.moveToPosition(i); // TODO: throw exception here if returns
                                     
// false?
           
local[i] = new EntryMetadata(cursor.getLong(0), cursor.getLong(1), cursor.getInt(2));
       
}
        cursor
.close();
       
Arrays.sort(local);

       
// Merge the truth from the API into the local database.
       
final EntrySchema photoSchema = PhotoEntry.SCHEMA;
       
final int[] displayIndex = { 0 };
       
final AccountManager accountManager = AccountManager.get(getContext());
       
int result = context.api.getAlbumPhotos(accountManager, syncResult, album, new GDataParser.EntryHandler() {
           
public void handleEntry(Entry entry) {
               
PhotoEntry photo = (PhotoEntry) entry;
               
long photoId = photo.id;
               
int newDisplayIndex = displayIndex[0];
                key
.id = photoId;
               
int index = Arrays.binarySearch(local, key);
               
EntryMetadata metadata = index >= 0 ? local[index] : null;
               
if (metadata == null || metadata.dateEdited < photo.dateEdited || metadata.displayIndex != newDisplayIndex) {

                   
// Insert / update.
                   
// Log.i(TAG, "insert / update photo " + photo.title);
                    photo
.syncAccount = account;
                    photo
.displayIndex = newDisplayIndex;
                    photoSchema
.insertOrReplace(db, photo);
                   
++syncResult.stats.numUpdates;
               
} else {
                   
// Up-to-date.
                   
// Log.i(TAG, "up-to-date photo " + photo.title);
               
}

               
// Mark item as surviving so it is not deleted.
               
if (metadata != null) {
                    metadata
.survived = true;
               
}

               
// Increment the display index.
                displayIndex
[0] = newDisplayIndex + 1;
           
}
       
});

       
// Return if not modified or on error.
       
switch (result) {
       
case PicasaApi.RESULT_ERROR:
           
++syncResult.stats.numParseExceptions;
           
Log.e(TAG, "syncAlbumPhotos error");
       
case PicasaApi.RESULT_NOT_MODIFIED:
           
// Log.e(TAG, "result not modified");
           
return;
       
}

       
// Delete all entries not present in the API response.
       
for (int i = 0; i != localCount; ++i) {
           
EntryMetadata metadata = local[i];
           
if (!metadata.survived) {
                deletePhoto
(db, metadata.id);
               
++syncResult.stats.numDeletes;
               
// Log.i(TAG, "delete photo " + metadata.id);
           
}
       
}

       
// Mark album as no longer dirty and store the new ETag.
        album
.photosDirty = false;
       
AlbumEntry.SCHEMA.insertOrReplace(db, album);
       
// Log.i(TAG, "Clearing dirty bit on album " + albumId);

       
// Mark that photos changed.
       
// context.photosChanged = true;
        getContext
().getContentResolver().notifyChange(ALBUMS_URI, null, false);
        getContext
().getContentResolver().notifyChange(PHOTOS_URI, null, false);
   
}

   
private void deleteUser(SQLiteDatabase db, String account) {
       
Log.w(TAG, "deleteUser(" + account + ")");

       
// Select albums owned by the user.
       
String albumTableName = AlbumEntry.SCHEMA.getTableName();
       
String[] whereArgs = { account };
       
Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), Entry.ID_PROJECTION, WHERE_ACCOUNT, whereArgs, null, null, null);

       
// Delete contained photos for each album.
       
if (cursor.moveToFirst()) {
           
do {
                deleteAlbumPhotos
(db, cursor.getLong(0));
           
} while (cursor.moveToNext());
       
}
        cursor
.close();

       
// Delete all albums.
        db
.delete(albumTableName, WHERE_ACCOUNT, whereArgs);

       
// Delete the user entry.
        db
.delete(UserEntry.SCHEMA.getTableName(), "account=?", whereArgs);
   
}

   
private void deleteAlbum(SQLiteDatabase db, long albumId) {
       
// Delete contained photos.
        deleteAlbumPhotos
(db, albumId);

       
// Delete the album.
       
AlbumEntry.SCHEMA.deleteWithId(db, albumId);
   
}

   
private void deleteAlbumPhotos(SQLiteDatabase db, long albumId) {
       
Log.v(TAG, "deleteAlbumPhotos(" + albumId + ")");
       
String photoTableName = PhotoEntry.SCHEMA.getTableName();
       
String[] whereArgs = { Long.toString(albumId) };
       
Cursor cursor = db.query(photoTableName, Entry.ID_PROJECTION, WHERE_ALBUM_ID, whereArgs, null, null, null);

       
// Delete cache entry for each photo.
       
if (cursor.moveToFirst()) {
           
do {
                deletePhotoCache
(cursor.getLong(0));
           
} while (cursor.moveToNext());
       
}
        cursor
.close();

       
// Delete all photos.
        db
.delete(photoTableName, WHERE_ALBUM_ID, whereArgs);
   
}

   
private void deletePhoto(SQLiteDatabase db, long photoId) {
       
PhotoEntry.SCHEMA.deleteWithId(db, photoId);
        deletePhotoCache
(photoId);
   
}

   
private void deletePhotoCache(long photoId) {
       
// TODO: implement it.
   
}

   
private final class SyncContext {
       
// List of all authenticated user accounts.
       
public PicasaApi.AuthAccount[] accounts;

       
// A connection to the Picasa API for a specific user account. Initially
       
// null.
       
public PicasaApi api = new PicasaApi(getContext().getContentResolver());

       
// A handle to the Picasa databse.
       
public SQLiteDatabase db;

       
// List of album IDs that were added during the sync.
       
public final ArrayList<Long> albumsAdded = new ArrayList<Long>();

       
// Set to true if albums were changed.
       
public boolean albumsChanged = false;

       
// Set to true if photos were changed.
       
public boolean photosChanged = false;

       
public SyncContext() {
            db
= mDatabase.getWritableDatabase();
       
}

       
public void reloadAccounts() {
            accounts
= PicasaApi.getAuthenticatedAccounts(getContext());
       
}

       
public void finish() {
           
// Send notifications if needed and reset state.
           
ContentResolver cr = getContext().getContentResolver();
           
if (albumsChanged) {
                cr
.notifyChange(ALBUMS_URI, null, false);
           
}
           
if (photosChanged) {
                cr
.notifyChange(PHOTOS_URI, null, false);
           
}
            albumsChanged
= false;
            photosChanged
= false;
       
}

       
public boolean login(String user) {
           
if (accounts == null) {
                reloadAccounts
();
           
}
           
final PicasaApi.AuthAccount[] authAccounts = accounts;
           
for (PicasaApi.AuthAccount auth : authAccounts) {
               
if (auth.user.equals(user)) {
                    api
.setAuth(auth);
                   
return true;
               
}
           
}
           
return false;
       
}
   
}

   
/**
     * Minimal metadata gathered during sync.
     */

   
private static final class EntryMetadata implements Comparable<EntryMetadata> {
       
public long id;
       
public long dateEdited;
       
public int displayIndex;
       
public boolean survived = false;

       
public EntryMetadata() {
       
}

       
public EntryMetadata(long id, long dateEdited, int displayIndex) {
           
this.id = id;
           
this.dateEdited = dateEdited;
           
this.displayIndex = displayIndex;
       
}

       
public int compareTo(EntryMetadata other) {
           
return Long.signum(id - other.id);
       
}

   
}
}