/*
* Copyright (c) 2013 Andrew Fontaine, James Finlay, Jesse Tucker, Jacob Viau, and
* Evan DeGraff
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package ca.cmput301f13t03.adventure_datetime.model;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.util.Log;
import ca.cmput301f13t03.adventure_datetime.R;
import ca.cmput301f13t03.adventure_datetime.model.Interfaces.*;
import java.util.*;
import junit.framework.Assert;
/**
* Manages all transactions between views, controllers, and models.
* Creates new Stories, StoryFragments, etc.
* Fetches and caches Stories and StoryFragments
*/
public final class StoryManager implements IStoryModelPresenter,
IStoryModelDirector {
final String DEFAULT_FRAGMENT_TEXT = "<insert content here...>";
private static final String TAG = "StoryManager";
private ILocalStorage m_db = null;
private Context m_context = null;
private WebStorage m_webStorage = null;
private ThreadPool m_threadPool = null;
// Current focus
private Story m_currentStory = null;
private StoryFragment m_currentFragment = null;
private Map<UUID, Story> m_stories = null;
private Map<UUID, Story> m_onlineStories = null;
private Map<UUID, Bookmark> m_bookmarkList = null;
private Map<UUID, StoryFragment> m_fragmentList = null;
private Map<UUID, List<Comment>> m_comments = null;
// Listeners
private Set<ICurrentFragmentListener> m_fragmentListeners = new HashSet<ICurrentFragmentListener>();
private Set<ICurrentStoryListener> m_storyListeners = new HashSet<ICurrentStoryListener>();
private Set<ILocalStoriesListener> m_localStoriesListeners = new HashSet<ILocalStoriesListener>();
private Set<IOnlineStoriesListener> m_onlineStoriesListeners = new HashSet<IOnlineStoriesListener>();
private Set<IBookmarkListListener> m_bookmarkListListeners = new HashSet<IBookmarkListListener>();
private Set<IAllFragmentsListener> m_allFragmentListeners = new HashSet<IAllFragmentsListener>();
private Map<UUID, ICommentsListener> m_commentsListeners = new HashMap<UUID, ICommentsListener>();
private Object syncLock = new Object();
/**
* Create a new story manager and initializes other components using the provided context.
* The provided context MUST be the application context
*/
public StoryManager(Context context)
{
m_context = context;
m_db = new StoryDB(context);
m_webStorage = new WebStorage();
m_threadPool = new ThreadPool();
m_fragmentList = new HashMap<UUID, StoryFragment>();
m_comments = new HashMap<UUID, List<Comment>>();
}
// ============================================================
//
// IStoryModelPresenter
//
// The design is such that a publish will to the subscriber will
// occur immediately if data is available. If not the data will
// be supplied later once it is available.
//
// ============================================================
/**
* Subscribe for changes to the current fragment
*/
public void Subscribe(ICurrentFragmentListener fragmentListener) {
synchronized (syncLock)
{
m_fragmentListeners.add(fragmentListener);
if (m_currentFragment != null) {
fragmentListener.OnCurrentFragmentChange(m_currentFragment);
}
}
}
/**
* Subscribe for changes to the current story
*/
public void Subscribe(ICurrentStoryListener storyListener) {
synchronized (syncLock)
{
m_storyListeners.add(storyListener);
if (m_currentStory != null) {
storyListener.OnCurrentStoryChange(m_currentStory);
}
}
}
/**
* Subscribe to changes for the current list of stories
*/
public void Subscribe(ILocalStoriesListener localStoriesListener) {
synchronized (syncLock)
{
m_localStoriesListeners.add(localStoriesListener);
if (m_stories != null) {
localStoriesListener.OnLocalStoriesChange(m_stories);
} else {
LoadStories();
PublishStoriesChanged();
}
}
}
public void Subscribe(IOnlineStoriesListener onlineStoriesListener) {
synchronized (syncLock)
{
m_onlineStoriesListeners.add(onlineStoriesListener);
if (m_onlineStories != null) {
onlineStoriesListener.OnOnlineStoriesChange(m_onlineStories);
} else {
LoadOnlineStories();
}
}
}
public void Subscribe(IBookmarkListListener bookmarkListListener) {
synchronized (syncLock)
{
m_bookmarkListListeners.add(bookmarkListListener);
if (m_bookmarkList != null) {
bookmarkListListener.OnBookmarkListChange(m_bookmarkList);
} else {
LoadBookmarks();
PublishBookmarkListChanged();
}
}
}
public void Subscribe(IAllFragmentsListener allFragmentsListener)
{
synchronized (syncLock)
{
m_allFragmentListeners.add(allFragmentsListener);
if(m_fragmentList != null && m_currentStory != null)
{
Map<UUID, StoryFragment> currentFrags = GetAllCurrentFragments();
allFragmentsListener.OnAllFragmentsChange(currentFrags);
}
}
}
public void Subscribe(ICommentsListener commentsListener, UUID id) {
synchronized (syncLock)
{
m_commentsListeners.put(id, commentsListener);
LoadComments(id);
}
}
/**
* Unsubscribe from callbacks when the current fragment changes
*/
public void Unsubscribe(ICurrentFragmentListener fragmentListener) {
synchronized (syncLock)
{
m_fragmentListeners.remove(fragmentListener);
}
}
/**
* Unsubscribe from callbacks when the current story changes
*/
public void Unsubscribe(ICurrentStoryListener storyListener) {
synchronized (syncLock)
{
m_storyListeners.remove(storyListener);
}
}
/**
* Unsubscribe from callbakcs when the current list of stories changes
*/
public void Unsubscribe(ILocalStoriesListener storyListListener) {
synchronized (syncLock)
{
m_localStoriesListeners.remove(storyListListener);
}
}
public void Unsubscribe(IOnlineStoriesListener storyListListener) {
synchronized (syncLock)
{
m_onlineStoriesListeners.remove(storyListListener);
}
}
public void Unsubscribe(IBookmarkListListener bookmarkListListener) {
synchronized (syncLock)
{
m_bookmarkListListeners.remove(bookmarkListListener);
}
}
public void Unsubscribe(IAllFragmentsListener allFragmentsListener)
{
synchronized (syncLock)
{
m_allFragmentListeners.remove(allFragmentsListener);
}
}
public void Unsubscribe(UUID id) {
synchronized (syncLock)
{
m_commentsListeners.remove(id);
}
}
// ============================================================
//
// Publish
//
// ============================================================
/**
* Publish a change to the current story to all listeners
*/
private void PublishCurrentStoryChanged() {
synchronized (syncLock)
{
for (ICurrentStoryListener storyListener : m_storyListeners) {
storyListener.OnCurrentStoryChange(m_currentStory);
}
// whenever the current story changes so does the list of current fragments
PublishAllFragmentsChanged();
}
}
/**
* Publish a change to the current fragment to all listeners
*/
private void PublishCurrentFragmentChanged() {
synchronized (syncLock)
{
for (ICurrentFragmentListener fragmentListener : m_fragmentListeners) {
fragmentListener.OnCurrentFragmentChange(m_currentFragment);
}
}
}
/**
* Publish a changed to the current list of stories to all listeners
*/
private void PublishStoriesChanged() {
synchronized (syncLock)
{
for (ILocalStoriesListener localStoriesListener : m_localStoriesListeners) {
localStoriesListener.OnLocalStoriesChange(m_stories);
}
}
}
private void PublishOnlineStoriesChanged() {
synchronized (syncLock)
{
for (IOnlineStoriesListener onlineStoriesListener : m_onlineStoriesListeners) {
onlineStoriesListener.OnOnlineStoriesChange(m_onlineStories);
}
}
}
private void PublishBookmarkListChanged() {
synchronized (syncLock)
{
for (IBookmarkListListener bookmarkListener : m_bookmarkListListeners) {
bookmarkListener.OnBookmarkListChange(m_bookmarkList);
}
}
}
private void PublishCommentsChanged(UUID finalId) {
synchronized (syncLock)
{
m_commentsListeners.get(finalId).OnCommentsChange(m_comments.get(finalId));
}
}
private void PublishAllFragmentsChanged()
{
synchronized (syncLock)
{
if(m_currentStory != null && m_fragmentList != null)
{
Map<UUID, StoryFragment> currentStoryFragments = GetAllCurrentFragments();
for(IAllFragmentsListener allFragListener : m_allFragmentListeners)
{
allFragListener.OnAllFragmentsChange(currentStoryFragments);
}
}
}
}
// ============================================================
//
// IStoryModelDirector
//
// ============================================================
/**
* Select a story
*/
public void selectStory(UUID storyId)
{
synchronized (syncLock)
{
Story newStory = getStory(storyId);
if(newStory != m_currentStory)
{
m_currentStory = newStory;
PublishCurrentStoryChanged();
}
}
}
/**
* Select a fragment as the current fragment
*/
public void selectFragment(UUID fragmentId) {
synchronized (syncLock)
{
m_currentFragment = getFragment(fragmentId);
if(m_currentFragment != null)
PublishCurrentFragmentChanged();
else {
getFragmentOnline(fragmentId, false);
}
}
}
/**
* Create a new story and head fragment and insert them into the local database
*/
public Story CreateNewStory()
{
synchronized (syncLock)
{
Story newStory = new Story();
StoryFragment headFragment = new StoryFragment(newStory.getId(), DEFAULT_FRAGMENT_TEXT);
newStory.setHeadFragmentId(headFragment);
if(m_stories == null)
{
LoadStories();
}
m_stories.put(newStory.getId(), newStory);
m_currentStory = newStory;
SaveStory();
m_db.setAuthoredStory(newStory);
m_fragmentList.put(headFragment.getFragmentID(), headFragment);
PublishCurrentStoryChanged();
return newStory;
}
}
public StoryFragment CreateNewStoryFragment()
{
synchronized (syncLock)
{
StoryFragment newFrag = new StoryFragment(m_currentStory.getId(), "");
m_fragmentList.put(newFrag.getFragmentID(), newFrag);
m_currentStory.addFragment(newFrag);
PublishCurrentStoryChanged();
PublishAllFragmentsChanged();
return newFrag;
}
}
public boolean SaveStory()
{
synchronized (syncLock)
{
// Set default image if needed
if(m_currentStory == null)
return false;
if (m_currentStory.getThumbnail() == null)
m_currentStory.setThumbnail(BitmapFactory.decodeResource(
m_context.getResources(), R.drawable.grumpy_cat));
m_currentStory.updateTimestamp();
boolean result = m_db.setStory(m_currentStory);
if(result)
{
m_stories.put(m_currentStory.getId(), m_currentStory);
SaveAllFrags();
PublishStoriesChanged();
}
return result;
}
}
private void SaveAllFrags()
{
synchronized (syncLock)
{
Map<UUID, StoryFragment> currentFrags = GetAllCurrentFragments();
for(StoryFragment frag : currentFrags.values())
{
if(!m_db.setStoryFragment(frag))
{
Log.w(TAG, "Failed to save fragment to database!");
}
}
}
}
/**
* Delete a story from the database
*/
public void deleteStory(UUID storyId) {
synchronized (syncLock)
{
m_db.deleteStory(storyId);
m_stories.remove(storyId);
PublishStoriesChanged();
}
}
public void deleteImage(UUID imageId) {
m_db.deleteImage(imageId);
for(Image image : m_currentFragment.getStoryMedia()) {
if(image.getId().equals(imageId))
m_currentFragment.removeMedia(image);
}
SaveStory();
PublishCurrentFragmentChanged();
}
/**
* Get a story from the database or cloud
*/
public Story getStory(UUID storyId) {
synchronized (syncLock)
{
if(m_stories == null)
{
LoadStories();
}
Story story = m_stories.get(storyId);
if(story == null)
story = m_onlineStories.get(storyId);
return story;
}
}
/**
* Save a fragment to the database
*/
public boolean putFragment(StoryFragment fragment) {
synchronized (syncLock)
{
// this really should be transactional...
boolean result = m_db.setStoryFragment(fragment);
if(result)
{
result = m_db.setStory(m_currentStory);
PublishAllFragmentsChanged();
}
return result;
}
}
/**
* Delete a fragment from the database
*/
public void deleteFragment(UUID fragmentId) {
synchronized (syncLock)
{
m_db.deleteStoryFragment(fragmentId);
m_fragmentList.remove(fragmentId);
List<Choice> choicesToRemove = new ArrayList<Choice>();
// Now iterate over all fragments and find those that referenced this one
// remove those choices so they cannot be selected
for(StoryFragment frag : m_fragmentList.values())
{
choicesToRemove.clear();
for(Choice choice : frag.getChoices())
{
if(choice.getTarget().equals(fragmentId))
{
choicesToRemove.add(choice);
}
}
for(Choice choice : choicesToRemove)
{
frag.removeChoice(choice);
}
}
// have to save after a deletion or the memory and database will be out of sync
SaveStory();
PublishAllFragmentsChanged();
}
}
/**
* Get a fragment from the database
*/
public StoryFragment getFragment(UUID theId) {
synchronized (syncLock)
{
StoryFragment result = null;
if(m_fragmentList.containsKey(theId))
{
// great we have it cached!
result = m_fragmentList.get(theId);
}
else
{
//Try loading from db
result = m_db.getStoryFragment(theId);
if(result != null)
{
m_fragmentList.put(result.getFragmentID(), result);
}
else
{
// TODO check webstorage...?
Log.w(TAG, "Attempted to load a fragment that wasn't cached or in the database!");
}
}
return result;
}
}
private void getFragmentOnline(UUID fragmentId, boolean storeDB)
{
synchronized (syncLock)
{
// Fetch fragment asynchronously
final UUID finalId = fragmentId;
final boolean finalStoreDB = storeDB;
m_threadPool.execute(
new Runnable()
{
public void run() {
try
{
m_currentFragment = m_webStorage.getFragment(finalId);
if(m_currentFragment != null)
{
// afterwards place into cache
m_fragmentList.put(m_currentFragment.getFragmentID(), m_currentFragment);
PublishCurrentFragmentChanged();
if(finalStoreDB)
{
m_db.setStoryFragment(m_currentFragment);
}
}
else
{
Log.e(TAG, "Rx'd a NULL value from the webstorage for a fragment!");
}
} catch (Exception e)
{
Log.e(TAG, "StoryManager: ", e);
}
}
});
}
}
public ArrayList<Story> getStoriesAuthoredBy(String author) {
synchronized (syncLock)
{
if(m_stories == null)
{
LoadStories();
}
ArrayList<Story> results = new ArrayList<Story>();
for(Story story : m_stories.values())
{
if(author.equalsIgnoreCase(story.getAuthor()))
{
results.add(story);
}
}
return results;
}
}
/**
* Fetch a bookmark from local database
*/
public Bookmark getBookmark(UUID id) {
synchronized (syncLock)
{
if(m_bookmarkList == null)
{
LoadBookmarks();
}
return m_bookmarkList.get(id);
}
}
public void setBookmark(UUID fragmentId) {
synchronized (syncLock)
{
Bookmark newBookmark = new Bookmark(m_currentStory.getId(), fragmentId);
m_bookmarkList.remove(m_currentStory.getId());
m_bookmarkList.put(m_currentStory.getId(), newBookmark);
m_db.setBookmark(newBookmark);
PublishBookmarkListChanged();
}
}
public void deleteBookmark() {
synchronized (syncLock)
{
m_db.deleteBookmarkByStory(m_currentStory.getId());
m_bookmarkList.remove(m_currentStory.getId());
PublishBookmarkListChanged();
}
}
public void addComment(Comment comment) {
synchronized(syncLock)
{
final Comment finalComment = comment;
m_threadPool.execute(new Runnable() {
public void run()
{
try {
m_webStorage.putComment(finalComment);
Thread.sleep(1000);
LoadComments(finalComment.getTargetId());
} catch (Exception e) {
Log.e(TAG, "Error: ", e);
}
}});
}
}
private void LoadStories()
{
synchronized (syncLock)
{
m_stories = new HashMap<UUID, Story>();
ArrayList<Story> localStories = m_db.getStories();
for(Story story : localStories)
{
m_stories.put(story.getId(), story);
}
}
}
private void LoadOnlineStories()
{
synchronized (syncLock)
{
m_onlineStories = new HashMap<UUID, Story>();
// Fetch stories from web asynchronously.
m_threadPool.execute(new Runnable() {
public void run() {
try {
List<Story> onlineStories;
int size = 10;
int i = 0;
while(size == 10) {
onlineStories = m_webStorage.getStories(i, 10);
for(Story story : onlineStories)
{
m_onlineStories.put(story.getId(), story);
}
size = onlineStories.size();
i += 10;
}
PublishOnlineStoriesChanged();
} catch (Exception e) {
Log.e(TAG, "Error: ", e);
}
}
});
}
}
private void LoadBookmarks()
{
synchronized (syncLock)
{
m_bookmarkList = new HashMap<UUID, Bookmark>();
ArrayList<Bookmark> bookmarks = m_db.getAllBookmarks();
for(Bookmark bookmark : bookmarks)
{
m_bookmarkList.put(bookmark.getStoryID(), bookmark);
}
}
}
private void LoadComments(UUID id)
{
synchronized (syncLock)
{
final UUID finalId = id;
m_threadPool.execute(new Runnable() {
public void run() {
try {
if(m_comments.get(finalId) != null)
m_comments.remove(finalId);
List<Comment> tempComments;
List<Comment> onlineComments = new ArrayList<Comment>();
int size = 10;
int i = 0;
while(size == 10) {
tempComments = m_webStorage.getComments(finalId, i, 10);
for(Comment comment : tempComments)
{
onlineComments.add(comment);
}
size = tempComments.size();
i += 10;
}
m_comments.put(finalId, onlineComments);
PublishCommentsChanged(finalId);
} catch (Exception e) {
Log.e(TAG, "Error: ", e);
}
}
});
}
}
private Map<UUID, StoryFragment> GetAllCurrentFragments()
{
synchronized (syncLock)
{
Map<UUID, StoryFragment> currentFragments = new HashMap<UUID, StoryFragment>();
for(UUID fragmentId : m_currentStory.getFragments())
{
// first try to fetch from local cache
StoryFragment frag = this.getFragment(fragmentId);
if(frag != null)
{
currentFragments.put(frag.getFragmentID(), frag);
}
else
{
Log.w(TAG, "Attempted to fetch fragments that aren't cached or in local DB!");
}
}
return currentFragments;
}
}
public void uploadCurrentStory() {
synchronized (syncLock)
{
m_threadPool.execute(new Runnable() {
public void run() {
try {
m_webStorage.publishStory(m_currentStory, new ArrayList<StoryFragment>(GetAllCurrentFragments().values()));
} catch (Exception e) {
Log.e(TAG, "Error: ", e);
}
}
});
}
}
public void download() {
synchronized (syncLock)
{
if(m_currentStory != null) {
m_stories.put(m_currentStory.getId(), m_currentStory);
m_db.setStory(m_currentStory);
for(UUID fragmentId : m_currentStory.getFragments()) {
getFragmentOnline(fragmentId, true);
}
}
}
}
public void search(String searchTerm) {
m_onlineStories = new HashMap<UUID, Story>();
final String finalTerm = searchTerm;
// Fetch stories from web asynchronously.
m_threadPool.execute(new Runnable() {
public void run() {
try {
List<Story> onlineStories;
int size = 10;
int i = 0;
while(size == 10) {
onlineStories = m_webStorage.queryStories(finalTerm, i, 10);
for(Story story : onlineStories)
{
m_onlineStories.put(story.getId(), story);
}
size = onlineStories.size();
i += 10;
}
PublishOnlineStoriesChanged();
} catch (Exception e) {
Log.e(TAG, "Error: ", e);
}
}
});
}
@Override
public UUID setStoryToAuthor(UUID storyId, String username) {
synchronized (syncLock)
{
if(m_db.getAuthoredStory(storyId))
return storyId;
String bit = getStory(storyId).getThumbnail().getEncodedBitmap();
Story story = getStory(storyId).newId();
List<StoryFragment> newFragments = new ArrayList<StoryFragment>();
Map<UUID,UUID> oldToNew = new HashMap<UUID, UUID>();
for(UUID fragmentId : story.getFragments()) {
try {
StoryFragment fragment = getFragment(fragmentId).newId();
fragment.setStoryID(story.getId());
newFragments.add(fragment);
oldToNew.put(fragmentId, fragment.getFragmentID());
} catch(NullPointerException e) {
Log.e(TAG, "Error: ", e);
}
}
for(StoryFragment fragment : newFragments) {
for(Choice choice : fragment.getChoices()) {
if(choice != null)
choice.setTarget(oldToNew.get(choice.getTarget()));
}
m_db.setStoryFragment(fragment);
m_fragmentList.put(fragment.getFragmentID(), fragment);
story.addFragment(fragment);
}
story.setThumbnail((String)null);
story.setThumbnail(bit);
story.setHeadFragmentId(oldToNew.get(story.getHeadFragmentId()));
story.setAuthor(username);
selectStory(storyId);
SaveStory();
m_db.setAuthoredStory(story);
LoadStories();
return story.getId();
}
}
@Override
public boolean isAuthored(UUID storyId) {
synchronized (syncLock)
{
return m_db.getAuthoredStory(storyId);
}
}
}