/* * Copyright (C) 2011 The original author or authors. * * 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.zapta.apps.maniana.model; import java.util.HashMap; import java.util.HashSet; import java.util.ListIterator; import java.util.Map; import java.util.Set; import com.zapta.apps.maniana.annotations.ApplicationScope; import com.zapta.apps.maniana.annotations.VisibleForTesting; import com.zapta.apps.maniana.util.LogUtil; /** * Contains the app data. Persisted across app activations. Controlled by the app controller. * Observed by the viewer via the ItemListViewAdapter. * * @author Tal Dayan */ @ApplicationScope public class AppModel { /** Selected to not match any valid timestamp. */ private static final String DEFAULT_DATE_STAMP = ""; /** Model of Today page. */ private final PageModel mTodayPageModel; /** Model of Tomorrow page. */ private final PageModel mTomorrowPageMode; /** True if current state is not persisted */ private boolean mIsDirty = true; /** * Last date in which items were pushed from Tomorrow to Today pages. Used to determine when * next push should be done. Using an empty string to indicate no datestamp. * * NOTE(tal): for now the format of this timestamp is opaque though it must be consistent. */ private String mLastPushDateStamp; public AppModel() { this.mTodayPageModel = new PageModel(); this.mTomorrowPageMode = new PageModel(); this.mLastPushDateStamp = DEFAULT_DATE_STAMP; } public final boolean isDirty() { return mIsDirty; } public final void setDirty() { if (!mIsDirty) { LogUtil.info("Model became dirty"); mIsDirty = true; } } public final void setClean() { if (mIsDirty) { LogUtil.info("Model became clean"); mIsDirty = false; } } /** Get the model of given page. */ @VisibleForTesting final PageModel getPageModel(PageKind pageKind) { return pageKind.isToday() ? mTodayPageModel : mTomorrowPageMode; } /** Get read only aspect of the item of given index in given page. */ public final ItemModelReadOnly getItemReadOnly(PageKind pageKind, int itemIndex) { return getPageModel(pageKind).getItem(itemIndex); } /** Get a mutable item of given page and index. */ // TODO: replace with a setItem(,,,) method. Safer this way. public final ItemModel getItemForMutation(PageKind pageKind, int itemIndex) { setDirty(); return getPageModel(pageKind).getItem(itemIndex); } /** Get number of items in given page. */ public final int getPageItemCount(PageKind pageKind) { return getPageModel(pageKind).itemCount(); } /** Get number of incomplete items in given page. */ public final int getPagePendingItemCount(PageKind pageKind) { return getPageModel(pageKind).pendingItemCount(); } /** Get total number of items. */ public final int getItemCount() { return mTodayPageModel.itemCount() + mTomorrowPageMode.itemCount(); } /** Clear the model. */ public final void clear() { mTodayPageModel.clear(); mTomorrowPageMode.clear(); mLastPushDateStamp = DEFAULT_DATE_STAMP; setDirty(); } /** Clear undo buffers of both pages. */ public final void clearAllUndo() { mTodayPageModel.clearUndo(); mTomorrowPageMode.clearUndo(); // NOTE(tal): does not affect dirty flag. } /** Clear undo buffer of given page. */ public final void clearPageUndo(PageKind pageKind) { getPageModel(pageKind).clearUndo(); // NOTE(tal): does not affect dirty flag. } /** Test if given page has an active undo buffer. */ public final boolean pageHasUndo(PageKind pageKind) { return getPageModel(pageKind).hasUndo(); } /** Test if the page items are already sorted. */ public final boolean isPageSorted(PageKind pageKind) { return getPageModel(pageKind).isPageSorted(); } /** Insert item to given page at given item index. */ public final void insertItem(PageKind pageKind, int itemIndex, ItemModel item) { setDirty(); getPageModel(pageKind).insertItem(itemIndex, item); } /** Add an item to the end of given page. */ public void appendItem(PageKind pageKind, ItemModel item) { getPageModel(pageKind).appendItem(item); setDirty(); } /** Remove item of given index from given page. */ public final ItemModel removeItem(PageKind pageKind, int itemIndex) { setDirty(); ItemModel result = getPageModel(pageKind).removeItem(itemIndex); return result; } /** Remove item of given idnex from given page and set a corresponding undo at that page. */ public final void removeItemWithUndo(PageKind pageKind, int itemIndex) { setDirty(); getPageModel(pageKind).removeItemWithUndo(itemIndex); } public final void restoreBackup(AppModel newModel) { setDirty(); mTodayPageModel.restoreBackup(newModel.mTodayPageModel); mTomorrowPageMode.restoreBackup(newModel.mTomorrowPageMode); } /** * Organize the given page with undo. See details at * {@link PageModel#organizePageWithUndo(boolean, PageOrganizeResult)()}. */ public final void organizePageWithUndo(PageKind pageKind, boolean deleteCompletedItems, int itemOfInteresetIndex, OrganizePageSummary summary) { getPageModel(pageKind).organizePageWithUndo(deleteCompletedItems, itemOfInteresetIndex, summary); if (summary.pageChanged()) { setDirty(); } } /** * Apply active undo operation of given page. The method asserts that the page has an active * undo. * * @return the number of items resotred by the undo operation. */ public final int applyUndo(PageKind pageKind) { final int result = getPageModel(pageKind).performUndo(); setDirty(); return result; } /** * Copy cloned items from other model. All existing items are deleted. Dirty is set. Undo buffer * and other model properties are not changed. */ public final void copyItemsFrom(AppModel otherModel) { setDirty(); mTodayPageModel.copyItemsFrom(otherModel.mTodayPageModel); mTomorrowPageMode.copyItemsFrom(otherModel.mTomorrowPageMode); } /** * Move non locked items from Tomorow to Today. It's the caller responsibility to also set the * last push datestamp. This method clears any previous undo buffer content of both pages. * * @param expireAllLocks if true, locked items are also pushed, after changing their status to * unlocked. * * @param deleteCompletedItems if true, delete completed items, leaving them in the undo buffers * of their respective pages. */ public final void pushToToday(boolean expireAllLocks, boolean deleteCompletedItems) { clearAllUndo(); setDirty(); // Process Tomorrow items { int itemsMoved = 0; final ListIterator<ItemModel> iterator = mTomorrowPageMode.listIterator(); while (iterator.hasNext()) { final ItemModel item = iterator.next(); // Expire lock if needed if (expireAllLocks && item.isLocked()) { item.setIsLocked(false); } // If delete completed and item is completed (even if blocked), move it to undo // buffer. if (deleteCompletedItems && item.isCompleted()) { iterator.remove(); mTomorrowPageMode.appendItemToUndo(item); continue; } // If item is not unlocked, move Today page. if (!item.isLocked()) { // We move the items to the beginning of Today page, preserving there // relative order from Tomorrow page. iterator.remove(); mTodayPageModel.insertItem(itemsMoved, item); itemsMoved++; continue; } // Otherwise leave item in place. } } // If need to delete completed items, scan also Today list and move // completed items to the Today's undo buffer. if (deleteCompletedItems) { final ListIterator<ItemModel> iterator = mTodayPageModel.listIterator(); while (iterator.hasNext()) { final ItemModel item = iterator.next(); if (item.isCompleted()) { iterator.remove(); mTodayPageModel.appendItemToUndo(item); } } } } /** Get the datestamp of last item push. */ public final String getLastPushDateStamp() { return mLastPushDateStamp; } /** Set the last item push datestamp. */ public final void setLastPushDateStamp(String lastPushDateStamp) { // TODO: no need to set the dirty bit, right? this.mLastPushDateStamp = lastPushDateStamp; } public static class ItemReference { PageKind pageKind; int itemIndex; ItemModelReadOnly itemModel; public ItemReference(PageKind pageKind, int itemIndex, ItemModelReadOnly itemModel) { this.pageKind = pageKind; this.itemIndex = itemIndex; this.itemModel = itemModel; } } /** * Merge the items of the other model into this one. The other model is not modified. This * operation is not symmetric (A.mergeFrom(B) != B.mergeFrom(A). Does not do item sorting. * Caller need to invoke sorting if needed. */ public final void mergeFrom(AppModel otherModel) { setDirty(); clearAllUndo(); Map<String, ItemReference> otherItems = new HashMap<String, ItemReference>(); // Collect 'other' items for (PageKind pageKind : PageKind.values()) { final PageModel pageModel = otherModel.getPageModel(pageKind); for (int i = 0; i < pageModel.itemCount(); i++) { final ItemModelReadOnly otherItem = pageModel.getItem(i); ItemReference existingOtherItemRef = otherItems.get(otherItem.getText()); if (existingOtherItemRef != null) { // Other model has multiple items with this text. Keep merging the // properties, keeping only a single copy. final ItemModel replacementItem = new ItemModel(existingOtherItemRef.itemModel); replacementItem.mergePropertiesFrom(otherItem); existingOtherItemRef.itemModel = replacementItem; } else { otherItems.put(otherItem.getText(), new ItemReference(pageKind, i, otherItem)); } } } // Strike out 'other' items already in this. for (PageKind pageKind : PageKind.values()) { final PageModel pageModel = getPageModel(pageKind); for (int i = 0; i < pageModel.itemCount(); i++) { final ItemModel item = pageModel.getItem(i); ItemReference otherItemRef = otherItems.get(item.getText()); if (otherItemRef != null) { // This model has the same item as the other, remove the other // so we don't insert it and merge its properties into this one. // // NOTE: of this model has multiple copies of this item, only the first // one will have its properties merged since we remove the other from // the map. Alternatively we could keep it there marked as 'do-not-insert' // and merging the properties with all the copies in this model. It is // not clear what will be more intuitive. // item.mergePropertiesFrom(otherItemRef.itemModel); otherItems.remove(item.getText()); } } } // TODO: sort to preserve other items order and be deterministic for (ItemReference otherRef : otherItems.values()) { final ItemModelReadOnly otherItem = otherRef.itemModel; final ItemModel newItem = new ItemModel(otherItem); // Today page cannot have locked items if (newItem.isLocked()) { newItem.setIsLocked(false); } mTodayPageModel.insertItem(0, newItem); } } // TODO: move to somewhere else? public static class ProjectedImportStats { public int mergeDelete = 0; public int mergeKeep = 0; public int mergeAdd = 0; public int replaceDelete = 0; public int replaceKeep = 0; public int replaceAdd = 0; public final void clear() { mergeDelete = 0; mergeKeep = 0; mergeAdd = 0; replaceDelete = 0; replaceKeep = 0; replaceAdd = 0; } } public final ProjectedImportStats projectedImportStats(AppModel otherModel) { ProjectedImportStats result = new ProjectedImportStats(); Map<String, Integer> thisMultiset = itemTextMultiset(); Map<String, Integer> otherMultiset = otherModel.itemTextMultiset(); // Compute merge stats // NOTE: mergeDelete is always zero for merge oepration. for (Integer count : thisMultiset.values()) { result.mergeKeep += count; } for (String text : otherMultiset.keySet()) { if (!thisMultiset.containsKey(text)) { // NOTE: we add excactly one, even if the other model contains // multiple items with this text. The merge operation will collaps them. result.mergeAdd++; } } // Compute replace stats Set<String> textSet = new HashSet<String>(); textSet.addAll(thisMultiset.keySet()); textSet.addAll(otherMultiset.keySet()); for (String text : textSet) { final Integer thisValue = thisMultiset.get(text); final Integer otherValue = otherMultiset.get(text); final int thisCount = (thisValue == null) ? 0 : thisValue; final int otherCount = (otherValue == null) ? 0 : otherValue; if (thisCount < otherCount) { result.replaceKeep += thisCount; result.replaceAdd += (otherCount - thisCount); } else { result.replaceKeep += otherCount; result.replaceDelete += (thisCount - otherCount); } } return result; } /** Return a multiset of the text string of all items. */ private final Map<String, Integer> itemTextMultiset() { Map<String, Integer> result = new HashMap<String, Integer>(); for (PageKind pageKind : PageKind.values()) { final PageModel pageModel = getPageModel(pageKind); for (int i = 0; i < pageModel.itemCount(); i++) { final ItemModelReadOnly item = pageModel.getItem(i); final Integer previousCount = result.get(item.getText()); result.put(item.getText(), (previousCount == null) ? 1 : previousCount + 1); } } return result; } }