/* * Created by Itzik Braun on 12/3/2015. * Copyright (c) 2015 deluge. All rights reserved. * * Last Modification at: 3/12/15 4:34 PM */ package com.braunster.androidchatsdk.firebaseplugin.firebase; import android.content.Context; import android.support.annotation.NonNull; import com.braunster.androidchatsdk.firebaseplugin.R; import com.braunster.androidchatsdk.firebaseplugin.firebase.backendless.ChatSDKReceiver; import com.braunster.androidchatsdk.firebaseplugin.firebase.wrappers.BMessageWrapper; import com.braunster.androidchatsdk.firebaseplugin.firebase.wrappers.BThreadWrapper; import com.braunster.androidchatsdk.firebaseplugin.firebase.wrappers.BUserWrapper; import com.braunster.chatsdk.Utils.Debug; import com.braunster.chatsdk.dao.BMessage; import com.braunster.chatsdk.dao.BThread; import com.braunster.chatsdk.dao.BUser; import com.braunster.chatsdk.dao.FollowerLink; import com.braunster.chatsdk.dao.core.DaoCore; import com.braunster.chatsdk.network.AbstractNetworkAdapter; import com.braunster.chatsdk.network.BDefines; import com.braunster.chatsdk.network.BFirebaseDefines; import com.braunster.chatsdk.object.BError; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; 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.ProgressCallback; import org.jdeferred.Promise; import org.jdeferred.impl.DeferredObject; import org.jdeferred.multiple.MasterDeferredObject; import org.jdeferred.multiple.MasterProgress; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import timber.log.Timber; import static com.braunster.chatsdk.network.BDefines.Keys; import static com.braunster.chatsdk.network.BDefines.Prefs; public class BChatcatNetworkAdapter extends BFirebaseNetworkAdapter { private static final String TAG = BChatcatNetworkAdapter.class.getSimpleName(); private static boolean DEBUG = Debug.BFirebaseNetworkAdapter; public BChatcatNetworkAdapter(Context context){ super(context); } public Promise<BUser, BError, Void> handleFAUser(final FirebaseUser authData){ if (DEBUG) Timber.v("handleFAUser"); final Deferred<BUser, BError, Void> deferred = new DeferredObject<>(); authingStatus = AuthStatus.HANDLING_F_USER; if (authData == null) { resetAuth(); // If the user isn't authenticated they'll need to login deferred.reject(new BError(BError.Code.SESSION_CLOSED)); } else { // Flag that the user has been authenticated setAuthenticated(true); // Save the authentication ID for the current user // Set the current user final Map<String, Object> loginInfoMap = new HashMap<String, Object>(); String aid = authData.getUid(); Timber.v("Uid: " + aid); loginInfoMap.put(Prefs.AuthenticationID, aid); String provider = AbstractNetworkAdapter.provider; if(provider.equals("")) { provider = getLoginInfo().get(Prefs.AccountTypeKey).toString(); loginInfoMap.put(Prefs.AccountTypeKey, (Integer.getInteger(provider))); } else { loginInfoMap.put(Prefs.AccountTypeKey, FirebasePaths.providerToInt(provider)); } Timber.v("Provider: " + provider); String token = AbstractNetworkAdapter.token; if(getLoginInfo().get(Prefs.TokenKey) != null && token.equals("")) token = getLoginInfo().get(Prefs.TokenKey).toString(); Timber.v("Token: " + token); loginInfoMap.put(Prefs.TokenKey, token); setLoginInfo(loginInfoMap); resetAuth(); // Doint a once() on the user to push its details to firebase. final BUserWrapper wrapper = BUserWrapper.initWithAuthData(authData); wrapper.once().then(new DoneCallback<BUser>() { @Override public void onDone(BUser bUser) { if (DEBUG) Timber.v("OnDone, user was pulled from firebase."); DaoCore.updateEntity(bUser); getEventManager().userOn(bUser); // TODO push a default image of the user to the cloud. if(!pushHandler.subscribeToPushChannel(wrapper.pushChannel())) { deferred.reject(new BError(BError.Code.BACKENDLESS_EXCEPTION)); } goOnline(); wrapper.push().done(new DoneCallback<BUser>() { @Override public void onDone(BUser u) { if (DEBUG) Timber.v("OnDone, user was pushed from firebase."); resetAuth(); deferred.resolve(u); } }).fail(new FailCallback<BError>() { @Override public void onFail(BError error) { resetAuth(); deferred.reject(error); } }); } }, new FailCallback<BError>() { @Override public void onFail(BError bError) { deferred.reject(bError); } }); } return deferred.promise(); } @Override public Promise<BUser, BError, Void> checkUserAuthenticated() { if (DEBUG) Timber.v("checkUserAuthenticatedWithCallback, %s", getLoginInfo().get(Prefs.AccountTypeKey)); final Deferred<BUser, BError, Void> deferred = new DeferredObject<>(); if (isAuthing()) { if (DEBUG) Timber.d("Already Authing!, Status: %s", authingStatus.name()); deferred.reject(BError.getError(BError.Code.AUTH_IN_PROCESS, "Cant run two auth in parallel")); } else { authingStatus = AuthStatus.CHECKING_IF_AUTH; if (!getLoginInfo().containsKey(Prefs.AccountTypeKey)) { if (DEBUG) Timber.d(TAG, "No account type key"); resetAuth(); deferred.reject(new BError(BError.Code.NO_LOGIN_INFO)); } FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser(); if (user!=null) { handleFAUser(user).done(new DoneCallback<BUser>() { @Override public void onDone(BUser bUser) { resetAuth(); deferred.resolve(bUser); } }).fail(new FailCallback<BError>() { @Override public void onFail(BError bError) { resetAuth(); deferred.reject(bError); } }); } else{ resetAuth(); deferred.reject(BError.getError(BError.Code.NO_AUTH_DATA, "No auth data found")); } } return deferred.promise(); } @Override public Promise<Void, BError, Void> changePassword(String email, String oldPassword, String newPassword){ final Deferred<Void, BError, Void> deferred = new DeferredObject<>(); FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser(); OnCompleteListener<Void> resultHandler = new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { if (task.isSuccessful()) { deferred.resolve(null); } else { deferred.reject(getFirebaseError(DatabaseError.fromException(task.getException()))); } } }; user.updatePassword(newPassword).addOnCompleteListener(resultHandler); return deferred.promise(); } @Override public Promise<Void, BError, Void> sendPasswordResetMail(String email){ final Deferred<Void, BError, Void> deferred = new DeferredObject<>(); OnCompleteListener<Void> resultHandler = new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { if (task.isSuccessful()) { if(DEBUG) Timber.v("Email sent"); deferred.resolve(null); } else { deferred.reject(getFirebaseError(DatabaseError.fromException(task.getException()))); } } }; FirebaseAuth.getInstance().sendPasswordResetEmail(email).addOnCompleteListener(resultHandler); return deferred.promise(); } @Override /** Unlike the iOS code the current user need to be saved before you call this method.*/ public Promise<BUser, BError, Void> pushUser() { return currentUser().push(); } @Override public BUser currentUserModel() { String authID = getCurrentUserAuthenticationId(); if (StringUtils.isNotEmpty(authID)) { BUser currentUser = DaoCore.fetchEntityWithEntityID(BUser.class, authID); if(DEBUG) { if (currentUser == null) Timber.e("Current user is null"); else if (StringUtils.isEmpty(currentUser.getEntityID())) Timber.e("Current user entity id is null"); } return currentUser; } if (DEBUG) Timber.e("getCurrentUserAuthenticationIdr is null"); return null; } public BUserWrapper currentUser(){ return BUserWrapper.initWithModel(currentUserModel()); } @Override public void logout() { BUser user = currentUserModel(); /* No need to logout from facebook due to the fact that the logout from facebook event will trigger this event. * The logout from fb is taking care of by the fb login button.*/ setAuthenticated(false); // Stop listening to user related alerts. (added message or thread.) getEventManager().userOff(user); // Removing the push channel if (user != null) pushHandler.unsubscribeToPushChannel(user.getPushChannel()); // Obtaining the simple login object from the ref. DatabaseReference ref = FirebasePaths.firebaseRef(); // Login out if (user != null) { DatabaseReference userOnlineRef = FirebasePaths.userOnlineRef(user.getEntityID()); userOnlineRef.setValue(false); } FirebaseAuth.getInstance().signOut(); } @Override public void setUserOnline() { BUser current = currentUserModel(); if (current != null && StringUtils.isNotEmpty(current.getEntityID())) { currentUser().goOnline(); } } @Override public void setUserOffline() { BUser current = currentUserModel(); if (current != null && StringUtils.isNotEmpty(current.getEntityID())) { currentUser().goOffline(); updateLastOnline(); } } @Override public void goOffline() { DatabaseReference.goOffline(); setUserOffline(); } @Override public void goOnline() { DatabaseReference.goOnline(); setUserOnline(); } public Promise<Boolean, BError, Void> isOnline(){ final Deferred<Boolean, BError, Void> deferred = new DeferredObject<>(); if (currentUserModel() == null) { return deferred.reject(BError.getError(BError.Code.NULL, "Current user is null")); } FirebasePaths.userOnlineRef(currentUserModel().getEntityID()).addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { updateLastOnline(); deferred.resolve((Boolean) snapshot.getValue()); } @Override public void onCancelled(DatabaseError firebaseError) { deferred.reject(getFirebaseError(firebaseError)); } }); return deferred.promise(); } /** Send a message, * The message need to have a owner thread attached to it or it cant be added. * If the destination thread is public the system will add the user to the message thread if needed. * The uploading to the server part can bee seen her {@see BFirebaseNetworkAdapter#PushMessageWithComplition}.*/ @Override public Promise<BMessage, BError, BMessage> sendMessage(final BMessage message){ if (DEBUG) Timber.v("sendMessage"); return new BMessageWrapper(message).send().done(new DoneCallback<BMessage>() { @Override public void onDone(BMessage message) { // Setting the time stamp for the last message added to the thread. DatabaseReference threadRef = FirebasePaths.threadRef(message.getThread().getEntityID()).child(BFirebaseDefines.Path.BDetailsPath); threadRef.updateChildren(FirebasePaths.getMap(new String[]{Keys.BLastMessageAdded}, ServerValue.TIMESTAMP)); // Pushing the message to all offline users. we cant push it before the message was // uploaded as the date is saved by the firebase server using the timestamp. pushForMessage(message); } }); } /** Indexing * To allow searching we're going to implement a simple index. Strings can be registered and * associated with users i.e. if there's a user called John Smith we could make a new index * like this: * * indexes/[index ID (priority is: johnsmith)]/[entity ID of John Smith] * * This will allow us to find the user*/ @Override public Promise<List<BUser>, BError, Integer> usersForIndex(final String index, final String value) { final Deferred<List<BUser>, BError, Integer> deferred = new DeferredObject<>(); if (StringUtils.isBlank(value)) { return deferred.reject(BError.getError(BError.Code.NULL, "Value is blank")); } Query query = FirebasePaths.indexRef().orderByChild(index).startAt( processForQuery(value)).limitToFirst(BFirebaseDefines.NumberOfUserToLoadForIndex); query.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { if (snapshot.getValue() != null) { Map<String, Objects> values = (Map<String, Objects>) snapshot.getValue(); final List<BUser> usersToGo = new ArrayList<BUser>(); List<String> keys = new ArrayList<String>(); // So we dont have to call the db for each key. String currentUserEntityID = currentUserModel().getEntityID(); // Adding all keys to the list, Except the current user key. for (String key : values.keySet()) if (!key.equals(currentUserEntityID)) keys.add(key); // Fetch or create users in the local db. BUser bUser; if (keys.size() > 0) { for (String entityID : keys) { // Making sure that we wont try to get users with a null object id in the index section // If we will try the query will never end and there would be no result from the index. if(StringUtils.isNotBlank(entityID) && !entityID.equals(Keys.BNull) && !entityID.equals("(null)")) { bUser = DaoCore.fetchOrCreateEntityWithEntityID(BUser.class, entityID); usersToGo.add(bUser); } } Promise[] promises = new Promise[keys.size()]; int count = 0; for (final BUser user : usersToGo) { final Deferred<BUser, BError, Integer> d = new DeferredObject<>(); BUserWrapper.initWithModel(user) .once() .done(new DoneCallback<BUser>() { @Override public void onDone(BUser bUser) { if (DEBUG) Timber.d("onDone, index: %s, Value: %s", index, value); // Notify that a user has been found. // Making sure the user due start with the wanted name if (processForQuery(bUser.metaStringForKey(index)).startsWith(processForQuery(value))) { d.resolve(bUser); } else { if (DEBUG) Timber.d("Not valid result, " + "index: %s, UserValue: %s Value: %s", index, bUser.metaStringForKey(index), value); // Remove the not valid user from the list. usersToGo.remove(user); d.resolve(null); } } }) .fail(new FailCallback<BError>() { @Override public void onFail(BError bError) { if (DEBUG) Timber.e("usersForIndex, onDoneWithError."); // Notify that an error occurred while selecting. d.reject(bError); } }); promises[count] = d.promise(); count++; } MasterDeferredObject masterDeferredObject = new MasterDeferredObject(promises); masterDeferredObject.progress(new ProgressCallback<MasterProgress>() { @Override public void onProgress(MasterProgress masterProgress) { if (DEBUG) Timber.d("MasterDeferredProgress, done: %s, failed: %s, total: %s", masterProgress.getDone(), masterProgress.getFail(), masterProgress.getTotal()); // Reject the promise if all promises failed. if (masterProgress.getFail() == masterProgress.getTotal()) { deferred.reject(BError.getError(BError.Code.OPERATION_FAILED, "All promises failed")); } // If all was done lets resolve the promise. else if (masterProgress.getFail() + masterProgress.getDone() == masterProgress.getTotal()) deferred.resolve(usersToGo); } }); } else deferred.reject(BError.getError(BError.Code.NO_USER_FOUND, "Unable to found user.")); } else { if (DEBUG) Timber.d("Value is null"); deferred.reject(BError.getError(BError.Code.NO_USER_FOUND, "Unable to found user.")); } } @Override public void onCancelled(DatabaseError firebaseError) { deferred.reject(getFirebaseError(firebaseError)); } }); return deferred.promise(); } @Override public Promise<List<BMessage>, Void, Void> loadMoreMessagesForThread(BThread thread) { return new BThreadWrapper(thread).loadMoreMessages(BFirebaseDefines.NumberOfMessagesPerBatch); } @Override public Promise<BThread, BError, Void> createPublicThreadWithName(String name) { final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); // Crating the new thread. // This thread would not be saved to the local db until it is successfully uploaded to the firebase server. final BThread thread = new BThread(); BUser curUser = currentUserModel(); thread.setCreator(curUser); thread.setCreatorEntityId(curUser.getEntityID()); thread.setType(BThread.Type.Public); thread.setName(name); // Add the path and API key // This allows you to restrict public threads to a particular // API key or root key thread.setRootKey(BDefines.BRootPath); thread.setApiKey(""); // Save the entity to the local db. DaoCore.createEntity(thread); BThreadWrapper wrapper = new BThreadWrapper(thread); wrapper.push() .done(new DoneCallback<BThread>() { @Override public void onDone(final BThread thread) { DaoCore.updateEntity(thread); if (DEBUG) Timber.d("public thread is pushed and saved."); // Add the thread to the list of public threads DatabaseReference publicThreadRef = FirebasePaths.publicThreadsRef() .child(thread.getEntityID()) .child("null"); publicThreadRef.setValue("", new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, DatabaseReference firebase) { if (error == null) deferred.resolve(thread); else { if (DEBUG) Timber.e("Unable to add thread to public thread ref."); DaoCore.deleteEntity(thread); deferred.reject(getFirebaseError(error)); } } }); } }) .fail(new FailCallback<BError>() { @Override public void onFail(BError error) { if (DEBUG) Timber.e("Failed to push thread to ref."); DaoCore.deleteEntity(thread); deferred.reject(error); } }); return deferred.promise(); } /** Create thread for given users. * When the thread is added to the server the "onMainFinished" will be invoked, * If an error occurred the error object would not be null. * For each user that was successfully added the "onItem" method will be called, * For any item adding failure the "onItemFailed will be called. * If the main task will fail the error object in the "onMainFinished" method will be called."*/ @Override public Promise<BThread, BError, Void> createThreadWithUsers(String name, final List<BUser> users) { final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); ThreadRecovery.checkForAndRecoverThreadWithUsers(users) .done(new DoneCallback<BThread>() { @Override public void onDone(final BThread thread) { deferred.resolve(thread); } }) .fail(new FailCallback<BError>() { @Override public void onFail(BError error) { // Didn't find a new thread so we create a new. final BThread thread = new BThread(); thread.setCreator(currentUserModel()); thread.setCreatorEntityId(currentUserModel().getEntityID()); // If we're assigning users then the thread is always going to be private thread.setType(BThread.Type.Private); // Save the thread to the database. DaoCore.createEntity(thread); DaoCore.connectUserAndThread(currentUserModel(),thread); updateLastOnline(); new BThreadWrapper(thread).push() .done(new DoneCallback<BThread>() { @Override public void onDone(BThread thread) { // Save the thread to the local db. DaoCore.updateEntity(thread); // Add users, For each added user the listener passed here will get a call. addUsersToThread(thread, users).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); } }); } }) .fail(new FailCallback<BError>() { @Override public void onFail(BError error) { // Delete the thread if failed to push DaoCore.deleteEntity(thread); deferred.reject(error); } }); } }); return deferred.promise(); } /** Add given users list to the given thread. * The RepetitiveCompletionListenerWithError will notify by his "onItem" method for each user that was successfully added. * In the "onItemFailed" you can get all users that the system could not add to the server. * When all users are added the system will call the "onDone" method.*/ @Override public Promise<BThread, BError, Void> addUsersToThread(final BThread thread, final List<BUser> users) { final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); if (thread == null) { if (DEBUG) Timber.e("addUsersToThread, Thread is null" ); return deferred.reject(new BError(BError.Code.NULL, "Thread is null")); } if (DEBUG) Timber.d("Users Amount: %s", users.size()); Promise[] promises = new Promise[users.size()]; BThreadWrapper threadWrapper = new BThreadWrapper(thread); int count = 0; for (BUser user : users){ // Add the user to the thread if (!user.hasThread(thread)) { DaoCore.connectUserAndThread(user, thread); } promises[count] = threadWrapper.addUser(BUserWrapper.initWithModel(user)); count++; } MasterDeferredObject masterDeferredObject = new MasterDeferredObject(promises); masterDeferredObject.progress(new ProgressCallback<MasterProgress>() { @Override public void onProgress(MasterProgress masterProgress) { if (masterProgress.getFail() + masterProgress.getDone() == masterProgress.getTotal()) { // Reject the promise if all promisses failed. if (masterProgress.getFail() == masterProgress.getTotal()) { deferred.reject(BError.getError(BError.Code.OPERATION_FAILED, "All promises failed")); } else deferred.resolve(thread); } } }); return deferred.promise(); } @Override public Promise<BThread, BError, Void> removeUsersFromThread(final BThread thread, List<BUser> users) { final Deferred<BThread, BError, Void> deferred = new DeferredObject<>(); if (thread == null) { if (DEBUG) Timber.e("addUsersToThread, Thread is null" ); return deferred.reject(new BError(BError.Code.NULL, "Thread is null")); } if (DEBUG) Timber.d("Users Amount: %s", users.size()); Promise[] promises = new Promise[users.size()]; BThreadWrapper threadWrapper = new BThreadWrapper(thread); int count = 0; for (BUser user : users){ // Breaking the connection in the internal database between the thread and the user. DaoCore.breakUserAndThread(user, thread); promises[count] = threadWrapper.removeUser(BUserWrapper.initWithModel(user)); count++; } MasterDeferredObject masterDeferredObject = new MasterDeferredObject(promises); masterDeferredObject.progress(new ProgressCallback<MasterProgress>() { @Override public void onProgress(MasterProgress masterProgress) { if (masterProgress.getFail() + masterProgress.getDone() == masterProgress.getTotal()) { // Reject the promise if all promisses failed. if (masterProgress.getFail() == masterProgress.getTotal()) { deferred.reject(null); } else deferred.resolve(thread); } } }); return deferred.promise(); } @Override public Promise<BThread, BError, Void> pushThread(BThread thread) { return new BThreadWrapper(thread).push(); } @Override public Promise<Void, BError, Void> deleteThreadWithEntityID(final String entityID) { final BThread thread = DaoCore.fetchEntityWithEntityID(BThread.class, entityID); BUser user = currentUserModel(); updateLastOnline(); return new BThreadWrapper(thread).deleteThread(); } @Override public Promise<Void, BError, Void> followUser(final BUser userToFollow) { if (!BDefines.EnableFollowers) throw new IllegalStateException("You need to enable followers in defines before you can use this method."); final Deferred<Void, BError, Void> deferred = new DeferredObject<>(); final BUser user = currentUserModel(); // Add the current user to the userToFollow "followers" path DatabaseReference userToFollowRef = FirebasePaths.userRef(userToFollow.getEntityID()) .child(BFirebaseDefines.Path.FollowerLinks) .child(user.getEntityID()); if (DEBUG) Timber.d("followUser, userToFollowRef: ", userToFollowRef.toString()); userToFollowRef.setValue("null", new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError firebaseError, DatabaseReference firebase) { if (firebaseError!=null) { deferred.reject(getFirebaseError(firebaseError)); } else { FollowerLink follows = user.fetchOrCreateFollower(userToFollow, FollowerLink.Type.FOLLOWS); user.addContact(userToFollow); // Add the user to follow to the current user follow DatabaseReference curUserFollowsRef = FirebasePaths.firebaseRef().child(follows.getBPath().getPath()); if (DEBUG) Timber.d("followUser, curUserFollowsRef: %s", curUserFollowsRef.toString()); curUserFollowsRef.setValue("null", new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError firebaseError, DatabaseReference firebase) { // Send a push to the user that is now followed. if (DEBUG) Timber.v("pushutils sendfollowpush"); JSONObject data = new JSONObject(); try { data.put(BDefines.Keys.ACTION, ChatSDKReceiver.ACTION_FOLLOWER_ADDED); data.put(BDefines.Keys.CONTENT, user.getMetaName() + " " + context.getString(R.string.not_follower_content)); // For iOS data.put(BDefines.Keys.BADGE, BDefines.Keys.INCREMENT); data.put(BDefines.Keys.ALERT, user.getMetaName() + " " + context.getString(R.string.not_follower_content)); // For making sound in iOS data.put(BDefines.Keys.SOUND, BDefines.Keys.Default); } catch (JSONException e) { e.printStackTrace(); } List<String> channels = new ArrayList<String>(); channels.add(userToFollow.getPushChannel()); pushHandler.pushToChannels(channels, data); deferred.resolve(null); } }); } } }); return deferred.promise(); } @Override public void unFollowUser(BUser userToUnfollow) { if (!BDefines.EnableFollowers) throw new IllegalStateException("You need to enable followers in defines before you can use this method."); final BUser user = currentUserModel(); // Remove the current user to the userToFollow "followers" path DatabaseReference userToFollowRef = FirebasePaths.userRef(userToUnfollow.getEntityID()) .child(BFirebaseDefines.Path.FollowerLinks) .child(user.getEntityID()); userToFollowRef.removeValue(); FollowerLink follows = user.fetchOrCreateFollower(userToUnfollow, FollowerLink.Type.FOLLOWS); // Add the user to follow to the current user follow DatabaseReference curUserFollowsRef = FirebasePaths.firebaseRef().child(follows.getBPath().getPath()); curUserFollowsRef.removeValue(); DaoCore.deleteEntity(follows); } @Override public Promise<List<BUser>, BError, Void> getFollowers(String entityId){ if (DEBUG) Timber.v("getFollowers, Id: %s", entityId); final Deferred<List<BUser>, BError, Void> deferred = new DeferredObject<>(); if (StringUtils.isEmpty(entityId)) { return deferred.reject(BError.getError(BError.Code.NULL, "Entity id is empty")); } final BUser user = DaoCore.fetchOrCreateEntityWithEntityID(BUser.class, entityId); DatabaseReference followersRef = FirebasePaths.userFollowersRef(entityId); followersRef.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { final List<BUser> followers = new ArrayList<BUser>(); for (DataSnapshot snap : snapshot.getChildren()) { String followingUserID = snap.getKey(); if (StringUtils.isNotEmpty(followingUserID)) { BUser follwer = DaoCore.fetchOrCreateEntityWithEntityID(BUser.class, followingUserID); FollowerLink f = user.fetchOrCreateFollower(follwer, FollowerLink.Type.FOLLOWER); followers.add(follwer); } else if (DEBUG) Timber.e("Follower id is empty"); } Promise[] promises= new Promise[followers.size()]; int count = 0; for (BUser u : followers) { promises[count] = BUserWrapper.initWithModel(u).once(); count++; } MasterDeferredObject masterDeferredObject = new MasterDeferredObject(promises); masterDeferredObject.progress(new ProgressCallback<MasterProgress>() { @Override public void onProgress(MasterProgress masterProgress) { if (DEBUG) Timber.d("MasterDeferredProgress, done: %s, failed: %s, total: %s", masterProgress.getDone(), masterProgress.getFail(), masterProgress.getTotal()); // Reject the promise if all promisses failed. if (masterProgress.getFail() == masterProgress.getTotal()) { deferred.reject(BError.getError(BError.Code.OPERATION_FAILED, "All promises failed")); } else deferred.resolve(followers); } }); } @Override public void onCancelled(DatabaseError firebaseError) { deferred.reject(getFirebaseError(firebaseError)); } }); return deferred.promise(); } @Override public Promise<List<BUser>, BError, Void> getFollows(String entityId){ if (DEBUG) Timber.v("getFollowers, Id: %s", entityId); final Deferred<List<BUser>, BError, Void> deferred = new DeferredObject<>(); if (StringUtils.isEmpty(entityId)) { return deferred.reject(BError.getError(BError.Code.NULL, "Entity id is empty")); } final BUser user = DaoCore.fetchOrCreateEntityWithEntityID(BUser.class, entityId); DatabaseReference followersRef = FirebasePaths.userFollowsRef(entityId); followersRef.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { final List<BUser> followers = new ArrayList<BUser>(); for (DataSnapshot snap : snapshot.getChildren()) { String followingUserID = snap.getKey(); if (StringUtils.isNotEmpty(followingUserID)) { BUser follwer = DaoCore.fetchOrCreateEntityWithEntityID(BUser.class, followingUserID); FollowerLink f = user.fetchOrCreateFollower(follwer, FollowerLink.Type.FOLLOWS); followers.add(follwer); } } Promise[] promises= new Promise[followers.size()]; int count = 0; for (BUser u : followers) { promises[count] = BUserWrapper.initWithModel(u).once(); count++; } MasterDeferredObject masterDeferredObject = new MasterDeferredObject(promises); masterDeferredObject.progress(new ProgressCallback<MasterProgress>() { @Override public void onProgress(MasterProgress masterProgress) { if (DEBUG) Timber.d("MasterDeferredProgress, done: %s, failed: %s, total: %s", masterProgress.getDone(), masterProgress.getFail(), masterProgress.getTotal()); // Reject the promise if all promisses failed. if (masterProgress.getFail() == masterProgress.getTotal()) { deferred.reject(BError.getError(BError.Code.OPERATION_FAILED, "All promises failed")); } else deferred.resolve(followers); } }); } @Override public void onCancelled(DatabaseError firebaseError) { deferred.reject(getFirebaseError(firebaseError)); } }); return deferred.promise(); } @Override public void setLastOnline(Date lastOnline) { BUser currentUser = currentUserModel(); currentUser.setLastOnline(lastOnline); DaoCore.updateEntity(currentUser); pushUser(); } private void updateLastOnline(){ // FIXME to implement? } }