465 lines
18 KiB
Kotlin
465 lines
18 KiB
Kotlin
package com.gh.base
|
||
|
||
import android.app.Application
|
||
import android.content.Intent
|
||
import android.graphics.Bitmap
|
||
import android.media.ThumbnailUtils
|
||
import android.provider.MediaStore
|
||
import android.text.TextUtils
|
||
import androidx.lifecycle.AndroidViewModel
|
||
import androidx.lifecycle.MediatorLiveData
|
||
import androidx.lifecycle.MutableLiveData
|
||
import com.gh.gamecenter.R
|
||
import com.gh.gamecenter.common.base.fragment.WaitingDialogFragment
|
||
import com.gh.gamecenter.common.retrofit.Response
|
||
import com.gh.gamecenter.common.utils.*
|
||
import com.gh.gamecenter.core.runOnUiThread
|
||
import com.gh.gamecenter.core.utils.MD5Utils
|
||
import com.gh.gamecenter.core.utils.ToastUtils
|
||
import com.gh.gamecenter.common.entity.ErrorEntity
|
||
import com.gh.gamecenter.entity.LocalVideoEntity
|
||
import com.gh.gamecenter.entity.QuoteCountEntity
|
||
import com.gh.gamecenter.qa.BbsType
|
||
import com.gh.gamecenter.retrofit.RetrofitManager
|
||
import com.gh.gamecenter.retrofit.service.ApiService
|
||
import com.gh.gamecenter.video.upload.OnUploadListener
|
||
import com.gh.gamecenter.video.upload.UploadManager
|
||
import com.google.gson.JsonObject
|
||
import com.lightgame.utils.Utils
|
||
import com.zhihu.matisse.Matisse
|
||
import com.zhihu.matisse.internal.utils.PathUtils
|
||
import io.reactivex.disposables.Disposable
|
||
import okhttp3.ResponseBody
|
||
import retrofit2.HttpException
|
||
import java.io.File
|
||
import java.io.FileOutputStream
|
||
import kotlin.collections.set
|
||
|
||
// TODO: 移动到module_bbs模块
|
||
abstract class BaseRichEditorViewModel(application: Application) : AndroidViewModel(application) {
|
||
val mApi: ApiService = RetrofitManager.getInstance().api
|
||
val processDialog = MediatorLiveData<WaitingDialogFragment.WaitingDialogData>()
|
||
val chooseImagesUpload = MutableLiveData<LinkedHashMap<String, String>>()
|
||
val chooseImagesUploadSuccess = MutableLiveData<LinkedHashMap<String, String>>()
|
||
var uploadImageSubscription: Disposable? = null
|
||
val mapImages = HashMap<String, String>()
|
||
val localVideoList = ArrayList<LocalVideoEntity>()
|
||
val uploadVideoErrorList = ArrayList<LocalVideoEntity>()
|
||
var currentUploadingVideo: LocalVideoEntity? = null
|
||
var type: String = "" //游戏论坛:game_bbs 官方论坛:official_bbs
|
||
private var mUploadVideoListener: UploadVideoListener? = null
|
||
val TITLE_MIN_LENGTH = 6
|
||
val MIN_TEXT_LENGTH = 6
|
||
val MAX_TEXT_LENGTH = 10000
|
||
val FILE_HOST = "file:///"
|
||
var id = ""//视频标记
|
||
var videoId = ""//更改封面视频id
|
||
val quoteCountEntity = QuoteCountEntity()//数据上报用
|
||
|
||
fun setUploadVideoListener(uploadVideoListener: UploadVideoListener) {
|
||
this.mUploadVideoListener = uploadVideoListener
|
||
}
|
||
|
||
//检查图片是否符合规则并上传图片
|
||
fun uploadPic(data: Intent) {
|
||
val uris = Matisse.obtainResult(data)
|
||
val rawImgUrlList = ArrayList<String>() // 需要上传图片的原始地址列表
|
||
val uploadingImgList = ArrayList<String>() // 正在上传图片的地址列表(已压缩处理)
|
||
val compressedImgUrlList = ArrayList<String>() // 压缩处理后图片的地址列表
|
||
|
||
for (uri in uris) {
|
||
val picturePath = PathUtils.getPath(getApplication(), uri)
|
||
if (picturePath != null) {
|
||
if (File(picturePath).length() > ImageUtils.getUploadFileMaxSize()) {
|
||
val count = ImageUtils.getUploadFileMaxSize() / 1024 / 1024
|
||
val application: Application = getApplication()
|
||
Utils.toast(
|
||
getApplication(),
|
||
application.getString(R.string.pic_max_hint, count)
|
||
)
|
||
continue
|
||
}
|
||
Utils.log("picturePath = $picturePath")
|
||
rawImgUrlList.add(picturePath)
|
||
} else {
|
||
Utils.log("picturePath is null")
|
||
}
|
||
}
|
||
if (rawImgUrlList.size == 0) return
|
||
val imageType = when (getRichType()) {
|
||
RichType.ARTICLE -> UploadImageUtils.UploadType.community_article
|
||
RichType.QUESTION -> UploadImageUtils.UploadType.question
|
||
else -> UploadImageUtils.UploadType.poster
|
||
}
|
||
uploadImageSubscription = UploadImageUtils.compressAndUploadImageList(
|
||
imageType,
|
||
rawImgUrlList,
|
||
false,
|
||
object : UploadImageUtils.OnUploadImageListListener {
|
||
override fun onProgress(total: Long, progress: Long) {}
|
||
|
||
override fun onCompressSuccess(imageUrls: List<String>) {
|
||
val chooseImageMd5Map = LinkedHashMap<String, String>()
|
||
imageUrls.forEach {
|
||
chooseImageMd5Map[MD5Utils.getUrlMD5(it)] = ""
|
||
}
|
||
uploadingImgList.addAll(imageUrls)
|
||
compressedImgUrlList.addAll(imageUrls)
|
||
chooseImagesUpload.postValue(chooseImageMd5Map)
|
||
}
|
||
|
||
override fun onSingleSuccess(imageUrlMap: Map<String, String>) {
|
||
imageUrlMap.forEach {
|
||
if (uploadingImgList.contains(it.key)) {
|
||
uploadingImgList.remove(it.key)
|
||
}
|
||
}
|
||
val map = LinkedHashMap<String, String>()
|
||
for (key in imageUrlMap.keys) {
|
||
map[MD5Utils.getUrlMD5(key)] = FILE_HOST + key.decodeURI()
|
||
mapImages[TextUtils.htmlEncode(key).decodeURI()] = imageUrlMap[key] ?: ""
|
||
}
|
||
chooseImagesUploadSuccess.value = map
|
||
}
|
||
|
||
override fun onSuccess(imageUrlMap: LinkedHashMap<String, String>, errorMap: Map<String, Exception>) {
|
||
imageUrlMap.forEach {
|
||
if (uploadingImgList.contains(it.key)) {
|
||
uploadingImgList.remove(it.key)
|
||
}
|
||
}
|
||
|
||
var errorSize = compressedImgUrlList.size - imageUrlMap.size
|
||
if (errorSize > 0 || uploadingImgList.isNotEmpty()) {
|
||
val errorImageMap = LinkedHashMap<String, String>()
|
||
for (key in errorMap.keys) {
|
||
errorImageMap[MD5Utils.getUrlMD5(key)] = ""
|
||
}
|
||
|
||
for (rawImgUrl in compressedImgUrlList) {
|
||
if (!imageUrlMap.containsKey(rawImgUrl)) {
|
||
errorImageMap[MD5Utils.getUrlMD5(rawImgUrl)] = ""
|
||
}
|
||
}
|
||
errorSize = if (errorMap.isEmpty()) {
|
||
errorImageMap.size
|
||
} else {
|
||
errorMap.size + errorImageMap.size
|
||
}
|
||
|
||
// value为空会删除PlaceholderImage
|
||
chooseImagesUploadSuccess.value = errorImageMap
|
||
|
||
if (handleUploadError(errorSize, errorMap)) return
|
||
|
||
if (errorSize > 0) {
|
||
ToastUtils.showToast(errorSize.toString() + "张图片上传失败")
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onError(errorMap: Map<String, Exception>) {
|
||
val errorSize = uploadingImgList.size
|
||
if (uploadingImgList.size > 0) {
|
||
val map = LinkedHashMap<String, String>()
|
||
for (key in uploadingImgList) {
|
||
map[MD5Utils.getUrlMD5(key)] = ""
|
||
}
|
||
// value为空会删除PlaceholderImage
|
||
chooseImagesUploadSuccess.value = map
|
||
}
|
||
|
||
if (handleUploadError(errorSize, errorMap)) return
|
||
|
||
if (errorSize == 0) return
|
||
|
||
if (errorSize == 1) {
|
||
ToastUtils.showToast("图片上传失败")
|
||
} else {
|
||
ToastUtils.showToast(errorSize.toString() + "张图片上传失败")
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 处理上传错误 (包括超时 cancel 的异常)
|
||
* @return 如果 toast 过了就返回 true
|
||
*/
|
||
private fun handleUploadError(
|
||
errorSize: Int,
|
||
errorMap: Map<String, Exception>
|
||
): Boolean {
|
||
for (error in errorMap.values) {
|
||
if (error is HttpException && error.code() == 403) {
|
||
val e = error.response()?.errorBody()?.string()?.toObject<ErrorEntity>()
|
||
if (e != null && e.code == 403017) {
|
||
ToastUtils.showToast(errorSize.toString() + "张图片的宽或高超过限制,请裁剪后上传")
|
||
} else {
|
||
ToastUtils.showToast(errorSize.toString() + "张违规图片上传失败")
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
fun uploadPoster(picturePath: String) {
|
||
processDialog.postValue(WaitingDialogFragment.WaitingDialogData("封面上传中...", true))
|
||
uploadImageSubscription =
|
||
UploadImageUtils.compressAndUploadImage(
|
||
UploadImageUtils.UploadType.poster,
|
||
picturePath,
|
||
false,
|
||
object : UploadImageUtils.OnUploadImageListener {
|
||
override fun onSuccess(imageUrl: String) {
|
||
patchVideoPoster(imageUrl)
|
||
}
|
||
|
||
override fun onError(e: Throwable?) {
|
||
handleUploadPosterResult(true)
|
||
}
|
||
|
||
override fun onProgress(total: Long, progress: Long) {
|
||
|
||
}
|
||
})
|
||
}
|
||
|
||
private fun patchVideoPoster(poster: String) {
|
||
if (id.isEmpty() || videoId.isEmpty()) return
|
||
val map = hashMapOf("poster" to poster, "type" to getVideoType())
|
||
mApi.patchInsertVideo(videoId, map.toRequestBody())
|
||
.compose(observableToMain())
|
||
.subscribe(object : Response<ResponseBody>() {
|
||
override fun onResponse(response: ResponseBody?) {
|
||
super.onResponse(response)
|
||
mUploadVideoListener?.changePoster(id, poster)
|
||
handleUploadPosterResult(false)
|
||
}
|
||
|
||
override fun onFailure(e: HttpException?) {
|
||
super.onFailure(e)
|
||
handleUploadPosterResult(true)
|
||
}
|
||
})
|
||
}
|
||
|
||
private fun handleUploadPosterResult(isFailure: Boolean = false) {
|
||
processDialog.postValue(WaitingDialogFragment.WaitingDialogData("封面上传中...", false))
|
||
if (isFailure) {
|
||
ToastUtils.showToast("封面更改失败")
|
||
}
|
||
id = ""
|
||
videoId = ""
|
||
}
|
||
|
||
fun deleteVideo(id: String) {
|
||
if (localVideoList.isNotEmpty()) {
|
||
val video = localVideoList.find { it.id == id }
|
||
if (video != null) {
|
||
if (UploadManager.isUploading(video.filePath)) {
|
||
UploadManager.cancelTask(video.filePath)
|
||
}
|
||
localVideoList.remove(video)
|
||
}
|
||
}
|
||
if (uploadVideoErrorList.isNotEmpty()) {
|
||
val video = uploadVideoErrorList.find { it.id == id }
|
||
if (video != null) {
|
||
uploadVideoErrorList.remove(video)
|
||
}
|
||
}
|
||
if (currentUploadingVideo?.id == id) {
|
||
currentUploadingVideo = null
|
||
uploadVideo()
|
||
}
|
||
}
|
||
|
||
fun uploadVideo() {
|
||
if (currentUploadingVideo != null) return
|
||
if (localVideoList.isEmpty()) return
|
||
currentUploadingVideo = localVideoList[0]
|
||
UploadManager.createUploadTask(currentUploadingVideo?.filePath
|
||
?: "", object : OnUploadListener {
|
||
override fun onProgressChanged(
|
||
uploadFilePath: String,
|
||
currentSize: Long,
|
||
totalSize: Long,
|
||
speed: Long
|
||
) {
|
||
runOnUiThread {
|
||
val percent = (currentSize * 100 / totalSize.toFloat()).roundTo(1)
|
||
currentUploadingVideo?.id?.let {
|
||
mUploadVideoListener?.updateVideoProgress(it, percent.toString())
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onUploadSuccess(uploadFilePath: String, url: String) {
|
||
if (currentUploadingVideo != null) {
|
||
postVideoPosterAndInfo(uploadFilePath, url)
|
||
}
|
||
}
|
||
|
||
override fun onUploadFailure(uploadFilePath: String, errorMsg: String) {
|
||
uploadVideoFailure()
|
||
}
|
||
})
|
||
}
|
||
|
||
private fun postVideoPosterAndInfo(uploadFilePath: String, url: String) {
|
||
val localVideoPoster =
|
||
getApplication<Application>().cacheDir.absolutePath + File.separator + System.currentTimeMillis() + ".jpg"
|
||
try {
|
||
val bmp = ThumbnailUtils.createVideoThumbnail(
|
||
uploadFilePath,
|
||
MediaStore.Images.Thumbnails.MINI_KIND
|
||
)
|
||
// bmp 可能为空
|
||
FileOutputStream(localVideoPoster).use { out ->
|
||
bmp?.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||
}
|
||
} catch (e: java.lang.Exception) {
|
||
e.printStackTrace()
|
||
ToastUtils.showToast("视频封面操作失败")
|
||
uploadVideoFailure()
|
||
return
|
||
}
|
||
uploadImageSubscription =
|
||
UploadImageUtils.compressAndUploadImage(
|
||
UploadImageUtils.UploadType.poster,
|
||
localVideoPoster,
|
||
false,
|
||
object : UploadImageUtils.OnUploadImageListener {
|
||
override fun onSuccess(imageUrl: String) {
|
||
postVideoInfo(url, imageUrl)
|
||
}
|
||
|
||
override fun onError(e: Throwable?) {
|
||
uploadVideoFailure()
|
||
}
|
||
|
||
override fun onProgress(total: Long, progress: Long) {
|
||
|
||
}
|
||
})
|
||
}
|
||
|
||
private fun postVideoInfo(url: String, poster: String) {
|
||
val map = HashMap<String, Any>().apply {
|
||
put("poster", poster)
|
||
put("url", url)
|
||
put("format", currentUploadingVideo?.format ?: "")
|
||
put("size", currentUploadingVideo?.size ?: 0)
|
||
put("length", (currentUploadingVideo?.duration ?: 0) / 1000)
|
||
put("type", getVideoType())
|
||
}
|
||
val requestBody = map.toRequestBody()
|
||
mApi.insertVideo(requestBody)
|
||
.compose(observableToMain())
|
||
.subscribe(object : Response<JsonObject>() {
|
||
override fun onResponse(response: JsonObject?) {
|
||
super.onResponse(response)
|
||
if (response != null) {
|
||
uploadVideoSuccess(poster, url, response)
|
||
}
|
||
}
|
||
|
||
override fun onFailure(e: HttpException?) {
|
||
super.onFailure(e)
|
||
uploadVideoFailure()
|
||
}
|
||
})
|
||
}
|
||
|
||
private fun uploadVideoSuccess(poster: String, url: String, data: JsonObject) {
|
||
processDialog.postValue(WaitingDialogFragment.WaitingDialogData("封面上传中...", false))
|
||
currentUploadingVideo?.let {
|
||
mUploadVideoListener?.changePoster(it.id, poster)
|
||
mUploadVideoListener?.videoUploadFinished(it.id, url, data)
|
||
UploadManager.cancelTask(it.filePath)
|
||
localVideoList.remove(it)
|
||
}
|
||
currentUploadingVideo = null
|
||
uploadVideo()
|
||
}
|
||
|
||
private fun uploadVideoFailure() {
|
||
processDialog.postValue(WaitingDialogFragment.WaitingDialogData("封面上传中...", false))
|
||
currentUploadingVideo?.let {
|
||
runOnUiThread {
|
||
mUploadVideoListener?.videoUploadFailed(it.id)
|
||
}
|
||
uploadVideoErrorList.add(it)
|
||
localVideoList.remove(it)
|
||
UploadManager.cancelTask(it.filePath)
|
||
}
|
||
currentUploadingVideo = null
|
||
uploadVideo()
|
||
}
|
||
|
||
fun checkIsAllUploadedAndToast(): Boolean {
|
||
if (localVideoList.isNotEmpty() || uploadVideoErrorList.isNotEmpty()) {
|
||
ToastUtils.showToast("视频未上传完成,视频内容保存失败")
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
private fun getVideoType(): String {
|
||
return when (type) {
|
||
BbsType.GAME_BBS.value -> {
|
||
when (getRichType()) {
|
||
RichType.ARTICLE -> BbsType.GAME_BBS_ARTICLE_INSERT.value
|
||
RichType.QUESTION -> BbsType.GAME_BBS_QUESTION_INSERT.value
|
||
else -> ""
|
||
}
|
||
}
|
||
BbsType.OFFICIAL_BBS.value -> {
|
||
when (getRichType()) {
|
||
RichType.ARTICLE -> BbsType.OFFICIAL_BBS_ARTICLE_INSERT.value
|
||
RichType.QUESTION -> BbsType.OFFICIAL_BBS_QUESTION_INSERT.value
|
||
else -> ""
|
||
}
|
||
}
|
||
else -> ""
|
||
}
|
||
}
|
||
|
||
abstract fun getRichType(): RichType
|
||
}
|
||
|
||
interface UploadVideoListener {
|
||
/**
|
||
* 插入视频占位图
|
||
*/
|
||
fun insertPlaceholderVideo(id: String, poster: String)
|
||
|
||
/**
|
||
* 更新视频进度条
|
||
*/
|
||
fun updateVideoProgress(id: String, progress: String)
|
||
|
||
/**
|
||
* 上传视频完成
|
||
*/
|
||
fun videoUploadFinished(id: String, url: String, msg: JsonObject)
|
||
|
||
/**
|
||
* 更换封面图
|
||
*/
|
||
fun changePoster(id: String, poster: String)
|
||
|
||
/**
|
||
* 上传失败
|
||
*/
|
||
fun videoUploadFailed(id: String)
|
||
}
|
||
|
||
enum class RichType {
|
||
ARTICLE,
|
||
QUESTION,
|
||
ANSWER
|
||
} |