package com.gettingmobile.google.reader.db; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import com.gettingmobile.google.reader.ElementId; import com.gettingmobile.google.reader.Item; import com.gettingmobile.google.reader.ItemState; import com.gettingmobile.google.reader.ItemTagChangeEvent; import java.util.*; public final class ItemTagChangeDatabaseAdapter { private final Set<ItemTagChangeListener> listeners = new HashSet<ItemTagChangeListener>(); private final Map<Long, Map<ElementId, ItemTagChangeEvent>> tagChangeEventsByItemKey = new HashMap<Long, Map<ElementId, ItemTagChangeEvent>>(); private final Map<Long, Boolean> readStateByItemKey = new HashMap<Long, Boolean>(); public static boolean hasGlobalChanges(SQLiteDatabase db) { return new ItemTagChangeEventDatabaseAdapter().readHasChanges(db) || new ItemDatabaseAdapter().hasUpdatedReadStates(db); } public static void dismissGlobalChanges(SQLiteDatabase db) { db.beginTransaction(); try { new ItemTagChangeEventDatabaseAdapter().delete(db); new ItemDatabaseAdapter().updateSyncedRead(db); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } public boolean addListener(ItemTagChangeListener listener) { return listeners.add(listener); } public boolean removeListener(ItemTagChangeListener listener) { return listeners.remove(listener); } public synchronized void markItemRead(Long itemKey, Boolean read) { readStateByItemKey.put(itemKey, read); fireOnItemReadStateChanged(itemKey, read); } public void markItemRead(Item item, Boolean read) { item.setRead(read); markItemRead(item.getKey(), read); } public void markItemsRead(Collection<Item> items) { for (Item item : items) { markItemRead(item, true); } } public void markItemsReadByKey(Collection<Long> itemKeys) { for (Long itemKey : itemKeys) { markItemRead(itemKey, true); } } public void addItemTag(Item item, ElementId tag) { changeItemTag(item, tag, true); } public void removeItemTag(Item item, ElementId tag) { changeItemTag(item, tag, false); } public void changeItemTag(Item item, ElementId tagId, boolean add) { /* * at first enqueue an event entry to the sync database */ getOrCreateItemTagChangeEvents(item.getKey()).put(tagId, new ItemTagChangeEvent(item, add, tagId)); /* * now adjust the object's tag list */ if (add) { item.getTagIds().add(tagId); } else { item.getTagIds().remove(tagId); } fireOnItemTagChanged(item.getKey(), tagId, add); } public void setItemTags(Item item, Set<ElementId> tags) { // determine which tags to be removed from the item for (ElementId tag : item.getTagIds()) { if (!tags.contains(tag)) { changeItemTag(item, tag, false); } } // determine which tags to be added to the item for (ElementId tag : tags) { if (!item.getTagIds().contains(tag)) { changeItemTag(item, tag, true); } } } public synchronized boolean needsCommit() { return !readStateByItemKey.isEmpty() || !tagChangeEventsByItemKey.isEmpty(); } /** * Writes the pending tag and read state changes to the database. * @param db the database to write the changes to. * @return whether there were changes which have been commited or not. */ public synchronized boolean commitChanges(SQLiteDatabase db) { if (!needsCommit()) return false; db.beginTransaction(); try { boolean changes = false; /* * write item read state to database */ final ItemDatabaseAdapter itemAdapter = new ItemDatabaseAdapter(); final SQLiteStatement updateItemReadStatement = itemAdapter.compileMarkItemReadStatement(db); for (Map.Entry<Long, Boolean> itemReadState : readStateByItemKey.entrySet()) { itemAdapter.markItemRead(updateItemReadStatement, itemReadState.getKey(), itemReadState.getValue()); changes = true; } /* * create tag change events */ final ItemTagChangeEventDatabaseAdapter eventAdapter = new ItemTagChangeEventDatabaseAdapter(); final SQLiteStatement insertEventStatement = eventAdapter.compileInsertStatement(db); for (Map.Entry<Long, Map<ElementId, ItemTagChangeEvent>> entry : tagChangeEventsByItemKey.entrySet()) { final long itemKey = entry.getKey(); final Map<ElementId, ItemTagChangeEvent> tagChangeEvents = entry.getValue(); for (Map.Entry<ElementId, ItemTagChangeEvent> tagChangeEvent : tagChangeEvents.entrySet()) { final ElementId tagId = tagChangeEvent.getKey(); final ItemTagChangeEvent e = tagChangeEvent.getValue(); /* * adjust the item in the database */ if (e.isAddOperation()) { itemAdapter.addItemTag(db, itemKey, tagId); } else { itemAdapter.removeItemTag(db, itemKey, tagId); } /* * write change event to the database */ eventAdapter.insert(insertEventStatement, e); } changes = true; } db.setTransactionSuccessful(); /* * dismiss stored changes */ readStateByItemKey.clear(); tagChangeEventsByItemKey.clear(); return changes; } finally { db.endTransaction(); } } /** * Adjust the specified item's tags and read state based on the enqueued changes. * @param item the item to be adjusted. * @return returns the passed in item. */ public synchronized Item adjustItemTags(Item item) { /* * process read state */ final Boolean read = readStateByItemKey.get(item.getKey()); if (read != null) { item.setRead(read); } /* * process tag changes */ final Map<ElementId, ItemTagChangeEvent> tagChangeEvents = tagChangeEventsByItemKey.get(item.getKey()); if (tagChangeEvents != null) { for (ItemTagChangeEvent tagChangeEvent : tagChangeEvents.values()) { if (ItemState.READ.getId().equals(tagChangeEvent.getTagId())) { // read state changes need to be handled in a special manner as they are no tags // from an item's point of view item.setRead(tagChangeEvent.isAddOperation()); } else if (!isImplicitItemState(tagChangeEvent.getTagId())) { // generic handling for all other tags if (tagChangeEvent.isAddOperation()) { item.getTagIds().add(tagChangeEvent.getTagId()); } else { item.getTagIds().remove(tagChangeEvent.getTagId()); } } } } return item; } private synchronized Map<ElementId, ItemTagChangeEvent> getOrCreateItemTagChangeEvents(Long key) { Map<ElementId, ItemTagChangeEvent> events = tagChangeEventsByItemKey.get(key); if (events == null) { events = new HashMap<ElementId, ItemTagChangeEvent>(); tagChangeEventsByItemKey.put(key, events); } return events; } private static boolean isImplicitItemState(ElementId tagId) { return ItemState.KEPT_UNREAD.getId().equals(tagId) || ItemState.TRACKING_KEPT_UNREAD.getId().equals(tagId); } /* * event handling */ private void fireOnItemReadStateChanged(long itemKey, boolean read) { for (ItemTagChangeListener l : listeners) { l.onItemReadStateChanged(itemKey, read); } } private void fireOnItemTagChanged(long itemKey, ElementId tagId, boolean added) { for (ItemTagChangeListener l : listeners) { l.onItemTagChanged(itemKey, tagId, added); } } }