diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/models/UploadPictureResultBean.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/models/UploadPictureResultBean.kt new file mode 100644 index 00000000..4555f44f --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/models/UploadPictureResultBean.kt @@ -0,0 +1,34 @@ +package com.huanchengfly.tieba.post.api.models + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UploadPictureResultBean( + @SerialName("error_code") + @SerializedName("error_code") + val errorCode: String, + @SerialName("error_msg") + @SerializedName("error_msg") + val errorMsg: String, + val resourceId: String, + val chunkNo: String, + val picId: String, + val picInfo: PicInfo +) + +@Serializable +data class PicInfo( + val originPic: PicInfoItem, + val bigPic: PicInfoItem, + val smallPic: PicInfoItem, +) + +@Serializable +data class PicInfoItem( + val width: String, + val height: String, + val type: String, + val picUrl: String +) \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/body/MyMultipartBody.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/body/MyMultipartBody.kt index 60022368..9ac54be9 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/body/MyMultipartBody.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/body/MyMultipartBody.kt @@ -12,6 +12,15 @@ import okio.ByteString.Companion.encodeUtf8 import java.io.IOException import java.util.* +fun buildMultipartBody( + boundary: String = UUID.randomUUID().toString(), + builder: MyMultipartBody.Builder.() -> Unit +): MyMultipartBody { + return MyMultipartBody.Builder(boundary) + .apply(builder) + .build() +} + @Suppress("NAME_SHADOWING") class MyMultipartBody internal constructor( private val boundaryByteString: ByteString, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialTiebaApi.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialTiebaApi.kt index ad47130f..8d8b5ae3 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialTiebaApi.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialTiebaApi.kt @@ -17,6 +17,7 @@ import com.huanchengfly.tieba.post.utils.MobileInfoUtil import com.huanchengfly.tieba.post.utils.UIDUtil import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow +import okhttp3.RequestBody import retrofit2.Call import retrofit2.http.* @@ -457,4 +458,16 @@ interface OfficialTiebaApi { @Field("tbs") tbs: String = AccountUtil.getLoginInfo()!!.tbs, @Field("stoken") stoken: String = AccountUtil.getSToken()!! ): Flow + + @Headers( + "${Header.FORCE_LOGIN}: ${Header.FORCE_LOGIN_TRUE}", + "${Header.DROP_HEADERS}: ${Header.CHARSET},${Header.CLIENT_TYPE}", + "${Header.NO_COMMON_PARAMS}: ${Param.SWAN_GAME_VER},${Param.SDK_VER}", + ) + @POST("/c/s/uploadPicture") + fun uploadPicture( + @Body body: RequestBody, + @retrofit2.http.Header(Header.COOKIE) cookie: String = "ka=open;BAIDUID=${ClientUtils.baiduId}".takeIf { ClientUtils.baiduId != null } + ?: "ka=open", + ): Flow } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/components/ImageUploader.kt b/app/src/main/java/com/huanchengfly/tieba/post/components/ImageUploader.kt new file mode 100644 index 00000000..4f680754 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/components/ImageUploader.kt @@ -0,0 +1,132 @@ +package com.huanchengfly.tieba.post.components + +import android.graphics.BitmapFactory +import android.util.Log +import com.huanchengfly.tieba.post.api.BOUNDARY +import com.huanchengfly.tieba.post.api.booleanToString +import com.huanchengfly.tieba.post.api.models.UploadPictureResultBean +import com.huanchengfly.tieba.post.api.retrofit.RetrofitTiebaApi +import com.huanchengfly.tieba.post.api.retrofit.body.MyMultipartBody +import com.huanchengfly.tieba.post.api.retrofit.body.buildMultipartBody +import com.huanchengfly.tieba.post.api.retrofit.exception.TiebaException +import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode +import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage +import com.huanchengfly.tieba.post.utils.MD5Util +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.withContext +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File +import java.io.RandomAccessFile + +class ImageUploader( + private val forumName: String, + private val chunkSize: Int = DEFAULT_CHUNK_SIZE +) { + companion object { + const val DEFAULT_CHUNK_SIZE = 512000 + + const val IMAGE_MAX_SIZE = 5242880 + const val ORIGIN_IMAGE_MAX_SIZE = 10485760 + + const val PIC_WATER_TYPE_NO = 0 + const val PIC_WATER_TYPE_USER_NAME = 1 + const val PIC_WATER_TYPE_FORUM_NAME = 2 + } + + fun uploadImages( + filePaths: List, + isOriginImage: Boolean = false, + ): Flow> { + return filePaths.asFlow() + .map { filePath -> + uploadSinglePicture(filePath, isOriginImage) + } + .runningFold>(initial = mutableListOf()) { list, result -> + list.add(result) + list + } + .filter { it.size == filePaths.size } + } + + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun uploadSinglePicture( + filePath: String, + isOriginImage: Boolean = false, + ): UploadPictureResultBean { + val option = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeFile(filePath, option) + val width = option.outWidth + val height = option.outHeight + check(width > 0 && height > 0) { "图片宽高不正确" } + val file = File(filePath) + val fileLength = file.length() + val maxSize = if (isOriginImage) ORIGIN_IMAGE_MAX_SIZE else IMAGE_MAX_SIZE + check(fileLength <= maxSize) { "图片大小超过限制" } + val fileMd5 = MD5Util.toMd5(file) + val isMultipleChunkSize = fileLength % chunkSize == 0L + val totalChunkNum = fileLength / chunkSize + if (isMultipleChunkSize) 0 else 1 + Log.i("ImageUploader", "fileLength=$fileLength, totalChunkNum=$totalChunkNum") + val requestBodies = (0 until totalChunkNum).map { chunk -> + val isFinish = chunk == totalChunkNum - 1 + val curChunkSize = if (isFinish) { + if (isMultipleChunkSize) { + chunkSize + } else { + fileLength % chunkSize + } + } else { + chunkSize + }.toInt() + val chunkBytes = ByteArray(curChunkSize) + withContext(Dispatchers.IO) { + RandomAccessFile(file, "r").use { + it.seek(chunk * chunkSize.toLong()) + it.read(chunkBytes) + } + } + buildMultipartBody(BOUNDARY) { + setType(MyMultipartBody.FORM) + addFormDataPart("alt", "json") + addFormDataPart("chunkNo", "${chunk + 1}") + if (forumName.isNotEmpty()) addFormDataPart("forum_name", forumName) + addFormDataPart("groupId", "1") + addFormDataPart("height", "$height") + addFormDataPart("isFinish", isFinish.booleanToString()) + addFormDataPart("is_bjh", "0") + addFormDataPart("pic_water_type", "2") + addFormDataPart("resourceId", "$fileMd5$chunkSize") + addFormDataPart("saveOrigin", isOriginImage.booleanToString()) + addFormDataPart("size", "$fileLength") + if (forumName.isNotEmpty()) addFormDataPart("small_flow_fname", forumName) + addFormDataPart("width", "$width") + addFormDataPart("chunk", "file", chunkBytes.toRequestBody()) + } + } + return requestBodies.asFlow() + .flatMapConcat { RetrofitTiebaApi.OFFICIAL_TIEBA_API.uploadPicture(it) } + .catch { + throw UploadPictureFailedException(it.getErrorCode(), it.getErrorMessage()) + } + .onEach { + Log.i("ImageUploader", "uploadSinglePicture: $it") + } + .last() + } +} + +class UploadPictureFailedException( + override val code: Int = -1, + override val message: String = "上传图片失败", +) : TiebaException(message) \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/utils/MD5Util.java b/app/src/main/java/com/huanchengfly/tieba/post/utils/MD5Util.java index 11e0ea03..31a0f7e1 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/utils/MD5Util.java +++ b/app/src/main/java/com/huanchengfly/tieba/post/utils/MD5Util.java @@ -1,5 +1,9 @@ package com.huanchengfly.tieba.post.utils; +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.FileInputStream; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -51,4 +55,27 @@ public class MD5Util { } return null; } + + @NonNull + public static String toMd5(@NonNull File file) { + if (!file.isFile()) { + return ""; + } + MessageDigest digest; + FileInputStream in; + byte[] buffer = new byte[1024]; + int len; + try { + digest = MessageDigest.getInstance("MD5"); + in = new FileInputStream(file); + while ((len = in.read(buffer, 0, 1024)) != -1) { + digest.update(buffer, 0, len); + } + in.close(); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + return toHexString(digest.digest()); + } } \ No newline at end of file