Kaynağa Gözat

add ability to upload one or multiple files from local storage #254

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 4 yıl önce
ebeveyn
işleme
0b35971ee5

+ 0 - 2
app/build.gradle

@@ -60,8 +60,6 @@ android {
         lintOptions {
             disable 'InvalidPackage'
             disable 'MissingTranslation'
-            disable "ValidController"
-            disable "ValidControllerChangeHandler"
         }
 
         javaCompileOptions {

+ 9 - 3
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -20,6 +20,8 @@
  */
 package com.nextcloud.talk.api;
 
+import androidx.annotation.Nullable;
+
 import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
 import com.nextcloud.talk.models.json.chat.ChatOverall;
 import com.nextcloud.talk.models.json.conversations.RoomOverall;
@@ -39,7 +41,6 @@ import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
 import java.util.List;
 import java.util.Map;
 
-import androidx.annotation.Nullable;
 import io.reactivex.Observable;
 import okhttp3.RequestBody;
 import okhttp3.ResponseBody;
@@ -327,7 +328,6 @@ public interface NcApi {
     @PUT
     Observable<GenericOverall> setReadOnlyState(@Header("Authorization") String authorization, @Url String url, @Field("state") int state);
 
-
     @FormUrlEncoded
     @POST
     Observable<Void> createRemoteShare(@Nullable @Header("Authorization") String authorization, @Url String url,
@@ -338,15 +338,21 @@ public interface NcApi {
     @FormUrlEncoded
     @PUT
     Observable<GenericOverall> setLobbyForConversation(@Header("Authorization") String authorization,
-                                      @Url String url, @Field("state") Integer state,
+                                                       @Url String url, @Field("state") Integer state,
                                                        @Field("timer") Long timer);
 
     @POST
     Observable<GenericOverall> setReadStatusPrivacy(@Header("Authorization") String authorization,
                                                     @Url String url,
                                                     @Body RequestBody body);
+
     @POST
     Observable<ContactsByNumberOverall> searchContactsByPhoneNumber(@Header("Authorization") String authorization,
                                                                     @Url String url,
                                                                     @Body RequestBody search);
+
+    @PUT
+    Observable<Response<GenericOverall>> uploadFile(@Header("Authorization") String authorization,
+                                                    @Url String url,
+                                                    @Body RequestBody body);
 }

+ 91 - 23
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -20,6 +20,7 @@
 
 package com.nextcloud.talk.controllers
 
+import android.app.Activity.RESULT_OK
 import android.content.ClipData
 import android.content.Context
 import android.content.Intent
@@ -44,6 +45,9 @@ import androidx.emoji.widget.EmojiEditText
 import androidx.emoji.widget.EmojiTextView
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
 import autodagger.AutoInjector
 import butterknife.BindView
 import butterknife.OnClick
@@ -70,6 +74,7 @@ import com.nextcloud.talk.components.filebrowser.controllers.BrowserController
 import com.nextcloud.talk.controllers.base.BaseController
 import com.nextcloud.talk.events.UserMentionClickEvent
 import com.nextcloud.talk.events.WebSocketCommunicationEvent
+import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
 import com.nextcloud.talk.models.database.UserEntity
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ChatOverall
@@ -80,11 +85,9 @@ import com.nextcloud.talk.models.json.conversations.RoomsOverall
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.models.json.mention.Mention
 import com.nextcloud.talk.presenters.MentionAutocompletePresenter
+import com.nextcloud.talk.ui.dialog.AttachmentDialog
 import com.nextcloud.talk.utils.*
 import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
-import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
-import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
 import com.nextcloud.talk.utils.database.user.UserUtils
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
@@ -123,46 +126,60 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
     @Inject
     @JvmField
     var ncApi: NcApi? = null
+
     @Inject
     @JvmField
     var userUtils: UserUtils? = null
+
     @Inject
     @JvmField
     var appPreferences: AppPreferences? = null
+
     @Inject
     @JvmField
     var context: Context? = null
+
     @Inject
     @JvmField
     var eventBus: EventBus? = null
+
     @BindView(R.id.messagesListView)
     @JvmField
     var messagesListView: MessagesList? = null
+
     @BindView(R.id.messageInputView)
     @JvmField
     var messageInputView: MessageInput? = null
+
     @BindView(R.id.messageInput)
     @JvmField
     var messageInput: EmojiEditText? = null
+
     @BindView(R.id.popupBubbleView)
     @JvmField
     var popupBubble: PopupBubble? = null
+
     @BindView(R.id.progressBar)
     @JvmField
     var loadingProgressBar: ProgressBar? = null
+
     @BindView(R.id.smileyButton)
     @JvmField
     var smileyButton: ImageButton? = null
+
     @BindView(R.id.lobby_view)
     @JvmField
     var lobbyView: RelativeLayout? = null
+
     @BindView(R.id.lobby_text_view)
     @JvmField
     var conversationLobbyText: TextView? = null
     val disposableList = ArrayList<Disposable>()
+
     @JvmField
     @BindView(R.id.quotedChatMessageView)
     var quotedChatMessageView: RelativeLayout? = null
+
     @BindView(R.id.callControlToggleChat)
     @JvmField
     var toggleChat: SimpleDraweeView? = null
@@ -378,7 +395,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         messagesListView?.setAdapter(adapter)
         adapter?.setLoadMoreListener(this)
         adapter?.setDateHeadersFormatter { format(it) }
-        adapter?.setOnMessageViewLongClickListener { view, message ->  onMessageViewLongClick(view, message)}
+        adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
 
         layoutManager = messagesListView?.layoutManager as LinearLayoutManager?
 
@@ -400,8 +417,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
             toggleChat?.visibility = View.VISIBLE
             wasDetached = true
         }
-        
-        toggleChat?.setOnClickListener{
+
+        toggleChat?.setOnClickListener {
             (activity as MagicCallActivity).showCall()
         }
 
@@ -467,8 +484,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         })
 
         messageInputView?.setAttachmentsListener {
-            showBrowserScreen(BrowserController
-                    .BrowserType.DAV_BROWSER)
+            activity?.let { AttachmentDialog(it, this).show() };
         }
 
         messageInputView?.button?.setOnClickListener { v -> submitMessage() }
@@ -495,7 +511,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
 
     private fun checkReadOnlyState() {
         if (currentConversation != null) {
-            if (currentConversation?.shouldShowLobby(conversationUser)?: false || currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY) {
+            if (currentConversation?.shouldShowLobby(conversationUser) ?: false || currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY) {
 
                 conversationVoiceCallMenuItem?.icon?.alpha = 99
                 conversationVideoMenuItem?.icon?.alpha = 99
@@ -559,7 +575,58 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         }
     }
 
-    private fun showBrowserScreen(browserType: BrowserController.BrowserType) {
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (requestCode == REQUEST_CODE_CHOOSE_FILE) {
+            if (resultCode == RESULT_OK) {
+                uploadFile(data)
+            }
+        }
+    }
+
+    private fun uploadFile(intentData: Intent?) {
+        try {
+            checkNotNull(intentData)
+            val files: MutableList<String> = ArrayList()
+            intentData.clipData?.let {
+                for (index in 0 until it.itemCount) {
+                    files.add(it.getItemAt(index).uri.toString())
+                }
+            } ?: run {
+                checkNotNull(intentData.data)
+                intentData.data.let {
+                    files.add(intentData.data.toString())
+                }
+            }
+            require(files.isNotEmpty())
+            val data: Data = Data.Builder()
+                    .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, files.toTypedArray())
+                    .putString(UploadAndShareFilesWorker.NC_TARGETPATH, conversationUser?.getAttachmentFolder())
+                    .putString(UploadAndShareFilesWorker.ROOM_TOKEN, roomToken)
+                    .build()
+            val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java)
+                    .setInputData(data)
+                    .build()
+            WorkManager.getInstance().enqueue(uploadWorker)
+        } catch (e: IllegalStateException) {
+            Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
+            Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
+        } catch (e: IllegalArgumentException) {
+            Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
+            Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
+        }
+    }
+
+    fun sendSelectLocalFileIntent() {
+        val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+            type = "*/*"
+            addCategory(Intent.CATEGORY_OPENABLE)
+            putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+        }
+        startActivityForResult(Intent.createChooser(action, context?.resources?.getString(
+                R.string.nc_upload_choose_local_files)), REQUEST_CODE_CHOOSE_FILE);
+    }
+
+    fun showBrowserScreen(browserType: BrowserController.BrowserType) {
         val bundle = Bundle()
         bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap<BrowserController.BrowserType>(browserType))
         bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap<UserEntity>(conversationUser))
@@ -618,13 +685,13 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
 
         emojiPopup = messageInput?.let {
             EmojiPopup.Builder.fromRootView(view).setOnEmojiPopupShownListener {
-            if (resources != null) {
-                smileyButton?.setColorFilter(resources!!.getColor(R.color.colorPrimary), PorterDuff.Mode.SRC_IN)
-            }
-        }.setOnEmojiPopupDismissListener {
-            smileyButton?.setColorFilter(resources!!.getColor(R.color.emoji_icons),
-                    PorterDuff.Mode.SRC_IN)
-        }.setOnEmojiClickListener { emoji, imageView -> messageInput?.editableText?.append(" ") }.build(it)
+                if (resources != null) {
+                    smileyButton?.setColorFilter(resources!!.getColor(R.color.colorPrimary), PorterDuff.Mode.SRC_IN)
+                }
+            }.setOnEmojiPopupDismissListener {
+                smileyButton?.setColorFilter(resources!!.getColor(R.color.emoji_icons),
+                        PorterDuff.Mode.SRC_IN)
+            }.setOnEmojiClickListener { emoji, imageView -> messageInput?.editableText?.append(" ") }.build(it)
         }
 
         if (activity != null) {
@@ -656,7 +723,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
 
     override fun onDetach(view: View) {
         super.onDetach(view)
-        
+
         if (!isLeavingForConversation) {
             // current room is still "active", we need the info
             ApplicationWideCurrentRoomHolder.getInstance().clear()
@@ -878,11 +945,11 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
 
         if (conversationUser != null) {
             ncApi!!.sendChatMessage(
-                    credentials, ApiUtils.getUrlForChat(
-                    conversationUser.baseUrl,
-                    roomToken
-            ),
-                    message, conversationUser.displayName, replyTo
+                    credentials,
+                    ApiUtils.getUrlForChat(conversationUser.baseUrl, roomToken),
+                    message,
+                    conversationUser.displayName,
+                    replyTo
             )
                     ?.subscribeOn(Schedulers.io())
                     ?.observeOn(AndroidSchedulers.mainThread())
@@ -1457,5 +1524,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         private val TAG = "ChatController"
         private val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
         private val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
+        val REQUEST_CODE_CHOOSE_FILE: Int = 555
     }
 }

+ 178 - 0
app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt

@@ -0,0 +1,178 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.jobs
+
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.OpenableColumns
+import android.util.Log
+import androidx.work.*
+import autodagger.AutoInjector
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
+import com.nextcloud.talk.utils.database.user.UserUtils
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.MediaType
+import okhttp3.RequestBody
+import retrofit2.Response
+import java.io.InputStream
+import java.util.*
+import javax.inject.Inject
+
+
+@AutoInjector(NextcloudTalkApplication::class)
+class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerParameters) :
+        Worker(context, workerParameters) {
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    @Inject
+    lateinit var userUtils: UserUtils
+
+    @Inject
+    lateinit var appPreferences: AppPreferences
+
+    override fun doWork(): Result {
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+
+        try {
+            val currentUser = userUtils.currentUser
+            val sourcefiles = inputData.getStringArray(DEVICE_SOURCEFILES)
+            val ncTargetpath = inputData.getString(NC_TARGETPATH)
+            val roomToken = inputData.getString(ROOM_TOKEN)
+
+            checkNotNull(currentUser)
+            checkNotNull(sourcefiles)
+            require(sourcefiles.isNotEmpty())
+            checkNotNull(ncTargetpath)
+            checkNotNull(roomToken)
+
+            for (index in sourcefiles.indices) {
+                val sourcefileUri = Uri.parse(sourcefiles[index])
+                var filename = getFileName(sourcefileUri)
+                val requestBody = createRequestBody(sourcefileUri)
+                uploadFile(currentUser, ncTargetpath, filename, roomToken, requestBody)
+            }
+        } catch (e: IllegalStateException) {
+            Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
+            return Result.failure()
+        } catch (e: IllegalArgumentException) {
+            Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
+            return Result.failure()
+        }
+        return Result.success()
+    }
+
+    private fun createRequestBody(sourcefileUri: Uri): RequestBody? {
+        var requestBody: RequestBody? = null
+        try {
+            val input: InputStream = context.contentResolver.openInputStream(sourcefileUri)!!
+            val buf = ByteArray(input.available())
+            while (input.read(buf) != -1);
+            requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), buf)
+        } catch (e: Exception) {
+            Log.e(javaClass.simpleName, "failed to create RequestBody for $sourcefileUri", e)
+        }
+        return requestBody
+    }
+
+    private fun uploadFile(currentUser: UserEntity, ncTargetpath: String?, filename: String?, roomToken: String?, requestBody: RequestBody?) {
+        ncApi.uploadFile(
+                ApiUtils.getCredentials(currentUser.username, currentUser.token),
+                ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetpath, filename),
+                requestBody
+        )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(object : Observer<Response<GenericOverall>> {
+                    override fun onSubscribe(d: Disposable) {
+                    }
+
+                    override fun onNext(t: Response<GenericOverall>) {
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "failed to upload file $filename")
+                    }
+
+                    override fun onComplete() {
+                        shareFile(roomToken, currentUser, ncTargetpath, filename)
+                    }
+                })
+    }
+
+    private fun shareFile(roomToken: String?, currentUser: UserEntity, ncTargetpath: String?, filename: String?) {
+        val paths: MutableList<String> = ArrayList()
+        paths.add("$ncTargetpath/$filename")
+
+        val data = Data.Builder()
+                .putLong(KEY_INTERNAL_USER_ID, currentUser.id)
+                .putString(KEY_ROOM_TOKEN, roomToken)
+                .putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
+                .build()
+        val shareWorker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
+                .setInputData(data)
+                .build()
+        WorkManager.getInstance().enqueue(shareWorker)
+    }
+
+    private fun getFileName(uri: Uri): String? {
+        var filename: String? = null
+        if (uri.scheme == "content") {
+            val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
+            try {
+                if (cursor != null && cursor.moveToFirst()) {
+                    filename = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
+                }
+            } finally {
+                cursor?.close()
+            }
+        }
+        if (filename == null) {
+            Log.e(TAG, "failed to get DISPLAY_NAME from uri. using fallback.")
+            filename = uri.path
+            val lastIndexOfSlash = filename!!.lastIndexOf('/')
+            if (lastIndexOfSlash != -1) {
+                filename = filename.substring(lastIndexOfSlash + 1)
+            }
+        }
+        return filename
+    }
+
+    companion object {
+        const val TAG = "UploadFileWorker"
+        const val DEVICE_SOURCEFILES = "DEVICE_SOURCEFILES"
+        const val NC_TARGETPATH = "NC_TARGETPATH"
+        const val ROOM_TOKEN = "ROOM_TOKEN"
+    }
+}

+ 26 - 5
app/src/main/java/com/nextcloud/talk/models/database/User.java

@@ -138,7 +138,7 @@ public interface User extends Parcelable, Persistable, Serializable {
                 capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
                 return capabilities != null &&
                         capabilities.getSpreedCapability() != null &&
-                        capabilities.getSpreedCapability().getFeatures() != null && 
+                        capabilities.getSpreedCapability().getFeatures() != null &&
                         capabilities.getSpreedCapability().getFeatures().contains("phonebook-search");
             } catch (IOException e) {
                 e.printStackTrace();
@@ -165,15 +165,15 @@ public interface User extends Parcelable, Persistable, Serializable {
         }
         return false;
     }
-    
+
     default boolean isReadStatusPrivate() {
         if (getCapabilities() != null) {
             Capabilities capabilities;
             try {
                 capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities != null && 
-                        capabilities.getSpreedCapability() != null && 
-                        capabilities.getSpreedCapability().getConfig() != null && 
+                if (capabilities != null &&
+                        capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getConfig() != null &&
                         capabilities.getSpreedCapability().getConfig().containsKey("chat")) {
                     HashMap<String, String> map = capabilities.getSpreedCapability().getConfig().get("chat");
                     if (map != null && map.containsKey("read-privacy")) {
@@ -186,4 +186,25 @@ public interface User extends Parcelable, Persistable, Serializable {
         }
         return false;
     }
+
+    default String getAttachmentFolder() {
+        if (getCapabilities() != null) {
+            Capabilities capabilities;
+            try {
+                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
+                if (capabilities != null &&
+                        capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getConfig() != null &&
+                        capabilities.getSpreedCapability().getConfig().containsKey("attachments")) {
+                    HashMap<String, String> map = capabilities.getSpreedCapability().getConfig().get("attachments");
+                    if (map != null && map.containsKey("folder")) {
+                        return map.get("folder");
+                    }
+                }
+            } catch (IOException e) {
+                Log.e("User.java", "Failed to get attachment folder", e);
+            }
+        }
+        return "/Talk";
+    }
 }

+ 66 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt

@@ -0,0 +1,66 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe <marcel.hibbe@nextcloud.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.ui.dialog
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.ViewGroup
+import androidx.appcompat.widget.AppCompatTextView
+import butterknife.BindView
+import butterknife.ButterKnife
+import butterknife.Unbinder
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.nextcloud.talk.R
+import com.nextcloud.talk.components.filebrowser.controllers.BrowserController
+import com.nextcloud.talk.controllers.ChatController
+
+
+class AttachmentDialog(val activity: Activity, var chatController :ChatController) : BottomSheetDialog(activity) {
+
+    @BindView(R.id.txt_attach_file_from_local)
+    @JvmField
+    var attachFromLocal: AppCompatTextView? = null
+
+    @BindView(R.id.txt_attach_file_from_cloud)
+    @JvmField
+    var attachFromCloud: AppCompatTextView? = null
+
+    private var unbinder: Unbinder? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState);
+        val view = layoutInflater.inflate(R.layout.dialog_attachment, null);
+        setContentView(view);
+
+        window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+        unbinder = ButterKnife.bind(this, view);
+
+        attachFromLocal?.setOnClickListener {
+            chatController.sendSelectLocalFileIntent();
+            dismiss()
+        }
+        attachFromCloud?.setOnClickListener {
+            chatController.showBrowserScreen(BrowserController.BrowserType.DAV_BROWSER)
+            dismiss()
+        }
+    }
+}

+ 8 - 3
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -22,6 +22,8 @@ package com.nextcloud.talk.utils;
 import android.net.Uri;
 import android.text.TextUtils;
 
+import androidx.annotation.DimenRes;
+
 import com.nextcloud.talk.BuildConfig;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
@@ -32,7 +34,6 @@ import java.util.Map;
 
 import javax.annotation.Nullable;
 
-import androidx.annotation.DimenRes;
 import okhttp3.Credentials;
 
 public class ApiUtils {
@@ -245,7 +246,7 @@ public class ApiUtils {
     }
 
     public static String getUrlForAvatarWithNameForGuests(String baseUrl, String name,
-                                                  @DimenRes int avatarSize) {
+                                                          @DimenRes int avatarSize) {
         avatarSize = Math.round(NextcloudTalkApplication
                 .Companion.getSharedApplication().getResources().getDimension(avatarSize));
 
@@ -283,8 +284,12 @@ public class ApiUtils {
     public static String getUrlForReadOnlyState(String baseUrl, String roomToken) {
         return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + roomToken + "/read-only";
     }
-    
+
     public static String getUrlForSearchByNumber(String baseUrl) {
         return baseUrl + ocsApiVersion + "/cloud/users/search/by-phone";
     }
+
+    public static String getUrlForFileUpload(String baseUrl, String user, String attachmentFolder, String filename) {
+        return baseUrl + "/remote.php/dav/files/" + user + attachmentFolder + "/" + filename;
+    }
 }

+ 1 - 1
app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt

@@ -54,7 +54,7 @@ object LoggingUtils {
     fun sendMailWithAttachment(context: Context) {
         val logFile = context.getFileStreamPath("nc_log.txt")
         val emailIntent = Intent(Intent.ACTION_SEND)
-        val mailto = "android@nextcloud.com"
+        val mailto = "mario@nextcloud.com"
         emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(mailto))
         emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Talk logs")
         emailIntent.type = "text/plain"

+ 45 - 0
app/src/main/res/layout/dialog_attachment.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2021 Marcel Hibbe <marcel.hibbe@nextcloud.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingBottom="8dp">
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/txt_attach_file_from_local"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:padding="15dp"
+        android:text="@string/nc_upload_local_file"
+        android:textSize="20sp" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/txt_attach_file_from_cloud"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:padding="15dp"
+        android:text="@string/nc_upload_from_nextcloud"
+        android:textSize="20sp" />
+
+</LinearLayout>

+ 6 - 0
app/src/main/res/values/strings.xml

@@ -321,6 +321,12 @@
     <string name="nc_copy_message">Copy</string>
     <string name="nc_reply">Reply</string>
 
+    <!-- Upload -->
+    <string name="nc_upload_local_file">Upload local file</string>
+    <string name="nc_upload_from_nextcloud">Upload from Nextcloud</string>
+    <string name="nc_upload_failed">Failed to upload file</string>
+    <string name="nc_upload_choose_local_files">Choose files</string>
+
     <!-- Non-translatable strings -->
 
     <string name="path_password_strike_through" translatable="false"