Simplify uploading room and user avatar.

This commit is contained in:
onurays 2020-06-29 08:30:18 +03:00 committed by Benoit Marty
parent a93cbf3548
commit 56f8e52352
5 changed files with 41 additions and 211 deletions

View File

@ -32,7 +32,6 @@ import im.vector.matrix.android.internal.session.account.AccountModule
import im.vector.matrix.android.internal.session.cache.CacheModule import im.vector.matrix.android.internal.session.cache.CacheModule
import im.vector.matrix.android.internal.session.call.CallModule import im.vector.matrix.android.internal.session.call.CallModule
import im.vector.matrix.android.internal.session.content.ContentModule import im.vector.matrix.android.internal.session.content.ContentModule
import im.vector.matrix.android.internal.session.content.UploadAvatarWorker
import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.content.UploadContentWorker
import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.filter.FilterModule
import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GetGroupDataWorker
@ -118,8 +117,6 @@ internal interface SessionComponent {
fun inject(worker: UploadContentWorker) fun inject(worker: UploadContentWorker)
fun inject(worker: UploadAvatarWorker)
fun inject(worker: SyncWorker) fun inject(worker: SyncWorker)
fun inject(worker: AddHttpPusherWorker) fun inject(worker: AddHttpPusherWorker)

View File

@ -16,12 +16,16 @@
package im.vector.matrix.android.internal.session.content package im.vector.matrix.android.internal.session.content
import android.content.Context
import android.net.Uri
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.Authenticated
import im.vector.matrix.android.internal.network.ProgressRequestBody import im.vector.matrix.android.internal.network.ProgressRequestBody
import im.vector.matrix.android.internal.network.awaitResponse import im.vector.matrix.android.internal.network.awaitResponse
import im.vector.matrix.android.internal.network.toFailure import im.vector.matrix.android.internal.network.toFailure
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -31,12 +35,14 @@ import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
internal class FileUploader @Inject constructor(@Authenticated internal class FileUploader @Inject constructor(@Authenticated
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val eventBus: EventBus, private val eventBus: EventBus,
private val context: Context,
contentUrlResolver: ContentUrlResolver, contentUrlResolver: ContentUrlResolver,
moshi: Moshi) { moshi: Moshi) {
@ -59,6 +65,19 @@ internal class FileUploader @Inject constructor(@Authenticated
return upload(uploadBody, filename, progressListener) return upload(uploadBody, filename, progressListener)
} }
suspend fun uploadFromUri(uri: Uri,
filename: String?,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val inputStream = withContext(Dispatchers.IO) {
context.contentResolver.openInputStream(uri)
} ?: throw FileNotFoundException()
inputStream.use {
return uploadByteArray(it.readBytes(), filename, mimeType, progressListener)
}
}
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()

View File

@ -1,128 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.content
import android.content.Context
import android.net.Uri
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.worker.SessionWorkerParams
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: None
* Possible next worker : None
*/
internal class UploadAvatarWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
val queryUri: Uri,
val fileName: String,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@JsonClass(generateAdapter = true)
internal data class OutputParams(
override val sessionId: String,
val imageUrl: String? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var fileUploader: FileUploader
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
Timber.v("Starting upload media work with params $params")
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
// Just defensive code to ensure that we never have an uncaught exception that could break the queue
return try {
internalDoWork(params)
} catch (failure: Throwable) {
Timber.e(failure)
handleFailure(params, failure)
}
}
private suspend fun internalDoWork(params: Params): Result {
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
try {
val inputStream = context.contentResolver.openInputStream(params.queryUri)
?: return Result.success(
WorkerParamsFactory.toData(
params.copy(
lastFailureMessage = "Cannot openInputStream for file: " + params.queryUri.toString()
)
)
)
inputStream.use {
return try {
Timber.v("## UploadAvatarWorker - Uploading avatar...")
val response = fileUploader.uploadByteArray(inputStream.readBytes(), params.fileName, "image/jpeg")
Timber.v("## UploadAvatarWorker - Uploadeded avatar: ${response.contentUri}")
handleSuccess(params, response.contentUri)
} catch (t: Throwable) {
Timber.e(t, "## UploadAvatarWorker - Uploading avatar failed...")
handleFailure(params, t)
}
}
} catch (e: Exception) {
Timber.e(e)
return Result.success(
WorkerParamsFactory.toData(
params.copy(
lastFailureMessage = e.localizedMessage
)
)
)
}
}
private fun handleFailure(params: Params, failure: Throwable): Result {
return Result.success(
WorkerParamsFactory.toData(
params.copy(
lastFailureMessage = failure.localizedMessage
)
)
)
}
private fun handleSuccess(params: Params, imageUrl: String): Result {
Timber.v("handleSuccess $imageUrl, work is stopped $isStopped")
val sendParams = OutputParams(params.sessionId, imageUrl, params.lastFailureMessage)
return Result.success(WorkerParamsFactory.toData(sendParams))
}
}

View File

@ -19,29 +19,23 @@ package im.vector.matrix.android.internal.session.profile
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.model.UserThreePidEntity import im.vector.matrix.android.internal.database.model.UserThreePidEntity
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.content.UploadAvatarWorker import im.vector.matrix.android.internal.session.content.FileUploader
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import io.realm.kotlin.where import io.realm.kotlin.where
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK" private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK"
@ -54,7 +48,8 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
private val refreshUserThreePidsTask: RefreshUserThreePidsTask, private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
private val getProfileInfoTask: GetProfileInfoTask, private val getProfileInfoTask: GetProfileInfoTask,
private val setDisplayNameTask: SetDisplayNameTask, private val setDisplayNameTask: SetDisplayNameTask,
private val setAvatarUrlTask: SetAvatarUrlTask) : ProfileService { private val setAvatarUrlTask: SetAvatarUrlTask,
private val fileUploader: FileUploader) : ProfileService {
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable { override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
val params = GetProfileInfoTask.Params(userId) val params = GetProfileInfoTask.Params(userId)
@ -83,38 +78,14 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
} }
override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable { override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
val cancelableBag = CancelableBag() return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) {
val workerParams = UploadAvatarWorker.Params(sessionId, newAvatarUri, fileName) val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg")
val workerData = WorkerParamsFactory.toData(workerParams) setAvatarUrlTask
.configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) {
val uploadAvatarWork = workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadAvatarWorker>() callback = matrixCallback
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(workerData)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
workManagerProvider.workManager
.beginUniqueWork("${userId}_$UPLOAD_AVATAR_WORK", ExistingWorkPolicy.REPLACE, uploadAvatarWork)
.enqueue()
cancelableBag.add(CancelableWork(workManagerProvider.workManager, uploadAvatarWork.id))
taskExecutor.executorScope.launch(coroutineDispatchers.main) {
workManagerProvider.workManager.getWorkInfoByIdLiveData(uploadAvatarWork.id)
.observeForever { info ->
if (info != null && info.state.isFinished) {
val result = WorkerParamsFactory.fromData<UploadAvatarWorker.OutputParams>(info.outputData)
cancelableBag.add(
setAvatarUrlTask
.configureWith(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = result!!.imageUrl!!)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
)
}
} }
.executeBy(taskExecutor)
} }
return cancelableBag
} }
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable { override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {

View File

@ -18,8 +18,6 @@ package im.vector.matrix.android.internal.session.room.state
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
@ -28,19 +26,15 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.content.UploadAvatarWorker import im.vector.matrix.android.internal.session.content.FileUploader
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK" private const val UPLOAD_AVATAR_WORK = "UPLOAD_AVATAR_WORK"
@ -50,7 +44,8 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
private val sendStateTask: SendStateTask, private val sendStateTask: SendStateTask,
@SessionId private val sessionId: String, @SessionId private val sessionId: String,
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val fileUploader: FileUploader
) : StateService { ) : StateService {
@AssistedInject.Factory @AssistedInject.Factory
@ -130,38 +125,14 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
} }
override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable { override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable {
val cancelableBag = CancelableBag() return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
val workerParams = UploadAvatarWorker.Params(sessionId, avatarUri, fileName) val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg")
val workerData = WorkerParamsFactory.toData(workerParams) sendStateEvent(
eventType = EventType.STATE_ROOM_AVATAR,
val uploadAvatarWork = workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadAvatarWorker>() body = mapOf("url" to response.contentUri),
.setConstraints(WorkManagerProvider.workConstraints) callback = callback,
.setInputData(workerData) stateKey = null
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) )
.build()
workManagerProvider.workManager
.beginUniqueWork("${roomId}_$UPLOAD_AVATAR_WORK", ExistingWorkPolicy.REPLACE, uploadAvatarWork)
.enqueue()
cancelableBag.add(CancelableWork(workManagerProvider.workManager, uploadAvatarWork.id))
taskExecutor.executorScope.launch(coroutineDispatchers.main) {
workManagerProvider.workManager.getWorkInfoByIdLiveData(uploadAvatarWork.id)
.observeForever { info ->
if (info != null && info.state.isFinished) {
val result = WorkerParamsFactory.fromData<UploadAvatarWorker.OutputParams>(info.outputData)
cancelableBag.add(
sendStateEvent(
eventType = EventType.STATE_ROOM_AVATAR,
body = mapOf("url" to result?.imageUrl!!),
callback = callback,
stateKey = null
)
)
}
}
} }
return cancelableBag
} }
} }