// // Copyright 2011 Thomas Gumprecht, Robert Jacob, Thomas Pieronczyk // // 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 net.sourcewalker.garanbot.data.sync; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import net.sourcewalker.garanbot.api.AuthenticationException; import net.sourcewalker.garanbot.api.ClientException; import net.sourcewalker.garanbot.api.GaranboClient; import net.sourcewalker.garanbot.api.Item; import net.sourcewalker.garanbot.data.GaranboItemsProvider; import net.sourcewalker.garanbot.data.GaranbotDBMetaData; import net.sourcewalker.garanbot.data.ImageCache; import net.sourcewalker.garanbot.data.LocalState; import android.accounts.Account; import android.accounts.AccountManager; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.SyncResult; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; /** * This class provides the sync adapter used to merge the local database with * the Garanbo server. * * @author Xperimental */ public class GaranboSyncAdapter extends AbstractThreadedSyncAdapter { public static final String BROADCAST_ACTION = "net.sourcewalker.garanbot.broadcast.sync"; public static final String EXTRA_RUNNING = "running"; private static final String TAG = "GaranboSyncAdapter"; private final AccountManager accountManager; public GaranboSyncAdapter(Context context) { super(context, true); this.accountManager = AccountManager.get(context); } /* * (non-Javadoc) * @see * android.content.AbstractThreadedSyncAdapter#onPerformSync(android.accounts * .Account, android.os.Bundle, java.lang.String, * android.content.ContentProviderClient, android.content.SyncResult) */ @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { Log.d(TAG, "Sync started."); sendStatusBroadcast(true); String username = account.name; String password = accountManager.getPassword(account); GaranboClient client = new GaranboClient(username, password); try { List<Integer> idList = client.item().list(); if (getDbCount(provider) == 0) { // First time synchronization, just save all items from server Log.d(TAG, "Nothing in DB. Simple copy..."); for (Integer id : idList) { Item item = client.item().get(id); provider.insert(GaranboItemsProvider.CONTENT_URI_ITEMS, item.toContentValues()); Log.d(TAG, "Saved item: " + id); syncResult.stats.numInserts++; } } else if (idList.size() == 0) { // Other simple solution (copy all to server) Log.d(TAG, "Only local content. Copy to server..."); List<Item> localItems = getAllDbItems(provider); for (Item item : localItems) { item.setServerId(Item.UNKNOWN_ID); int id = client.item().create(item); item.setServerId(id); provider.update(ContentUris.withAppendedId( GaranboItemsProvider.CONTENT_URI_ITEMS, item.getLocalId()), item.toContentValues(), null, null); Log.d(TAG, "Uploaded item: " + id); syncResult.stats.numInserts++; } } else { // More complicated sync... Log.d(TAG, "Both have content: have to sync..."); Map<Integer, List<Integer>> localIdMap = getLocalIdMap(provider); synchronizeItems(client, provider, idList, localIdMap, syncResult); } } catch (AuthenticationException e) { Log.e(TAG, "Authentication error."); syncResult.stats.numAuthExceptions++; } catch (ClientException e) { Log.e(TAG, "Client exception: " + e); syncResult.stats.numIoExceptions++; } catch (RemoteException e) { Log.e(TAG, "DB exception: " + e); syncResult.stats.numIoExceptions++; } Log.d(TAG, "Sync ended."); sendStatusBroadcast(false); } private void sendStatusBroadcast(boolean running) { Intent intent = new Intent(BROADCAST_ACTION); intent.putExtra(EXTRA_RUNNING, running); getContext().sendBroadcast(intent); } /** * Do a bi-directional sync with the server. * * @param client * Client object for communicating with the server. * @param provider * Content provider to use. * @param idList * List of items on server. * @param localIdMap * For mapping server IDs to local IDs. * @param syncResult * Used for synchronization statistics. * @throws ClientException * If communication with the server fails. * @throws RemoteException * If communication with the database fails. */ private void synchronizeItems(GaranboClient client, ContentProviderClient provider, List<Integer> idList, Map<Integer, List<Integer>> localIdMap, SyncResult syncResult) throws RemoteException, ClientException { if (localIdMap.containsKey(Item.UNKNOWN_ID)) { // First upload all new items... Log.d(TAG, "Sync: local-only items..."); List<Integer> newItems = localIdMap.remove(Item.UNKNOWN_ID); for (Integer id : newItems) { Item localItem = getLocalItem(provider, id); int serverId = client.item().create(localItem); localItem.setServerId(serverId); localItem.setLocalState(new LocalState()); provider.update(ContentUris.withAppendedId( GaranboItemsProvider.CONTENT_URI_ITEMS, id), localItem .toContentValues(), null, null); syncResult.stats.numInserts++; Log.d(TAG, "Uploaded local-only: " + id + " --> " + serverId); } } for (int serverId : idList) { Log.d(TAG, "Sync server item " + serverId); if (localIdMap.containsKey(serverId)) { // Item exists locally... Log.d(TAG, " Local available."); List<Integer> localIdList = localIdMap.remove(serverId); if (localIdList.size() != 1) { // Something is wrong... Log.e(TAG, "More than one local item for server ID: " + serverId + " , " + localIdList); } int localId = localIdList.get(0); Item localItem = getLocalItem(provider, localId); if (localItem.getLocalState().isDeleted()) { Log.d(TAG, " Deleted locally."); client.item().delete(serverId); provider.delete(ContentUris.withAppendedId( GaranboItemsProvider.CONTENT_URI_ITEMS, localId), null, null); } else { Item serverItem = client.item().getIfNewer(serverId, localItem.getLastModified()); if (serverItem == null) { // Server copy not modified Log.d(TAG, " Server item not modified."); if (localItem.getLocalState().changed()) { // Local copy modified -> upload Log.d(TAG, " Client item modified -> upload."); if (localItem.getLocalState().pictureChanged()) { uploadItemPicture(client, localItem); } client.item().update(localItem); serverItem = client.item().get(serverId); provider.update(ContentUris.withAppendedId( GaranboItemsProvider.CONTENT_URI_ITEMS, localId), serverItem.toContentValues(), null, null); syncResult.stats.numUpdates++; } else { Log.d(TAG, " Both versions not modified."); } } else { // Server copy modified Log.d(TAG, " Server item modified."); if (localItem.getLocalState().changed()) { // Both modified -> conflict (see GBOT-22) Log.w(TAG, " CONFLICT: Both modified."); } else { // Local not modified -> download Log.d(TAG, " Server item modified -> download."); provider.update(ContentUris.withAppendedId( GaranboItemsProvider.CONTENT_URI_ITEMS, localId), serverItem.toContentValues(), null, null); ImageCache.deleteImage(getContext(), localId); syncResult.stats.numUpdates++; } } } } else { // Item is new... Log.d(TAG, " No local item. Creating..."); Item serverItem = client.item().get(serverId); Uri itemUri = provider.insert( GaranboItemsProvider.CONTENT_URI_ITEMS, serverItem.toContentValues()); syncResult.stats.numInserts++; Log.d(TAG, " New local item: " + itemUri); } } // All remaining entries in map can be deleted... while (!localIdMap.isEmpty()) { Log.d(TAG, "Deleting items not on server anymore..."); int serverId = localIdMap.keySet().iterator().next(); List<Integer> localIdList = localIdMap.remove(serverId); for (int localId : localIdList) { provider.delete(ContentUris.withAppendedId( GaranboItemsProvider.CONTENT_URI_ITEMS, localId), null, null); syncResult.stats.numDeletes++; Log.d(TAG, "Deleted: " + localId); } } } /** * Upload a locally saved picture to the server. The picture is read from * the local {@link ImageCache}. * * @param client * Client object to use for server communication. * @param localItem * Item to upload picture for. * @throws ClientException * If the upload failed. */ private void uploadItemPicture(GaranboClient client, final Item localItem) throws ClientException { final int id = localItem.getLocalId(); File pictureFile = ImageCache.getFile(getContext(), id); client.item().uploadPicture(id, pictureFile); } /** * Retrieve an item from the local database. * * @param provider * Content provider to query. * @param id * Local ID of item. * @return Item with specified ID or <code>null</code> if not found. * @throws RemoteException * If communication with database fails. * @throws ClientException * If the item can not be parsed. */ private Item getLocalItem(ContentProviderClient provider, Integer id) throws RemoteException, ClientException { Cursor cursor = provider.query(ContentUris.withAppendedId( GaranboItemsProvider.CONTENT_URI_ITEMS, id), null, null, null, null); if (cursor.moveToFirst()) { Item result = Item.fromCursor(cursor); cursor.close(); return result; } else { return null; } } /** * Return a map that maps server IDs to local item IDs. The value list * should only contain one item for each server ID, unless the key is * {@link Item#UNKNOWN_ID}. * * @param provider * Content provider to query. * @return Map to get local items from server ID. * @throws RemoteException * If communication with local database fails. */ private Map<Integer, List<Integer>> getLocalIdMap( ContentProviderClient provider) throws RemoteException { Map<Integer, List<Integer>> idMap = new HashMap<Integer, List<Integer>>(); Cursor cursor = provider.query( GaranboItemsProvider.CONTENT_URI_ITEMS_ALL, new String[] { GaranbotDBMetaData._ID, GaranbotDBMetaData.SERVER_ID }, null, null, null); while (cursor.moveToNext()) { int localId = cursor.getInt(0); int serverId = cursor.getInt(1); if (!idMap.containsKey(serverId)) { idMap.put(serverId, new ArrayList<Integer>()); } idMap.get(serverId).add(localId); } cursor.close(); return idMap; } /** * Get a list of all database items. * * @param provider * Content provider to query. * @return List of all items in database. * @throws RemoteException * If there is an error communicating with the database. * @throws ClientException * If an item cannot be parsed. */ private List<Item> getAllDbItems(ContentProviderClient provider) throws RemoteException, ClientException { List<Item> result = new ArrayList<Item>(); Cursor query = provider.query( GaranboItemsProvider.CONTENT_URI_ITEMS_ALL, GaranbotDBMetaData.DEFAULT_PROJECTION, null, null, null); while (query.moveToNext()) { Item item = Item.fromCursor(query); result.add(item); } query.close(); return result; } /** * Returns the number of items in the local database. * * @param provider * Content provider to query. * @return Number of items in local database. * @throws RemoteException * If there is an error communicating with the database. */ private int getDbCount(ContentProviderClient provider) throws RemoteException { Cursor query = provider.query( GaranboItemsProvider.CONTENT_URI_ITEMS_ALL, new String[] { GaranbotDBMetaData._ID }, null, null, null); int result = query.getCount(); query.close(); return result; } }