229 lines
9.0 KiB
Kotlin
229 lines
9.0 KiB
Kotlin
/* Copyright 2019 Tusky Contributors
|
|
*
|
|
* This file is a part of Tusky.
|
|
*
|
|
* 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.
|
|
*
|
|
* Tusky 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 Tusky; if not,
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
package com.keylesspalace.tusky.components.compose
|
|
|
|
import android.content.Context
|
|
import android.net.Uri
|
|
import android.os.Environment
|
|
import android.util.Log
|
|
import android.webkit.MimeTypeMap
|
|
import androidx.core.content.FileProvider
|
|
import androidx.core.net.toUri
|
|
import com.keylesspalace.tusky.BuildConfig
|
|
import com.keylesspalace.tusky.R
|
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
|
import com.keylesspalace.tusky.entity.Attachment
|
|
import com.keylesspalace.tusky.network.MastodonApi
|
|
import com.keylesspalace.tusky.network.ProgressRequestBody
|
|
import com.keylesspalace.tusky.util.*
|
|
import io.reactivex.Observable
|
|
import io.reactivex.Single
|
|
import io.reactivex.schedulers.Schedulers
|
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
import okhttp3.MultipartBody
|
|
import java.io.File
|
|
import java.io.FileOutputStream
|
|
import java.io.IOException
|
|
import java.util.*
|
|
|
|
sealed class UploadEvent {
|
|
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
|
data class FinishedEvent(val attachment: Attachment) : UploadEvent()
|
|
}
|
|
|
|
fun createNewImageFile(context: Context): File {
|
|
// Create an image file name
|
|
val randomId = randomAlphanumericString(12)
|
|
val imageFileName = "Husky_${randomId}_"
|
|
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
|
return File.createTempFile(
|
|
imageFileName, /* prefix */
|
|
".jpg", /* suffix */
|
|
storageDir /* directory */
|
|
)
|
|
}
|
|
|
|
data class PreparedMedia(val type: Int, val uri: Uri, val size: Long)
|
|
|
|
interface MediaUploader {
|
|
fun prepareMedia(inUri: Uri, videoLimit: Long, imageLimit: Long, filename: String?): Single<PreparedMedia>
|
|
fun uploadMedia(media: QueuedMedia, videoLimit: Long, imageLimit: Long): Observable<UploadEvent>
|
|
}
|
|
|
|
class AudioSizeException : Exception()
|
|
class VideoSizeException : Exception()
|
|
class MediaSizeException : Exception()
|
|
class MediaTypeException : Exception()
|
|
class CouldNotOpenFileException : Exception()
|
|
|
|
class MediaUploaderImpl(
|
|
private val context: Context,
|
|
private val mastodonApi: MastodonApi
|
|
) : MediaUploader {
|
|
override fun uploadMedia(media: QueuedMedia, videoLimit: Long, imageLimit: Long): Observable<UploadEvent> {
|
|
return Observable
|
|
.fromCallable {
|
|
if (shouldResizeMedia(media, imageLimit)) {
|
|
downsize(media, imageLimit)
|
|
} else media
|
|
}
|
|
.switchMap { upload(it) }
|
|
.subscribeOn(Schedulers.io())
|
|
}
|
|
|
|
private fun getMimeTypeAndSuffixFromFilenameOrUri(uri: Uri, filename: String?) : Pair<String?, String> {
|
|
val mimeType = contentResolver.getType(uri)
|
|
return if(mimeType == null && filename != null) {
|
|
val extension = filename.substringAfterLast('.', "tmp")
|
|
Pair(MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension), ".$extension")
|
|
} else {
|
|
Pair(mimeType, "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp"))
|
|
}
|
|
}
|
|
|
|
override fun prepareMedia(inUri: Uri, videoLimit: Long, imageLimit: Long, filename: String?): Single<PreparedMedia> {
|
|
return Single.fromCallable {
|
|
var mediaSize = getMediaSize(contentResolver, inUri)
|
|
var uri = inUri
|
|
val (mimeType, suffix) = getMimeTypeAndSuffixFromFilenameOrUri(uri, filename)
|
|
|
|
try {
|
|
contentResolver.openInputStream(inUri).use { input ->
|
|
if (input == null) {
|
|
Log.w(TAG, "Media input is null")
|
|
uri = inUri
|
|
return@use
|
|
}
|
|
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
|
FileOutputStream(file.absoluteFile).use { out ->
|
|
input.copyTo(out)
|
|
uri = FileProvider.getUriForFile(context,
|
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
|
file)
|
|
mediaSize = getMediaSize(contentResolver, uri)
|
|
}
|
|
|
|
}
|
|
} catch (e: IOException) {
|
|
Log.w(TAG, e)
|
|
uri = inUri
|
|
}
|
|
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
|
throw CouldNotOpenFileException()
|
|
}
|
|
|
|
if (mimeType != null) {
|
|
val topLevelType = mimeType.substring(0, mimeType.indexOf('/'))
|
|
when (topLevelType) {
|
|
"video" -> {
|
|
if (mediaSize > videoLimit) {
|
|
throw VideoSizeException()
|
|
}
|
|
PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
|
|
}
|
|
"image" -> {
|
|
PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
|
|
}
|
|
"audio" -> {
|
|
if (mediaSize > videoLimit) { // TODO: CHANGE!!11
|
|
throw AudioSizeException()
|
|
}
|
|
PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
|
|
}
|
|
else -> {
|
|
if (mediaSize > videoLimit) {
|
|
throw MediaSizeException()
|
|
}
|
|
PreparedMedia(QueuedMedia.Type.UNKNOWN, uri, mediaSize)
|
|
// throw MediaTypeException()
|
|
}
|
|
}
|
|
} else {
|
|
throw MediaTypeException()
|
|
}
|
|
}
|
|
}
|
|
|
|
private val contentResolver = context.contentResolver
|
|
|
|
private fun upload(media: QueuedMedia): Observable<UploadEvent> {
|
|
return Observable.create { emitter ->
|
|
var (mimeType, fileExtension) = getMimeTypeAndSuffixFromFilenameOrUri(media.uri, media.originalFileName)
|
|
val filename = String.format("%s_%s_%s%s",
|
|
context.getString(R.string.app_name),
|
|
Date().time.toString(),
|
|
randomAlphanumericString(10),
|
|
fileExtension)
|
|
|
|
val stream = contentResolver.openInputStream(media.uri)
|
|
|
|
if (mimeType == null) mimeType = "multipart/form-data"
|
|
|
|
var lastProgress = -1
|
|
val fileBody = ProgressRequestBody(stream, media.mediaSize,
|
|
mimeType.toMediaTypeOrNull()) { percentage ->
|
|
if (percentage != lastProgress) {
|
|
emitter.onNext(UploadEvent.ProgressEvent(percentage))
|
|
}
|
|
lastProgress = percentage
|
|
}
|
|
|
|
val body = MultipartBody.Part.createFormData("file", filename, fileBody)
|
|
|
|
val uploadDisposable = mastodonApi.uploadMedia(body)
|
|
.subscribe({ attachment ->
|
|
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
|
emitter.onComplete()
|
|
}, { e ->
|
|
emitter.onError(e)
|
|
})
|
|
|
|
// Cancel the request when our observable is cancelled
|
|
emitter.setDisposable(uploadDisposable)
|
|
}
|
|
}
|
|
|
|
private fun downsize(media: QueuedMedia, imageLimit: Long): QueuedMedia {
|
|
val file = createNewImageFile(context)
|
|
DownsizeImageTask.resize(arrayOf(media.uri), imageLimit, context.contentResolver, file)
|
|
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
|
}
|
|
|
|
private fun shouldResizeMedia(media: QueuedMedia, imageLimit: Long): Boolean {
|
|
// resize only images
|
|
if(media.type == QueuedMedia.Type.IMAGE) {
|
|
// resize when exceed image limit
|
|
if(media.mediaSize >= imageLimit)
|
|
return true
|
|
|
|
// don't resize when instance permits any image resolution(Pleroma)
|
|
if(media.noChanges)
|
|
return false
|
|
|
|
// resize when exceed pixel limit
|
|
if(getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private companion object {
|
|
private const val TAG = "MediaUploaderImpl"
|
|
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
|
|
}
|
|
}
|