Commit 8876ac1e authored by Yusuke Iwaki's avatar Yusuke Iwaki

FIX #61 Merge branch 'upload_file' into develop

parents c4b60e82 9cf77f45
package chat.rocket.android.api;
import android.content.Context;
import bolts.Task;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.realm_helper.RealmHelper;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* MethodCall for uploading file.
*/
public class FileUploadingHelper extends MethodCallHelper {
public FileUploadingHelper(Context context, String serverConfigId) {
super(context, serverConfigId);
}
public FileUploadingHelper(RealmHelper realmHelper, DDPClientWraper ddpClient) {
super(realmHelper, ddpClient);
}
public Task<JSONObject> uploadRequest(String filename, long filesize, String mimeType,
String roomId) {
return call("slingshot/uploadRequest", TIMEOUT_MS, () -> new JSONArray()
.put("rocketchat-uploads")
.put(new JSONObject()
.put("name", filename)
.put("size", filesize)
.put("type", mimeType))
.put(new JSONObject().put("rid", roomId)))
.onSuccessTask(CONVERT_TO_JSON_OBJECT);
}
public Task<JSONObject> sendFileMessage(String roomId, String storageType, JSONObject fileObj) {
return call("sendFileMessage", TIMEOUT_MS, () -> new JSONArray()
.put(roomId)
.put(TextUtils.isEmpty(storageType) ? JSONObject.NULL : storageType)
.put(fileObj))
.onSuccessTask(CONVERT_TO_JSON_OBJECT);
}
public Task<JSONObject> ufsCreate(String filename, long filesize, String mimeType, String store,
String roomId) {
return call("ufsCreate", TIMEOUT_MS, () -> new JSONArray().put(new JSONObject()
.put("name", filename)
.put("size", filesize)
.put("type", mimeType)
.put("store", store)
.put("rid", roomId)
)).onSuccessTask(CONVERT_TO_JSON_OBJECT);
}
public Task<JSONObject> ufsComplete(String fileId, String token, String store) {
return call("ufsComplete", TIMEOUT_MS, () -> new JSONArray()
.put(fileId)
.put(store)
.put(token)
).onSuccessTask(CONVERT_TO_JSON_OBJECT);
}
}
......@@ -8,6 +8,7 @@ import chat.rocket.android.helper.CheckSum;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.ddp.Message;
import chat.rocket.android.model.ddp.PublicSetting;
import chat.rocket.android.model.ddp.RoomSubscription;
import chat.rocket.android.model.internal.MethodCall;
import chat.rocket.android.model.internal.Session;
......@@ -26,10 +27,10 @@ import org.json.JSONObject;
*/
public class MethodCallHelper {
private final Context context;
private final RealmHelper realmHelper;
private final DDPClientWraper ddpClient;
private static final long TIMEOUT_MS = 4000;
protected final Context context;
protected final RealmHelper realmHelper;
protected final DDPClientWraper ddpClient;
protected static final long TIMEOUT_MS = 4000;
@Deprecated
/**
......@@ -92,15 +93,15 @@ public class MethodCallHelper {
});
}
private interface ParamBuilder {
protected interface ParamBuilder {
JSONArray buildParam() throws JSONException;
}
private Task<String> call(String methodName, long timeout) {
protected final Task<String> call(String methodName, long timeout) {
return injectErrorHandler(executeMethodCall(methodName, null, timeout));
}
private Task<String> call(String methodName, long timeout, ParamBuilder paramBuilder) {
protected final Task<String> call(String methodName, long timeout, ParamBuilder paramBuilder) {
try {
final JSONArray params = paramBuilder.buildParam();
return injectErrorHandler(executeMethodCall(methodName,
......@@ -110,10 +111,10 @@ public class MethodCallHelper {
}
}
private static final Continuation<String, Task<JSONObject>> CONVERT_TO_JSON_OBJECT =
protected static final Continuation<String, Task<JSONObject>> CONVERT_TO_JSON_OBJECT =
task -> Task.forResult(new JSONObject(task.getResult()));
private static final Continuation<String, Task<JSONArray>> CONVERT_TO_JSON_ARRAY =
protected static final Continuation<String, Task<JSONArray>> CONVERT_TO_JSON_ARRAY =
task -> Task.forResult(new JSONArray(task.getResult()));
/**
......@@ -321,7 +322,7 @@ public class MethodCallHelper {
/**
* Send message object.
*/
public Task<JSONObject> sendMessage(final JSONObject messageJson) {
private Task<JSONObject> sendMessage(final JSONObject messageJson) {
return call("sendMessage", TIMEOUT_MS, () -> new JSONArray().put(messageJson))
.onSuccessTask(CONVERT_TO_JSON_OBJECT)
.onSuccessTask(task -> Task.forResult(Message.customizeJson(task.getResult())));
......@@ -334,4 +335,22 @@ public class MethodCallHelper {
return call("readMessages", TIMEOUT_MS, () -> new JSONArray().put(roomId))
.onSuccessTask(task -> Task.forResult(null));
}
public Task<Void> getPublicSettings() {
return call("public-settings/get", TIMEOUT_MS)
.onSuccessTask(CONVERT_TO_JSON_ARRAY)
.onSuccessTask(task -> {
final JSONArray settings = task.getResult();
for (int i = 0; i < settings.length(); i++) {
PublicSetting.customizeJson(settings.getJSONObject(i));
}
return realmHelper.executeTransaction(realm -> {
realm.delete(PublicSetting.class);
realm.createOrUpdateAllFromJson(PublicSetting.class, settings);
return null;
});
});
}
}
package chat.rocket.android.fragment.chatroom;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
......@@ -12,10 +14,13 @@ import android.support.v7.widget.RecyclerView;
import android.view.View;
import chat.rocket.android.R;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.fragment.chatroom.dialog.FileUploadProgressDialogFragment;
import chat.rocket.android.fragment.chatroom.dialog.UsersOfRoomDialogFragment;
import chat.rocket.android.helper.LoadMoreScrollListener;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.OnBackPressListener;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.helper.FileUploadHelper;
import chat.rocket.android.layouthelper.chatroom.MessageComposerManager;
import chat.rocket.android.layouthelper.chatroom.MessageListAdapter;
import chat.rocket.android.layouthelper.chatroom.PairedMessage;
......@@ -45,6 +50,8 @@ import org.json.JSONObject;
public class RoomFragment extends AbstractChatRoomFragment
implements OnBackPressListener, RealmModelListAdapter.OnItemClickListener<PairedMessage> {
private static final int RC_UPL = 0x12;
private String serverConfigId;
private RealmHelper realmHelper;
private String roomId;
......@@ -128,6 +135,7 @@ public class RoomFragment extends AbstractChatRoomFragment
setupSideMenu();
setupMessageComposer();
setupFileUploader();
}
@Override public void onItemClick(PairedMessage pairedMessage) {
......@@ -196,7 +204,7 @@ public class RoomFragment extends AbstractChatRoomFragment
final MessageComposer messageComposer =
(MessageComposer) rootView.findViewById(R.id.message_composer);
messageComposerManager = new MessageComposerManager(fabCompose, messageComposer);
messageComposerManager.setCallback(messageText ->
messageComposerManager.setSendMessageCallback(messageText ->
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(Message.class, new JSONObject()
.put("_id", UUID.randomUUID().toString())
......@@ -204,6 +212,43 @@ public class RoomFragment extends AbstractChatRoomFragment
.put("ts", System.currentTimeMillis())
.put("rid", roomId)
.put("msg", messageText))));
messageComposerManager.setVisibilityChangedListener(shown -> {
FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id.fab_upload_file);
if (shown) {
fab.hide();
} else {
fab.show();
}
});
}
private void setupFileUploader() {
rootView.findViewById(R.id.fab_upload_file).setOnClickListener(view -> {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent, "Select Picture to Upload"), RC_UPL);
});
}
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != RC_UPL || resultCode != Activity.RESULT_OK) {
return;
}
if (data == null || data.getData() == null) {
return;
}
String uplId = new FileUploadHelper(getContext(), realmHelper)
.requestUploading(roomId, data.getData());
if (!TextUtils.isEmpty(uplId)) {
FileUploadProgressDialogFragment.create(serverConfigId, roomId, uplId)
.show(getFragmentManager(), FileUploadProgressDialogFragment.class.getSimpleName());
} else {
//show error.
}
}
private void onRenderRoom(RoomSubscription roomSubscription) {
......
package chat.rocket.android.fragment.chatroom.dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import chat.rocket.android.R;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.internal.FileUploading;
import chat.rocket.android.realm_helper.RealmObjectObserver;
import chat.rocket.android.renderer.FileUploadingRenderer;
/**
* dialog fragment to display progress of file uploading.
*/
public class FileUploadProgressDialogFragment extends AbstractChatroomDialogFragment {
private String uplId;
private RealmObjectObserver<FileUploading> fileUploadingObserver;
public FileUploadProgressDialogFragment() {}
public static FileUploadProgressDialogFragment create(String serverConfigId,
String roomId, String uplId) {
Bundle args = new Bundle();
args.putString("serverConfigId", serverConfigId);
args.putString("roomId", roomId);
args.putString("uplId", uplId);
FileUploadProgressDialogFragment fragment = new FileUploadProgressDialogFragment();
fragment.setArguments(args);
return fragment;
}
@Override protected void handleArgs(@NonNull Bundle args) {
super.handleArgs(args);
uplId = args.getString("uplId");
}
@Override public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fileUploadingObserver = realmHelper
.createObjectObserver(realm -> realm.where(FileUploading.class).equalTo("uplId", uplId))
.setOnUpdateListener(this::onRenderFileUploadingState);
}
@Override protected int getLayout() {
return R.layout.dialog_file_uploading;
}
@Override protected void onSetupDialog() {
}
private void onRenderFileUploadingState(FileUploading state) {
if (state == null) {
return;
}
int syncstate = state.getSyncstate();
if (syncstate == SyncState.SYNCED) {
dismiss();
} else if (syncstate == SyncState.FAILED) {
Toast.makeText(getContext(), state.getError(), Toast.LENGTH_SHORT).show();
//TODO: prompt retry.
dismiss();
} else {
new FileUploadingRenderer(getContext(), state)
.progressInto((ProgressBar) getDialog().findViewById(R.id.progressBar))
.progressTextInto(
(TextView) getDialog().findViewById(R.id.txt_filesize_uploaded),
(TextView) getDialog().findViewById(R.id.txt_filesize_total));
}
}
@Override public void onResume() {
super.onResume();
fileUploadingObserver.sub();
}
@Override public void onPause() {
fileUploadingObserver.unsub();
super.onPause();
}
@Override public void onCancel(DialogInterface dialog) {
//TODO: should cancel uploading? or continue with showing notification with progress?
}
}
......@@ -3,6 +3,7 @@ package chat.rocket.android.fragment.server_config;
import android.support.design.widget.Snackbar;
import android.widget.TextView;
import chat.rocket.android.R;
import chat.rocket.android.RocketChatCache;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig;
......@@ -38,6 +39,10 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
final String hostname =
TextUtils.or(TextUtils.or(editor.getText(), editor.getHint()), "").toString();
RocketChatCache.get(getContext()).edit()
.putString(RocketChatCache.KEY_SELECTED_SERVER_CONFIG_ID, serverConfigId)
.apply();
RealmStore.getDefault().executeTransaction(
realm -> realm.createOrUpdateObjectFromJson(ServerConfig.class,
new JSONObject().put("serverConfigId", serverConfigId)
......
package chat.rocket.android.helper;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import chat.rocket.android.log.RCLog;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.ddp.PublicSetting;
import chat.rocket.android.model.internal.FileUploading;
import chat.rocket.android.realm_helper.RealmHelper;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.UUID;
import org.json.JSONObject;
/**
* utility class for uploading file.
*/
public class FileUploadHelper {
private final Context context;
private final RealmHelper realmHelper;
public FileUploadHelper(Context context, RealmHelper realmHelper) {
this.context = context;
this.realmHelper = realmHelper;
}
/**
* requestUploading file.
* returns id for observing progress.
*/
public @Nullable String requestUploading(String roomId, Uri uri) {
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
String filename = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
long filesize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
String mimeType = context.getContentResolver().getType(uri);
return insertRequestRecord(roomId, uri, filename, filesize, mimeType);
} else if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
String filename = uri.getLastPathSegment();
long filesize = detectFileSizeFor(uri);
String mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(uri.toString()));
return insertRequestRecord(roomId, uri, filename, filesize, mimeType);
}
}
return null;
}
private String insertRequestRecord(String roomId,
Uri uri, String filename, long filesize, String mimeType) {
final String uplId = UUID.randomUUID().toString();
final String storageType =
PublicSetting.getString(realmHelper, "FileUpload_Storage_Type", null);
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, new JSONObject()
.put("uplId", uplId)
.put("syncstate", SyncState.NOT_SYNCED)
.put("storageType", TextUtils.isEmpty(storageType) ? JSONObject.NULL : storageType)
.put("uri", uri.toString())
.put("filename", filename)
.put("filesize", filesize)
.put("mimeType", mimeType)
.put("roomId", roomId)
.put("error", JSONObject.NULL)
)
).continueWith(new LogcatIfError());
return uplId;
}
private long detectFileSizeFor(Uri uri) {
ParcelFileDescriptor pfd = null;
try {
pfd = context.getContentResolver().openFileDescriptor(uri, "r");
return Math.max(pfd.getStatSize(), 0);
} catch (final FileNotFoundException exception) {
RCLog.w(exception);
} finally {
if (pfd != null) {
try {
pfd.close();
} catch (final IOException e) {
// Do nothing.
}
}
}
return -1;
}
}
......@@ -8,8 +8,18 @@ import okhttp3.OkHttpClient;
* Helper class for OkHttp client.
*/
public class OkHttpHelper {
private static OkHttpClient sHttpClientForUplFile;
private static OkHttpClient sHttpClientForWS;
public static OkHttpClient getClientForUploadFile() {
if (sHttpClientForUplFile == null) {
sHttpClientForUplFile = new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor())
.build();
}
return sHttpClientForUplFile;
}
/**
* acquire OkHttpClient instance for WebSocket connection.
*/
......
......@@ -8,13 +8,18 @@ import chat.rocket.android.widget.message.MessageComposer;
* handling visibility of FAB-compose and MessageComposer.
*/
public class MessageComposerManager {
public interface Callback {
public interface SendMessageCallback {
Task<Void> onSubmit(String messageText);
}
public interface VisibilityChangedListener {
void onVisibilityChanged(boolean shown);
}
private final FloatingActionButton fabCompose;
private final MessageComposer messageComposer;
private Callback callback;
private SendMessageCallback sendMessageCallback;
private VisibilityChangedListener visibilityChangedListener;
public MessageComposerManager(FloatingActionButton fabCompose, MessageComposer messageComposer) {
this.fabCompose = fabCompose;
......@@ -29,9 +34,9 @@ public class MessageComposerManager {
messageComposer.setOnActionListener(new MessageComposer.ActionListener() {
@Override public void onSubmit(String message) {
if (callback != null) {
if (sendMessageCallback != null) {
messageComposer.setEnabled(false);
callback.onSubmit(message).onSuccess(task -> {
sendMessageCallback.onSubmit(message).onSuccess(task -> {
clearComposingText();
return null;
}).continueWith(task -> {
......@@ -49,8 +54,12 @@ public class MessageComposerManager {
setMessageComposerVisibility(false);
}
public void setCallback(Callback callback) {
this.callback = callback;
public void setSendMessageCallback(SendMessageCallback sendMessageCallback) {
this.sendMessageCallback = sendMessageCallback;
}
public void setVisibilityChangedListener(VisibilityChangedListener listener) {
this.visibilityChangedListener = listener;
}
public void clearComposingText() {
......@@ -60,9 +69,18 @@ public class MessageComposerManager {
private void setMessageComposerVisibility(boolean show) {
if (show) {
fabCompose.hide();
messageComposer.show(null);
messageComposer.show(() -> {
if (visibilityChangedListener != null) {
visibilityChangedListener.onVisibilityChanged(true);
}
});
} else {
messageComposer.hide(fabCompose::show);
messageComposer.hide(() -> {
fabCompose.show();
if (visibilityChangedListener != null) {
visibilityChangedListener.onVisibilityChanged(false);
}
});
}
}
......
package chat.rocket.android.model.ddp;
import android.support.annotation.Nullable;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
import org.json.JSONException;
import org.json.JSONObject;
/**
* public setting model.
*/
@SuppressWarnings({"PMD.ShortClassName", "PMD.ShortVariable",
"PMD.MethodNamingConventions", "PMD.VariableNamingConventions"})
public class PublicSetting extends RealmObject {
@PrimaryKey private String _id;
private String group;
private String type;
private String value; //any type is available...!
private long _updatedAt;
private String meta; //JSON
public String get_id() {
return _id;
}
public void set_id(String _id) {
this._id = _id;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public long get_updatedAt() {
return _updatedAt;
}
public void set_updatedAt(long _updatedAt) {
this._updatedAt = _updatedAt;
}
public String getMeta() {
return meta;
}
public void setMeta(String meta) {
this.meta = meta;
}
public static JSONObject customizeJson(JSONObject settingJson) throws JSONException {
if (!settingJson.isNull("_updatedAt")) {
long updatedAt = settingJson.getJSONObject("_updatedAt").getLong("$date");
settingJson.remove("_updatedAt");
settingJson.put("_updatedAt", updatedAt);
}
return settingJson;
}
private static @Nullable PublicSetting get(RealmHelper realmHelper, String _id) {
return realmHelper.executeTransactionForRead(realm ->
realm.where(PublicSetting.class).equalTo("_id", _id).findFirst());
}
public static @Nullable String getString(RealmHelper realmHelper,
String _id, String defaultValue) {
PublicSetting setting = get(realmHelper, _id);
if (setting != null) {
return setting.getValue();
}
return defaultValue;
}
public static @Nullable boolean getBoolean(RealmHelper realmHelper,
String _id, boolean defaultValue) {
PublicSetting setting = get(realmHelper, _id);
if (setting != null) {
return Boolean.parseBoolean(setting.getValue());
}
return defaultValue;
}
}
package chat.rocket.android.model.internal;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
/**
* holding statuses for uploading file.
*/
public class FileUploading extends RealmObject {
public static final String STORAGE_TYPE_S3 = "AmazonS3";
public static final String STORAGE_TYPE_GRID_FS = "GridFS";
public static final String STORAGE_TYPE_FILE_SYSTEM = "FileSystem";
@PrimaryKey private String uplId;
private int syncstate;
private String storageType;
private String uri;
private String filename;
private long filesize;
private String mimeType;
private String roomId;
private long uploadedSize;
private String error;
public String getUplId() {
return uplId;
}
public void setUplId(String uplId) {
this.uplId = uplId;
}
public int getSyncstate() {
return syncstate;
}
public void setSyncstate(int syncstate) {
this.syncstate = syncstate;
}
public String getStorageType() {
return storageType;
}
public void setStorageType(String storageType) {
this.storageType = storageType;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public long getFilesize() {
return filesize;
}
public void setFilesize(long filesize) {
this.filesize = filesize;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public String getRoomId() {
return roomId;
}
public void setRoomId(String roomId) {
this.roomId = roomId;
}
public long getUploadedSize() {
return uploadedSize;
}
public void setUploadedSize(long uploadedSize) {
this.uploadedSize = uploadedSize;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
package chat.rocket.android.renderer;
import android.content.Context;
import android.widget.ProgressBar;
import android.widget.TextView;
import chat.rocket.android.model.internal.FileUploading;
/**
* rendering FileUploading status.
*/
public class FileUploadingRenderer extends AbstractRenderer<FileUploading> {
public FileUploadingRenderer(Context context, FileUploading object) {
super(context, object);
}
public FileUploadingRenderer progressInto(ProgressBar progressBar) {
if (!shouldHandle(progressBar)) {
return this;
}
if (object.getFilesize() >= Integer.MAX_VALUE) {
int max = 1000;
int progress = (int) (max * object.getUploadedSize() / object.getFilesize());
progressBar.setProgress(progress);
progressBar.setMax(max);
} else {
progressBar.setProgress((int) object.getUploadedSize());
progressBar.setMax((int) object.getFilesize());
}
return this;
}
public FileUploadingRenderer progressTextInto(TextView uploadedSizeText, TextView totalSizeText) {
if (!shouldHandle(uploadedSizeText) || !shouldHandle(totalSizeText)) {
return this;
}
long uploaded = object.getUploadedSize();
long total = object.getFilesize();
if (total < 50 * 1024) { //<50KB
uploadedSizeText.setText(String.format("%,d", uploaded));
totalSizeText.setText(String.format("%,d", total));
} else if (total < 8 * 1048576) { //<8MB
uploadedSizeText.setText(String.format("%,d", uploaded/1024));
totalSizeText.setText(String.format("%,d KB", total/1024));
} else {
uploadedSizeText.setText(String.format("%,d", uploaded/1048576));
totalSizeText.setText(String.format("%,d MB", total/1048576));
}
return this;
}
}
......@@ -19,11 +19,13 @@ import chat.rocket.android.service.ddp.base.LoginServiceConfigurationSubscriber;
import chat.rocket.android.service.ddp.base.UserDataSubscriber;
import chat.rocket.android.service.observer.CurrentUserObserver;
import chat.rocket.android.service.observer.GetUsersOfRoomsProcedureObserver;
import chat.rocket.android.service.observer.FileUploadingWithUfsObserver;
import chat.rocket.android.service.observer.LoadMessageProcedureObserver;
import chat.rocket.android.service.observer.MethodCallObserver;
import chat.rocket.android.service.observer.NewMessageObserver;
import chat.rocket.android.service.observer.NotificationItemObserver;
import chat.rocket.android.service.observer.ReactiveNotificationManager;
import chat.rocket.android.service.observer.FileUploadingToS3Observer;
import chat.rocket.android.service.observer.SessionObserver;
import chat.rocket.android.service.observer.TokenLoginObserver;
import chat.rocket.android_ddp.DDPClientCallback;
......@@ -49,7 +51,9 @@ public class RocketChatWebSocketThread extends HandlerThread {
NewMessageObserver.class,
CurrentUserObserver.class,
ReactiveNotificationManager.class,
NotificationItemObserver.class
NotificationItemObserver.class,
FileUploadingToS3Observer.class,
FileUploadingWithUfsObserver.class
};
private final Context appContext;
private final String serverConfigId;
......
package chat.rocket.android.service.observer;
import android.content.Context;
import android.net.Uri;
import bolts.Task;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.api.FileUploadingHelper;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.OkHttpHelper;
import chat.rocket.android.log.RCLog;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.internal.FileUploading;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.Realm;
import io.realm.RealmResults;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* execute file uploading and requesting sendMessage with attachment.
*/
public class FileUploadingToS3Observer extends AbstractModelObserver<FileUploading> {
private FileUploadingHelper methodCall;
public FileUploadingToS3Observer(Context context, String hostname,
RealmHelper realmHelper, DDPClientWraper ddpClient) {
super(context, hostname, realmHelper, ddpClient);
methodCall = new FileUploadingHelper(realmHelper, ddpClient);
realmHelper.executeTransaction(realm -> {
// resume pending operations.
RealmResults<FileUploading> pendingUploadRequests = realm.where(FileUploading.class)
.equalTo("syncstate", SyncState.SYNCING)
.equalTo("storageType", FileUploading.STORAGE_TYPE_S3)
.findAll();
for (FileUploading req : pendingUploadRequests) {
req.setSyncstate(SyncState.NOT_SYNCED);
}
// clean up records.
realm.where(FileUploading.class)
.beginGroup()
.equalTo("syncstate", SyncState.SYNCED)
.or()
.equalTo("syncstate", SyncState.FAILED)
.endGroup()
.equalTo("storageType", FileUploading.STORAGE_TYPE_S3)
.findAll().deleteAllFromRealm();
return null;
}).continueWith(new LogcatIfError());
}
@Override public RealmResults<FileUploading> queryItems(Realm realm) {
return realm.where(FileUploading.class)
.equalTo("syncstate", SyncState.NOT_SYNCED)
.equalTo("storageType", FileUploading.STORAGE_TYPE_S3)
.findAll();
}
@Override public void onUpdateResults(List<FileUploading> results) {
if (results.isEmpty()) {
return;
}
List<FileUploading> uploadingList = realmHelper.executeTransactionForReadResults(realm ->
realm.where(FileUploading.class).equalTo("syncstate", SyncState.SYNCING).findAll());
if (uploadingList.size() >= 3) {
// do not upload more than 3 files simultaneously
return;
}
FileUploading fileUploading = results.get(0);
final String roomId = fileUploading.getRoomId();
final String uplId = fileUploading.getUplId();
final String filename = fileUploading.getFilename();
final long filesize = fileUploading.getFilesize();
final String mimeType = fileUploading.getMimeType();
final Uri fileUri = Uri.parse(fileUploading.getUri());
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, new JSONObject()
.put("uplId", uplId)
.put("syncstate", SyncState.SYNCING)
)
).onSuccessTask(_task -> methodCall.uploadRequest(filename, filesize, mimeType, roomId)
).onSuccessTask(task -> {
final JSONObject info = task.getResult();
final String uploadUrl = info.getString("upload");
final String downloadUrl = info.getString("download");
final JSONArray postDataList = info.getJSONArray("postData");
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
for (int i = 0; i < postDataList.length(); i++) {
JSONObject postData = postDataList.getJSONObject(i);
bodyBuilder.addFormDataPart(postData.getString("name"), postData.getString("value"));
}
bodyBuilder.addFormDataPart("file", filename,
new RequestBody() {
private long numBytes = 0;
@Override public MediaType contentType() {
return MediaType.parse(mimeType);
}
@Override public long contentLength() throws IOException {
return filesize;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
InputStream inputStream = context.getContentResolver().openInputStream(fileUri);
try (Source source = Okio.source(inputStream)) {
long readBytes;
while ((readBytes = source.read(sink.buffer(), 8192)) > 0) {
numBytes += readBytes;
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, new JSONObject()
.put("uplId", uplId)
.put("uploadedSize", numBytes)))
.continueWith(new LogcatIfError());
}
}
}
});
Request request = new Request.Builder()
.url(uploadUrl)
.post(bodyBuilder.build())
.build();
Response response = OkHttpHelper.getClientForUploadFile().newCall(request).execute();
if (response.isSuccessful()) {
return Task.forResult(downloadUrl);
} else {
return Task.forError(new Exception(response.message()));
}
}).onSuccessTask(task -> {
String downloadUrl = task.getResult();
return methodCall.sendFileMessage(roomId, "s3", new JSONObject()
.put("_id", Uri.parse(downloadUrl).getLastPathSegment())
.put("type", mimeType)
.put("size", filesize)
.put("name", filename)
.put("url", downloadUrl)
);
}).onSuccessTask(task -> realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, new JSONObject()
.put("uplId", uplId)
.put("syncstate", SyncState.SYNCED)
.put("error", JSONObject.NULL)
)
)).continueWithTask(task -> {
if (task.isFaulted()) {
RCLog.w(task.getError());
return realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, new JSONObject()
.put("uplId", uplId)
.put("syncstate", SyncState.FAILED)
.put("error", task.getError().getMessage())
));
} else {
return Task.forResult(null);
}
});
}
}
package chat.rocket.android.service.observer;
import android.content.Context;
import android.net.Uri;
import bolts.Task;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.api.FileUploadingHelper;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.OkHttpHelper;
import chat.rocket.android.log.RCLog;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.ddp.User;
import chat.rocket.android.model.internal.FileUploading;
import chat.rocket.android.model.internal.Session;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.Realm;
import io.realm.RealmResults;
import java.io.InputStream;
import java.util.List;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
/**
* execute file uploading and requesting sendMessage with attachment.
*/
public class FileUploadingWithUfsObserver extends AbstractModelObserver<FileUploading> {
private FileUploadingHelper methodCall;
public FileUploadingWithUfsObserver(Context context, String hostname,
RealmHelper realmHelper, DDPClientWraper ddpClient) {
super(context, hostname, realmHelper, ddpClient);
methodCall = new FileUploadingHelper(realmHelper, ddpClient);
realmHelper.executeTransaction(realm -> {
// resume pending operations.
RealmResults<FileUploading> pendingUploadRequests = realm.where(FileUploading.class)
.equalTo("syncstate", SyncState.SYNCING)
.beginGroup()
.equalTo("storageType", FileUploading.STORAGE_TYPE_GRID_FS)
.or()
.equalTo("storageType", FileUploading.STORAGE_TYPE_FILE_SYSTEM)
.endGroup()
.findAll();
for (FileUploading req : pendingUploadRequests) {
req.setSyncstate(SyncState.NOT_SYNCED);
}
// clean up records.
realm.where(FileUploading.class)
.beginGroup()
.equalTo("syncstate", SyncState.SYNCED)
.or()
.equalTo("syncstate", SyncState.FAILED)
.endGroup()
.beginGroup()
.equalTo("storageType", FileUploading.STORAGE_TYPE_GRID_FS)
.or()
.equalTo("storageType", FileUploading.STORAGE_TYPE_FILE_SYSTEM)
.endGroup()
.findAll().deleteAllFromRealm();
return null;
}).continueWith(new LogcatIfError());
}
@Override public RealmResults<FileUploading> queryItems(Realm realm) {
return realm.where(FileUploading.class)
.equalTo("syncstate", SyncState.NOT_SYNCED)
.beginGroup()
.equalTo("storageType", FileUploading.STORAGE_TYPE_GRID_FS)
.or()
.equalTo("storageType", FileUploading.STORAGE_TYPE_FILE_SYSTEM)
.endGroup()
.findAll();
}
@Override public void onUpdateResults(List<FileUploading> results) {
if (results.isEmpty()) {
return;
}
List<FileUploading> uploadingList = realmHelper.executeTransactionForReadResults(realm ->
realm.where(FileUploading.class).equalTo("syncstate", SyncState.SYNCING).findAll());
if (uploadingList.size() >= 1) {
// do not upload multiple files simultaneously
return;
}
User currentUser = realmHelper.executeTransactionForRead(realm ->
User.queryCurrentUser(realm).findFirst());
Session session = realmHelper.executeTransactionForRead(realm ->
Session.queryDefaultSession(realm).findFirst());
if (currentUser == null || session == null) {
return;
}
final String cookie = String.format("rc_uid=%s; rc_token=%s",
currentUser.get_id(), session.getToken());
FileUploading fileUploading = results.get(0);
final String roomId = fileUploading.getRoomId();
final String uplId = fileUploading.getUplId();
final String filename = fileUploading.getFilename();
final long filesize = fileUploading.getFilesize();
final String mimeType = fileUploading.getMimeType();
final Uri fileUri = Uri.parse(fileUploading.getUri());
final String store = FileUploading.STORAGE_TYPE_GRID_FS.equals(fileUploading.getStorageType())
? "rocketchat_uploads"
: (FileUploading.STORAGE_TYPE_FILE_SYSTEM.equals(fileUploading.getStorageType())
? "fileSystem" : null);
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, new JSONObject()
.put("uplId", uplId)
.put("syncstate", SyncState.SYNCING)
)
).onSuccessTask(_task -> methodCall.ufsCreate(filename, filesize, mimeType, store, roomId)
).onSuccessTask(task -> {
final JSONObject info = task.getResult();
final String fileId = info.getString("fileId");
final String token = info.getString("token");
final String url = info.getString("url");
final int bufSize = 16384; //16KB
final byte[] buffer = new byte[bufSize];
int offset = 0;
final MediaType contentType = MediaType.parse(mimeType);
try (InputStream inputStream = context.getContentResolver().openInputStream(fileUri)) {
int read;
while ((read = inputStream.read(buffer)) > 0) {
offset += read;
double progress = 1.0 * offset / filesize;
Request request = new Request.Builder()
.url(url + "&progress=" + progress)
.header("Cookie", cookie)
.post(RequestBody.create(contentType, buffer, 0, read))
.build();
Response response = OkHttpHelper.getClientForUploadFile().newCall(request).execute();
if (response.isSuccessful()) {
final JSONObject obj = new JSONObject()
.put("uplId", uplId)
.put("uploadedSize", offset);
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, obj));
} else {
return Task.forError(new Exception(response.message()));
}
}
}
return methodCall.ufsComplete(fileId, token, store);
}).onSuccessTask(task -> methodCall.sendFileMessage(roomId, null, task.getResult())
).onSuccessTask(task -> realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, new JSONObject()
.put("uplId", uplId)
.put("syncstate", SyncState.SYNCED)
.put("error", JSONObject.NULL)
)
)).continueWithTask(task -> {
if (task.isFaulted()) {
RCLog.w(task.getError());
return realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(FileUploading.class, new JSONObject()
.put("uplId", uplId)
.put("syncstate", SyncState.FAILED)
.put("error", task.getError().getMessage())
));
} else {
return Task.forResult(null);
}
});
}
}
......@@ -2,7 +2,9 @@ package chat.rocket.android.service.observer;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.model.ddp.PublicSetting;
import chat.rocket.android.model.internal.GetUsersOfRoomsProcedure;
import chat.rocket.android.model.internal.LoadMessageProcedure;
import chat.rocket.android.model.internal.MethodCall;
......@@ -62,6 +64,8 @@ public class SessionObserver extends AbstractModelObserver<Session> {
@DebugLog private void onLogin() {
streamNotifyMessage.register();
new MethodCallHelper(realmHelper, ddpClient).getPublicSettings()
.continueWith(new LogcatIfError());
}
@DebugLog private void onLogout() {
......@@ -69,6 +73,7 @@ public class SessionObserver extends AbstractModelObserver<Session> {
realmHelper.executeTransaction(realm -> {
// remove all tables. ONLY INTERNAL TABLES!.
realm.delete(PublicSetting.class);
realm.delete(MethodCall.class);
realm.delete(LoadMessageProcedure.class);
realm.delete(GetUsersOfRoomsProcedure.class);
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:alpha="0.78">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="2dp" />
<solid android:color="#12000000"/>
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="2dp" />
<solid android:color="@color/colorAccent_a40"/>
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="2dp" />
<solid android:color="@color/colorAccent"/>
</shape>
</clip>
</item>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/margin_16"
android:paddingEnd="@dimen/margin_16"
android:paddingBottom="@dimen/margin_16"
android:orientation="vertical"
>
<FrameLayout
android:id="@+id/room_user_titlebar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/file_uploading_title"
android:textAppearance="@style/TextAppearance.AppCompat.Title" />
</FrameLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/AppTheme.ProgressBar.Horizontal"
android:layout_marginTop="@dimen/margin_8"
android:layout_marginBottom="@dimen/margin_8"
tools:progress="12"
tools:max="120"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:paddingEnd="2dp"
>
<TextView
android:id="@+id/txt_filesize_uploaded"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="12"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="/"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
/>
<TextView
android:id="@+id/txt_filesize_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="120"
/>
</LinearLayout>
</LinearLayout>
\ No newline at end of file
......@@ -20,6 +20,16 @@
android:layout_gravity="bottom"
android:background="@android:color/white"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_upload_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom"
android:layout_margin="@dimen/margin_16"
app:fabSize="mini"
app:srcCompat="@drawable/ic_insert_photo_white_24dp"
/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_compose"
android:layout_width="wrap_content"
......
......@@ -18,6 +18,8 @@
<string name="resend">Resend</string>
<string name="discard">Discard</string>
<string name="file_uploading_title">Uploading...</string>
<string name="dialog_user_registration_email">Email</string>
<string name="dialog_user_registration_username">Username</string>
<string name="dialog_user_registration_password">Password</string>
......
......@@ -38,4 +38,9 @@
<item name="colorControlActivated">@color/colorAccentDark</item>
<item name="android:textColorHighlight">@color/colorAccent_a40</item>
</style>
<style name="AppTheme.ProgressBar.Horizontal" parent="Widget.AppCompat.ProgressBar.Horizontal">
<item name="android:minHeight">11dp</item>
<item name="android:progressDrawable">@drawable/progress_bar</item>
</style>
</resources>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment