画像と動画の選択をJetpack ActivityのPickVisualMediaに移行する
This commit is contained in:
parent
5f7f4c34ec
commit
ceca568a1a
|
@ -6,7 +6,7 @@ import java.util.Properties
|
|||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.google.devtools.ksp")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
}
|
||||
|
|
|
@ -63,10 +63,14 @@
|
|||
android:maxSdkVersion="32"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<!-- Devices running Android 13 (API level 33) or higher -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<!-- To handle the reselection within the app on Android 14 (API level 34) -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
|
|
@ -14,43 +14,75 @@ import android.text.Editable
|
|||
import android.text.SpannableString
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.Spinner
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.jrummyapps.android.colorpicker.ColorPickerDialog
|
||||
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.auth.AuthBase
|
||||
import jp.juggler.subwaytooter.api.auth.authRepo
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.ServiceType
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||
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.TootVisibility
|
||||
import jp.juggler.subwaytooter.api.entity.parseItem
|
||||
import jp.juggler.subwaytooter.api.runApiTask
|
||||
import jp.juggler.subwaytooter.api.runApiTask2
|
||||
import jp.juggler.subwaytooter.api.showApiError
|
||||
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||
import jp.juggler.subwaytooter.notification.*
|
||||
import jp.juggler.subwaytooter.notification.checkNotificationImmediate
|
||||
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
|
||||
import jp.juggler.subwaytooter.notification.resetNotificationTracking
|
||||
import jp.juggler.subwaytooter.push.PushBase
|
||||
import jp.juggler.subwaytooter.push.pushRepo
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.table.daoAcctColor
|
||||
import jp.juggler.subwaytooter.table.daoSavedAccount
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
import jp.juggler.util.*
|
||||
import jp.juggler.util.backPressed
|
||||
import jp.juggler.util.coroutine.AppDispatchers
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.coroutine.launchMain
|
||||
import jp.juggler.util.coroutine.launchProgress
|
||||
import jp.juggler.util.data.*
|
||||
import jp.juggler.util.data.UriSerializer
|
||||
import jp.juggler.util.data.getDocumentName
|
||||
import jp.juggler.util.data.getStreamSize
|
||||
import jp.juggler.util.data.notZero
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.showToast
|
||||
import jp.juggler.util.log.withCaption
|
||||
import jp.juggler.util.long
|
||||
import jp.juggler.util.media.ResizeConfig
|
||||
import jp.juggler.util.media.ResizeType
|
||||
import jp.juggler.util.media.createResizedBitmap
|
||||
import jp.juggler.util.network.toPatch
|
||||
import jp.juggler.util.network.toPost
|
||||
import jp.juggler.util.network.toPostRequestBuilder
|
||||
import jp.juggler.util.ui.*
|
||||
import jp.juggler.util.ui.ActivityResultHandler
|
||||
import jp.juggler.util.ui.attrColor
|
||||
import jp.juggler.util.ui.isEnabledAlpha
|
||||
import jp.juggler.util.ui.isOk
|
||||
import jp.juggler.util.ui.scan
|
||||
import jp.juggler.util.ui.vg
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import okhttp3.MediaType
|
||||
|
@ -164,61 +196,39 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
loadLanguageList()
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
private var permissionCamera = permissionSpecCamera.requester {
|
||||
openCamera()
|
||||
}
|
||||
|
||||
private var pickImageLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||
|
||||
private val pickImageCallback = ActivityResultCallback<Uri?> {
|
||||
handlePickImageResult(it)
|
||||
}
|
||||
|
||||
private val arCameraImage = ActivityResultHandler(log) {
|
||||
handleCameraResult(it)
|
||||
}
|
||||
|
||||
private val arShowAcctColor = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
showAcctColor()
|
||||
if (r.isOk) showAcctColor()
|
||||
}
|
||||
|
||||
private val arAddAttachment = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
r.data
|
||||
?.handleGetContentResult(contentResolver)
|
||||
?.firstOrNull()
|
||||
?.let {
|
||||
uploadImage(
|
||||
state.propName,
|
||||
it.uri,
|
||||
it.uri.resolveMimeType(it.mimeType, this),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val arCameraImage = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) {
|
||||
// 失敗したら DBからデータを削除
|
||||
state.uriCameraImage?.let {
|
||||
contentResolver.delete(it, null, null)
|
||||
}
|
||||
state.uriCameraImage = null
|
||||
} else {
|
||||
// 画像のURL
|
||||
val uri = r.data?.data ?: state.uriCameraImage
|
||||
if (uri != null) {
|
||||
uploadImage(
|
||||
state.propName,
|
||||
uri,
|
||||
uri.resolveMimeType(null, this),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val prPickAvater = permissionSpecImagePicker.requester { openPicker(it) }
|
||||
private val prPickHeader = permissionSpecImagePicker.requester { openPicker(it) }
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
backPressed { handleBackPressed() }
|
||||
|
||||
prPickAvater.register(this)
|
||||
prPickHeader.register(this)
|
||||
pickImageLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.PickVisualMedia(),
|
||||
pickImageCallback,
|
||||
)
|
||||
|
||||
permissionCamera.register(this)
|
||||
arShowAcctColor.register(this)
|
||||
arAddAttachment.register(this)
|
||||
arCameraImage.register(this)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
|
@ -1335,64 +1345,89 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
}
|
||||
|
||||
private fun pickAvatarImage() {
|
||||
openPicker(prPickAvater)
|
||||
openImagePickerOrCamera("avatar")
|
||||
}
|
||||
|
||||
private fun pickHeaderImage() {
|
||||
openPicker(prPickHeader)
|
||||
openImagePickerOrCamera("header")
|
||||
}
|
||||
|
||||
private fun openPicker(permissionRequester: PermissionRequester) {
|
||||
private fun openImagePickerOrCamera(propName: String) {
|
||||
state.propName = propName
|
||||
launchAndShowError {
|
||||
if (!permissionRequester.checkOrLaunch()) return@launchAndShowError
|
||||
val propName = when (permissionRequester) {
|
||||
prPickHeader -> "header"
|
||||
else -> "avatar"
|
||||
}
|
||||
actionsDialog {
|
||||
action(getString(R.string.pick_image)) {
|
||||
performAttachment(propName)
|
||||
openPickImage()
|
||||
}
|
||||
action(getString(R.string.image_capture)) {
|
||||
performCamera(propName)
|
||||
openCamera()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performAttachment(propName: String) {
|
||||
try {
|
||||
state.propName = propName
|
||||
val intent = intentGetContent(false, getString(R.string.pick_image), arrayOf("image/*"))
|
||||
arAddAttachment.launch(intent)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "performAttachment failed.")
|
||||
showToast(ex, "performAttachment failed.")
|
||||
}
|
||||
private fun openPickImage() {
|
||||
(pickImageLauncher ?: error("pickImageLauncher not registered")).launch(
|
||||
PickVisualMediaRequest(
|
||||
ActivityResultContracts.PickVisualMedia.ImageOnly,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun performCamera(propName: String) {
|
||||
private fun handlePickImageResult(uri: Uri?) {
|
||||
uri ?: return
|
||||
uploadImage(
|
||||
state.propName,
|
||||
uri,
|
||||
uri.resolveMimeType(null, this),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
private fun openCamera() {
|
||||
if (!permissionCamera.checkOrLaunch()) return
|
||||
launchAndShowError(errorCaption = "openCamera failed.") {
|
||||
// カメラで撮影
|
||||
val filename = System.currentTimeMillis().toString() + ".jpg"
|
||||
val values = ContentValues()
|
||||
values.put(MediaStore.Images.Media.TITLE, filename)
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.TITLE, filename)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
}
|
||||
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
state.uriCameraImage = uri
|
||||
.also { state.uriCameraImage = it }
|
||||
|
||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
||||
|
||||
state.propName = propName
|
||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||
putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
||||
}
|
||||
arCameraImage.launch(intent)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "opening camera app failed.")
|
||||
showToast(ex, "opening camera app failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCameraResult(r: ActivityResult) {
|
||||
when {
|
||||
r.isOk -> {
|
||||
// 画像のURL
|
||||
val uri = r.data?.data ?: state.uriCameraImage
|
||||
if (uri != null) {
|
||||
uploadImage(
|
||||
state.propName,
|
||||
uri,
|
||||
uri.resolveMimeType(null, this),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// 失敗したら DBからデータを削除
|
||||
state.uriCameraImage?.let {
|
||||
contentResolver.delete(it, null, null)
|
||||
}
|
||||
state.uriCameraImage = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
|
||||
internal interface InputStreamOpener {
|
||||
val mimeType: String
|
||||
val uri: Uri
|
||||
|
|
|
@ -22,7 +22,6 @@ import jp.juggler.subwaytooter.actpost.CompletionHelper
|
|||
import jp.juggler.subwaytooter.actpost.FeaturedTagCache
|
||||
import jp.juggler.subwaytooter.actpost.addAttachment
|
||||
import jp.juggler.subwaytooter.actpost.applyMushroomText
|
||||
import jp.juggler.subwaytooter.actpost.rearrangeAttachments
|
||||
import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl
|
||||
import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl
|
||||
import jp.juggler.subwaytooter.actpost.openAttachment
|
||||
|
@ -33,6 +32,7 @@ import jp.juggler.subwaytooter.actpost.performAttachmentClick
|
|||
import jp.juggler.subwaytooter.actpost.performMore
|
||||
import jp.juggler.subwaytooter.actpost.performPost
|
||||
import jp.juggler.subwaytooter.actpost.performSchedule
|
||||
import jp.juggler.subwaytooter.actpost.rearrangeAttachments
|
||||
import jp.juggler.subwaytooter.actpost.removeReply
|
||||
import jp.juggler.subwaytooter.actpost.resetSchedule
|
||||
import jp.juggler.subwaytooter.actpost.restoreState
|
||||
|
@ -82,7 +82,7 @@ import java.util.concurrent.ConcurrentHashMap
|
|||
class ActPost : AppCompatActivity(),
|
||||
View.OnClickListener,
|
||||
PostAttachment.Callback,
|
||||
MyClickableSpanHandler, AttachmentPicker.Callback {
|
||||
MyClickableSpanHandler {
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("ActPost")
|
||||
|
@ -215,7 +215,21 @@ class ActPost : AppCompatActivity(),
|
|||
appState = App1.getAppState(this)
|
||||
handler = appState.handler
|
||||
attachmentUploader = AttachmentUploader(this, handler)
|
||||
attachmentPicker = AttachmentPicker(this, this)
|
||||
attachmentPicker = AttachmentPicker(this, object : AttachmentPicker.Callback {
|
||||
override suspend fun onPickAttachment(uri: Uri, mimeType: String?) {
|
||||
addAttachment(uri, mimeType)
|
||||
}
|
||||
|
||||
override suspend fun onPickCustomThumbnail(
|
||||
attachmentId: String?,
|
||||
src: GetContentResultEntry,
|
||||
) {
|
||||
val pa = attachmentList.find { it.attachment?.id?.toString() == attachmentId }
|
||||
?: error("missing attachment for attachmentId=$attachmentId")
|
||||
onPickCustomThumbnailImpl(pa, src)
|
||||
}
|
||||
})
|
||||
|
||||
density = resources.displayMetrics.density
|
||||
arMushroom.register(this)
|
||||
|
||||
|
@ -315,6 +329,7 @@ class ActPost : AppCompatActivity(),
|
|||
R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList(
|
||||
featuredTagCache[account?.acct?.ascii ?: ""]?.list
|
||||
)
|
||||
|
||||
R.id.btnAttachmentsRearrange -> rearrangeAttachments()
|
||||
R.id.ibSchedule -> performSchedule()
|
||||
R.id.ibScheduleReset -> resetSchedule()
|
||||
|
@ -337,10 +352,6 @@ class ActPost : AppCompatActivity(),
|
|||
openBrowser(span.linkInfo.url)
|
||||
}
|
||||
|
||||
override fun onPickAttachment(uri: Uri, mimeType: String?) {
|
||||
addAttachment(uri, mimeType)
|
||||
}
|
||||
|
||||
override fun onPostAttachmentProgress() {
|
||||
launchIO {
|
||||
try {
|
||||
|
@ -355,15 +366,6 @@ class ActPost : AppCompatActivity(),
|
|||
onPostAttachmentCompleteImpl(pa)
|
||||
}
|
||||
|
||||
override fun resumeCustomThumbnailTarget(id: String?): PostAttachment? {
|
||||
id ?: return null
|
||||
return attachmentList.find { it.attachment?.id?.toString() == id }
|
||||
}
|
||||
|
||||
override fun onPickCustomThumbnail(pa: PostAttachment, src: GetContentResultEntry) {
|
||||
onPickCustomThumbnailImpl(pa, src)
|
||||
}
|
||||
|
||||
fun initUI() {
|
||||
setContentView(views.root)
|
||||
|
||||
|
|
|
@ -269,7 +269,9 @@ fun ActPost.performAttachmentClick(idx: Int) {
|
|||
TootAttachmentType.GIFV,
|
||||
TootAttachmentType.Video,
|
||||
-> action(getString(R.string.custom_thumbnail)) {
|
||||
attachmentPicker.openCustomThumbnail(pa)
|
||||
attachmentPicker.openCustomThumbnail(
|
||||
attachmentId = pa.attachment?.id?.toString()
|
||||
)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
|
|
|
@ -4,6 +4,11 @@ import android.content.ContentValues
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||
|
@ -17,8 +22,8 @@ import jp.juggler.util.log.LogCategory
|
|||
import jp.juggler.util.log.showToast
|
||||
import jp.juggler.util.ui.ActivityResultHandler
|
||||
import jp.juggler.util.ui.isNotOk
|
||||
import jp.juggler.util.ui.isOk
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
class AttachmentPicker(
|
||||
|
@ -33,9 +38,8 @@ class AttachmentPicker(
|
|||
|
||||
// callback after media selected
|
||||
interface Callback {
|
||||
fun onPickAttachment(uri: Uri, mimeType: String? = null)
|
||||
fun onPickCustomThumbnail(pa: PostAttachment, src: GetContentResultEntry)
|
||||
fun resumeCustomThumbnailTarget(id: String?): PostAttachment?
|
||||
suspend fun onPickAttachment(uri: Uri, mimeType: String? = null)
|
||||
suspend fun onPickCustomThumbnail(attachmentId: String?, src: GetContentResultEntry)
|
||||
}
|
||||
|
||||
// actions after permission granted
|
||||
|
@ -43,7 +47,6 @@ class AttachmentPicker(
|
|||
|
||||
@Serializable
|
||||
data class States(
|
||||
|
||||
@Serializable(with = UriSerializer::class)
|
||||
var uriCameraImage: Uri? = null,
|
||||
|
||||
|
@ -55,59 +58,48 @@ class AttachmentPicker(
|
|||
////////////////////////////////////////////////////////////////////////
|
||||
// activity result handlers
|
||||
|
||||
private val arAttachmentChooser = ActivityResultHandler(log) { r ->
|
||||
private var pickThumbnailLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||
|
||||
private val pickThumbnailCallback = ActivityResultCallback<Uri?> {
|
||||
handleThumbnailResult(it)
|
||||
}
|
||||
|
||||
private var pickVisualMediaLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||
|
||||
private val pickVisualMediaCallback = ActivityResultCallback<List<Uri>?> { uris ->
|
||||
uris?.handleGetContentResult(activity.contentResolver)?.pickAll()
|
||||
}
|
||||
|
||||
private val prPickAudio = permissionSpecAudioPicker.requester { openAudioPicker() }
|
||||
private val arPickAudio = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
r.data?.handleGetContentResult(activity.contentResolver)?.pickAll()
|
||||
}
|
||||
|
||||
private val arCamera = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) {
|
||||
// 失敗したら DBからデータを削除
|
||||
states.uriCameraImage?.let { uri ->
|
||||
activity.contentResolver.delete(uri, null, null)
|
||||
states.uriCameraImage = null
|
||||
}
|
||||
} else {
|
||||
// 画像のURL
|
||||
when (val uri = r.data?.data ?: states.uriCameraImage) {
|
||||
null -> activity.showToast(false, "missing image uri")
|
||||
else -> callback.onPickAttachment(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val prCamera = permissionSpecCamera.requester { openStillCamera() }
|
||||
private val arCamera = ActivityResultHandler(log) { handleCameraResult(it) }
|
||||
|
||||
private val arCapture = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
r.data?.data?.let { callback.onPickAttachment(it) }
|
||||
}
|
||||
|
||||
private val arCustomThumbnail = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
r.data
|
||||
?.handleGetContentResult(activity.contentResolver)
|
||||
?.firstOrNull()
|
||||
?.let {
|
||||
callback.resumeCustomThumbnailTarget(states.customThumbnailTargetId)?.let { pa ->
|
||||
callback.onPickCustomThumbnail(pa, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val prPickAttachment = permissionSpecImagePicker.requester { openPicker() }
|
||||
|
||||
private val prPickCustomThumbnail = permissionSpecImagePicker.requester {
|
||||
callback.resumeCustomThumbnailTarget(states.customThumbnailTargetId)
|
||||
?.let { openCustomThumbnail(it) }
|
||||
}
|
||||
private val prCapture = permissionSpecCapture.requester { openPicker() }
|
||||
private val arCapture = ActivityResultHandler(log) { handleCaptureResult(it) }
|
||||
|
||||
init {
|
||||
// must register all ARHs before onStart
|
||||
prPickAttachment.register(activity)
|
||||
prPickCustomThumbnail.register(activity)
|
||||
arAttachmentChooser.register(activity)
|
||||
prPickAudio.register(activity)
|
||||
arPickAudio.register(activity)
|
||||
|
||||
prCamera.register(activity)
|
||||
arCamera.register(activity)
|
||||
|
||||
arCapture.register(activity)
|
||||
arCustomThumbnail.register(activity)
|
||||
|
||||
pickVisualMediaLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.PickMultipleVisualMedia(4),
|
||||
pickVisualMediaCallback,
|
||||
)
|
||||
pickThumbnailLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.PickVisualMedia(),
|
||||
pickThumbnailCallback,
|
||||
)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
@ -132,21 +124,17 @@ class AttachmentPicker(
|
|||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
fun openPicker() {
|
||||
if (!prPickAttachment.checkOrLaunch()) return
|
||||
activity.run {
|
||||
launchAndShowError {
|
||||
actionsDialog {
|
||||
action(getString(R.string.pick_images)) {
|
||||
openAttachmentChooser(R.string.pick_images, "image/*", "video/*")
|
||||
}
|
||||
action(getString(R.string.pick_videos)) {
|
||||
openAttachmentChooser(R.string.pick_videos, "video/*")
|
||||
action(getString(R.string.pick_images_or_video)) {
|
||||
openVisualMediaPicker()
|
||||
}
|
||||
action(getString(R.string.pick_audios)) {
|
||||
openAttachmentChooser(R.string.pick_audios, "audio/*")
|
||||
openAudioPicker()
|
||||
}
|
||||
action(getString(R.string.image_capture)) {
|
||||
performCamera()
|
||||
openStillCamera()
|
||||
}
|
||||
action(getString(R.string.video_capture)) {
|
||||
performCapture(
|
||||
|
@ -165,43 +153,74 @@ class AttachmentPicker(
|
|||
}
|
||||
}
|
||||
|
||||
private fun openAttachmentChooser(titleId: Int, vararg mimeTypes: String) {
|
||||
// SAFのIntentで開く
|
||||
try {
|
||||
val intent = intentGetContent(true, activity.getString(titleId), mimeTypes)
|
||||
arAttachmentChooser.launch(intent)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "openAttachmentChooser failed.")
|
||||
activity.showToast(ex, "openAttachmentChooser failed.")
|
||||
private fun openVisualMediaPicker() {
|
||||
(pickVisualMediaLauncher
|
||||
?: error("pickVisualMediaLauncher is not registered."))
|
||||
.launch(
|
||||
PickVisualMediaRequest(
|
||||
ActivityResultContracts.PickVisualMedia.ImageAndVideo
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun openAudioPicker() {
|
||||
if (!prPickAudio.checkOrLaunch()) return
|
||||
activity.launchAndShowError {
|
||||
val intent = intentGetContent(
|
||||
allowMultiple = true,
|
||||
caption = activity.getString(R.string.pick_audios),
|
||||
mimeTypes = arrayOf("audio/*"),
|
||||
)
|
||||
arPickAudio.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performCamera() {
|
||||
try {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.TITLE, "${System.currentTimeMillis()}.jpg")
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
}
|
||||
|
||||
val newUri =
|
||||
activity.contentResolver.insert(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
values
|
||||
)
|
||||
.also { states.uriCameraImage = it }
|
||||
private fun openStillCamera() {
|
||||
if (!prCamera.checkOrLaunch()) return
|
||||
activity.launchAndShowError {
|
||||
val newUri = activity.contentResolver.insert(
|
||||
/* url = */
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
/* values = */
|
||||
ContentValues().apply {
|
||||
put(MediaStore.Images.Media.TITLE, "${System.currentTimeMillis()}.jpg")
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
},
|
||||
).also { states.uriCameraImage = it }
|
||||
|
||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||
putExtra(MediaStore.EXTRA_OUTPUT, newUri)
|
||||
}
|
||||
|
||||
arCamera.launch(intent)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "performCamera failed.")
|
||||
activity.showToast(ex, "performCamera failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun performCapture(action: String, errorCaption: String) {
|
||||
private fun handleCameraResult(r: ActivityResult) {
|
||||
activity.launchAndShowError {
|
||||
when {
|
||||
r.isOk -> when (val uri = r.data?.data ?: states.uriCameraImage) {
|
||||
null -> activity.showToast(false, "missing image uri")
|
||||
else -> callback.onPickAttachment(uri)
|
||||
}
|
||||
// 失敗したら DBからデータを削除
|
||||
else -> states.uriCameraImage?.let { uri ->
|
||||
activity.contentResolver.delete(uri, null, null)
|
||||
states.uriCameraImage = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 動画や音声をキャプチャする
|
||||
* - Uriは呼び出し先に任せっきり
|
||||
*/
|
||||
private fun performCapture(
|
||||
action: String,
|
||||
errorCaption: String,
|
||||
) {
|
||||
if (!prCapture.checkOrLaunch()) return
|
||||
try {
|
||||
arCapture.launch(Intent(action))
|
||||
} catch (ex: Throwable) {
|
||||
|
@ -210,28 +229,46 @@ class AttachmentPicker(
|
|||
}
|
||||
}
|
||||
|
||||
private fun ArrayList<GetContentResultEntry>.pickAll() =
|
||||
forEach { callback.onPickAttachment(it.uri, it.mimeType) }
|
||||
private fun handleCaptureResult(r: ActivityResult) {
|
||||
activity.launchAndShowError {
|
||||
if (r.isOk) {
|
||||
when (val uri = r.data?.data) {
|
||||
null -> activity.showToast(false, "missing media uri")
|
||||
else -> callback.onPickAttachment(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<GetContentResultEntry>.pickAll() {
|
||||
activity.launchAndShowError {
|
||||
forEach { callback.onPickAttachment(it.uri, it.mimeType) }
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Mastodon's custom thumbnail
|
||||
|
||||
fun openCustomThumbnail(pa: PostAttachment) {
|
||||
try {
|
||||
states.customThumbnailTargetId = pa.attachment?.id?.toString()
|
||||
?: return
|
||||
if (!prPickCustomThumbnail.checkOrLaunch()) return
|
||||
// SAFのIntentで開く
|
||||
arCustomThumbnail.launch(
|
||||
intentGetContent(
|
||||
false,
|
||||
activity.getString(R.string.pick_images),
|
||||
arrayOf("image/*")
|
||||
fun openCustomThumbnail(attachmentId: String?) {
|
||||
states.customThumbnailTargetId = attachmentId
|
||||
?: error("attachmentId is null")
|
||||
activity.launchAndShowError {
|
||||
(pickThumbnailLauncher
|
||||
?: error("pickThumbnailLauncher is not registered."))
|
||||
.launch(
|
||||
PickVisualMediaRequest(
|
||||
ActivityResultContracts.PickVisualMedia.ImageOnly,
|
||||
)
|
||||
)
|
||||
)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "openCustomThumbnail failed.")
|
||||
activity.showToast(ex, "openCustomThumbnail failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleThumbnailResult(uri: Uri?) {
|
||||
uri ?: return
|
||||
activity.launchAndShowError {
|
||||
listOf(uri).handleGetContentResult(activity.contentResolver).firstOrNull()?.let {
|
||||
callback.onPickCustomThumbnail(states.customThumbnailTargetId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,50 +43,111 @@ class PermissionSpec(
|
|||
}
|
||||
}
|
||||
|
||||
val permissionSpecNotification = if (Build.VERSION.SDK_INT >= 33) {
|
||||
PermissionSpec(
|
||||
val permissionSpecNotification = when {
|
||||
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||
permissions = listOf(
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
),
|
||||
deniedId = R.string.permission_denied_notifications,
|
||||
rationalId = R.string.permission_rational_notifications,
|
||||
)
|
||||
} else {
|
||||
PermissionSpec(
|
||||
else -> PermissionSpec(
|
||||
permissions = emptyList(),
|
||||
deniedId = R.string.permission_denied_notifications,
|
||||
rationalId = R.string.permission_rational_notifications,
|
||||
)
|
||||
}
|
||||
|
||||
val permissionSpecMediaDownload = if (Build.VERSION.SDK_INT >= 33) {
|
||||
PermissionSpec(
|
||||
val permissionSpecMediaDownload = when {
|
||||
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||
permissions = listOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
deniedId = R.string.permission_denied_download_manager,
|
||||
rationalId = R.string.permission_rational_download_manager,
|
||||
)
|
||||
} else {
|
||||
PermissionSpec(
|
||||
else -> PermissionSpec(
|
||||
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
}
|
||||
|
||||
val permissionSpecImagePicker = if (Build.VERSION.SDK_INT >= 33) {
|
||||
PermissionSpec(
|
||||
/**
|
||||
* 静止画のみを端末から選ぶ
|
||||
*/
|
||||
val permissionSpecImagePicker = when {
|
||||
Build.VERSION.SDK_INT >= 34 -> PermissionSpec(
|
||||
permissions = emptyList(),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||
permissions = listOf(
|
||||
Manifest.permission.READ_MEDIA_IMAGES,
|
||||
Manifest.permission.READ_MEDIA_VIDEO,
|
||||
Manifest.permission.READ_MEDIA_AUDIO,
|
||||
),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
} else {
|
||||
PermissionSpec(
|
||||
else -> PermissionSpec(
|
||||
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* オーディオのみを端末から選ぶ
|
||||
*/
|
||||
val permissionSpecAudioPicker = when {
|
||||
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||
permissions = listOf(
|
||||
Manifest.permission.READ_MEDIA_AUDIO,
|
||||
),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
else -> {
|
||||
PermissionSpec(
|
||||
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* カメラ撮影用の実行時パーミッション
|
||||
* https://developer.android.com/media/camera/camera-deprecated/photobasics?hl=ja#TaskPhotoView
|
||||
*/
|
||||
val permissionSpecCamera = when {
|
||||
Build.VERSION.SDK_INT >= 29 -> PermissionSpec(
|
||||
permissions = emptyList(),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
else -> PermissionSpec(
|
||||
permissions = listOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* 動画や音声のキャプチャ前
|
||||
*/
|
||||
val permissionSpecCapture = when {
|
||||
Build.VERSION.SDK_INT >= 29 -> PermissionSpec(
|
||||
permissions = emptyList(),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
else -> PermissionSpec(
|
||||
permissions = listOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
),
|
||||
deniedId = R.string.permission_denied_media_access,
|
||||
rationalId = R.string.permission_rational_media_access,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -537,6 +537,7 @@
|
|||
<string name="performance">性能</string>
|
||||
<string name="pick_image">画像を選択</string>
|
||||
<string name="pick_images">画像を選択…</string>
|
||||
<string name="pick_images_or_video">画像や動画を選択…</string>
|
||||
<string name="pick_videos">動画を選択…</string>
|
||||
<string name="pick_audios">音声ファイルを選択…</string>
|
||||
<string name="please_add_account">アカウントがありません。事前にアカウントの追加を行ってください。</string>
|
||||
|
|
|
@ -320,6 +320,7 @@
|
|||
<string name="color_and_background">Color and Background…</string>
|
||||
<string name="column_background">Column background</string>
|
||||
<string name="pick_image">Pick an image</string>
|
||||
<string name="pick_images_or_video">Pick images or video…</string>
|
||||
<string name="pick_images">Pick image(s)…</string>
|
||||
<string name="pick_videos">Pick video(s)…</string>
|
||||
<string name="pick_audios">Choose audio file(s)…</string>
|
||||
|
|
|
@ -105,7 +105,7 @@ dependencies {
|
|||
api("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Vers.kotlinxCoroutinesVersion}")
|
||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Vers.kotlinxCoroutinesVersion}")
|
||||
api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json:${Vers.kotlinxSerializationLibVersion}")
|
||||
api("ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0")
|
||||
|
||||
//non-OSS dependency api "androidx.media3:media3-cast:$media3Version"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package jp.juggler.util.data
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -283,28 +284,26 @@ data class GetContentResultEntry(
|
|||
var time: Long? = null,
|
||||
)
|
||||
|
||||
// returns list of pair of uri and mime-type.
|
||||
fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<GetContentResultEntry> {
|
||||
val urlList = ArrayList<GetContentResultEntry>()
|
||||
// 単一選択
|
||||
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))
|
||||
fun MutableList<GetContentResultEntry>.addNoDuplicate(
|
||||
contentResolver: ContentResolver,
|
||||
uri: Uri?,
|
||||
type: String? = null,
|
||||
) {
|
||||
uri ?: return
|
||||
if (any { it.uri == uri }) return
|
||||
val mimeType = try {
|
||||
type ?: contentResolver.getType(uri)
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "contentResolver.getType failed. uri=$uri")
|
||||
return
|
||||
}
|
||||
// 複数選択
|
||||
this.clipData?.let { clipData ->
|
||||
for (uri in (0 until clipData.itemCount).mapNotNull { clipData.getItemAt(it)?.uri }) {
|
||||
if (urlList.none { it.uri == uri }) {
|
||||
urlList.add(GetContentResultEntry(uri))
|
||||
}
|
||||
}
|
||||
}
|
||||
urlList.forEach {
|
||||
add(GetContentResultEntry(uri, mimeType))
|
||||
}
|
||||
|
||||
fun List<GetContentResultEntry>.grantPermissions(
|
||||
contentResolver: ContentResolver,
|
||||
) {
|
||||
forEach {
|
||||
try {
|
||||
contentResolver.takePersistableUriPermission(
|
||||
it.uri,
|
||||
|
@ -313,5 +312,29 @@ fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<G
|
|||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
return urlList
|
||||
}
|
||||
|
||||
// returns list of pair of uri and mime-type.
|
||||
fun List<Uri>.handleGetContentResult(contentResolver: ContentResolver) =
|
||||
buildList {
|
||||
this@handleGetContentResult.forEach {
|
||||
addNoDuplicate(contentResolver, it)
|
||||
}
|
||||
grantPermissions(contentResolver)
|
||||
}
|
||||
|
||||
val ClipData.uris
|
||||
get() = (0 until itemCount).mapNotNull { getItemAt(it)?.uri }
|
||||
|
||||
// returns list of pair of uri and mime-type.
|
||||
fun Intent.handleGetContentResult(contentResolver: ContentResolver) =
|
||||
buildList {
|
||||
// 単一選択
|
||||
addNoDuplicate(contentResolver, data, type)
|
||||
|
||||
// 複数選択
|
||||
clipData?.uris?.forEach {
|
||||
addNoDuplicate(contentResolver, it)
|
||||
}
|
||||
grantPermissions(contentResolver)
|
||||
}
|
||||
|
|
|
@ -378,6 +378,9 @@ val AppCompatActivity.isLiveActivity: Boolean
|
|||
val ActivityResult.isNotOk
|
||||
get() = Activity.RESULT_OK != resultCode
|
||||
|
||||
val ActivityResult.isOk
|
||||
get() = Activity.RESULT_OK == resultCode
|
||||
|
||||
/**
|
||||
* Ringtone pickerの処理結果のUriまたはnull
|
||||
*/
|
||||
|
|
|
@ -21,7 +21,8 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id("org.jetbrains.kotlin.jvm") version (Vers.kotlinVersion) apply false
|
||||
kotlin("jvm") version (Vers.kotlinVersion) apply false
|
||||
kotlin("plugin.serialization") version (Vers.kotlinxSerializationPluginVersion) apply true // !!
|
||||
id("org.jetbrains.kotlin.android") version (Vers.kotlinVersion) apply false
|
||||
id("com.google.devtools.ksp") version (Vers.kspVersion) apply false
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ object Vers {
|
|||
const val koinVersion = "3.5.0"
|
||||
const val kotlinTestVersion = kotlinVersion // "1.9.22"
|
||||
const val kotlinxCoroutinesVersion = "1.7.3"
|
||||
const val kotlinxSerializationPluginVersion = kotlinVersion
|
||||
const val kotlinxSerializationLibVersion = "1.6.2"
|
||||
const val kspVersion = "$kotlinVersion-1.0.16"
|
||||
const val lifecycleVersion = "2.6.2"
|
||||
const val materialVersion = "1.11.0"
|
||||
|
|
Loading…
Reference in New Issue