Compare commits
3 Commits
ceca568a1a
...
f8aaedf86a
Author | SHA1 | Date |
---|---|---|
tateisu | f8aaedf86a | |
tateisu | 2cbbbcce43 | |
tateisu | e703d7460b |
|
@ -25,8 +25,8 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
targetSdk = Vers.stTargetSdkVersion
|
targetSdk = Vers.stTargetSdkVersion
|
||||||
minSdk = Vers.stMinSdkVersion
|
minSdk = Vers.stMinSdkVersion
|
||||||
versionCode = 544
|
versionCode = 545
|
||||||
versionName = "5.544"
|
versionName = "5.545"
|
||||||
applicationId = "jp.juggler.subwaytooter"
|
applicationId = "jp.juggler.subwaytooter"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package jp.juggler.subwaytooter
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
@ -9,7 +8,6 @@ import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
@ -21,11 +19,6 @@ import android.widget.CompoundButton
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.Spinner
|
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.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
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.launchAndShowError
|
||||||
import jp.juggler.util.coroutine.launchMain
|
import jp.juggler.util.coroutine.launchMain
|
||||||
import jp.juggler.util.coroutine.launchProgress
|
import jp.juggler.util.coroutine.launchProgress
|
||||||
|
import jp.juggler.util.data.UriAndType
|
||||||
import jp.juggler.util.data.UriSerializer
|
import jp.juggler.util.data.UriSerializer
|
||||||
import jp.juggler.util.data.getDocumentName
|
import jp.juggler.util.data.getDocumentName
|
||||||
import jp.juggler.util.data.getStreamSize
|
import jp.juggler.util.data.getStreamSize
|
||||||
|
@ -198,18 +192,12 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private var permissionCamera = permissionSpecCamera.requester {
|
private val cameraOpener = CameraOpener {
|
||||||
openCamera()
|
uploadImage(state.propName, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var pickImageLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
private val visualMediaPicker = VisualMediaPickerCompat {
|
||||||
|
uploadImage(state.propName, it?.firstOrNull())
|
||||||
private val pickImageCallback = ActivityResultCallback<Uri?> {
|
|
||||||
handlePickImageResult(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val arCameraImage = ActivityResultHandler(log) {
|
|
||||||
handleCameraResult(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val arShowAcctColor = ActivityResultHandler(log) { r ->
|
private val arShowAcctColor = ActivityResultHandler(log) { r ->
|
||||||
|
@ -222,14 +210,10 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
backPressed { handleBackPressed() }
|
backPressed { handleBackPressed() }
|
||||||
|
|
||||||
pickImageLauncher = registerForActivityResult(
|
visualMediaPicker.register(this)
|
||||||
ActivityResultContracts.PickVisualMedia(),
|
cameraOpener.register(this)
|
||||||
pickImageCallback,
|
|
||||||
)
|
|
||||||
|
|
||||||
permissionCamera.register(this)
|
|
||||||
arShowAcctColor.register(this)
|
arShowAcctColor.register(this)
|
||||||
arCameraImage.register(this)
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
savedInstanceState.getString(ACTIVITY_STATE)
|
savedInstanceState.getString(ACTIVITY_STATE)
|
||||||
|
@ -1357,75 +1341,15 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
launchAndShowError {
|
launchAndShowError {
|
||||||
actionsDialog {
|
actionsDialog {
|
||||||
action(getString(R.string.pick_image)) {
|
action(getString(R.string.pick_image)) {
|
||||||
openPickImage()
|
visualMediaPicker.open()
|
||||||
}
|
}
|
||||||
action(getString(R.string.image_capture)) {
|
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 {
|
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) {
|
if (mimeType == null) {
|
||||||
showToast(false, "mime type is not provided.")
|
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.coroutine.launchProgress
|
||||||
import jp.juggler.util.data.cast
|
import jp.juggler.util.data.cast
|
||||||
import jp.juggler.util.data.defaultLocale
|
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.intentOpenDocument
|
||||||
import jp.juggler.util.data.notEmpty
|
import jp.juggler.util.data.notEmpty
|
||||||
import jp.juggler.util.data.notZero
|
import jp.juggler.util.data.notZero
|
||||||
|
@ -132,7 +132,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
|
||||||
|
|
||||||
private val arImportAppData = ActivityResultHandler(log) { r ->
|
private val arImportAppData = ActivityResultHandler(log) { r ->
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
if (r.isNotOk) return@ActivityResultHandler
|
||||||
r.data?.handleGetContentResult(contentResolver)
|
r.data?.checkMimeTypeAndGrant(contentResolver)
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.uri?.let { importAppData2(false, it) }
|
?.uri?.let { importAppData2(false, it) }
|
||||||
}
|
}
|
||||||
|
@ -1031,7 +1031,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
|
||||||
|
|
||||||
private fun handleFontResult(item: AppSettingItem?, data: Intent, fileName: String) {
|
private fun handleFontResult(item: AppSettingItem?, data: Intent, fileName: String) {
|
||||||
item ?: error("handleFontResult : setting item is null")
|
item ?: error("handleFontResult : setting item is null")
|
||||||
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
|
data.checkMimeTypeAndGrant(contentResolver).firstOrNull()?.uri?.let {
|
||||||
val file = saveTimelineFont(it, fileName)
|
val file = saveTimelineFont(it, fileName)
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
(item.pref as? StringPref)?.value = file.absolutePath
|
(item.pref as? StringPref)?.value = file.absolutePath
|
||||||
|
|
|
@ -74,7 +74,7 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
|
||||||
|
|
||||||
private val arColumnBackgroundImage = ActivityResultHandler(log) { r ->
|
private val arColumnBackgroundImage = ActivityResultHandler(log) { r ->
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
if (r.isNotOk) return@ActivityResultHandler
|
||||||
r.data?.handleGetContentResult(contentResolver)
|
r.data?.checkMimeTypeAndGrant(contentResolver)
|
||||||
?.firstOrNull()?.uri?.let { updateBackground(it) }
|
?.firstOrNull()?.uri?.let { updateBackground(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,7 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
|
||||||
|
|
||||||
private val arImport = ActivityResultHandler(log) { r ->
|
private val arImport = ActivityResultHandler(log) { r ->
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
if (r.isNotOk) return@ActivityResultHandler
|
||||||
r.data?.handleGetContentResult(contentResolver)
|
r.data?.checkMimeTypeAndGrant(contentResolver)
|
||||||
?.firstOrNull()?.uri?.let { import2(it) }
|
?.firstOrNull()?.uri?.let { import2(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
@ -65,7 +64,7 @@ import jp.juggler.util.backPressed
|
||||||
import jp.juggler.util.coroutine.launchAndShowError
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
import jp.juggler.util.coroutine.launchIO
|
import jp.juggler.util.coroutine.launchIO
|
||||||
import jp.juggler.util.coroutine.launchMain
|
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.LogCategory
|
||||||
import jp.juggler.util.log.showToast
|
import jp.juggler.util.log.showToast
|
||||||
import jp.juggler.util.string
|
import jp.juggler.util.string
|
||||||
|
@ -216,14 +215,15 @@ class ActPost : AppCompatActivity(),
|
||||||
handler = appState.handler
|
handler = appState.handler
|
||||||
attachmentUploader = AttachmentUploader(this, handler)
|
attachmentUploader = AttachmentUploader(this, handler)
|
||||||
attachmentPicker = AttachmentPicker(this, object : AttachmentPicker.Callback {
|
attachmentPicker = AttachmentPicker(this, object : AttachmentPicker.Callback {
|
||||||
override suspend fun onPickAttachment(uri: Uri, mimeType: String?) {
|
override suspend fun onPickAttachment(item: UriAndType) {
|
||||||
addAttachment(uri, mimeType)
|
addAttachment(item.uri, item.mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPickCustomThumbnail(
|
override suspend fun onPickCustomThumbnail(
|
||||||
attachmentId: String?,
|
attachmentId: String?,
|
||||||
src: GetContentResultEntry,
|
src: UriAndType?,
|
||||||
) {
|
) {
|
||||||
|
src ?: return
|
||||||
val pa = attachmentList.find { it.attachment?.id?.toString() == attachmentId }
|
val pa = attachmentList.find { it.attachment?.id?.toString() == attachmentId }
|
||||||
?: error("missing attachment for attachmentId=$attachmentId")
|
?: error("missing attachment for attachmentId=$attachmentId")
|
||||||
onPickCustomThumbnailImpl(pa, src)
|
onPickCustomThumbnailImpl(pa, src)
|
||||||
|
|
|
@ -29,9 +29,8 @@ import jp.juggler.subwaytooter.util.AttachmentRequest
|
||||||
import jp.juggler.subwaytooter.util.PostAttachment
|
import jp.juggler.subwaytooter.util.PostAttachment
|
||||||
import jp.juggler.subwaytooter.view.MyNetworkImageView
|
import jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||||
import jp.juggler.util.coroutine.launchAndShowError
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
import jp.juggler.util.coroutine.launchMain
|
|
||||||
import jp.juggler.util.data.CharacterGroup
|
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.buildJsonObject
|
||||||
import jp.juggler.util.data.decodeJsonArray
|
import jp.juggler.util.data.decodeJsonArray
|
||||||
import jp.juggler.util.data.notEmpty
|
import jp.juggler.util.data.notEmpty
|
||||||
|
@ -269,9 +268,7 @@ fun ActPost.performAttachmentClick(idx: Int) {
|
||||||
TootAttachmentType.GIFV,
|
TootAttachmentType.GIFV,
|
||||||
TootAttachmentType.Video,
|
TootAttachmentType.Video,
|
||||||
-> action(getString(R.string.custom_thumbnail)) {
|
-> action(getString(R.string.custom_thumbnail)) {
|
||||||
attachmentPicker.openCustomThumbnail(
|
attachmentPicker.openThumbnailPicker(pa)
|
||||||
attachmentId = pa.attachment?.id?.toString()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
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) {
|
when (val account = this.account) {
|
||||||
null -> showToast(false, R.string.account_select_please)
|
null -> showToast(false, R.string.account_select_please)
|
||||||
else -> launchMain {
|
else -> if (pa.attachment?.isEdit == true) {
|
||||||
if (pa.attachment?.isEdit == true) {
|
showToast(
|
||||||
showToast(
|
true,
|
||||||
true,
|
"Sorry, updateing thumbnail is not yet supported in case of editing post."
|
||||||
"Sorry, updateing thumbnail is not yet supported in case of editing post."
|
)
|
||||||
)
|
} else {
|
||||||
} else {
|
val result = attachmentUploader.uploadCustomThumbnail(account, src, pa)
|
||||||
val result = attachmentUploader.uploadCustomThumbnail(account, src, pa)
|
result?.error?.let { showToast(true, it) }
|
||||||
result?.error?.let { showToast(true, it) }
|
showMediaAttachment()
|
||||||
showMediaAttachment()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,11 @@ package jp.juggler.subwaytooter.actpost
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import jp.juggler.subwaytooter.ActPost
|
import jp.juggler.subwaytooter.ActPost
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
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.kJson
|
||||||
import jp.juggler.subwaytooter.util.AttachmentPicker
|
import jp.juggler.subwaytooter.util.AttachmentPicker
|
||||||
import jp.juggler.subwaytooter.util.PostAttachment
|
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.data.toJsonArray
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
|
||||||
private val log = LogCategory("ActPostStates")
|
private val log = LogCategory("ActPostStates")
|
||||||
|
|
|
@ -3,10 +3,12 @@ package jp.juggler.subwaytooter.pref
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.net.Uri
|
||||||
import androidx.startup.AppInitializer
|
import androidx.startup.AppInitializer
|
||||||
import androidx.startup.Initializer
|
import androidx.startup.Initializer
|
||||||
|
import jp.juggler.util.data.mayUri
|
||||||
import jp.juggler.util.os.applicationContextSafe
|
import jp.juggler.util.os.applicationContextSafe
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
class PrefDevice(context: Context) {
|
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_TIME_LAST_ENDPOINT_REGISTER = "timeLastEndpointRegister"
|
||||||
private const val PREF_SUPRESS_REQUEST_NOTIFICATION_PERMISSION =
|
private const val PREF_SUPRESS_REQUEST_NOTIFICATION_PERMISSION =
|
||||||
"supressRequestNotificationPermission"
|
"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_FCM = "fcm"
|
||||||
const val PUSH_DISTRIBUTOR_NONE = "none"
|
const val PUSH_DISTRIBUTOR_NONE = "none"
|
||||||
|
@ -153,6 +159,31 @@ class PrefDevice(context: Context) {
|
||||||
value.saveTo(PREF_SUPRESS_REQUEST_NOTIFICATION_PERMISSION)
|
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
|
package jp.juggler.subwaytooter.util
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.MediaStore
|
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 androidx.appcompat.app.AppCompatActivity
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||||
import jp.juggler.subwaytooter.kJson
|
import jp.juggler.subwaytooter.kJson
|
||||||
import jp.juggler.util.coroutine.launchAndShowError
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
import jp.juggler.util.data.GetContentResultEntry
|
import jp.juggler.util.data.UriAndType
|
||||||
import jp.juggler.util.data.UriSerializer
|
|
||||||
import jp.juggler.util.data.handleGetContentResult
|
|
||||||
import jp.juggler.util.data.intentGetContent
|
|
||||||
import jp.juggler.util.log.LogCategory
|
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.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
|
||||||
|
@ -38,18 +23,15 @@ class AttachmentPicker(
|
||||||
|
|
||||||
// callback after media selected
|
// callback after media selected
|
||||||
interface Callback {
|
interface Callback {
|
||||||
suspend fun onPickAttachment(uri: Uri, mimeType: String? = null)
|
suspend fun onPickAttachment(item: UriAndType)
|
||||||
suspend fun onPickCustomThumbnail(attachmentId: String?, src: GetContentResultEntry)
|
suspend fun onPickCustomThumbnail(attachmentId: String?, src: UriAndType?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// actions after permission granted
|
// // actions after permission granted
|
||||||
enum class AfterPermission { Attachment, CustomThumbnail, }
|
// enum class AfterPermission { Attachment, CustomThumbnail, }
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class States(
|
data class States(
|
||||||
@Serializable(with = UriSerializer::class)
|
|
||||||
var uriCameraImage: Uri? = null,
|
|
||||||
|
|
||||||
var customThumbnailTargetId: String? = null,
|
var customThumbnailTargetId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,67 +40,43 @@ class AttachmentPicker(
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
// activity result handlers
|
// activity result handlers
|
||||||
|
|
||||||
private var pickThumbnailLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
private val visualMediaPickerThumbnail = VisualMediaPickerCompat {
|
||||||
|
callback.onPickCustomThumbnail(states.customThumbnailTargetId, it?.firstOrNull())
|
||||||
private val pickThumbnailCallback = ActivityResultCallback<Uri?> {
|
|
||||||
handleThumbnailResult(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var pickVisualMediaLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
|
private val visualMediaPickerAttachment = VisualMediaPickerCompat { it?.pickAll() }
|
||||||
|
|
||||||
private val pickVisualMediaCallback = ActivityResultCallback<List<Uri>?> { uris ->
|
private val audioPicker = AudioPicker { it?.pickAll() }
|
||||||
uris?.handleGetContentResult(activity.contentResolver)?.pickAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val prPickAudio = permissionSpecAudioPicker.requester { openAudioPicker() }
|
private val cameraOpener = CameraOpener { callback.onPickAttachment(it) }
|
||||||
private val arPickAudio = ActivityResultHandler(log) { r ->
|
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
|
||||||
r.data?.handleGetContentResult(activity.contentResolver)?.pickAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val prCamera = permissionSpecCamera.requester { openStillCamera() }
|
private val captureOpener = CaptureOpener { callback.onPickAttachment(it) }
|
||||||
private val arCamera = ActivityResultHandler(log) { handleCameraResult(it) }
|
|
||||||
|
|
||||||
private val prCapture = permissionSpecCapture.requester { openPicker() }
|
|
||||||
private val arCapture = ActivityResultHandler(log) { handleCaptureResult(it) }
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// must register all ARHs before onStart
|
visualMediaPickerAttachment.register(activity)
|
||||||
prPickAudio.register(activity)
|
visualMediaPickerThumbnail.register(activity)
|
||||||
arPickAudio.register(activity)
|
cameraOpener.register(activity)
|
||||||
|
audioPicker.register(activity)
|
||||||
prCamera.register(activity)
|
captureOpener.register(activity)
|
||||||
arCamera.register(activity)
|
|
||||||
|
|
||||||
arCapture.register(activity)
|
|
||||||
|
|
||||||
pickVisualMediaLauncher = activity.registerForActivityResult(
|
|
||||||
ActivityResultContracts.PickMultipleVisualMedia(4),
|
|
||||||
pickVisualMediaCallback,
|
|
||||||
)
|
|
||||||
pickThumbnailLauncher = activity.registerForActivityResult(
|
|
||||||
ActivityResultContracts.PickVisualMedia(),
|
|
||||||
pickThumbnailCallback,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
// states
|
// states
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
states.uriCameraImage = null
|
cameraOpener.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encodeState(): String {
|
fun encodeState(): String {
|
||||||
val encoded = kJson.encodeToString(states)
|
val encoded = kJson.encodeToString(states)
|
||||||
val decoded = kJson.decodeFromString<States>(encoded)
|
val decoded = kJson.decodeFromString<States>(encoded)
|
||||||
log.d("encodeState: ${decoded.uriCameraImage},$encoded")
|
log.d("encodeState: states=$states, encoded=$encoded, decoded=$decoded")
|
||||||
return encoded
|
return encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreState(encoded: String) {
|
fun restoreState(encoded: String) {
|
||||||
states = kJson.decodeFromString(encoded)
|
states = kJson.decodeFromString(encoded)
|
||||||
log.d("restoreState: ${states.uriCameraImage},$encoded")
|
log.d("restoreState: states=$states, encoded=$encoded")
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -128,22 +86,25 @@ class AttachmentPicker(
|
||||||
launchAndShowError {
|
launchAndShowError {
|
||||||
actionsDialog {
|
actionsDialog {
|
||||||
action(getString(R.string.pick_images_or_video)) {
|
action(getString(R.string.pick_images_or_video)) {
|
||||||
openVisualMediaPicker()
|
visualMediaPickerAttachment.open(
|
||||||
|
multiple = true,
|
||||||
|
allowVideo = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
action(getString(R.string.pick_audios)) {
|
action(getString(R.string.pick_audios)) {
|
||||||
openAudioPicker()
|
audioPicker.open()
|
||||||
}
|
}
|
||||||
action(getString(R.string.image_capture)) {
|
action(getString(R.string.image_capture)) {
|
||||||
openStillCamera()
|
cameraOpener.open()
|
||||||
}
|
}
|
||||||
action(getString(R.string.video_capture)) {
|
action(getString(R.string.video_capture)) {
|
||||||
performCapture(
|
captureOpener.open(
|
||||||
MediaStore.ACTION_VIDEO_CAPTURE,
|
MediaStore.ACTION_VIDEO_CAPTURE,
|
||||||
"can't open video capture app."
|
"can't open video capture app."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
action(getString(R.string.voice_capture)) {
|
action(getString(R.string.voice_capture)) {
|
||||||
performCapture(
|
captureOpener.open(
|
||||||
MediaStore.Audio.Media.RECORD_SOUND_ACTION,
|
MediaStore.Audio.Media.RECORD_SOUND_ACTION,
|
||||||
"can't open voice capture app."
|
"can't open voice capture app."
|
||||||
)
|
)
|
||||||
|
@ -153,122 +114,15 @@ class AttachmentPicker(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openVisualMediaPicker() {
|
private suspend fun List<UriAndType>.pickAll() {
|
||||||
(pickVisualMediaLauncher
|
forEach { callback.onPickAttachment(it) }
|
||||||
?: error("pickVisualMediaLauncher is not registered."))
|
|
||||||
.launch(
|
|
||||||
PickVisualMediaRequest(
|
|
||||||
ActivityResultContracts.PickVisualMedia.ImageAndVideo
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAudioPicker() {
|
// ActPostAttachmentから呼ばれる
|
||||||
if (!prPickAudio.checkOrLaunch()) return
|
fun openThumbnailPicker(pa: PostAttachment) {
|
||||||
activity.launchAndShowError {
|
states.customThumbnailTargetId =
|
||||||
val intent = intentGetContent(
|
pa.attachment?.id?.toString()
|
||||||
allowMultiple = true,
|
?: error("attachmentId is null")
|
||||||
caption = activity.getString(R.string.pick_audios),
|
visualMediaPickerThumbnail.open()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import jp.juggler.subwaytooter.api.runApiTask
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.util.coroutine.AppDispatchers
|
import jp.juggler.util.coroutine.AppDispatchers
|
||||||
import jp.juggler.util.coroutine.launchIO
|
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.asciiPattern
|
||||||
import jp.juggler.util.data.buildJsonObject
|
import jp.juggler.util.data.buildJsonObject
|
||||||
import jp.juggler.util.data.encodeHex
|
import jp.juggler.util.data.encodeHex
|
||||||
|
@ -316,7 +316,7 @@ class AttachmentUploader(
|
||||||
// 添付データのカスタムサムネイル
|
// 添付データのカスタムサムネイル
|
||||||
suspend fun uploadCustomThumbnail(
|
suspend fun uploadCustomThumbnail(
|
||||||
account: SavedAccount,
|
account: SavedAccount,
|
||||||
src: GetContentResultEntry,
|
src: UriAndType,
|
||||||
pa: PostAttachment,
|
pa: PostAttachment,
|
||||||
): TootApiResult? = try {
|
): TootApiResult? = try {
|
||||||
safeContext.runApiTask(account) { client ->
|
safeContext.runApiTask(account) { client ->
|
||||||
|
@ -335,8 +335,9 @@ class AttachmentUploader(
|
||||||
val maxBytesImage = ar.maxBytesImage(instance, mediaConfig)
|
val maxBytesImage = ar.maxBytesImage(instance, mediaConfig)
|
||||||
|
|
||||||
val opener = ar.createOpener()
|
val opener = ar.createOpener()
|
||||||
try {
|
pa.progress = ""
|
||||||
|
|
||||||
|
try {
|
||||||
if (opener.contentLength > maxBytesImage.toLong()) {
|
if (opener.contentLength > maxBytesImage.toLong()) {
|
||||||
return@runApiTask TootApiResult(
|
return@runApiTask TootApiResult(
|
||||||
getString(
|
getString(
|
||||||
|
@ -354,6 +355,7 @@ class AttachmentUploader(
|
||||||
if (account.isMisskey) {
|
if (account.isMisskey) {
|
||||||
TootApiResult("custom thumbnail is not supported on misskey account.")
|
TootApiResult("custom thumbnail is not supported on misskey account.")
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
val result = client.request(
|
val result = client.request(
|
||||||
"/api/v1/media/${pa.attachment?.id}",
|
"/api/v1/media/${pa.attachment?.id}",
|
||||||
MultipartBody.Builder()
|
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 var getContext: (() -> Context?)? = null
|
||||||
|
|
||||||
private val activity
|
val activity
|
||||||
get() = getContext?.invoke() as? FragmentActivity
|
get() = getContext?.invoke() as? FragmentActivity
|
||||||
|
|
||||||
// ActivityのonCreate()から呼び出す
|
// 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を返す
|
* 実行時権限が全て揃っているならtrueを返す
|
||||||
* そうでなければ権限の要求を行い、falseを返す
|
* そうでなければ権限の要求を行い、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)
|
return Intent.createChooser(intent, caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GetContentResultEntry(
|
data class UriAndType(val uri: Uri, val mimeType: String?)
|
||||||
val uri: Uri,
|
|
||||||
val mimeType: String? = null,
|
|
||||||
var time: Long? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MutableList<GetContentResultEntry>.addNoDuplicate(
|
fun MutableList<UriAndType>.addNoDuplicate(
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
uri: Uri?,
|
uri: Uri?,
|
||||||
type: String? = null,
|
type: String? = null,
|
||||||
|
@ -295,12 +291,12 @@ fun MutableList<GetContentResultEntry>.addNoDuplicate(
|
||||||
type ?: contentResolver.getType(uri)
|
type ?: contentResolver.getType(uri)
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
log.w(ex, "contentResolver.getType failed. uri=$uri")
|
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,
|
contentResolver: ContentResolver,
|
||||||
) {
|
) {
|
||||||
forEach {
|
forEach {
|
||||||
|
@ -314,27 +310,32 @@ fun List<GetContentResultEntry>.grantPermissions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns list of pair of uri and mime-type.
|
/**
|
||||||
fun List<Uri>.handleGetContentResult(contentResolver: ContentResolver) =
|
* URIのリストに対してMIMEタイプの取得とtakePersistableUriPermissionを行う
|
||||||
buildList {
|
* @return UriAndTypeのリスト
|
||||||
this@handleGetContentResult.forEach {
|
*/
|
||||||
addNoDuplicate(contentResolver, it)
|
fun List<Uri>.checkMimeTypeAndGrant(
|
||||||
}
|
contentResolver: ContentResolver,
|
||||||
grantPermissions(contentResolver)
|
) = buildList {
|
||||||
}
|
this@checkMimeTypeAndGrant.forEach { addNoDuplicate(contentResolver, it) }
|
||||||
|
grantPermissions(contentResolver)
|
||||||
|
}
|
||||||
|
|
||||||
val ClipData.uris
|
val ClipData.uris
|
||||||
get() = (0 until itemCount).mapNotNull { getItemAt(it)?.uri }
|
get() = (0 until itemCount).mapNotNull { getItemAt(it)?.uri }
|
||||||
|
|
||||||
// returns list of pair of uri and mime-type.
|
/**
|
||||||
fun Intent.handleGetContentResult(contentResolver: ContentResolver) =
|
* ピッカーが返したIntentからURIのリストを読み、MIMEタイプの取得とtakePersistableUriPermissionを行う
|
||||||
buildList {
|
* @return UriAndTypeのリスト
|
||||||
// 単一選択
|
*/
|
||||||
addNoDuplicate(contentResolver, data, type)
|
fun Intent.checkMimeTypeAndGrant(
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
) = buildList {
|
||||||
|
// 単一選択
|
||||||
|
addNoDuplicate(contentResolver, data, type)
|
||||||
|
|
||||||
// 複数選択
|
// 複数選択
|
||||||
clipData?.uris?.forEach {
|
clipData?.uris?.forEach { addNoDuplicate(contentResolver, it) }
|
||||||
addNoDuplicate(contentResolver, it)
|
|
||||||
}
|
grantPermissions(contentResolver)
|
||||||
grantPermissions(contentResolver)
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ import android.widget.PopupWindow
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe
|
||||||
import jp.juggler.util.coroutine.runOnMainLooper
|
import jp.juggler.util.coroutine.runOnMainLooper
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
@ -181,7 +180,7 @@ fun Activity.dialogOrToast(message: String?) {
|
||||||
fun Activity.dialogOrToast(@StringRes stringId: Int, vararg args: Any) =
|
fun Activity.dialogOrToast(@StringRes stringId: Int, vararg args: Any) =
|
||||||
dialogOrToast(getString(stringId, *args))
|
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)")
|
log.e(ex, caption ?: "(showError)")
|
||||||
|
|
||||||
// キャンセル例外はUIに表示しない
|
// キャンセル例外は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
|
package jp.juggler.util.ui
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.content.res.TypedArray
|
import android.content.res.TypedArray
|
||||||
|
@ -26,18 +24,13 @@ import android.view.View
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import jp.juggler.util.data.clip
|
import jp.juggler.util.data.clip
|
||||||
import jp.juggler.util.data.notZero
|
import jp.juggler.util.data.notZero
|
||||||
import jp.juggler.util.getUriExtra
|
import jp.juggler.util.getUriExtra
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.log.showToast
|
|
||||||
|
|
||||||
private val log = LogCategory("UiUtils")
|
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
|
val AppCompatActivity.isLiveActivity: Boolean
|
||||||
get() = !(isFinishing || isDestroyed)
|
get() = !(isFinishing || isDestroyed)
|
||||||
|
|
||||||
val ActivityResult.isNotOk
|
|
||||||
get() = Activity.RESULT_OK != resultCode
|
|
||||||
|
|
||||||
val ActivityResult.isOk
|
|
||||||
get() = Activity.RESULT_OK == resultCode
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ringtone pickerの処理結果のUriまたはnull
|
* Ringtone pickerの処理結果のUriまたはnull
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue