添付データのMime Type 判定の改善。サーバ側issueのあるHEIC,HEIF,AVIFを強制的に変換する

This commit is contained in:
tateisu 2023-05-14 14:29:23 +09:00
parent f45ebd780f
commit 1bf4d2b8c9
8 changed files with 397 additions and 299 deletions

View File

@ -483,7 +483,6 @@ class ActPost : AppCompatActivity(),
views.scrollView.viewTreeObserver.addOnScrollChangedListener(scrollListener)
views.etContent.contentMineTypeArray = AttachmentUploader.acceptableMimeTypes.toTypedArray()
views.etContent.contentCallback = { addAttachment(it) }
views.spLanguage.adapter = ArrayAdapter(

View File

@ -28,6 +28,7 @@ import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.AttachmentRequest
import jp.juggler.subwaytooter.util.AttachmentUploader
import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.subwaytooter.util.resolveMimeType
import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
@ -128,9 +129,19 @@ fun ActPost.addAttachment(
// onUploadEnd: () -> Unit = {},
) {
val account = this.account
val mimeType = attachmentUploader.getMimeType(uri, mimeTypeArg)?.notEmpty()
if (account == null) {
dialogOrToast(R.string.account_select_please)
return
}
val instance = TootInstance.getCached(account)
if (instance == null) {
dialogOrToast("missing instance imformation.")
return
}
val mimeType = uri.resolveMimeType(mimeTypeArg, this, instance)
?.notEmpty()
val isReply = states.inReplyToId != null
val instance = account?.let { TootInstance.getCached(it) }
when {
attachmentList.size >= 4 -> {
@ -138,21 +149,11 @@ fun ActPost.addAttachment(
return
}
account == null -> {
dialogOrToast(R.string.account_select_please)
return
}
mimeType == null -> {
dialogOrToast(R.string.mime_type_missing)
return
}
instance == null -> {
dialogOrToast("missing instance information")
return
}
instance.instanceType == InstanceType.Pixelfed && isReply -> {
AttachmentUploader.log.e("pixelfed_does_not_allow_reply_with_media")
dialogOrToast(R.string.pixelfed_does_not_allow_reply_with_media)

View File

@ -7,13 +7,9 @@ import android.os.Build
import jp.juggler.media.generateTempFile
import jp.juggler.media.transcodeAudio
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.AttachmentUploader.Companion.MIME_TYPE_JPEG
import jp.juggler.subwaytooter.util.AttachmentUploader.Companion.MIME_TYPE_PNG
import jp.juggler.subwaytooter.util.AttachmentUploader.Companion.MIME_TYPE_WEBP
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.getStreamSize
import jp.juggler.util.log.LogCategory
@ -65,7 +61,7 @@ class AttachmentRequest(
suspend fun createOpener(): InputStreamOpener {
// GIFはそのまま投げる
if (mimeType == AttachmentUploader.MIME_TYPE_GIF) {
if (mimeType == MIME_TYPE_GIF) {
return contentUriOpener(context.contentResolver, uri, mimeType, isImage = true)
}
@ -132,28 +128,26 @@ class AttachmentRequest(
)
}
private fun hasServerSupport(mimeType: String) =
mediaConfig?.jsonArray("supported_mime_types")?.contains(mimeType)
?: when (instance.instanceType) {
InstanceType.Pixelfed -> AttachmentUploader.acceptableMimeTypesPixelfed
else -> AttachmentUploader.acceptableMimeTypes
}.contains(mimeType)
private fun createResizedImageOpener(): InputStreamOpener {
try {
pa.progress = context.getString(R.string.attachment_handling_compress)
val canUseWebP = try {
hasServerSupport(MIME_TYPE_WEBP) && PrefB.bpUseWebP.value
MIME_TYPE_WEBP.mimeTypeIsSupported(instance) && PrefB.bpUseWebP.value
} catch (ex: Throwable) {
log.w(ex, "can't check canUseWebP")
false
}
// サーバが読めない形式の画像なら強制的に再圧縮をかける
// もしくは、PNG画像も可能ならWebPに変換したい
val canUseOriginal = hasServerSupport(mimeType) &&
!(mimeType == MIME_TYPE_PNG && canUseWebP)
val canUseOriginal = when {
// WebPを使っていい場合、PNG画像をWebPに変換したい
canUseWebP && mimeType == MIME_TYPE_PNG -> false
// WebPを使わない場合、入力がWebPなら強制的にPNGかJPEGにする
!canUseWebP && mimeType == MIME_TYPE_WEBP -> false
// ほか、サーバが受け入れる形式でリサイズ不要ならオリジナルのまま送信
// ただしHEICやHEIFはサーバ側issueが落ち着くまで変換必須とする
else -> mimeType.mimeTypeIsSupported(instance)
}
createResizedBitmap(
context,
@ -338,7 +332,7 @@ class AttachmentRequest(
private suspend fun createResizedAudioOpener(srcBytes: Long): InputStreamOpener =
when {
hasServerSupport(mimeType) &&
mimeType.mimeTypeIsSupported(instance) &&
goodAudioType.contains(mimeType) &&
srcBytes <= maxBytesVideo -> contentUriOpener(
context.contentResolver,

View File

@ -1,40 +1,46 @@
package jp.juggler.subwaytooter.util
import android.content.ContentResolver
import android.net.Uri
import android.os.Handler
import android.os.SystemClock
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.MimeTypes
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.ServiceType
import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.parseItem
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchIO
import jp.juggler.util.data.*
import jp.juggler.util.log.*
import jp.juggler.util.data.GetContentResultEntry
import jp.juggler.util.data.asciiPattern
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.encodeHex
import jp.juggler.util.data.encodeUTF8
import jp.juggler.util.data.getDocumentName
import jp.juggler.util.data.groupEx
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption
import jp.juggler.util.media.ResizeConfig
import jp.juggler.util.media.ResizeType
import jp.juggler.util.network.toPost
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.network.toPut
import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import okhttp3.MultipartBody
import java.io.*
import java.util.concurrent.CancellationException
import kotlin.coroutines.coroutineContext
@ -44,145 +50,6 @@ class AttachmentUploader(
) {
companion object {
val log = LogCategory("AttachmentUploader")
internal const val MIME_TYPE_JPEG = "image/jpeg"
internal const val MIME_TYPE_PNG = "image/png"
internal const val MIME_TYPE_GIF = "image/gif"
internal const val MIME_TYPE_WEBP = "image/webp"
val acceptableMimeTypes = HashSet<String>().apply {
//
add("image/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
add("video/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
add("audio/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
//
add("image/jpeg")
add("image/png")
add("image/gif")
add("video/webm")
add("video/mp4")
add("video/quicktime")
//
add("audio/webm")
add("audio/ogg")
add("audio/mpeg")
add("audio/mp3")
add("audio/wav")
add("audio/wave")
add("audio/x-wav")
add("audio/x-pn-wav")
add("audio/flac")
add("audio/x-flac")
// https://github.com/tootsuite/mastodon/pull/11342
add("audio/aac")
add("audio/m4a")
add("audio/3gpp")
}
val acceptableMimeTypesPixelfed = HashSet<String>().apply {
//
add("image/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
add("video/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
//
add("image/jpeg")
add("image/png")
add("image/gif")
add("video/mp4")
add("video/m4v")
}
private val imageHeaderList = listOf(
Pair(
"image/jpeg",
intArrayOf(0xff, 0xd8, 0xff).toByteArray()
),
Pair(
"image/png",
intArrayOf(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A).toByteArray()
),
Pair(
"image/gif",
"GIF".toByteArray(Charsets.UTF_8)
),
Pair(
"audio/wav",
"RIFF".toByteArray(Charsets.UTF_8),
),
Pair(
"audio/ogg",
"OggS".toByteArray(Charsets.UTF_8),
),
Pair(
"audio/flac",
"fLaC".toByteArray(Charsets.UTF_8),
),
Pair(
"image/bmp",
"BM".toByteArray(Charsets.UTF_8),
),
Pair(
"image/webp",
"RIFF****WEBP".toByteArray(Charsets.UTF_8),
),
).sortedByDescending { it.second.size }
private val sig3gp = arrayOf(
"3ge6",
"3ge7",
"3gg6",
"3gp1",
"3gp2",
"3gp3",
"3gp4",
"3gp5",
"3gp6",
"3gp7",
"3gr6",
"3gr7",
"3gs6",
"3gs7",
"kddi"
).map { it.toCharArray().toLowerByteArray() }
private val sigM4a = arrayOf(
"M4A ",
"M4B ",
"M4P "
).map { it.toCharArray().toLowerByteArray() }
private val sigFtyp = "ftyp".toCharArray().toLowerByteArray()
private fun matchSig(
data: ByteArray,
dataOffset: Int,
sig: ByteArray,
sigSize: Int = sig.size,
): Boolean {
for (i in 0 until sigSize) {
if (data[dataOffset + i] != sig[i]) return false
}
return true
}
private const val wild = '?'.code.toByte()
private fun ByteArray.startWithWildcard(
key: ByteArray,
thisOffset: Int = 0,
keyOffset: Int = 0,
length: Int = key.size - keyOffset,
): Boolean {
if (thisOffset + length > this.size || keyOffset + length > key.size) {
return false
}
for (i in 0 until length) {
val cThis = this[i + thisOffset]
val cKey = key[i + keyOffset]
if (cKey != wild && cKey != cThis) return false
}
return true
}
}
private val safeContext = activity.applicationContext!!
@ -212,7 +79,7 @@ class AttachmentUploader(
}
}
val result = try {
if (request.pa.isCancelled == true) continue
if (request.pa.isCancelled) continue
withContext(request.pa.job + AppDispatchers.IO) {
request.upload()
}
@ -294,16 +161,7 @@ class AttachmentUploader(
)
}
val isAccepteble = instance.configuration
?.jsonObject("media_attachments")
?.jsonArray("supported_mime_types")
?.contains(opener.mimeType)
?: when (instance.instanceType) {
InstanceType.Pixelfed -> acceptableMimeTypesPixelfed
else -> acceptableMimeTypes
}.contains(opener.mimeType)
if (!isAccepteble) {
if (!opener.mimeType.mimeTypeIsSupported(instance)) {
return TootApiResult(
safeContext.getString(R.string.mime_type_not_acceptable, opener.mimeType)
)
@ -480,123 +338,25 @@ class AttachmentUploader(
pa.callback?.onPostAttachmentComplete(pa)
}
fun getMimeType(uri: Uri, mimeTypeArg: String?): String? {
// image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い
// クレームで消耗するのを減らすためにファイルヘッダを確認する
if (mimeTypeArg == null || mimeTypeArg.startsWith("image/")) {
val sv = findMimeTypeByFileHeader(safeContext.contentResolver, uri)
if (sv != null) return sv
}
// 既に引数で与えられてる
if (mimeTypeArg?.isNotEmpty() == true) {
return mimeTypeArg
}
// ContentResolverに尋ねる
var sv = safeContext.contentResolver.getType(uri)
if (sv?.isNotEmpty() == true) return sv
// gboardのステッカーではUriのクエリパラメータにmimeType引数がある
sv = uri.getQueryParameter("mimeType")
if (sv?.isNotEmpty() == true) return sv
return null
}
private fun findMimeTypeByFileHeader(
contentResolver: ContentResolver,
uri: Uri,
): String? {
try {
contentResolver.openInputStream(uri)?.use { inStream ->
val data = ByteArray(65536)
val nRead = inStream.read(data, 0, data.size)
for (pair in imageHeaderList) {
val type = pair.first
val header = pair.second
if (nRead >= header.size && data.startWithWildcard(header)) return type
}
// scan frame header
for (i in 0 until nRead - 8) {
if (!matchSig(data, i, sigFtyp)) continue
// 3gpp check
for (s in sig3gp) {
if (matchSig(data, i + 4, s)) return "audio/3gpp"
}
// m4a check
for (s in sigM4a) {
if (matchSig(data, i + 4, s)) return "audio/m4a"
}
}
// scan frame header
loop@ for (i in 0 until nRead - 2) {
// mpeg frame header
val b0 = data[i].toInt() and 255
if (b0 != 255) continue
val b1 = data[i + 1].toInt() and 255
if ((b1 and 0b11100000) != 0b11100000) continue
val mpegVersionId = ((b1 shr 3) and 3)
// 00 mpeg 2.5
// 01 not used
// 10 (mp3) mpeg 2 / (AAC) mpeg-4
// 11 (mp3) mpeg 1 / (AAC) mpeg-2
@Suppress("MoveVariableDeclarationIntoWhen")
val mpegLayerId = ((b1 shr 1) and 3)
// 00 (mp3)not used / (AAC) always 0
// 01 (mp3)layer III
// 10 (mp3)layer II
// 11 (mp3)layer I
when (mpegLayerId) {
0 -> when (mpegVersionId) {
2, 3 -> return "audio/aac"
else -> {
}
}
1 -> when (mpegVersionId) {
0, 2, 3 -> return "audio/mp3"
else -> {
}
}
}
}
}
} catch (ex: Throwable) {
log.e(ex, "findMimeTypeByFileHeader failed.")
}
return null
}
///////////////////////////////////////////////////////////////
// 添付データのカスタムサムネイル
// 添付データのカスタムサムネイル
suspend fun uploadCustomThumbnail(
account: SavedAccount,
src: GetContentResultEntry,
pa: PostAttachment,
): TootApiResult? = try {
safeContext.runApiTask(account) { client ->
val mimeType = getMimeType(src.uri, src.mimeType)?.notEmpty()
val (ti, ri) = TootInstance.get(client)
ti ?: return@runApiTask ri
val mimeType = src.uri.resolveMimeType(src.mimeType, safeContext, ti)
if (mimeType.isNullOrEmpty()) {
return@runApiTask TootApiResult(safeContext.getString(R.string.mime_type_missing))
}
val (ti, ri) = TootInstance.get(client)
ti ?: return@runApiTask ri
val mediaConfig = ti.configuration?.jsonObject("media_attachments")
val ar = AttachmentRequest(
context = applicationContext,
context = safeContext,
account = account,
pa = pa,
uri = src.uri,
@ -611,7 +371,10 @@ class AttachmentUploader(
val opener = ar.createOpener()
if (opener.contentLength > ar.maxBytesImage) {
return@runApiTask TootApiResult(
getString(R.string.file_size_too_big, ar.maxBytesImage / 1000000)
getString(
R.string.file_size_too_big,
ar.maxBytesImage / 1000000
)
)
}

View File

@ -0,0 +1,319 @@
package jp.juggler.subwaytooter.util
import android.content.ContentResolver
import android.content.Context
import android.media.MediaMetadataRetriever
import android.net.Uri
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.toByteArray
import jp.juggler.util.data.toLowerByteArray
import jp.juggler.util.log.LogCategory
import jp.juggler.util.media.bitmapMimeType
private val log = LogCategory("MimeTypeUtils")
const val MIME_TYPE_JPEG = "image/jpeg"
const val MIME_TYPE_PNG = "image/png"
const val MIME_TYPE_GIF = "image/gif"
const val MIME_TYPE_WEBP = "image/webp"
private val acceptableMimeTypes = HashSet<String>().apply {
//
add("image/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
add("video/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
add("audio/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
//
add("image/jpeg")
add("image/png")
add("image/gif")
add("video/webm")
add("video/mp4")
add("video/quicktime")
//
add("audio/webm")
add("audio/ogg")
add("audio/mpeg")
add("audio/mp3")
add("audio/wav")
add("audio/wave")
add("audio/x-wav")
add("audio/x-pn-wav")
add("audio/flac")
add("audio/x-flac")
// https://github.com/tootsuite/mastodon/pull/11342
add("audio/aac")
add("audio/m4a")
add("audio/3gpp")
}
private val acceptableMimeTypesPixelfed = HashSet<String>().apply {
//
add("image/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
add("video/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
//
add("image/jpeg")
add("image/png")
add("image/gif")
add("video/mp4")
add("video/m4v")
}
private val imageHeaderList = listOf(
Pair(
"image/jpeg",
intArrayOf(0xff, 0xd8, 0xff).toByteArray()
),
Pair(
"image/png",
intArrayOf(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A).toByteArray()
),
Pair(
"image/gif",
"GIF".toByteArray(Charsets.UTF_8)
),
Pair(
"audio/wav",
"RIFF".toByteArray(Charsets.UTF_8),
),
Pair(
"audio/ogg",
"OggS".toByteArray(Charsets.UTF_8),
),
Pair(
"audio/flac",
"fLaC".toByteArray(Charsets.UTF_8),
),
Pair(
"image/bmp",
"BM".toByteArray(Charsets.UTF_8),
),
Pair(
"image/webp",
"RIFF****WEBP".toByteArray(Charsets.UTF_8),
),
).sortedByDescending { it.second.size }
private val sig3gp = arrayOf(
"3ge6",
"3ge7",
"3gg6",
"3gp1",
"3gp2",
"3gp3",
"3gp4",
"3gp5",
"3gp6",
"3gp7",
"3gr6",
"3gr7",
"3gs6",
"3gs7",
"kddi"
).map { it.toCharArray().toLowerByteArray() }
private val sigM4a = arrayOf(
"M4A ",
"M4B ",
"M4P "
).map { it.toCharArray().toLowerByteArray() }
private const val wild = '?'.code.toByte()
private val sigFtyp = "ftyp".toCharArray().toLowerByteArray()
private fun matchSig(
data: ByteArray,
dataOffset: Int,
sig: ByteArray,
sigSize: Int = sig.size,
): Boolean {
for (i in 0 until sigSize) {
if (data[dataOffset + i] != sig[i]) return false
}
return true
}
private fun ByteArray.startWithWildcard(
key: ByteArray,
thisOffset: Int = 0,
keyOffset: Int = 0,
length: Int = key.size - keyOffset,
): Boolean {
if (thisOffset + length > this.size || keyOffset + length > key.size) {
return false
}
for (i in 0 until length) {
val cThis = this[i + thisOffset]
val cKey = key[i + keyOffset]
if (cKey != wild && cKey != cThis) return false
}
return true
}
private fun findMimeTypeByFileHeader(
contentResolver: ContentResolver,
uri: Uri,
): String? {
try {
contentResolver.openInputStream(uri)?.use { inStream ->
val data = ByteArray(65536)
val nRead = inStream.read(data, 0, data.size)
for (pair in imageHeaderList) {
val type = pair.first
val header = pair.second
if (nRead >= header.size && data.startWithWildcard(header)) return type
}
// scan frame header
for (i in 0 until nRead - 8) {
if (!matchSig(data, i, sigFtyp)) continue
// 3gpp check
for (s in sig3gp) {
if (matchSig(data, i + 4, s)) return "audio/3gpp"
}
// m4a check
for (s in sigM4a) {
if (matchSig(data, i + 4, s)) return "audio/m4a"
}
}
// scan frame header
loop@ for (i in 0 until nRead - 2) {
// mpeg frame header
val b0 = data[i].toInt() and 255
if (b0 != 255) continue
val b1 = data[i + 1].toInt() and 255
if ((b1 and 0b11100000) != 0b11100000) continue
val mpegVersionId = ((b1 shr 3) and 3)
// 00 mpeg 2.5
// 01 not used
// 10 (mp3) mpeg 2 / (AAC) mpeg-4
// 11 (mp3) mpeg 1 / (AAC) mpeg-2
@Suppress("MoveVariableDeclarationIntoWhen")
val mpegLayerId = ((b1 shr 1) and 3)
// 00 (mp3)not used / (AAC) always 0
// 01 (mp3)layer III
// 10 (mp3)layer II
// 11 (mp3)layer I
when (mpegLayerId) {
0 -> when (mpegVersionId) {
2, 3 -> return "audio/aac"
else -> {
}
}
1 -> when (mpegVersionId) {
0, 2, 3 -> return "audio/mp3"
else -> {
}
}
}
}
}
} catch (ex: Throwable) {
log.e(ex, "findMimeTypeByFileHeader failed.")
}
return null
}
private fun String.isProblematicImageType(instance: TootInstance) = when (instance.instanceType) {
InstanceType.Mastodon -> when (this) {
// https://github.com/mastodon/mastodon/issues/23588
"image/heic", "image/heif" -> true
// https://github.com/mastodon/mastodon/issues/20834
"image/avif" -> true
else -> false
}
InstanceType.Pixelfed -> when (this) {
// Pixelfed は PC Web UI で画像を開くダイアログの時点でHEIC,HEIF,AVIF を選択できない
"image/heic", "image/heif", "image/avif" -> true
else -> false
}
// PleromaやMisskeyでの問題は調べてない
else -> false
}
fun String.mimeTypeIsSupportedByServer(instance: TootInstance) =
instance.configuration
?.jsonObject("media_attachments")
?.jsonArray("supported_mime_types")
?.contains(this)
?: when (instance.instanceType) {
InstanceType.Pixelfed -> acceptableMimeTypesPixelfed
else -> acceptableMimeTypes
}.contains(this)
fun String.mimeTypeIsSupported(instance: TootInstance) = when {
isProblematicImageType(instance) -> false
else -> mimeTypeIsSupportedByServer(instance)
}
fun Uri.resolveMimeType(
mimeTypeArg1: String?,
context: Context,
instance: TootInstance,
): String? {
// image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い
// application/octet-stream などが誤設定されてることもある
// Androidが静止画を読めるならそのmimeType
bitmapMimeType(context.contentResolver)?.notEmpty()?.let { return it }
// 動画の一部は音声かもしれない
// データに動画や音声が含まれるか調べる
try {
MediaMetadataRetriever().use { mmr ->
mmr.setDataSource(context, this)
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
}?.notEmpty()?.let { return it }
} catch (ex: Throwable) {
log.w(ex, "not video or audio.")
}
// 引数のmimeTypeがサーバでサポートされているならソレ
try {
mimeTypeArg1
?.notEmpty()
?.takeIf { it.mimeTypeIsSupportedByServer(instance) }
?.let { return it }
} catch (ex: Throwable) {
AttachmentUploader.log.w(ex, "mimeTypeArg1 check failed.")
}
// ContentResolverに尋ねる
try {
context.contentResolver.getType(this)
?.notEmpty()
?.takeIf { it.mimeTypeIsSupportedByServer(instance) }
?.let { return it }
} catch (ex: Throwable) {
AttachmentUploader.log.w(ex, "contentResolver.getType failed.")
}
// gboardのステッカーではUriのクエリパラメータにmimeType引数がある
try {
getQueryParameter("mimeType")
?.notEmpty()
?.takeIf { it.mimeTypeIsSupportedByServer(instance) }
?.let { return it }
} catch (ex: Throwable) {
log.w(ex, "getQueryParameter(mimeType) failed.")
}
// ファイルヘッダを読んで判定する
findMimeTypeByFileHeader(context.contentResolver, this)
?.notEmpty()?.let { return it }
return null
}

View File

@ -32,8 +32,6 @@ class MyEditText @JvmOverloads constructor(
///////////////////////////////////////////////////////
// IMEから画像を送られてくることがあるらしい
var contentMineTypeArray: Array<String>? = null
private val receiveContentListener =
OnReceiveContentListener { _: View, payload: ContentInfoCompat ->
// コールバックが設定されていないなら何も受け取らない

View File

@ -11,6 +11,8 @@ import jp.juggler.util.log.LogCategory
import okhttp3.internal.closeQuietly
import java.io.InputStream
private val log = LogCategory("StorageUtils")
// internal object StorageUtils{
//
// private val log = LogCategory("StorageUtils")
@ -285,8 +287,14 @@ data class GetContentResultEntry(
fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<GetContentResultEntry> {
val urlList = ArrayList<GetContentResultEntry>()
// 単一選択
this.data?.let {
urlList.add(GetContentResultEntry(it, this.type))
data?.let {
val mimeType = try {
type ?: contentResolver.getType(it)
} catch (ex: Throwable) {
log.w(ex, "contentResolver.getType failed. uri=$it")
null
}
urlList.add(GetContentResultEntry(it, mimeType))
}
// 複数選択
this.clipData?.let { clipData ->

View File

@ -1,6 +1,7 @@
package jp.juggler.util.media
//import it.sephiroth.android.library.exif2.ExifInterface
import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@ -12,6 +13,7 @@ import android.graphics.PointF
import android.net.Uri
import androidx.annotation.StringRes
import androidx.exifinterface.media.ExifInterface
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import java.io.FileNotFoundException
@ -124,6 +126,20 @@ fun createResizedBitmap(
skipIfNoNeedToResizeAndRotate = skipIfNoNeedToResizeAndRotate
)
fun Uri.bitmapMimeType(contentResolver: ContentResolver): String? =
try {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
options.inScaled = false
contentResolver.openInputStream(this)?.use {
BitmapFactory.decodeStream(it, null, options)
}
options.outMimeType?.notEmpty()
} catch (ex: Throwable) {
log.w(ex, "bitmapMimeType: can't check bitmap mime type.")
null
}
fun createResizedBitmap(
context: Context,