/* * Created by Itzik Braun on 12/3/2015. * Copyright (c) 2015 deluge. All rights reserved. * * Last Modification at: 3/12/15 4:35 PM */ package com.braunster.androidchatsdk.firebaseplugin.firebase.wrappers; import com.braunster.androidchatsdk.firebaseplugin.firebase.FirebasePaths; import com.braunster.chatsdk.Utils.Debug; import com.braunster.chatsdk.Utils.sorter.MessageSorter; import com.braunster.chatsdk.dao.UserThreadLink; import com.braunster.chatsdk.dao.UserThreadLinkDao; import com.braunster.chatsdk.dao.BMessage; import com.braunster.chatsdk.dao.BMessageDao; import com.braunster.chatsdk.dao.BThread; import com.braunster.chatsdk.dao.BUser; import com.braunster.chatsdk.dao.core.DaoCore; import com.braunster.chatsdk.dao.entities.BThreadEntity; import com.braunster.chatsdk.network.BDefines; import com.braunster.chatsdk.network.BFirebaseDefines; import com.braunster.chatsdk.network.BNetworkManager; import com.braunster.chatsdk.object.BError; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.Query; import com.google.firebase.database.ServerValue; import com.google.firebase.database.ValueEventListener; import org.apache.commons.lang3.StringUtils; import org.jdeferred.Deferred; import org.jdeferred.DoneCallback; import org.jdeferred.FailCallback; import org.jdeferred.Promise; import org.jdeferred.impl.DeferredObject; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import de.greenrobot.dao.query.QueryBuilder; import jdeferred.android.AndroidDeferredObject; import jdeferred.android.AndroidExecutionScope; import timber.log.Timber; public class BThreadWrapper extends EntityWrapper<BThread> { public static final boolean DEBUG = Debug.BThread; public BThreadWrapper(BThread thread){ this.model = thread; this.entityId = thread.getEntityID(); } public BThreadWrapper(String entityId){ this(DaoCore.fetchOrCreateEntityWithEntityID(BThread.class, entityId)); } @Override public BThread getModel(){ return DaoCore.fetchEntityWithEntityID(BThread.class, entityId); } /** * Start listening to thread details changes. **/ public Promise<BThread, Void , Void> on(){ if (DEBUG) Timber.v("on"); Deferred<BThread, Void , Void> deferred = new DeferredObject<>(); AndroidDeferredObject<BThread, Void, Void> androidDeferredObject = new AndroidDeferredObject<BThread, Void, Void>(deferred.promise(), AndroidExecutionScope.UI); getNetworkAdapter().getEventManager().threadOn(entityId, deferred); return androidDeferredObject.promise(); } /** * Stop listening to thread details change **/ public void off(){ if (DEBUG) Timber.v("off"); getNetworkAdapter().getEventManager().threadOff(entityId); } /** * Start listening to incoming messages. **/ public Promise<BThread, Void, Void> messagesOn(){ if (DEBUG) Timber.v("messagesOn"); Deferred<BThread, Void, Void> deferred = new DeferredObject<>(); AndroidDeferredObject<BThread, Void, Void> androidDeferredObject = new AndroidDeferredObject<BThread, Void, Void>(deferred.promise(), AndroidExecutionScope.UI); getNetworkAdapter().getEventManager().messagesOn(entityId, deferred); return androidDeferredObject.promise(); } /** * Stop Lisetenig to incoming messages. **/ public void messagesOff(){ if (DEBUG) Timber.v("messagesOff"); getNetworkAdapter().getEventManager().messagesOff(entityId); } //Note the old listener that was used to process the thread data is still in use. /** * Start listening to users added to this thread. **/ public void usersOn(){ if (DEBUG) Timber.v("usersOn"); getNetworkAdapter().getEventManager().threadUsersAddedOn(entityId); } /** * Stop listening to users added to this thread. **/ public void usersOff(){ if (DEBUG) Timber.v("usersOff"); getNetworkAdapter().getEventManager().threadUsersAddedOff(entityId); } //Note - Maybe should reject when cant find value in the user deleted path. /** * Get the date when the thread was deleted * @return Promise On success return the date or Nil if the thread hasn't been deleted **/ public Promise<Long, DatabaseError, Void> threadDeletedDate(){ final Deferred<Long, DatabaseError, Void> deferred = new DeferredObject<>(); BUser user = getNetworkAdapter().currentUserModel(); DatabaseReference currentThreadUser = FirebasePaths.threadRef(entityId) .child(BFirebaseDefines.Path.BUsersPath) .child(user.getEntityID()) .child(BDefines.Keys.BDeleted);; currentThreadUser.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { if (snapshot.getValue() != null) { deferred.resolve((Long) snapshot.getValue()); } else deferred.resolve(null); } @Override public void onCancelled(DatabaseError firebaseError) { deferred.reject(firebaseError); } }); return deferred.promise(); } //Note - Maybe should treat group thread and one on one thread the same /** * Deleting a thread, Thread isn't always actually deleted from the db. * We mark the thread as deleted and mark the user in the thread users ref as deleted. **/ public Promise<Void, BError, Void> deleteThread(){ if (DEBUG) Timber.v("deleteThread"); final Deferred<Void, BError, Void> deferred = new DeferredObject<>(); BUser user = getNetworkAdapter().currentUserModel(); if (model.getTypeSafely() != BThreadEntity.Type.Private) return deferred.promise(); List<BMessage> messages = DaoCore.fetchEntitiesWithProperty(BMessage.class, BMessageDao.Properties.ThreadDaoId, model.getId()); for (BMessage m : messages) { DaoCore.deleteEntity(m); } DaoCore.updateEntity(model); if (model.getUsers().size() > 2) { // Removing the thread from the current user thread ref. DatabaseReference userThreadRef = FirebasePaths.firebaseRef() .child(user.getBPath().getPath()) .child(model.getBPath().getPath()); // Stop listening to thread events, Details change, User added and incoming messages. off(); messagesOff(); usersOff(); // Removing the thread from the user threads list. userThreadRef.removeValue(new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference firebase) { // Delete the thread if no error occurred when deleting from firebase. if (error == null) { // Adding a leave value to the user on the thread path so other users will know this user has left. DatabaseReference threadUserRef = FirebasePaths.threadRef(entityId) .child(BFirebaseDefines.Path.BUsersPath) .child(getNetworkAdapter().currentUserModel().getEntityID()) .child(BDefines.Keys.BLeaved); threadUserRef.setValue(true); List<UserThreadLink> list = DaoCore.fetchEntitiesWithProperty(UserThreadLink.class, UserThreadLinkDao.Properties.BThreadDaoId, model.getId()); DaoCore.deleteEntity(model); // Deleting all data relevant to the thread from the db. for (UserThreadLink d : list) DaoCore.deleteEntity(d); if (DEBUG) { BThread deletedThread = DaoCore.fetchEntityWithEntityID(BThread.class, entityId); if (deletedThread == null) Timber.d("Thread deleted successfully."); else Timber.d("Thread was not deleted."); } deferred.resolve(null); } else { deferred.reject(getFirebaseError(error)); } } }); } else { DatabaseReference threadUserRef = FirebasePaths.threadRef(entityId) .child(BFirebaseDefines.Path.BUsersPath) .child(getNetworkAdapter().currentUserModel().getEntityID()) .child(BDefines.Keys.BDeleted); // Set the deleted value in firebase threadUserRef.setValue(ServerValue.TIMESTAMP); model.setDeleted(true); DaoCore.updateEntity(model); DaoCore.deleteEntity(model); deferred.resolve(null); } return deferred.promise(); } public Promise<BThread, BError, Void> recoverPrivateThread(){ if (DEBUG) Timber.v("recoverPrivateThread"); final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); // Removing the deleted value from firebase. DatabaseReference threadUserRef = FirebasePaths.threadRef(entityId) .child(BFirebaseDefines.Path.BUsersPath) .child(BNetworkManager.sharedManager().getNetworkAdapter().currentUserModel().getEntityID()) .child(BDefines.Keys.BDeleted); threadUserRef.removeValue(); this.getModel().setDeleted(false); this.getModel().setType(BThread.Type.Private); final BThread toBeUpdated = this.getModel(); this.loadMessages().done(new DoneCallback<List<BMessage>>() { @Override public void onDone(List<BMessage> bMessages) { toBeUpdated.setMessages(bMessages); DaoCore.updateEntity(toBeUpdated); deferred.resolve(toBeUpdated); } }).fail(new FailCallback<Void>() { @Override public void onFail(Void aVoid) { deferred.resolve(toBeUpdated); } }); DaoCore.updateEntity(this.model); return deferred.promise(); } // TODO: Replace this with the loadMessages method (need to figure out the date ref first) public Promise<List<BMessage>, Void, Void> loadMoreMessages(final int numberOfMessages){ if (DEBUG) Timber.v("loadMoreMessages"); final Deferred<List<BMessage>, Void, Void> deferred = new DeferredObject<>(); final Date messageDate; List<BMessage> messages = model.getMessagesWithOrder(DaoCore.ORDER_ASC); BMessage earliestMessage = null; if (messages.size() > 0) earliestMessage = messages.get(0); // If we have a message in the database then we use the latest if (earliestMessage != null) { if(DEBUG) Timber.d("Msg: %s", earliestMessage.getText()); messageDate = earliestMessage.getDate(); } // Otherwise we use todays date else messageDate = new Date(); List<BMessage> list ; QueryBuilder<BMessage> qb = DaoCore.daoSession.queryBuilder(BMessage.class); qb.where(BMessageDao.Properties.ThreadDaoId.eq(model.getId())); // Making sure no null messages infected the sort. qb.where(BMessageDao.Properties.Date.isNotNull()); qb.where(BMessageDao.Properties.Date.lt(messageDate)); qb.limit(numberOfMessages + 1); qb.orderDesc(BMessageDao.Properties.Date); list = qb.list(); // If we have older messages in the db we get them if (list.size() > 0){ if (DEBUG) Timber.d("Loading messages from local db, Size: %s", list.size()); Collections.sort(list, new MessageSorter(DaoCore.ORDER_DESC)); deferred.resolve(list); } else { if (DEBUG) Timber.d("Loading messages from firebase"); DatabaseReference messageRef = FirebasePaths.threadRef(model.getEntityID()).child(BFirebaseDefines.Path.BMessagesPath); // Get # messages ending at the end date // Limit to # defined in BFirebaseDefines // We add one becase we'll also be returning the last message again // FIXME not sure if to use limitToFirst or limitToLast Query msgQuery; if(numberOfMessages == -1){ msgQuery = messageRef; }else { msgQuery = messageRef.endAt(messageDate.getTime()).limitToLast(numberOfMessages + 1); } msgQuery.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { if (snapshot.getValue() != null) { if (DEBUG) Timber.d("MessagesSnapShot: %s", snapshot.getValue().toString()); List<BMessage> msgs = new ArrayList<BMessage>(); BMessageWrapper msg; for (String key : ((Map<String, Object>) snapshot.getValue()).keySet()) { msg = new BMessageWrapper(BThreadWrapper.this.getModel(), snapshot.child(key)); DaoCore.updateEntity(msg.model); msgs.add(msg.model); } deferred.resolve(msgs); } else { deferred.reject(null); } } @Override public void onCancelled(DatabaseError firebaseError) { deferred.reject(null); } }); } return deferred.promise(); } public Promise<List<BMessage>, Void, Void> loadMessages(){ return loadMessages(-1); } public Promise<List<BMessage>, Void, Void> loadMessages(Integer lastNMessages){ final Deferred<List<BMessage>, Void, Void> deferred = new DeferredObject<>(); DatabaseReference messageRef = FirebasePaths.threadRef(model.getEntityID()) .child(BFirebaseDefines.Path.BMessagesPath); Query msgQuery; // If number of messages set to retrieve is -1, retrieve all messages if(lastNMessages == -1){ msgQuery = messageRef; }else { msgQuery = messageRef.limitToLast(lastNMessages); } msgQuery.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { if (snapshot.getValue() != null) { if (DEBUG) Timber.d("MessagesSnapShot: %s", snapshot.getValue().toString()); List<BMessage> msgs = new ArrayList<BMessage>(); BMessageWrapper msg; for (String key : ((Map<String, Object>) snapshot.getValue()).keySet()) { msg = new BMessageWrapper(BThreadWrapper.this.getModel() ,snapshot.child(key)); DaoCore.updateEntity(msg.model); msgs.add(msg.model); } deferred.resolve(msgs); } else { deferred.reject(null); } } @Override public void onCancelled(DatabaseError firebaseError) { deferred.reject(null); } }); return deferred.promise(); } /** * Converting the thread details to a map object. **/ Map<String, Object> serialize(){ Map<String , Object> value = new HashMap<String, Object>(); Map<String , Object> nestedMap = new HashMap<String, Object>(); // If the creation date is null we assume that the thread is now being created so we push the server timestamp with it. // Else we will push the saved creation date from the db. // No treating this as so can cause problems with firebase security rules. if (this.model.getCreationDate() == null) nestedMap.put(BDefines.Keys.BCreationDate, BFirebaseDefines.getServerTimestamp()); else nestedMap.put(BDefines.Keys.BCreationDate, this.model.getCreationDate().getTime()); nestedMap.put(BDefines.Keys.BName, this.model.getName()); nestedMap.put(BDefines.Keys.BType, this.model.getType()); if (this.model.getLastMessageAdded() != null) nestedMap.put(BDefines.Keys.BLastMessageAdded, this.model.getLastMessageAdded().getTime()); nestedMap.put(BDefines.Keys.BCreatorEntityId, this.model.getCreatorEntityId()); nestedMap.put(BDefines.Keys.BImageUrl, this.model.getImageUrl()); value.put(BFirebaseDefines.Path.BDetailsPath, nestedMap); return value; } /** * Updating thread details from given map **/ @SuppressWarnings("all")// To remove setType warning. void deserialize(Map<String, Object> value){ if (DEBUG) Timber.d("Update from map. Id: %s", entityId); if (value == null) { if (DEBUG) Timber.e("Thread update from map is null, Thread ID: %s", entityId); return; } if (value.containsKey(BDefines.Keys.BCreationDate)) { if (value.get(BDefines.Keys.BCreationDate) instanceof Long) { Long data = (Long) value.get(BDefines.Keys.BCreationDate); if (data != null && data > 0) this.model.setCreationDate(new Date(data)); } else if (value.get(BDefines.Keys.BCreationDate) instanceof Double) { Double data = (Double) value.get(BDefines.Keys.BCreationDate); if (data != null && data > 0) this.model.setCreationDate(new Date(data.longValue())); } } long type; if (value.containsKey(BDefines.Keys.BType)) { type = (Long) value.get(BDefines.Keys.BType); this.model.setType((int) type); if (DEBUG) Timber.d("Setting type to: %s, Id: %s", this.model.getType(), entityId); } if (value.containsKey(BDefines.Keys.BName) && !value.get(BDefines.Keys.BName).equals("")) this.model.setName((String) value.get(BDefines.Keys.BName)); Long lastMessageAdded = 0L; Object o = value.get(BDefines.Keys.BLastMessageAdded); if (o instanceof Long) lastMessageAdded = (Long) o; else if (o instanceof Double) lastMessageAdded = ((Double) o).longValue(); if (lastMessageAdded != null && lastMessageAdded > 0) { Date date = new Date(lastMessageAdded); if (this.model.getLastMessageAdded() == null || date.getTime() > this.model.getLastMessageAdded() .getTime()) this.model.setLastMessageAdded(date); } this.model.setImageUrl((String) value.get(BDefines.Keys.BImageUrl)); this.model.setCreatorEntityId((String) value.get(BDefines.Keys.BCreatorEntityId)); DaoCore.updateEntity(this.model); } /** * Push the thread to firebase. **/ public Promise<BThread, BError, Void> push(){ if (DEBUG) Timber.v("push"); final DeferredObject<BThread, BError, Void> deferred = new DeferredObject<>(); DatabaseReference ref = null; if (StringUtils.isNotEmpty(model.getEntityID())) { ref = FirebasePaths.threadRef(model.getEntityID()); } else { // Creating a new entry for this thread. ref = FirebasePaths.threadRef().push(); model.setEntityID(ref.getKey()); // Updating the database. DaoCore.updateEntity(model); } ref.updateChildren(serialize(), new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError firebaseError, DatabaseReference firebase) { if (firebaseError != null) { deferred.reject(getFirebaseError(firebaseError)); } else deferred.resolve(model); } }); return deferred.promise(); } /** * Add the thread from the given user threads ref. **/ public Promise<BThread, BError, Void> addUserWithEntityID(String entityId){ final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); DatabaseReference ref = FirebasePaths.threadRef(this.entityId) .child(BFirebaseDefines.Path.BUsersPath) .child(entityId); BUser user = DaoCore.fetchOrCreateEntityWithEntityID(BUser.class, entityId); Map<String, Object> values = new HashMap<String, Object>(); // If metaname is null the data wont be saved so we have to do so. values.put(BDefines.Keys.BName, (user.getMetaName() == null ? "no_name" : user.getMetaName())); ref.setValue(values, new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError firebaseError, DatabaseReference firebase) { if (firebaseError == null) deferred.resolve(BThreadWrapper.this.model); else deferred.reject(getFirebaseError(firebaseError)); } }); return deferred.promise(); } /** *Remove the thread from the given user threads ref. **/ public Promise<BThread, BError, Void> removeUserWithEntityID(String entityId){ final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); BUser user = DaoCore.fetchOrCreateEntityWithEntityID(BUser.class, entityId); DatabaseReference ref = FirebasePaths.threadRef(this.entityId).child(BFirebaseDefines.Path.BUsersPath).child(entityId); ref.removeValue(new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError firebaseError, DatabaseReference firebase) { if (firebaseError == null) deferred.resolve(BThreadWrapper.this.model); else deferred.reject(getFirebaseError(firebaseError)); } }); return deferred.promise(); } /** * Removing a user from thread. * If the thread is private the thread will be removed from the user thread ref. **/ public Promise<BThread, BError, Void> removeUser(final BUserWrapper user){ final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); removeUserWithEntityID(user.entityId).done(new DoneCallback<BThread>() { @Override public void onDone(BThread bThreadWrapper) { if (model.getType() == BThread.Type.Private) { removeUserWithEntityID(user.entityId).done(new DoneCallback<BThread>() { @Override public void onDone(BThread thread) { deferred.resolve(thread); } }).fail(new FailCallback<BError>() { @Override public void onFail(BError error) { deferred.reject(error); } }); } else deferred.resolve(BThreadWrapper.this.model); } }).fail(new FailCallback<BError>() { @Override public void onFail(BError error) { deferred.reject(error); } }); return deferred.promise(); } /** * Adding a user to the thread. * If the thread is private the thread will be added to the user thread ref. **/ public Promise<BThread, BError, Void> addUser(final BUserWrapper user){ final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); // Adding the user. addUserWithEntityID(user.entityId).done(new DoneCallback<BThread>() { @Override public void onDone(BThread bThreadWrapper) { // If the thread is private we are adding the thread to the user. if (model.getTypeSafely() == BThread.Type.Private) { user.addThreadWithEntityId(model.getEntityID()).done(new DoneCallback<BUserWrapper>() { @Override public void onDone(BUserWrapper bUserWrapper) { deferred.resolve(BThreadWrapper.this.model); } }).fail(new FailCallback<DatabaseError>() { @Override public void onFail(DatabaseError firebaseError) { deferred.reject(getFirebaseError(firebaseError)); } }); } else deferred.resolve(null); } }) .fail(new FailCallback<BError>() { @Override public void onFail(BError error) { deferred.reject(error); } }); return deferred.promise(); } }