Compare commits
3 Commits
ceca568a1a
...
f8aaedf86a
Author | SHA1 | Date |
---|---|---|
tateisu | f8aaedf86a | |
tateisu | 2cbbbcce43 | |
tateisu | e703d7460b |
|
@ -25,8 +25,8 @@ android {
|
|||
defaultConfig {
|
||||
targetSdk = Vers.stTargetSdkVersion
|
||||
minSdk = Vers.stMinSdkVersion
|
||||
versionCode = 544
|
||||
versionName = "5.544"
|
||||
versionCode = 545
|
||||
versionName = "5.545"
|
||||
applicationId = "jp.juggler.subwaytooter"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
|
@ -9,7 +8,6 @@ import android.graphics.Color
|
|||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.provider.MediaStore
|
||||
import android.text.Editable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextWatcher
|
||||
|
@ -21,11 +19,6 @@ 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
|
||||
|
@ -63,6 +56,7 @@ 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.UriAndType
|
||||
import jp.juggler.util.data.UriSerializer
|
||||
import jp.juggler.util.data.getDocumentName
|
||||
import jp.juggler.util.data.getStreamSize
|
||||
|
@ -198,18 +192,12 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
private var permissionCamera = permissionSpecCamera.requester {
|
||||
openCamera()
|
||||
private val cameraOpener = CameraOpener {
|
||||
uploadImage(state.propName, it)
|
||||
}
|
||||
|
||||
private var pickImageLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||
|
||||
private val pickImageCallback = ActivityResultCallback<Uri?> {
|
||||
handlePickImageResult(it)
|
||||
}
|
||||
|
||||
private val arCameraImage = ActivityResultHandler(log) {
|
||||
handleCameraResult(it)
|
||||
private val visualMediaPicker = VisualMediaPickerCompat {
|
||||
uploadImage(state.propName, it?.firstOrNull())
|
||||
}
|
||||
|
||||
private val arShowAcctColor = ActivityResultHandler(log) { r ->
|
||||
|
@ -222,14 +210,10 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
super.onCreate(savedInstanceState)
|
||||
backPressed { handleBackPressed() }
|
||||
|
||||
pickImageLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.PickVisualMedia(),
|
||||
pickImageCallback,
|
||||
)
|
||||
visualMediaPicker.register(this)
|
||||
cameraOpener.register(this)
|
||||
|
||||
permissionCamera.register(this)
|
||||
arShowAcctColor.register(this)
|
||||
arCameraImage.register(this)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
savedInstanceState.getString(ACTIVITY_STATE)
|
||||
|
@ -1357,75 +1341,15 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
launchAndShowError {
|
||||
actionsDialog {
|
||||
action(getString(R.string.pick_image)) {
|
||||
openPickImage()
|
||||
visualMediaPicker.open()
|
||||
}
|
||||
action(getString(R.string.image_capture)) {
|
||||
openCamera()
|
||||
cameraOpener.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPickImage() {
|
||||
(pickImageLauncher ?: error("pickImageLauncher not registered")).launch(
|
||||
PickVisualMediaRequest(
|
||||
ActivityResultContracts.PickVisualMedia.ImageOnly,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handlePickImageResult(uri: Uri?) {
|
||||
uri ?: return
|
||||
uploadImage(
|
||||
state.propName,
|
||||
uri,
|
||||
uri.resolveMimeType(null, this),
|
||||
)
|
||||
}
|
||||
|
||||
private fun openCamera() {
|
||||
if (!permissionCamera.checkOrLaunch()) return
|
||||
launchAndShowError(errorCaption = "openCamera failed.") {
|
||||
// カメラで撮影
|
||||
val filename = System.currentTimeMillis().toString() + ".jpg"
|
||||
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)
|
||||
.also { state.uriCameraImage = it }
|
||||
|
||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||
putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
||||
}
|
||||
arCameraImage.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -1515,7 +1439,10 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
private fun uploadImage(propName: String, uri: Uri, mimeType: String?) {
|
||||
private fun uploadImage(propName: String, src: UriAndType?) {
|
||||
src ?: return
|
||||
val uri = src.uri
|
||||
val mimeType = src.mimeType
|
||||
|
||||
if (mimeType == null) {
|
||||
showToast(false, "mime type is not provided.")
|
||||
|
|
|
@ -64,7 +64,7 @@ import jp.juggler.util.coroutine.launchAndShowError
|
|||
import jp.juggler.util.coroutine.launchProgress
|
||||
import jp.juggler.util.data.cast
|
||||
import jp.juggler.util.data.defaultLocale
|
||||
import jp.juggler.util.data.handleGetContentResult
|
||||
import jp.juggler.util.data.checkMimeTypeAndGrant
|
||||
import jp.juggler.util.data.intentOpenDocument
|
||||
import jp.juggler.util.data.notEmpty
|
||||
import jp.juggler.util.data.notZero
|
||||
|
@ -132,7 +132,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
|
|||
|
||||
private val arImportAppData = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
r.data?.handleGetContentResult(contentResolver)
|
||||
r.data?.checkMimeTypeAndGrant(contentResolver)
|
||||
?.firstOrNull()
|
||||
?.uri?.let { importAppData2(false, it) }
|
||||
}
|
||||
|
@ -1031,7 +1031,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
|
|||
|
||||
private fun handleFontResult(item: AppSettingItem?, data: Intent, fileName: String) {
|
||||
item ?: error("handleFontResult : setting item is null")
|
||||
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
|
||||
data.checkMimeTypeAndGrant(contentResolver).firstOrNull()?.uri?.let {
|
||||
val file = saveTimelineFont(it, fileName)
|
||||
if (file != null) {
|
||||
(item.pref as? StringPref)?.value = file.absolutePath
|
||||
|
|
|
@ -74,7 +74,7 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
|
|||
|
||||
private val arColumnBackgroundImage = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
r.data?.handleGetContentResult(contentResolver)
|
||||
r.data?.checkMimeTypeAndGrant(contentResolver)
|
||||
?.firstOrNull()?.uri?.let { updateBackground(it) }
|
||||
}
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
|
|||
|
||||
private val arImport = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
r.data?.handleGetContentResult(contentResolver)
|
||||
r.data?.checkMimeTypeAndGrant(contentResolver)
|
||||
?.firstOrNull()?.uri?.let { import2(it) }
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package jp.juggler.subwaytooter
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.text.Editable
|
||||
|
@ -65,7 +64,7 @@ import jp.juggler.util.backPressed
|
|||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.coroutine.launchIO
|
||||
import jp.juggler.util.coroutine.launchMain
|
||||
import jp.juggler.util.data.GetContentResultEntry
|
||||
import jp.juggler.util.data.UriAndType
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.showToast
|
||||
import jp.juggler.util.string
|
||||
|
@ -216,14 +215,15 @@ class ActPost : AppCompatActivity(),
|
|||
handler = appState.handler
|
||||
attachmentUploader = AttachmentUploader(this, handler)
|
||||
attachmentPicker = AttachmentPicker(this, object : AttachmentPicker.Callback {
|
||||
override suspend fun onPickAttachment(uri: Uri, mimeType: String?) {
|
||||
addAttachment(uri, mimeType)
|
||||
override suspend fun onPickAttachment(item: UriAndType) {
|
||||
addAttachment(item.uri, item.mimeType)
|
||||
}
|
||||
|
||||
override suspend fun onPickCustomThumbnail(
|
||||
attachmentId: String?,
|
||||
src: GetContentResultEntry,
|
||||
src: UriAndType?,
|
||||
) {
|
||||
src ?: return
|
||||
val pa = attachmentList.find { it.attachment?.id?.toString() == attachmentId }
|
||||
?: error("missing attachment for attachmentId=$attachmentId")
|
||||
onPickCustomThumbnailImpl(pa, src)
|
||||
|
|
|
@ -29,9 +29,8 @@ import jp.juggler.subwaytooter.util.AttachmentRequest
|
|||
import jp.juggler.subwaytooter.util.PostAttachment
|
||||
import jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.coroutine.launchMain
|
||||
import jp.juggler.util.data.CharacterGroup
|
||||
import jp.juggler.util.data.GetContentResultEntry
|
||||
import jp.juggler.util.data.UriAndType
|
||||
import jp.juggler.util.data.buildJsonObject
|
||||
import jp.juggler.util.data.decodeJsonArray
|
||||
import jp.juggler.util.data.notEmpty
|
||||
|
@ -269,9 +268,7 @@ fun ActPost.performAttachmentClick(idx: Int) {
|
|||
TootAttachmentType.GIFV,
|
||||
TootAttachmentType.Video,
|
||||
-> action(getString(R.string.custom_thumbnail)) {
|
||||
attachmentPicker.openCustomThumbnail(
|
||||
attachmentId = pa.attachment?.id?.toString()
|
||||
)
|
||||
attachmentPicker.openThumbnailPicker(pa)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
|
@ -436,20 +433,18 @@ suspend fun ActPost.editAttachmentDescription(
|
|||
}
|
||||
}
|
||||
|
||||
fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: GetContentResultEntry) {
|
||||
suspend fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: UriAndType) {
|
||||
when (val account = this.account) {
|
||||
null -> showToast(false, R.string.account_select_please)
|
||||
else -> launchMain {
|
||||
if (pa.attachment?.isEdit == true) {
|
||||
showToast(
|
||||
true,
|
||||
"Sorry, updateing thumbnail is not yet supported in case of editing post."
|
||||
)
|
||||
} else {
|
||||
val result = attachmentUploader.uploadCustomThumbnail(account, src, pa)
|
||||
result?.error?.let { showToast(true, it) }
|
||||
showMediaAttachment()
|
||||
}
|
||||
else -> if (pa.attachment?.isEdit == true) {
|
||||
showToast(
|
||||
true,
|
||||
"Sorry, updateing thumbnail is not yet supported in case of editing post."
|
||||
)
|
||||
} else {
|
||||
val result = attachmentUploader.uploadCustomThumbnail(account, src, pa)
|
||||
result?.error?.let { showToast(true, it) }
|
||||
showMediaAttachment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,11 @@ package jp.juggler.subwaytooter.actpost
|
|||
import android.os.Bundle
|
||||
import jp.juggler.subwaytooter.ActPost
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
import jp.juggler.subwaytooter.api.entity.EntityIdSerializer
|
||||
import jp.juggler.subwaytooter.api.entity.TootScheduled
|
||||
import jp.juggler.subwaytooter.api.entity.TootVisibility
|
||||
import jp.juggler.subwaytooter.api.entity.parseItem
|
||||
import jp.juggler.subwaytooter.kJson
|
||||
import jp.juggler.subwaytooter.util.AttachmentPicker
|
||||
import jp.juggler.subwaytooter.util.PostAttachment
|
||||
|
@ -11,7 +15,6 @@ import jp.juggler.util.data.decodeJsonObject
|
|||
import jp.juggler.util.data.toJsonArray
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
private val log = LogCategory("ActPostStates")
|
||||
|
|
|
@ -3,10 +3,12 @@ package jp.juggler.subwaytooter.pref
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import androidx.startup.AppInitializer
|
||||
import androidx.startup.Initializer
|
||||
import jp.juggler.util.data.mayUri
|
||||
import jp.juggler.util.os.applicationContextSafe
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
class PrefDevice(context: Context) {
|
||||
|
||||
|
@ -28,6 +30,10 @@ class PrefDevice(context: Context) {
|
|||
private const val PREF_TIME_LAST_ENDPOINT_REGISTER = "timeLastEndpointRegister"
|
||||
private const val PREF_SUPRESS_REQUEST_NOTIFICATION_PERMISSION =
|
||||
"supressRequestNotificationPermission"
|
||||
private const val PREF_MEDIA_PICKER_MULTIPLE = "mediaPickerMultiple"
|
||||
private const val PREF_CAMERA_OPENER_LAST_URI = "cameraOpenerLastUri"
|
||||
private const val PREF_CAPTURE_ACTION = "captureAction"
|
||||
private const val PREF_CAPTURE_ERROR_CAPTION = "captureErrorCaption"
|
||||
|
||||
const val PUSH_DISTRIBUTOR_FCM = "fcm"
|
||||
const val PUSH_DISTRIBUTOR_NONE = "none"
|
||||
|
@ -153,6 +159,31 @@ class PrefDevice(context: Context) {
|
|||
value.saveTo(PREF_SUPRESS_REQUEST_NOTIFICATION_PERMISSION)
|
||||
}
|
||||
|
||||
var mediaPickerMultiple: Boolean
|
||||
get() = boolean(PREF_MEDIA_PICKER_MULTIPLE) ?: false
|
||||
set(value) {
|
||||
value.saveTo(PREF_MEDIA_PICKER_MULTIPLE)
|
||||
}
|
||||
|
||||
var cameraOpenerLastUri: Uri?
|
||||
get() = string(PREF_CAMERA_OPENER_LAST_URI)?.mayUri()
|
||||
set(value) {
|
||||
(value?.toString() ?: "").saveTo(PREF_CAMERA_OPENER_LAST_URI)
|
||||
}
|
||||
|
||||
val captureAction
|
||||
get() = string(PREF_CAPTURE_ACTION)
|
||||
|
||||
val captureErrorCaption
|
||||
get() = string(PREF_CAPTURE_ERROR_CAPTION)
|
||||
|
||||
fun setCaptureParams(action: String, errorCaption: String) {
|
||||
edit {
|
||||
it.putString(PREF_CAPTURE_ACTION, action)
|
||||
it.putString(PREF_CAPTURE_ERROR_CAPTION, errorCaption)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// 以下は古い
|
||||
|
||||
|
|
|
@ -1,28 +1,13 @@
|
|||
package jp.juggler.subwaytooter.util
|
||||
|
||||
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
|
||||
import jp.juggler.subwaytooter.kJson
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.data.GetContentResultEntry
|
||||
import jp.juggler.util.data.UriSerializer
|
||||
import jp.juggler.util.data.handleGetContentResult
|
||||
import jp.juggler.util.data.intentGetContent
|
||||
import jp.juggler.util.data.UriAndType
|
||||
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.encodeToString
|
||||
|
||||
|
@ -38,18 +23,15 @@ class AttachmentPicker(
|
|||
|
||||
// callback after media selected
|
||||
interface Callback {
|
||||
suspend fun onPickAttachment(uri: Uri, mimeType: String? = null)
|
||||
suspend fun onPickCustomThumbnail(attachmentId: String?, src: GetContentResultEntry)
|
||||
suspend fun onPickAttachment(item: UriAndType)
|
||||
suspend fun onPickCustomThumbnail(attachmentId: String?, src: UriAndType?)
|
||||
}
|
||||
|
||||
// actions after permission granted
|
||||
enum class AfterPermission { Attachment, CustomThumbnail, }
|
||||
// // actions after permission granted
|
||||
// enum class AfterPermission { Attachment, CustomThumbnail, }
|
||||
|
||||
@Serializable
|
||||
data class States(
|
||||
@Serializable(with = UriSerializer::class)
|
||||
var uriCameraImage: Uri? = null,
|
||||
|
||||
var customThumbnailTargetId: String? = null,
|
||||
)
|
||||
|
||||
|
@ -58,67 +40,43 @@ class AttachmentPicker(
|
|||
////////////////////////////////////////////////////////////////////////
|
||||
// activity result handlers
|
||||
|
||||
private var pickThumbnailLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||
|
||||
private val pickThumbnailCallback = ActivityResultCallback<Uri?> {
|
||||
handleThumbnailResult(it)
|
||||
private val visualMediaPickerThumbnail = VisualMediaPickerCompat {
|
||||
callback.onPickCustomThumbnail(states.customThumbnailTargetId, it?.firstOrNull())
|
||||
}
|
||||
|
||||
private var pickVisualMediaLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||
private val visualMediaPickerAttachment = VisualMediaPickerCompat { it?.pickAll() }
|
||||
|
||||
private val pickVisualMediaCallback = ActivityResultCallback<List<Uri>?> { uris ->
|
||||
uris?.handleGetContentResult(activity.contentResolver)?.pickAll()
|
||||
}
|
||||
private val audioPicker = AudioPicker { it?.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 cameraOpener = CameraOpener { callback.onPickAttachment(it) }
|
||||
|
||||
private val prCamera = permissionSpecCamera.requester { openStillCamera() }
|
||||
private val arCamera = ActivityResultHandler(log) { handleCameraResult(it) }
|
||||
|
||||
private val prCapture = permissionSpecCapture.requester { openPicker() }
|
||||
private val arCapture = ActivityResultHandler(log) { handleCaptureResult(it) }
|
||||
private val captureOpener = CaptureOpener { callback.onPickAttachment(it) }
|
||||
|
||||
init {
|
||||
// must register all ARHs before onStart
|
||||
prPickAudio.register(activity)
|
||||
arPickAudio.register(activity)
|
||||
|
||||
prCamera.register(activity)
|
||||
arCamera.register(activity)
|
||||
|
||||
arCapture.register(activity)
|
||||
|
||||
pickVisualMediaLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.PickMultipleVisualMedia(4),
|
||||
pickVisualMediaCallback,
|
||||
)
|
||||
pickThumbnailLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.PickVisualMedia(),
|
||||
pickThumbnailCallback,
|
||||
)
|
||||
visualMediaPickerAttachment.register(activity)
|
||||
visualMediaPickerThumbnail.register(activity)
|
||||
cameraOpener.register(activity)
|
||||
audioPicker.register(activity)
|
||||
captureOpener.register(activity)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// states
|
||||
|
||||
fun reset() {
|
||||
states.uriCameraImage = null
|
||||
cameraOpener.reset()
|
||||
}
|
||||
|
||||
fun encodeState(): String {
|
||||
val encoded = kJson.encodeToString(states)
|
||||
val decoded = kJson.decodeFromString<States>(encoded)
|
||||
log.d("encodeState: ${decoded.uriCameraImage},$encoded")
|
||||
log.d("encodeState: states=$states, encoded=$encoded, decoded=$decoded")
|
||||
return encoded
|
||||
}
|
||||
|
||||
fun restoreState(encoded: String) {
|
||||
states = kJson.decodeFromString(encoded)
|
||||
log.d("restoreState: ${states.uriCameraImage},$encoded")
|
||||
log.d("restoreState: states=$states, encoded=$encoded")
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
@ -128,22 +86,25 @@ class AttachmentPicker(
|
|||
launchAndShowError {
|
||||
actionsDialog {
|
||||
action(getString(R.string.pick_images_or_video)) {
|
||||
openVisualMediaPicker()
|
||||
visualMediaPickerAttachment.open(
|
||||
multiple = true,
|
||||
allowVideo = true,
|
||||
)
|
||||
}
|
||||
action(getString(R.string.pick_audios)) {
|
||||
openAudioPicker()
|
||||
audioPicker.open()
|
||||
}
|
||||
action(getString(R.string.image_capture)) {
|
||||
openStillCamera()
|
||||
cameraOpener.open()
|
||||
}
|
||||
action(getString(R.string.video_capture)) {
|
||||
performCapture(
|
||||
captureOpener.open(
|
||||
MediaStore.ACTION_VIDEO_CAPTURE,
|
||||
"can't open video capture app."
|
||||
)
|
||||
}
|
||||
action(getString(R.string.voice_capture)) {
|
||||
performCapture(
|
||||
captureOpener.open(
|
||||
MediaStore.Audio.Media.RECORD_SOUND_ACTION,
|
||||
"can't open voice capture app."
|
||||
)
|
||||
|
@ -153,122 +114,15 @@ class AttachmentPicker(
|
|||
}
|
||||
}
|
||||
|
||||
private fun openVisualMediaPicker() {
|
||||
(pickVisualMediaLauncher
|
||||
?: error("pickVisualMediaLauncher is not registered."))
|
||||
.launch(
|
||||
PickVisualMediaRequest(
|
||||
ActivityResultContracts.PickVisualMedia.ImageAndVideo
|
||||
)
|
||||
)
|
||||
private suspend fun List<UriAndType>.pickAll() {
|
||||
forEach { callback.onPickAttachment(it) }
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
log.e(ex, errorCaption)
|
||||
activity.showToast(ex, errorCaption)
|
||||
}
|
||||
}
|
||||
|
||||
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(attachmentId: String?) {
|
||||
states.customThumbnailTargetId = attachmentId
|
||||
?: error("attachmentId is null")
|
||||
activity.launchAndShowError {
|
||||
(pickThumbnailLauncher
|
||||
?: error("pickThumbnailLauncher is not registered."))
|
||||
.launch(
|
||||
PickVisualMediaRequest(
|
||||
ActivityResultContracts.PickVisualMedia.ImageOnly,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleThumbnailResult(uri: Uri?) {
|
||||
uri ?: return
|
||||
activity.launchAndShowError {
|
||||
listOf(uri).handleGetContentResult(activity.contentResolver).firstOrNull()?.let {
|
||||
callback.onPickCustomThumbnail(states.customThumbnailTargetId, it)
|
||||
}
|
||||
}
|
||||
// ActPostAttachmentから呼ばれる
|
||||
fun openThumbnailPicker(pa: PostAttachment) {
|
||||
states.customThumbnailTargetId =
|
||||
pa.attachment?.id?.toString()
|
||||
?: error("attachmentId is null")
|
||||
visualMediaPickerThumbnail.open()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import jp.juggler.subwaytooter.api.runApiTask
|
|||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.coroutine.AppDispatchers
|
||||
import jp.juggler.util.coroutine.launchIO
|
||||
import jp.juggler.util.data.GetContentResultEntry
|
||||
import jp.juggler.util.data.UriAndType
|
||||
import jp.juggler.util.data.asciiPattern
|
||||
import jp.juggler.util.data.buildJsonObject
|
||||
import jp.juggler.util.data.encodeHex
|
||||
|
@ -316,7 +316,7 @@ class AttachmentUploader(
|
|||
// 添付データのカスタムサムネイル
|
||||
suspend fun uploadCustomThumbnail(
|
||||
account: SavedAccount,
|
||||
src: GetContentResultEntry,
|
||||
src: UriAndType,
|
||||
pa: PostAttachment,
|
||||
): TootApiResult? = try {
|
||||
safeContext.runApiTask(account) { client ->
|
||||
|
@ -335,8 +335,9 @@ class AttachmentUploader(
|
|||
val maxBytesImage = ar.maxBytesImage(instance, mediaConfig)
|
||||
|
||||
val opener = ar.createOpener()
|
||||
try {
|
||||
pa.progress = ""
|
||||
|
||||
try {
|
||||
if (opener.contentLength > maxBytesImage.toLong()) {
|
||||
return@runApiTask TootApiResult(
|
||||
getString(
|
||||
|
@ -354,6 +355,7 @@ class AttachmentUploader(
|
|||
if (account.isMisskey) {
|
||||
TootApiResult("custom thumbnail is not supported on misskey account.")
|
||||
} else {
|
||||
|
||||
val result = client.request(
|
||||
"/api/v1/media/${pa.attachment?.id}",
|
||||
MultipartBody.Builder()
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.data.UriAndType
|
||||
import jp.juggler.util.data.checkMimeTypeAndGrant
|
||||
import jp.juggler.util.data.intentGetContent
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.ui.ActivityResultHandler
|
||||
import jp.juggler.util.ui.isOk
|
||||
import jp.juggler.util.ui.launch
|
||||
|
||||
class AudioPicker(
|
||||
private val onPicked: suspend (list: List<UriAndType>?) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
private val log = LogCategory("AudioPicker")
|
||||
}
|
||||
|
||||
private lateinit var activity: AppCompatActivity
|
||||
|
||||
private val prPickAudio = permissionSpecAudioPicker.requester {
|
||||
activity.launchAndShowError {
|
||||
open()
|
||||
}
|
||||
}
|
||||
private val arPickAudio = ActivityResultHandler(log) { r ->
|
||||
activity.launchAndShowError {
|
||||
if (r.isOk) {
|
||||
onPicked(r.data?.checkMimeTypeAndGrant(activity.contentResolver))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun register(activity: AppCompatActivity) {
|
||||
this.activity = activity
|
||||
prPickAudio.register(activity)
|
||||
arPickAudio.register(activity)
|
||||
}
|
||||
|
||||
fun open() {
|
||||
if (!prPickAudio.checkOrLaunch()) return
|
||||
intentGetContent(
|
||||
allowMultiple = true,
|
||||
caption = activity.getString(R.string.pick_audios),
|
||||
mimeTypes = arrayOf("audio/*"),
|
||||
).launch(arPickAudio)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.pref.PrefDevice
|
||||
import jp.juggler.subwaytooter.pref.prefDevice
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.data.UriAndType
|
||||
import jp.juggler.util.data.checkMimeTypeAndGrant
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.ui.ActivityResultHandler
|
||||
import jp.juggler.util.ui.isOk
|
||||
|
||||
class CameraOpener(
|
||||
private val onCaptured: suspend (UriAndType) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
private val log = LogCategory("LogCategory")
|
||||
}
|
||||
|
||||
private lateinit var activity: AppCompatActivity
|
||||
|
||||
private val prefDevice: PrefDevice
|
||||
get() = activity.prefDevice
|
||||
|
||||
private val prCameraImage = permissionSpecCamera.requester { open() }
|
||||
private val arCameraImage = ActivityResultHandler(log) { handleCameraResult(it) }
|
||||
|
||||
fun register(activity: AppCompatActivity) {
|
||||
this.activity = activity
|
||||
prCameraImage.register(activity)
|
||||
arCameraImage.register(activity)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
prefDevice.cameraOpenerLastUri = null
|
||||
}
|
||||
|
||||
fun open() {
|
||||
if (!prCameraImage.checkOrLaunch()) return
|
||||
// カメラで撮影
|
||||
val filename = System.currentTimeMillis().toString() + ".jpg"
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.TITLE, filename)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
}
|
||||
val uri = activity.contentResolver.insert(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
values
|
||||
).also { prefDevice.cameraOpenerLastUri = it }
|
||||
|
||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||
putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
||||
}
|
||||
arCameraImage.launch(intent)
|
||||
}
|
||||
|
||||
private fun handleCameraResult(r: ActivityResult) {
|
||||
activity.launchAndShowError {
|
||||
when (
|
||||
val item = when {
|
||||
r.isOk -> listOfNotNull(
|
||||
r.data?.data
|
||||
?: prefDevice.cameraOpenerLastUri
|
||||
).checkMimeTypeAndGrant(activity.contentResolver)
|
||||
|
||||
else -> null
|
||||
}?.firstOrNull()
|
||||
) {
|
||||
null -> {
|
||||
// 失敗したら DBからデータを削除
|
||||
prefDevice.cameraOpenerLastUri?.let {
|
||||
activity.contentResolver.delete(it, null, null)
|
||||
}
|
||||
prefDevice.cameraOpenerLastUri = null
|
||||
}
|
||||
|
||||
else -> onCaptured(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.pref.PrefDevice
|
||||
import jp.juggler.subwaytooter.pref.prefDevice
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.data.UriAndType
|
||||
import jp.juggler.util.data.checkMimeTypeAndGrant
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.showToast
|
||||
import jp.juggler.util.ui.ActivityResultHandler
|
||||
import jp.juggler.util.ui.isOk
|
||||
|
||||
/**
|
||||
* 動画や音声をキャプチャする
|
||||
* - Uriは呼び出し先に任せっきり
|
||||
*/
|
||||
class CaptureOpener(
|
||||
private val onCaptured: suspend (UriAndType) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
private val log = LogCategory("CaptureOpener")
|
||||
}
|
||||
|
||||
private lateinit var activity: AppCompatActivity
|
||||
|
||||
private val prefDevice: PrefDevice
|
||||
get() = activity.prefDevice
|
||||
|
||||
private val prCapture = permissionSpecCapture.requester {
|
||||
open(
|
||||
prefDevice.captureAction,
|
||||
prefDevice.captureErrorCaption,
|
||||
)
|
||||
}
|
||||
|
||||
private val arCapture = ActivityResultHandler(log) { handleCaptureResult(it) }
|
||||
|
||||
fun register(activity: AppCompatActivity) {
|
||||
this.activity = activity
|
||||
prCapture.register(activity)
|
||||
arCapture.register(activity)
|
||||
}
|
||||
|
||||
fun open(
|
||||
action: String?,
|
||||
errorCaption: String?,
|
||||
) {
|
||||
action ?: return
|
||||
errorCaption ?: return
|
||||
prefDevice.setCaptureParams(action, errorCaption)
|
||||
if (!prCapture.checkOrLaunch()) return
|
||||
try {
|
||||
arCapture.launch(Intent(action))
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, errorCaption)
|
||||
activity.showToast(ex, errorCaption)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCaptureResult(r: ActivityResult) {
|
||||
activity.launchAndShowError {
|
||||
if (r.isOk) {
|
||||
r.data?.checkMimeTypeAndGrant(activity.contentResolver)?.firstOrNull()?.let {
|
||||
onCaptured(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ class PermissionRequester(
|
|||
|
||||
private var getContext: (() -> Context?)? = null
|
||||
|
||||
private val activity
|
||||
val activity
|
||||
get() = getContext?.invoke() as? FragmentActivity
|
||||
|
||||
// ActivityのonCreate()から呼び出す
|
||||
|
@ -63,6 +63,12 @@ class PermissionRequester(
|
|||
)
|
||||
}
|
||||
|
||||
fun hasPermissions() :Boolean{
|
||||
val activity = activity ?: error("missing activity.")
|
||||
val listNotGranted = spec.listNotGranded(activity)
|
||||
return listNotGranted.isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* 実行時権限が全て揃っているならtrueを返す
|
||||
* そうでなければ権限の要求を行い、falseを返す
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
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.pref.PrefDevice
|
||||
import jp.juggler.subwaytooter.pref.prefDevice
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.data.UriAndType
|
||||
import jp.juggler.util.data.checkMimeTypeAndGrant
|
||||
import jp.juggler.util.data.intentGetContent
|
||||
import jp.juggler.util.data.notEmpty
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.showError
|
||||
import jp.juggler.util.ui.isOk
|
||||
|
||||
class VisualMediaPickerCompat(
|
||||
private val onPicked: suspend (entries: List<UriAndType>?) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
private val log = LogCategory("VisualMediaPickerCompat")
|
||||
}
|
||||
|
||||
private var activity: AppCompatActivity? = null
|
||||
|
||||
private var prefDevice: PrefDevice? = null
|
||||
|
||||
private var pickMedia1: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||
|
||||
private var pickMediaMultiple: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
||||
|
||||
private var arSafPicker: ActivityResultLauncher<Intent>? = null
|
||||
|
||||
/**
|
||||
* SAFのピッカーを使うか判定するのに使う
|
||||
*/
|
||||
private val prSafPickerImage = when {
|
||||
// 34以降でも権限をチェック
|
||||
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||
permissions = listOf(
|
||||
Manifest.permission.READ_MEDIA_IMAGES,
|
||||
),
|
||||
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,
|
||||
)
|
||||
}.requester {
|
||||
openSafPicker(
|
||||
multiple = prefDevice?.mediaPickerMultiple ?: false,
|
||||
allowVideo = false,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* SAFのピッカーを使うか判定するのに使う
|
||||
*/
|
||||
private val prSafPickerImageAndVideo = when {
|
||||
// 34以降でも権限をチェック
|
||||
Build.VERSION.SDK_INT >= 33 -> PermissionSpec(
|
||||
permissions = listOf(
|
||||
Manifest.permission.READ_MEDIA_IMAGES,
|
||||
Manifest.permission.READ_MEDIA_VIDEO,
|
||||
),
|
||||
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,
|
||||
)
|
||||
}.requester {
|
||||
openSafPicker(
|
||||
multiple = prefDevice?.mediaPickerMultiple ?: false,
|
||||
allowVideo = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun register(activity: AppCompatActivity, multipleLimit: Int = 4) {
|
||||
|
||||
prefDevice = activity.prefDevice
|
||||
|
||||
this.activity = activity
|
||||
|
||||
pickMedia1 = activity.registerForActivityResult(
|
||||
ActivityResultContracts.PickVisualMedia(),
|
||||
) { uri ->
|
||||
activity.launchAndShowError {
|
||||
onPicked((uri?.let { listOf(it) })?.checkMimeTypeAndGrant(activity.contentResolver))
|
||||
}
|
||||
}
|
||||
|
||||
pickMediaMultiple = activity.registerForActivityResult(
|
||||
ActivityResultContracts.PickMultipleVisualMedia(multipleLimit),
|
||||
) { uris ->
|
||||
activity.launchAndShowError {
|
||||
onPicked(uris?.notEmpty()?.checkMimeTypeAndGrant(activity.contentResolver))
|
||||
}
|
||||
}
|
||||
|
||||
prSafPickerImage.register(activity)
|
||||
prSafPickerImageAndVideo.register(activity)
|
||||
|
||||
arSafPicker = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
) { r ->
|
||||
activity.launchAndShowError {
|
||||
if (r.isOk) {
|
||||
onPicked(
|
||||
r.data?.checkMimeTypeAndGrant(activity.contentResolver)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun open(
|
||||
multiple: Boolean = false,
|
||||
allowVideo: Boolean = false,
|
||||
) {
|
||||
// SAFのピッカーに必要な権限を持っているか?
|
||||
val hasSafPermissions = try {
|
||||
when {
|
||||
allowVideo -> prSafPickerImageAndVideo
|
||||
else -> prSafPickerImage
|
||||
}.hasPermissions()
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "can't check media permissions.")
|
||||
activity?.showError(ex, "can't check media permissions.")
|
||||
return
|
||||
}
|
||||
|
||||
when {
|
||||
// API 33まで、またはAPI34以降でも権限があればSAFのピッカーを使う
|
||||
Build.VERSION.SDK_INT < 34 || hasSafPermissions -> openSafPicker(
|
||||
multiple = multiple,
|
||||
allowVideo = allowVideo,
|
||||
)
|
||||
|
||||
// API34以降で権限が不十分ならJetPack Activity の写真選択ツールを使う
|
||||
else -> openVisualMediaPicker(
|
||||
multiple = multiple,
|
||||
allowVideo = allowVideo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openVisualMediaPicker(
|
||||
multiple: Boolean,
|
||||
allowVideo: Boolean,
|
||||
) {
|
||||
log.i("openVisualMediaPicker multiple=$multiple, allowVideo=$allowVideo")
|
||||
val mediaType = when (allowVideo) {
|
||||
true -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
|
||||
else -> ActivityResultContracts.PickVisualMedia.ImageOnly
|
||||
}
|
||||
val pickerLauncher = when {
|
||||
multiple -> pickMediaMultiple
|
||||
else -> pickMedia1
|
||||
}
|
||||
pickerLauncher ?: error("openVisualMediaPicker: pickerLauncher is not registered.")
|
||||
pickerLauncher.launch(PickVisualMediaRequest(mediaType))
|
||||
}
|
||||
|
||||
private fun openSafPicker(
|
||||
multiple: Boolean,
|
||||
allowVideo: Boolean,
|
||||
) {
|
||||
log.i("openSafPicker multiple=$multiple, allowVideo=$allowVideo")
|
||||
val activity = this.activity
|
||||
?: error("missing activity")
|
||||
|
||||
prefDevice?.mediaPickerMultiple = multiple
|
||||
|
||||
val permissionRequester = when {
|
||||
allowVideo -> prSafPickerImageAndVideo
|
||||
else -> prSafPickerImage
|
||||
}
|
||||
|
||||
if (!permissionRequester.checkOrLaunch()) return
|
||||
|
||||
// SAFのIntentで開く
|
||||
try {
|
||||
val captionId = when {
|
||||
multiple -> when {
|
||||
allowVideo -> R.string.pick_images_or_video
|
||||
else -> R.string.pick_images
|
||||
}
|
||||
|
||||
else -> R.string.pick_image
|
||||
}
|
||||
val intent = intentGetContent(
|
||||
allowMultiple = true,
|
||||
caption = activity.getString(captionId),
|
||||
mimeTypes = when {
|
||||
allowVideo -> arrayOf("image/*", "video/*")
|
||||
else -> arrayOf("image/*")
|
||||
}
|
||||
)
|
||||
arSafPicker!!.launch(intent)
|
||||
} catch (ex: Throwable) {
|
||||
activity.showError(ex, "openVisualMediaPicker33 failed.")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -278,13 +278,9 @@ fun intentGetContent(
|
|||
return Intent.createChooser(intent, caption)
|
||||
}
|
||||
|
||||
data class GetContentResultEntry(
|
||||
val uri: Uri,
|
||||
val mimeType: String? = null,
|
||||
var time: Long? = null,
|
||||
)
|
||||
data class UriAndType(val uri: Uri, val mimeType: String?)
|
||||
|
||||
fun MutableList<GetContentResultEntry>.addNoDuplicate(
|
||||
fun MutableList<UriAndType>.addNoDuplicate(
|
||||
contentResolver: ContentResolver,
|
||||
uri: Uri?,
|
||||
type: String? = null,
|
||||
|
@ -295,12 +291,12 @@ fun MutableList<GetContentResultEntry>.addNoDuplicate(
|
|||
type ?: contentResolver.getType(uri)
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "contentResolver.getType failed. uri=$uri")
|
||||
return
|
||||
null
|
||||
}
|
||||
add(GetContentResultEntry(uri, mimeType))
|
||||
add(UriAndType(uri, mimeType))
|
||||
}
|
||||
|
||||
fun List<GetContentResultEntry>.grantPermissions(
|
||||
fun List<UriAndType>.grantPermissions(
|
||||
contentResolver: ContentResolver,
|
||||
) {
|
||||
forEach {
|
||||
|
@ -314,27 +310,32 @@ fun List<GetContentResultEntry>.grantPermissions(
|
|||
}
|
||||
}
|
||||
|
||||
// returns list of pair of uri and mime-type.
|
||||
fun List<Uri>.handleGetContentResult(contentResolver: ContentResolver) =
|
||||
buildList {
|
||||
this@handleGetContentResult.forEach {
|
||||
addNoDuplicate(contentResolver, it)
|
||||
}
|
||||
grantPermissions(contentResolver)
|
||||
}
|
||||
/**
|
||||
* URIのリストに対してMIMEタイプの取得とtakePersistableUriPermissionを行う
|
||||
* @return UriAndTypeのリスト
|
||||
*/
|
||||
fun List<Uri>.checkMimeTypeAndGrant(
|
||||
contentResolver: ContentResolver,
|
||||
) = buildList {
|
||||
this@checkMimeTypeAndGrant.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)
|
||||
/**
|
||||
* ピッカーが返したIntentからURIのリストを読み、MIMEタイプの取得とtakePersistableUriPermissionを行う
|
||||
* @return UriAndTypeのリスト
|
||||
*/
|
||||
fun Intent.checkMimeTypeAndGrant(
|
||||
contentResolver: ContentResolver,
|
||||
) = buildList {
|
||||
// 単一選択
|
||||
addNoDuplicate(contentResolver, data, type)
|
||||
|
||||
// 複数選択
|
||||
clipData?.uris?.forEach {
|
||||
addNoDuplicate(contentResolver, it)
|
||||
}
|
||||
grantPermissions(contentResolver)
|
||||
}
|
||||
// 複数選択
|
||||
clipData?.uris?.forEach { addNoDuplicate(contentResolver, it) }
|
||||
|
||||
grantPermissions(contentResolver)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import android.widget.PopupWindow
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||
import jp.juggler.util.coroutine.runOnMainLooper
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
@ -181,7 +180,7 @@ fun Activity.dialogOrToast(message: String?) {
|
|||
fun Activity.dialogOrToast(@StringRes stringId: Int, vararg args: Any) =
|
||||
dialogOrToast(getString(stringId, *args))
|
||||
|
||||
fun AppCompatActivity.showError(ex: Throwable, caption: String? = null) {
|
||||
fun Activity.showError(ex: Throwable, caption: String? = null) {
|
||||
log.e(ex, caption ?: "(showError)")
|
||||
|
||||
// キャンセル例外はUIに表示しない
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package jp.juggler.util.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.showToast
|
||||
|
||||
class ActivityResultHandler(
|
||||
private val log: LogCategory,
|
||||
private val callback: (ActivityResult) -> Unit,
|
||||
) {
|
||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||
private var getContext: (() -> Context?)? = null
|
||||
|
||||
private val context
|
||||
get() = getContext?.invoke()
|
||||
|
||||
// startForActivityResultの代わりに呼び出す
|
||||
fun launch(intent: Intent, options: ActivityOptionsCompat? = null) = try {
|
||||
(launcher ?: error("ActivityResultHandler not registered."))
|
||||
.launch(intent, options)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "launch failed")
|
||||
context?.showToast(ex, "activity launch failed.")
|
||||
}
|
||||
|
||||
// onCreate時に呼び出す
|
||||
fun register(a: FragmentActivity) {
|
||||
getContext = { a.applicationContext }
|
||||
this.launcher = a.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { callback(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun Intent.launch(ar: ActivityResultHandler) = ar.launch(this)
|
||||
|
||||
val ActivityResult.isNotOk
|
||||
get() = Activity.RESULT_OK != resultCode
|
||||
|
||||
val ActivityResult.isOk
|
||||
get() = Activity.RESULT_OK == resultCode
|
|
@ -1,9 +1,7 @@
|
|||
package jp.juggler.util.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.content.res.TypedArray
|
||||
|
@ -26,18 +24,13 @@ import android.view.View
|
|||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import jp.juggler.util.data.clip
|
||||
import jp.juggler.util.data.notZero
|
||||
import jp.juggler.util.getUriExtra
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.showToast
|
||||
|
||||
private val log = LogCategory("UiUtils")
|
||||
|
||||
|
@ -342,45 +335,9 @@ var View.isEnabledAlpha: Boolean
|
|||
|
||||
/////////////////////////////////////////////////
|
||||
|
||||
class ActivityResultHandler(
|
||||
private val log: LogCategory,
|
||||
private val callback: (ActivityResult) -> Unit,
|
||||
) {
|
||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||
private var getContext: (() -> Context?)? = null
|
||||
|
||||
private val context
|
||||
get() = getContext?.invoke()
|
||||
|
||||
// startForActivityResultの代わりに呼び出す
|
||||
fun launch(intent: Intent, options: ActivityOptionsCompat? = null) = try {
|
||||
(launcher ?: error("ActivityResultHandler not registered."))
|
||||
.launch(intent, options)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "launch failed")
|
||||
context?.showToast(ex, "activity launch failed.")
|
||||
}
|
||||
|
||||
// onCreate時に呼び出す
|
||||
fun register(a: FragmentActivity) {
|
||||
getContext = { a.applicationContext }
|
||||
this.launcher = a.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { callback(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun Intent.launch(ar: ActivityResultHandler) = ar.launch(this)
|
||||
|
||||
val AppCompatActivity.isLiveActivity: Boolean
|
||||
get() = !(isFinishing || isDestroyed)
|
||||
|
||||
val ActivityResult.isNotOk
|
||||
get() = Activity.RESULT_OK != resultCode
|
||||
|
||||
val ActivityResult.isOk
|
||||
get() = Activity.RESULT_OK == resultCode
|
||||
|
||||
/**
|
||||
* Ringtone pickerの処理結果のUriまたはnull
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue