/*
* Created by Itzik Braun on 12/3/2015.
* Copyright (c) 2015 deluge. All rights reserved.
*
* Last Modification at: 3/12/15 4:27 PM
*/
package com.braunster.chatsdk.network;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import com.braunster.chatsdk.R;
import com.braunster.chatsdk.Utils.Debug;
import com.braunster.chatsdk.Utils.ImageUtils;
import com.braunster.chatsdk.Utils.sorter.ThreadsItemSorter;
import com.braunster.chatsdk.Utils.sorter.ThreadsSorter;
import com.braunster.chatsdk.Utils.volley.VolleyUtils;
import com.braunster.chatsdk.adapter.abstracted.ChatSDKAbstractThreadsListAdapter;
import com.braunster.chatsdk.dao.BMessage;
import com.braunster.chatsdk.dao.BThread;
import com.braunster.chatsdk.dao.BThreadDao;
import com.braunster.chatsdk.dao.BUser;
import com.braunster.chatsdk.dao.core.DaoCore;
import com.braunster.chatsdk.dao.entities.BMessageEntity;
import com.braunster.chatsdk.interfaces.BPushHandler;
import com.braunster.chatsdk.interfaces.BUploadHandler;
import com.braunster.chatsdk.network.events.AbstractEventManager;
import com.braunster.chatsdk.object.BError;
import com.braunster.chatsdk.object.SaveImageProgress;
import com.google.android.gms.maps.model.LatLng;
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 java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import timber.log.Timber;
import static com.braunster.chatsdk.dao.entities.BMessageEntity.Type.IMAGE;
import static com.braunster.chatsdk.dao.entities.BMessageEntity.Type.LOCATION;
import static com.braunster.chatsdk.dao.entities.BMessageEntity.Type.TEXT;
import static com.braunster.chatsdk.network.BDefines.Prefs.AuthenticationID;
/**
* Created by braunster on 23/06/14.
*/
public abstract class AbstractNetworkAdapter {
private static final String TAG = AbstractNetworkAdapter.class.getSimpleName();
private static final boolean DEBUG = Debug.AbstractNetworkAdapter;
private boolean authenticated = false;
public static String provider = "";
public static String token = "";
protected Context context;
private AbstractEventManager eventManager;
public BUploadHandler uploadHandler;
public BPushHandler pushHandler;
public AbstractNetworkAdapter(Context context){
this.context = context;
}
public abstract Promise<Object, BError, Void> authenticateWithMap(Map<String, Object> details);
public abstract Promise<BUser, BError, Void> checkUserAuthenticated();
/** Send a password change request to the server.*/
public abstract Promise<Void, BError, Void> changePassword(String email, String oldPassword, String newPassword);
/** Send a reset email request to the server.*/
public abstract Promise<Void, BError, Void> sendPasswordResetMail(String email);
public abstract Promise<BUser, BError, Void> pushUser();
public abstract BUser currentUserModel();
public abstract void goOnline();
public abstract void goOffline();
public abstract void setUserOnline();
public abstract void setUserOffline();
public abstract void logout();
/*** Send a request to the server to get the online status of the user. */
public abstract Promise<Boolean, BError, Void> isOnline();
public abstract Promise<List<BUser>, BError, Void> getFollowers(String entityId);
public abstract Promise<List<BUser>, BError, Void> getFollows(String entityId);
public abstract Promise<Void, BError, Void> followUser(BUser userToFollow);
public abstract void unFollowUser(BUser userToUnfollow);
protected abstract Promise<BMessage, BError, BMessage> sendMessage(BMessage messages);
/**
* Preparing a text message,
* This is only the build part of the send from here the message will passed to "sendMessage" Method.
* From there the message will be uploaded to the server if the upload fails the message will be deleted from the local db.
* If the upload is successful we will update the message entity so the entityId given from the server will be saved.
* The message will be received before sending in the onMainFinished Callback with a Status that its in the sending process.
* When the message is fully sent the status will be changed and the onItem callback will be invoked.
* When done or when an error occurred the calling method will be notified.
*/
public Promise<BMessage, BError, BMessage> sendMessageWithText(String text, long threadId) {
final Deferred<BMessage, BError, BMessage> deferred = new DeferredObject<>();
final BMessage message = new BMessage();
message.setText(text);
message.setThreadDaoId(threadId);
message.setType(TEXT);
message.setBUserSender(currentUserModel());
message.setStatus(BMessageEntity.Status.SENDING);
message.setDelivered(BMessageEntity.Delivered.No);
final BMessage bMessage = DaoCore.createEntity(message);
// Setting the temporary time of the message to be just after the last message that
// was added to the thread.
// Using this method we are avoiding time differences between the server time and the
// device local time.
Date date = message.getThread().getLastMessageAdded();
if (date == null)
date = new Date();
message.setDate( new Date(date.getTime() + 1) );
DaoCore.updateEntity(message);
sendMessage(bMessage, deferred);
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if(!deferred.isResolved()){
deferred.notify(message);
}
}
}, 100);
return deferred.promise();
}
/**
* Preparing a location message,
* This is only the build part of the send from here the message will passed to "sendMessage" Method.
* From there the message will be uploaded to the server if the upload fails the message will be deleted from the local db.
* If the upload is successful we will update the message entity so the entityId given from the server will be saved.
* When done or when an error occurred the calling method will be notified.
*
* @param filePath is a String representation of a bitmap that contain the image of the location wanted.
* @param location is the Latitude and Longitude of the picked location.
* @param threadId the id of the thread that the message is sent to.
*/
public Promise<BMessage, BError, BMessage> sendMessageWithLocation(final String filePath, final LatLng location, long threadId) {
final Deferred<BMessage, BError, BMessage> deferred = new DeferredObject<>();
final BMessage message = new BMessage();
message.setThreadDaoId(threadId);
message.setType(LOCATION);
message.setStatus(BMessageEntity.Status.SENDING);
message.setDelivered(BMessageEntity.Delivered.No);
message.setBUserSender(currentUserModel());
message.setResourcesPath(filePath);
DaoCore.createEntity(message);
// Setting the temporary time of the message to be just after the last message that
// was added to the thread.
// Using this method we are avoiding time differences between the server time and the
// device local time.
Date date = message.getThread().getLastMessageAdded();
if (date == null)
date = new Date();
message.setDate( new Date(date.getTime() + 1) );
DaoCore.updateEntity(message);
Bitmap image = ImageUtils.getCompressed(message.getResourcesPath());
Bitmap thumbnail = ImageUtils.getCompressed(message.getResourcesPath(),
BDefines.ImageProperties.MAX_IMAGE_THUMBNAIL_SIZE,
BDefines.ImageProperties.MAX_IMAGE_THUMBNAIL_SIZE);
message.setImageDimensions(ImageUtils.getDimensionAsString(image));
uploadImage(image, thumbnail)
.progress(new ProgressCallback<SaveImageProgress>() {
@Override
public void onProgress(SaveImageProgress saveImageProgress) {
deferred.notify(message);
}
})
.done(new DoneCallback<String[]>() {
@Override
public void onDone(String[] url) {
// Add the LatLng data to the message and the image url and thumbnail url
message.setText(String.valueOf(location.latitude)
+ BDefines.DIVIDER
+ String.valueOf(location.longitude)
+ BDefines.DIVIDER + url[0]
+ BDefines.DIVIDER + url[1]
+ BDefines.DIVIDER + message.getImageDimensions());
DaoCore.updateEntity(message);
// Sending the message, After it was uploaded to the server we can delte the file.
sendMessage(message, deferred)
.done(new DoneCallback<BMessage>() {
@Override
public void onDone(BMessage message) {
new File(filePath).delete();
}
});
}
})
.fail(new FailCallback<BError>() {
@Override
public void onFail(BError error) {
deferred.reject(error);
new File(filePath).delete();
}
});
return deferred.promise();
}
/**
* Preparing an image message,
* This is only the build part of the send from here the message will passed to "sendMessage" Method.
* From there the message will be uploaded to the server if the upload fails the message will be deleted from the local db.
* If the upload is successful we will update the message entity so the entityId given from the server will be saved.
* When done or when an error occurred the calling method will be notified.
*
* @param filePath is a file that contain the image. For now the file will be decoded to a Base64 image representation.
* @param threadId the id of the thread that the message is sent to.
*/
public Promise<BMessage, BError, BMessage> sendMessageWithImage(final String filePath, long threadId) {
final Deferred<BMessage, BError, BMessage> deferred = new DeferredObject<>();
final BMessage message = new BMessage();
message.setThreadDaoId(threadId);
message.setType(IMAGE);
message.setBUserSender(currentUserModel());
message.setStatus(BMessageEntity.Status.SENDING);
message.setDelivered(BMessageEntity.Delivered.No);
DaoCore.createEntity(message);
// Setting the temporary time of the message to be just after the last message that
// was added to the thread.
// Using this method we are avoiding time differences between the server time and the
// device local time.
Date date = message.getThread().getLastMessageAdded();
if (date == null)
date = new Date();
message.setDate( new Date(date.getTime() + 1) );
message.setResourcesPath(filePath);
DaoCore.updateEntity(message);
Bitmap image = ImageUtils.getCompressed(message.getResourcesPath());
Bitmap thumbnail = ImageUtils.getCompressed(message.getResourcesPath(),
BDefines.ImageProperties.MAX_IMAGE_THUMBNAIL_SIZE,
BDefines.ImageProperties.MAX_IMAGE_THUMBNAIL_SIZE);
message.setImageDimensions(ImageUtils.getDimensionAsString(image));
VolleyUtils.getBitmapCache().put(
VolleyUtils.BitmapCache.getCacheKey(message.getResourcesPath()),
thumbnail);
uploadImage(image, thumbnail)
.progress(new ProgressCallback<SaveImageProgress>() {
@Override
public void onProgress(SaveImageProgress saveImageProgress) {
deferred.notify(message);
}
})
.done(new DoneCallback<String[]>() {
@Override
public void onDone(String[] url) {
message.setText(url[0] + BDefines.DIVIDER + url[1] + BDefines.DIVIDER + message.getImageDimensions());
DaoCore.updateEntity(message);
sendMessage(message, deferred);
}
})
.fail(new FailCallback<BError>() {
@Override
public void onFail(BError error) {
deferred.reject(error);
}
});
return deferred.promise();
}
private Deferred<BMessage, BError, BMessage> sendMessage(final BMessage message, final Deferred<BMessage, BError, BMessage> deferred){
sendMessage(message).done(new DoneCallback<BMessage>() {
@Override
public void onDone(BMessage message) {
message.setStatus(BMessage.Status.SENT);
message = DaoCore.updateEntity(message);
// deferred.notify(message);
deferred.resolve(message);
}
}).fail(new FailCallback<BError>() {
@Override
public void onFail(BError bError) {
message.setStatus(BMessage.Status.FAILED);
deferred.reject(bError);
}
});
return deferred;
}
public abstract Promise<List<BMessage>, Void, Void> loadMoreMessagesForThread(BThread thread);
public int getUnreadMessagesAmount(boolean onePerThread){
List<BThread> threads = currentUserModel().getThreads(BThread.Type.Private);
int count = 0;
for (BThread t : threads)
{
if (onePerThread)
{
if(!t.isLastMessageWasRead())
{
if (DEBUG) Timber.d("HasUnread, ThreadName: %s", t.displayName());
count++;
}
}
else
{
count += t.getUnreadMessagesAmount();
}
}
return count;
}
public abstract Promise<List<BUser>, BError, Integer> usersForIndex(String index, String value);
public static String processForQuery(String query){
return StringUtils.isBlank(query) ? "" : query.replace(" ", "").toLowerCase();
}
/**
* 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 succesfully 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.
*/
public abstract Promise<BThread, BError, Void> createThreadWithUsers(String name, List<BUser> users);
public Promise<BThread, BError, Void> createThreadWithUsers(String name, BUser... users) {
return createThreadWithUsers(name, Arrays.asList(users));
}
public abstract Promise<BThread, BError, Void> createPublicThreadWithName(String name);
public abstract Promise<Void, BError, Void> deleteThreadWithEntityID(String entityID);
public Promise<Void, BError, Void> deleteThread(BThread thread){
return deleteThreadWithEntityID(thread.getEntityID());
}
public List<BThread> threadsWithType(int threadType) {
if (currentUserModel() == null) {
if (DEBUG) Timber.e("threadsWithType, Current user is null");
return new ArrayList<>();
}
BUser currentUser = currentUserModel(), threadCreator;
// Get the thread list ordered desc by the last message added date.
List<BThread> threadsFromDB;
if (threadType == BThread.Type.Private)
{
if (DEBUG) Timber.d("threadItemsWithType, loading private.");
threadsFromDB = currentUserModel().getThreads(BThread.Type.Private);
}
else threadsFromDB = DaoCore.fetchEntitiesWithProperty(BThread.class, BThreadDao.Properties.Type, threadType);
List<BThread> threads = new ArrayList<BThread>();
if (threadType == BThread.Type.Public)
{
for (BThread thread : threadsFromDB)
if (thread.getTypeSafely() == BThread.Type.Public)
threads.add(thread);
}
else {
for (BThread thread : threadsFromDB) {
if (DEBUG) Timber.i("threadItemsWithType, ThreadID: %s, Deleted: %s", thread.getId(), thread.getDeleted());
if (thread.isDeleted())
continue;
if (thread.getMessagesWithOrder(DaoCore.ORDER_DESC).size() > 0)
{
threads.add(thread);
continue;
}
if (StringUtils.isNotBlank(thread.getCreatorEntityId()) && thread.getEntityID().equals(currentUser.getEntityID()))
{
threads.add(thread);
}
else
{
threadCreator = thread.getCreator();
if (threadCreator != null )
{
if (threadCreator.equals(currentUser) && thread.hasUser(currentUser))
{
threads.add(thread);
}
}
}
}
}
if (DEBUG) Timber.d("threadsWithType, Type: %s, Found on db: %s, Threads List Size: %s" + threadType, threadsFromDB.size(), threads.size());
Collections.sort(threads, new ThreadsSorter());
return threads;
}
public <E extends ChatSDKAbstractThreadsListAdapter.ThreadListItem> List<E> threadItemsWithType(int threadType, ChatSDKAbstractThreadsListAdapter.ThreadListItemMaker<E> itemMaker) {
if (DEBUG) Timber.v("threadItemsWithType, Type: %s", threadType);
if (currentUserModel() == null) {
if (DEBUG) Timber.e("threadItemsWithType, Current user is null");
return null;
}
BUser currentUser = currentUserModel(), threadCreator;
// Get the thread list ordered desc by the last message added date.
List<BThread> threadsFromDB;
if (threadType == BThread.Type.Private)
{
if (DEBUG) Timber.v("threadItemsWithType, loading private.");
threadsFromDB = currentUserModel().getThreads(BThread.Type.Private);
}
else threadsFromDB = DaoCore.fetchEntitiesWithProperty(BThread.class, BThreadDao.Properties.Type, threadType);
List<E> threads = new ArrayList<E>();
if (DEBUG) Timber.v("threadItemsWithType, size: " + threadsFromDB.size());
if (threadType == BThread.Type.Public)
{
for (BThread thread : threadsFromDB)
if (thread.getTypeSafely() == BThread.Type.Public)
threads.add(itemMaker.fromBThread(thread));
}
else {
for (BThread thread : threadsFromDB) {
if (DEBUG) Timber.i("threadItemsWithType, ThreadID: %s", thread.getId());
if (thread.isDeleted())
continue;
if (thread.getMessagesWithOrder(DaoCore.ORDER_DESC).size() > 0)
{
threads.add(itemMaker.fromBThread(thread));
continue;
}
else if (DEBUG) Timber.e("threadItemsWithType, Thread has no messages.");
if (StringUtils.isNotBlank(thread.getCreatorEntityId()) && thread.getEntityID().equals(currentUser.getEntityID()))
{
threads.add(itemMaker.fromBThread(thread));
}
else
{
threadCreator = thread.getCreator();
if (threadCreator != null )
{
if (DEBUG) Timber.d("thread has creator. Entity ID: %s", thread.getEntityID());
if (threadCreator.equals(currentUser) && thread.hasUser(currentUser))
{
if (DEBUG) Timber.d("Current user is the creator.");
threads.add(itemMaker.fromBThread(thread));
}
}
}
}
}
if (DEBUG) Timber.v("threadItemsWithType, Type: %s, Found on db: &s, Threads List Size: %s",
threadType, threadsFromDB.size(), threads.size());
Collections.sort(threads, new ThreadsItemSorter());
return threads;
}
/**
* Add given users list to the given thread.
*/
public abstract Promise<BThread, BError, Void> addUsersToThread(BThread thread, List<BUser> users);
/**
* Add given users list to the given thread.
*/
public Promise<BThread, BError, Void> addUsersToThread(BThread thread, BUser... users) {
return addUsersToThread(thread, Arrays.asList(users));
}
/**
* Remove given users list to the given thread.
*/
public abstract Promise<BThread, BError, Void> removeUsersFromThread(BThread thread, List<BUser> users);
/**
* Remove given users list to the given thread.
*/
public Promise<BThread, BError, Void> removeUsersFromThread(BThread thread, BUser... users) {
return removeUsersFromThread(thread, Arrays.asList(users));
}
public abstract Promise<BThread, BError, Void> pushThread(BThread thread);
public abstract String getServerURL();
public boolean backendlessEnabled(){
return StringUtils.isNotEmpty(context.getString(R.string.backendless_app_id)) && StringUtils.isNotEmpty(context.getString(R.string.backendless_secret_key));
}
public boolean facebookEnabled(){
return StringUtils.isNotEmpty(context.getString(R.string.facebook_id));
}
public boolean googleEnabled(){
return false;
}
public boolean twitterEnabled(){
return (StringUtils.isNotEmpty(context.getString(R.string.twitter_consumer_key))
&& StringUtils.isNotEmpty(context.getString(R.string.twitter_consumer_secret)))
||
(StringUtils.isNotEmpty(context.getString(R.string.twitter_access_token))
&& StringUtils.isNotEmpty(context.getString(R.string.twitter_access_token_secret)));
}
public void setEventManager(AbstractEventManager eventManager) {
this.eventManager = eventManager;
}
public AbstractEventManager getEventManager() {
return eventManager;
}
public void setUploadHandler(BUploadHandler uploadHandler) {
this.uploadHandler = uploadHandler;
}
public BUploadHandler getUploadHandler() {
return uploadHandler;
}
public void setPushHandler(BPushHandler pushHandler) {
this.pushHandler = pushHandler;
}
public BPushHandler getPushHandler() {
return pushHandler;
}
/**
* Indicator that the current user in the adapter is authenticated.
*/
public boolean isAuthenticated() {
return authenticated;
}
public abstract void setLastOnline(Date date);
/**
* Set the current status of the adapter to not authenticated.
* The status can be retrieved by calling "isAuthenticated".
*/
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
}
/**
* @return the current user contacts list.
**/
public List<BUser> getContacs() {
return currentUserModel().getContacts();
}
public Map<String, ?> getLoginInfo() {
return BNetworkManager.preferences.getAll();
}
/**
* @return the save auth id saved in the preference manager.
* The preference manager is initialized when the BNetworkManager.Init(context) is called.
*/
public String getCurrentUserAuthenticationId() {
return BNetworkManager.preferences.getString(AuthenticationID, "");
}
/**
* Currently supporting only string and integers. Long and other values can be added later on.
*/
public void setLoginInfo(Map<String, Object> values) {
SharedPreferences.Editor keyValuesEditor = BNetworkManager.preferences.edit();
for (String s : values.keySet()) {
if (values.get(s) instanceof Integer)
keyValuesEditor.putInt(s, (Integer) values.get(s));
else if (values.get(s) instanceof String)
keyValuesEditor.putString(s, (String) values.get(s));
else if (values.get(s) instanceof Boolean)
keyValuesEditor.putBoolean(s, (Boolean) values.get(s));
else if (DEBUG) Timber.e("Cant add this --> %s to the prefs.", values.get(s));
}
keyValuesEditor.apply();
}
public void addLoginInfoData(String key, Object value){
SharedPreferences.Editor keyValuesEditor = BNetworkManager.preferences.edit();
if (value instanceof Integer)
keyValuesEditor.putInt(key, (Integer) value);
else if (value instanceof String)
keyValuesEditor.putString(key, (String) value);
else if (DEBUG) Timber.e("Cant add this --> %s to the prefs.", value);
keyValuesEditor.apply();
}
public static Map<String, Object> getMap(String[] keys, Object...values){
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0 ; i < keys.length; i++){
// More values then keys entered.
if (i == values.length)
break;
map.put(keys[i], values[i]);
}
return map;
}
public Promise<String[], BError, SaveImageProgress> uploadImage(final Bitmap image, final Bitmap thumbnail) {
if(image == null || thumbnail == null) return rejectMultiple();
final Deferred<String[], BError, SaveImageProgress> deferred = new DeferredObject<String[], BError, SaveImageProgress>();
final String[] urls = new String[2];
uploadHandler.uploadFile(ImageUtils.getImageByteArray(image), "image.jpg", "image/jpeg")
.done(new DoneCallback<String>() {
@Override
public void onDone(String url) {
urls[0] = url;
uploadHandler.uploadFile(ImageUtils.getImageByteArray(thumbnail), "thumbnail.jpg", "image/jpeg")
.done(new DoneCallback<String>() {
@Override
public void onDone(String url) {
urls[1] = url;
deferred.resolve(urls);
}
})
.fail(new FailCallback<BError>() {
@Override
public void onFail(BError error) {
deferred.reject(error);
}
});
}
})
.fail(new FailCallback<BError>() {
@Override
public void onFail(BError error) {
deferred.reject(error);
}
});
return deferred.promise();
}
public Promise<String, BError, SaveImageProgress> uploadImageWithoutThumbnail(final Bitmap image) {
if(image == null) return reject();
final Deferred<String, BError, SaveImageProgress> deferred = new DeferredObject<String, BError, SaveImageProgress>();
uploadHandler.uploadFile(ImageUtils.getImageByteArray(image), "image.jpg", "image/jpeg")
.done(new DoneCallback<String>() {
@Override
public void onDone(String url) {
deferred.resolve(url);
}
})
.fail(new FailCallback<BError>() {
@Override
public void onFail(BError error) {
deferred.reject(error);
}
});
return deferred.promise();
}
private static Promise<String, BError, SaveImageProgress> reject(){
return new DeferredObject<String, BError, SaveImageProgress>().reject(new BError(BError.Code.NULL, "Image Is Null"));
}
private static Promise<String[], BError, SaveImageProgress> rejectMultiple(){
return new DeferredObject<String[], BError, SaveImageProgress>().reject(new BError(BError.Code.NULL, "Image Is Null"));
}
}