From e703d7460b8789d2cce92069d66034d4f5115cc8 Mon Sep 17 00:00:00 2001 From: tateisu Date: Sun, 7 Jan 2024 00:53:28 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=A1=E3=83=87=E3=82=A3=E3=82=A2=E3=82=A2?= =?UTF-8?q?=E3=82=AF=E3=82=BB=E3=82=B9=E6=A8=A9=E9=99=90=E3=81=AE=E6=9C=89?= =?UTF-8?q?=E7=84=A1=E3=81=AB=E5=BF=9C=E3=81=98=E3=81=A6=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC=E3=82=92?= =?UTF-8?q?=E5=A4=89=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../juggler/subwaytooter/ActAccountSetting.kt | 99 ++------ .../jp/juggler/subwaytooter/ActAppSetting.kt | 6 +- .../subwaytooter/ActColumnCustomize.kt | 2 +- .../juggler/subwaytooter/ActLanguageFilter.kt | 2 +- .../java/jp/juggler/subwaytooter/ActPost.kt | 10 +- .../subwaytooter/actpost/ActPostAttachment.kt | 29 +-- .../juggler/subwaytooter/pref/PrefDevice.kt | 33 ++- .../subwaytooter/util/AttachmentPicker.kt | 212 +++-------------- .../subwaytooter/util/AttachmentUploader.kt | 8 +- .../juggler/subwaytooter/util/AudioPicker.kt | 50 ++++ .../juggler/subwaytooter/util/CameraOpener.kt | 85 +++++++ .../subwaytooter/util/CaptureOpener.kt | 72 ++++++ .../subwaytooter/util/PermissionRequester.kt | 8 +- .../util/VisualMediaPickerCompat.kt | 216 ++++++++++++++++++ .../java/jp/juggler/util/data/StorageUtils.kt | 57 ++--- .../java/jp/juggler/util/log/ToastUtils.kt | 3 +- .../juggler/util/ui/ActivityResultHandler.kt | 48 ++++ .../main/java/jp/juggler/util/ui/UiUtils.kt | 43 ---- 18 files changed, 613 insertions(+), 370 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/AudioPicker.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/CameraOpener.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/CaptureOpener.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/VisualMediaPickerCompat.kt create mode 100644 base/src/main/java/jp/juggler/util/ui/ActivityResultHandler.kt diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt index fa3915c7..7e561ab8 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt @@ -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? = null - - private val pickImageCallback = ActivityResultCallback { - 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.") diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt index 41c98874..3fb60f7a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt b/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt index 9523f8f8..7ad53010 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt @@ -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) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActLanguageFilter.kt b/app/src/main/java/jp/juggler/subwaytooter/ActLanguageFilter.kt index 3631a966..9005636d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActLanguageFilter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActLanguageFilter.kt @@ -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) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index deec86cb..e314e60a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt index a007deb9..3027e180 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt @@ -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() } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/pref/PrefDevice.kt b/app/src/main/java/jp/juggler/subwaytooter/pref/PrefDevice.kt index 0b7865e3..6d1c441d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/pref/PrefDevice.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/pref/PrefDevice.kt @@ -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) + } + } + ////////////////////////////////// // 以下は古い diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt index 9e9c7c69..ee5da65f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt @@ -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,8 +23,8 @@ 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 @@ -47,9 +32,6 @@ class AttachmentPicker( @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? = null - - private val pickThumbnailCallback = ActivityResultCallback { - handleThumbnailResult(it) + private val visualMediaPickerThumbnail = VisualMediaPickerCompat { + callback.onPickCustomThumbnail(states.customThumbnailTargetId, it?.firstOrNull()) } - private var pickVisualMediaLauncher: ActivityResultLauncher? = null + private val visualMediaPickerAttachment = VisualMediaPickerCompat { it?.pickAll() } - private val pickVisualMediaCallback = ActivityResultCallback?> { 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(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.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.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() } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt index 1e21face..19b08bd3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt @@ -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() diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AudioPicker.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AudioPicker.kt new file mode 100644 index 00000000..225c93e3 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AudioPicker.kt @@ -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?) -> 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) + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CameraOpener.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CameraOpener.kt new file mode 100644 index 00000000..46e8fdcf --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CameraOpener.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CaptureOpener.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CaptureOpener.kt new file mode 100644 index 00000000..aac0383c --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CaptureOpener.kt @@ -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) + } + } + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PermissionRequester.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PermissionRequester.kt index a7d2dde4..54e7b249 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PermissionRequester.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PermissionRequester.kt @@ -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を返す diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/VisualMediaPickerCompat.kt b/app/src/main/java/jp/juggler/subwaytooter/util/VisualMediaPickerCompat.kt new file mode 100644 index 00000000..bc20d7f6 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/VisualMediaPickerCompat.kt @@ -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?) -> Unit, +) { + companion object { + private val log = LogCategory("VisualMediaPickerCompat") + } + + private var activity: AppCompatActivity? = null + + private var prefDevice: PrefDevice? = null + + private var pickMedia1: ActivityResultLauncher? = null + + private var pickMediaMultiple: ActivityResultLauncher? = null + + private var arSafPicker: ActivityResultLauncher? = 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.") + } + } +} diff --git a/base/src/main/java/jp/juggler/util/data/StorageUtils.kt b/base/src/main/java/jp/juggler/util/data/StorageUtils.kt index fccaf4f0..820eac25 100644 --- a/base/src/main/java/jp/juggler/util/data/StorageUtils.kt +++ b/base/src/main/java/jp/juggler/util/data/StorageUtils.kt @@ -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.addNoDuplicate( +fun MutableList.addNoDuplicate( contentResolver: ContentResolver, uri: Uri?, type: String? = null, @@ -295,12 +291,12 @@ fun MutableList.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.grantPermissions( +fun List.grantPermissions( contentResolver: ContentResolver, ) { forEach { @@ -314,27 +310,32 @@ fun List.grantPermissions( } } -// returns list of pair of uri and mime-type. -fun List.handleGetContentResult(contentResolver: ContentResolver) = - buildList { - this@handleGetContentResult.forEach { - addNoDuplicate(contentResolver, it) - } - grantPermissions(contentResolver) - } +/** + * URIのリストに対してMIMEタイプの取得とtakePersistableUriPermissionを行う + * @return UriAndTypeのリスト + */ +fun List.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) +} diff --git a/base/src/main/java/jp/juggler/util/log/ToastUtils.kt b/base/src/main/java/jp/juggler/util/log/ToastUtils.kt index 8fc534de..2d5a9aff 100644 --- a/base/src/main/java/jp/juggler/util/log/ToastUtils.kt +++ b/base/src/main/java/jp/juggler/util/log/ToastUtils.kt @@ -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に表示しない diff --git a/base/src/main/java/jp/juggler/util/ui/ActivityResultHandler.kt b/base/src/main/java/jp/juggler/util/ui/ActivityResultHandler.kt new file mode 100644 index 00000000..c89e9455 --- /dev/null +++ b/base/src/main/java/jp/juggler/util/ui/ActivityResultHandler.kt @@ -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? = 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 diff --git a/base/src/main/java/jp/juggler/util/ui/UiUtils.kt b/base/src/main/java/jp/juggler/util/ui/UiUtils.kt index c239d6f6..e5f2157e 100644 --- a/base/src/main/java/jp/juggler/util/ui/UiUtils.kt +++ b/base/src/main/java/jp/juggler/util/ui/UiUtils.kt @@ -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? = 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 */