Skip to content

Commit 4867f77

Browse files
committed
use subfolders for attachments
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
1 parent 5dcb580 commit 4867f77

12 files changed

Lines changed: 369 additions & 21 deletions

app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import com.nextcloud.talk.conversationinfo.CreateRoomRequest
1212
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
1313
import com.nextcloud.talk.models.json.chat.ChatOverall
1414
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
15+
import com.nextcloud.talk.models.json.chatpostattachment.ChatPostAttachmentOverall
16+
import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ChatProbeAttachmentFolderOverall
17+
import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ProbeConversationAttachmentRequest
1518
import com.nextcloud.talk.models.json.conversations.RoomOverall
1619
import com.nextcloud.talk.models.json.conversations.RoomsOverall
1720
import com.nextcloud.talk.models.json.generic.GenericOverall
@@ -463,4 +466,24 @@ interface NcApiCoroutines {
463466
// Url is: /api/{apiVersion}/chat/{token}/read
464467
@DELETE
465468
suspend fun markRoomAsUnread(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
469+
470+
471+
@POST
472+
suspend fun probeConversationAttachmentFolder(
473+
@Header("Authorization") authorization: String,
474+
@Url url: String,
475+
@Body request: ProbeConversationAttachmentRequest
476+
): ChatProbeAttachmentFolderOverall
477+
478+
@Suppress("LongParameterList")
479+
@FormUrlEncoded
480+
@POST
481+
suspend fun postConversationAttachment(
482+
@Header("Authorization") authorization: String,
483+
@Url url: String,
484+
@Field("filePath") filePath: String,
485+
@Field("referenceId") referenceId: String,
486+
@Field("fileName") fileName: String,
487+
@Field("talkMetaData") talkMetaData: String?
488+
): ChatPostAttachmentOverall
466489
}

app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,22 @@ import androidx.work.OneTimeWorkRequest
2626
import androidx.work.WorkManager
2727
import androidx.work.Worker
2828
import androidx.work.WorkerParameters
29+
import at.bitfire.dav4jvm.DavResource
2930
import autodagger.AutoInjector
3031
import com.nextcloud.talk.R
3132
import com.nextcloud.talk.activities.MainActivity
3233
import com.nextcloud.talk.api.NcApi
34+
import com.nextcloud.talk.api.NcApiCoroutines
3335
import com.nextcloud.talk.application.NextcloudTalkApplication
36+
import com.nextcloud.talk.dagger.modules.RestModule
3437
import com.nextcloud.talk.data.user.model.User
38+
import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ChatProbeAttachmentData
39+
import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ProbeConversationAttachmentRequest
3540
import com.nextcloud.talk.upload.chunked.ChunkedFileUploader
3641
import com.nextcloud.talk.upload.chunked.OnDataTransferProgressListener
3742
import com.nextcloud.talk.upload.normal.FileUploader
3843
import com.nextcloud.talk.users.UserManager
44+
import com.nextcloud.talk.utils.ApiUtils
3945
import com.nextcloud.talk.utils.CapabilitiesUtil
4046
import com.nextcloud.talk.utils.FileUtils
4147
import com.nextcloud.talk.utils.NotificationUtils
@@ -45,9 +51,16 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
4551
import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
4652
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
4753
import com.nextcloud.talk.utils.preferences.AppPreferences
54+
import kotlinx.coroutines.runBlocking
55+
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
4856
import okhttp3.MediaType.Companion.toMediaTypeOrNull
4957
import okhttp3.OkHttpClient
58+
import okhttp3.Protocol
59+
import okhttp3.RequestBody
60+
import okhttp3.Response
5061
import java.io.File
62+
import java.io.IOException
63+
import java.util.UUID
5164
import javax.inject.Inject
5265

5366
@AutoInjector(NextcloudTalkApplication::class)
@@ -61,6 +74,9 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
6174
@Inject
6275
lateinit var userManager: UserManager
6376

77+
@Inject
78+
lateinit var ncApiCoroutines: NcApiCoroutines
79+
6480
@Inject
6581
lateinit var currentUserProvider: CurrentUserProviderOld
6682

@@ -107,9 +123,12 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
107123
file = FileUtils.getFileFromUri(context, sourceFileUri)
108124
val remotePath = getRemotePath(currentUser)
109125

126+
val useConversationSubfolders = CapabilitiesUtil.hasConversationSubfoldersForAttachments(
127+
currentUser.capabilities!!.spreedCapability!!
128+
)
110129
initNotificationSetup()
111130
file?.let { isChunkedUploading = it.length() > CHUNK_UPLOAD_THRESHOLD_SIZE }
112-
val uploadSuccess: Boolean = uploadFile(sourceFileUri, metaData, remotePath)
131+
val uploadSuccess: Boolean = uploadFile(sourceFileUri, metaData, remotePath, useConversationSubfolders)
113132

114133
if (uploadSuccess) {
115134
cancelNotification()
@@ -129,25 +148,138 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
129148
}
130149
}
131150

132-
private fun uploadFile(sourceFileUri: Uri, metaData: String?, remotePath: String): Boolean =
151+
private fun uploadFile(
152+
sourceFileUri: Uri,
153+
metaData: String?,
154+
remotePath: String,
155+
useConversationSubfolders: Boolean
156+
): Boolean =
133157
if (file == null) {
134158
false
159+
} else if (useConversationSubfolders) {
160+
uploadUsingConversationSubfolders(sourceFileUri, metaData)
135161
} else if (isChunkedUploading) {
136162
Log.d(TAG, "starting chunked upload because size is " + file!!.length())
137-
138163
initNotificationWithPercentage()
139164
val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
140-
141-
chunkedFileUploader = ChunkedFileUploader(okHttpClient, currentUser, roomToken, metaData, this)
165+
chunkedFileUploader = ChunkedFileUploader(
166+
okHttpClient,
167+
currentUser,
168+
roomToken,
169+
metaData,
170+
this,
171+
ncApiCoroutines,
172+
useConversationSubfolders
173+
)
142174
chunkedFileUploader!!.upload(file!!, mimeType, remotePath)
143175
} else {
144176
Log.d(TAG, "starting normal upload (not chunked) of $fileName")
145-
146177
FileUploader(okHttpClient, context, currentUser, roomToken, ncApi, file!!)
147178
.upload(sourceFileUri, fileName, remotePath, metaData)
148179
.blockingFirst()
149180
}
150181

182+
private fun uploadUsingConversationSubfolders(sourceFileUri: Uri, metaData: String?): Boolean =
183+
runBlocking {
184+
val credentials = ApiUtils.getCredentials(
185+
currentUser.username,
186+
currentUser.token
187+
) ?: return@runBlocking false
188+
val uploadId = UUID.randomUUID().toString()
189+
val request = ProbeConversationAttachmentRequest().apply {
190+
fileNames = listOf(fileName)
191+
}
192+
val probeResponse = ncApiCoroutines.probeConversationAttachmentFolder(
193+
credentials,
194+
ApiUtils.getUrlForChatAttachmentFolder(ApiUtils.API_V1, currentUser.baseUrl, roomToken),
195+
request)
196+
197+
val draftFolderPath = probeResponse.ocs?.data?.folder?.trimEnd('/')
198+
if (draftFolderPath.isNullOrEmpty()) {
199+
Log.e(TAG, "Draft folder path missing in probe response")
200+
return@runBlocking false
201+
}
202+
val predictedName = resolveFinalFileName(fileName, probeResponse.ocs?.data!!)
203+
val tempRemotePath = "$draftFolderPath/$uploadId-0-$fileName"
204+
205+
val uploadSuccess = if (isChunkedUploading) {
206+
initNotificationWithPercentage()
207+
val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
208+
chunkedFileUploader = ChunkedFileUploader(
209+
okHttpClient,
210+
currentUser,
211+
roomToken,
212+
metaData,
213+
this@UploadAndShareFilesWorker,
214+
ncApiCoroutines,
215+
true
216+
)
217+
chunkedFileUploader!!.upload(file!!, mimeType, tempRemotePath)
218+
} else {
219+
uploadFileToDraftPathUsingWebDav(sourceFileUri, tempRemotePath)
220+
}
221+
222+
if (!uploadSuccess) {
223+
return@runBlocking false
224+
}
225+
226+
runCatching {
227+
ncApiCoroutines.postConversationAttachment(
228+
credentials,
229+
ApiUtils.getUrlForChatAttachment(ApiUtils.API_V1, currentUser.baseUrl, roomToken),
230+
tempRemotePath,
231+
uploadId,
232+
predictedName,
233+
metaData
234+
)
235+
}
236+
.onFailure { Log.e(TAG, "Failed to finalize uploaded attachment", it) }
237+
.isSuccess
238+
}
239+
240+
private fun resolveFinalFileName(originalName: String, probeData: ChatProbeAttachmentData): String =
241+
probeData.renames?.get(originalName) ?: originalName
242+
243+
private fun uploadFileToDraftPathUsingWebDav(sourceFileUri: Uri, remotePath: String): Boolean =
244+
runCatching {
245+
val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
246+
val requestBody = RequestBody.create(mimeType, file!!)
247+
val uploadUrl = ApiUtils.getUrlForFileUpload(
248+
currentUser.baseUrl!!,
249+
currentUser.userId!!,
250+
remotePath
251+
)
252+
val davResource = DavResource(
253+
createWebDavClientWithAuth(),
254+
uploadUrl.toHttpUrlOrNull()!!
255+
)
256+
davResource.put(requestBody) { response: Response ->
257+
if (!response.isSuccessful) {
258+
throw IOException("Failed to upload file to draft folder. response code: ${response.code}")
259+
}
260+
}
261+
FileUtils.copyFileToCache(context, sourceFileUri, fileName)
262+
true
263+
}
264+
.onFailure { Log.e(TAG, "Failed to upload draft attachment via WebDAV", it) }
265+
.getOrDefault(false)
266+
267+
private fun createWebDavClientWithAuth(): OkHttpClient =
268+
okHttpClient.newBuilder()
269+
.followRedirects(false)
270+
.followSslRedirects(false)
271+
.protocols(listOf(Protocol.HTTP_1_1))
272+
.authenticator(
273+
RestModule.HttpAuthenticator(
274+
ApiUtils.getCredentials(
275+
currentUser.username,
276+
currentUser.token
277+
)!!,
278+
"Authorization"
279+
)
280+
)
281+
.build()
282+
151283
private fun getRemotePath(currentUser: User): String {
152284
val remotePath = CapabilitiesUtil.getAttachmentFolder(
153285
currentUser.capabilities!!.spreedCapability!!
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.chatpostattachment
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import kotlinx.parcelize.Parcelize
14+
15+
@Parcelize
16+
@JsonObject
17+
data class ChatPostAttachmentData(
18+
@JsonField(name = ["renames"])
19+
var renames: LinkedHashMap<String, String>? = null
20+
) : Parcelable {
21+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
22+
constructor() : this(null)
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.chatpostattachment
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import kotlinx.parcelize.Parcelize
14+
15+
@Parcelize
16+
@JsonObject
17+
data class ChatPostAttachmentOCS(
18+
@JsonField(name = ["data"])
19+
var data: ChatPostAttachmentData? = null
20+
) : Parcelable {
21+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
22+
constructor() : this(null)
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.chatpostattachment
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import kotlinx.parcelize.Parcelize
14+
15+
@Parcelize
16+
@JsonObject
17+
data class ChatPostAttachmentOverall(
18+
@JsonField(name = ["ocs"])
19+
var ocs: ChatPostAttachmentOCS? = null
20+
) : Parcelable {
21+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
22+
constructor() : this(null)
23+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.chatprobeattachmentfolder
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import kotlinx.parcelize.Parcelize
14+
15+
@Parcelize
16+
@JsonObject
17+
data class ChatProbeAttachmentData(
18+
@JsonField(name = ["folder"])
19+
var folder: String? = null,
20+
@JsonField(name = ["renames"])
21+
var renames: LinkedHashMap<String, String>? = null
22+
) : Parcelable {
23+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
24+
constructor() : this(null, null)
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.chatprobeattachmentfolder
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import com.nextcloud.talk.models.json.generic.GenericMeta
14+
import kotlinx.parcelize.Parcelize
15+
16+
@Parcelize
17+
@JsonObject
18+
data class ChatProbeAttachmentFolderOCS(
19+
@JsonField(name = ["meta"])
20+
var meta: GenericMeta?,
21+
@JsonField(name = ["data"])
22+
var data: ChatProbeAttachmentData? = null
23+
) : Parcelable {
24+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
25+
constructor() : this(null)
26+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.models.json.chatprobeattachmentfolder
9+
10+
import android.os.Parcelable
11+
import com.bluelinelabs.logansquare.annotation.JsonField
12+
import com.bluelinelabs.logansquare.annotation.JsonObject
13+
import kotlinx.parcelize.Parcelize
14+
15+
@Parcelize
16+
@JsonObject
17+
data class ChatProbeAttachmentFolderOverall(
18+
@JsonField(name = ["ocs"])
19+
var ocs: ChatProbeAttachmentFolderOCS?
20+
) : Parcelable {
21+
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
22+
constructor() : this(null)
23+
}

0 commit comments

Comments
 (0)