requestPermissionsをActivityResultContracts.RequestMultiplePermissionsに移行

This commit is contained in:
tateisu 2022-08-06 07:52:31 +09:00
parent bfed497666
commit c1afdbc0ab
18 changed files with 502 additions and 415 deletions

View File

@ -586,7 +586,7 @@ class GifDecoder(val callback : GifDecoderCallback) {
// GIFは最後まで読まないとフレーム数が分からない
if(frames.isEmpty()) throw error("there is no frame.")
if(frames.isEmpty()) error("there is no frame.")
callback.onGifHeader(header)
callback.onGifAnimationInfo(header, animationControl)
for(frame in frames) {

View File

@ -283,6 +283,9 @@ dependencies {
// video transcoder https://github.com/natario1/Transcoder
implementation "com.otaliastudios:transcoder:0.10.4"
// LiveEvent
implementation "com.github.hadilq:live-event:1.3.0"
}
repositories {

View File

@ -72,12 +72,12 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:maxAspectRatio="100"
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/AppTheme.Light"
tools:ignore="DataExtractionRules,UnusedAttribute">
<!-- android:localeConfig="@xml/locales_config" -->
<activity
android:name=".ActMain"

View File

@ -4,7 +4,6 @@ import android.Manifest
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.media.RingtoneManager
import android.net.Uri
@ -20,7 +19,6 @@ import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.Styler.defaultColorIcon
import jp.juggler.subwaytooter.action.accountRemove
import jp.juggler.subwaytooter.api.TootApiClient
@ -70,9 +68,6 @@ class ActAccountSetting : AppCompatActivity(),
internal const val max_length_note = 160
internal const val max_length_fields = 255
private const val PERMISSION_REQUEST_AVATAR = 1
private const val PERMISSION_REQUEST_HEADER = 2
internal const val MIME_TYPE_JPEG = "image/jpeg"
internal const val MIME_TYPE_PNG = "image/png"
@ -153,71 +148,79 @@ class ActAccountSetting : AppCompatActivity(),
///////////////////////////////////////////////////////////////////
private val arShowAcctColor = activityResultHandler { ar ->
if (ar?.resultCode == Activity.RESULT_OK) {
showAcctColor()
private val arShowAcctColor = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
showAcctColor()
}
private val arNotificationSound = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.decodeRingtonePickerResult()?.let { uri ->
notificationSoundUri = uri.toString()
saveUIToData()
// Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), uri);
// TextView ringView = (TextView) findViewById(R.id.ringtone);
// ringView.setText(ringtone.getTitle(getApplicationContext()));
// ringtone.setStreamType(AudioManager.STREAM_ALARM);
// ringtone.play();
// SystemClock.sleep(1000);
// ringtone.stop();
}
}
private val arNotificationSound = activityResultHandler { ar ->
if (ar?.resultCode == Activity.RESULT_OK) {
// RINGTONE_PICKERからの選択されたデータを取得する
val uri = ar.data?.extras?.get(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
if (uri is Uri) {
notificationSoundUri = uri.toString()
saveUIToData()
// Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), uri);
// TextView ringView = (TextView) findViewById(R.id.ringtone);
// ringView.setText(ringtone.getTitle(getApplicationContext()));
// ringtone.setStreamType(AudioManager.STREAM_ALARM);
// ringtone.play();
// SystemClock.sleep(1000);
// ringtone.stop();
private val arAddAttachment = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data
?.handleGetContentResult(contentResolver)
?.firstOrNull()
?.let {
uploadImage(
state.propName,
it.uri,
it.mimeType?.notEmpty() ?: contentResolver.getType(it.uri)
)
}
}
}
private val arAddAttachment = activityResultHandler { ar ->
if (ar?.resultCode == Activity.RESULT_OK) {
ar.data
?.handleGetContentResult(contentResolver)
?.firstOrNull()
?.let {
uploadImage(
state.propName,
it.uri,
it.mimeType?.notEmpty() ?: contentResolver.getType(it.uri)
)
}
}
}
private val arCameraImage = activityResultHandler { ar ->
if (ar?.resultCode == Activity.RESULT_OK) {
// 画像のURL
val uri = ar.data?.data ?: state.uriCameraImage
if (uri != null) {
val type = contentResolver.getType(uri)
uploadImage(state.propName, uri, type)
}
} else {
private val arCameraImage = ActivityResultHandler(log) { r ->
if (r.isNotOk) {
// 失敗したら DBからデータを削除
state.uriCameraImage?.let {
contentResolver.delete(it, null, null)
}
state.uriCameraImage = null
} else {
// 画像のURL
val uri = r.data?.data ?: state.uriCameraImage
if (uri != null) {
val type = contentResolver.getType(uri)
uploadImage(state.propName, uri, type)
}
}
}
private val prPickAvater = PermissionRequester(
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
deniedId = R.string.missing_permission_to_access_media,
) { openPicker(it) }
private val prPickHeader = PermissionRequester(
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
deniedId = R.string.missing_permission_to_access_media,
) { openPicker(it) }
///////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arShowAcctColor.register(this, log)
arNotificationSound.register(this, log)
arAddAttachment.register(this, log)
arCameraImage.register(this, log)
prPickAvater.register(this)
prPickHeader.register(this)
arShowAcctColor.register(this)
arNotificationSound.register(this)
arAddAttachment.register(this)
arCameraImage.register(this)
if (savedInstanceState != null) {
savedInstanceState.getString(ACTIVITY_STATE)
@ -1320,25 +1323,18 @@ class ActAccountSetting : AppCompatActivity(),
}
private fun pickAvatarImage() {
openPicker(PERMISSION_REQUEST_AVATAR)
openPicker(prPickAvater)
}
private fun pickHeaderImage() {
openPicker(PERMISSION_REQUEST_HEADER)
openPicker(prPickHeader)
}
private fun openPicker(requestCode: Int) {
val permissionCheck = ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
preparePermission(requestCode)
return
}
private fun openPicker(permissionRequester: PermissionRequester) {
if (!permissionRequester.checkOrLaunch()) return
val propName = when (requestCode) {
PERMISSION_REQUEST_HEADER -> "header"
val propName = when (permissionRequester) {
prPickHeader -> "header"
else -> "avatar"
}
@ -1364,23 +1360,6 @@ class ActAccountSetting : AppCompatActivity(),
showToast(true, R.string.missing_permission_to_access_media)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
when (requestCode) {
PERMISSION_REQUEST_AVATAR, PERMISSION_REQUEST_HEADER ->
// If request is cancelled, the result arrays are empty.
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openPicker(requestCode)
} else {
showToast(true, R.string.missing_permission_to_access_media)
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
private fun performAttachment(propName: String) {
try {
state.propName = propName

View File

@ -83,41 +83,38 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
private lateinit var adapter: MyAdapter
private lateinit var etSearch: EditText
private val arNoop = activityResultHandler { }
private val arNoop = ActivityResultHandler(log) { }
private val arImportAppData = activityResultHandler { ar ->
if (ar?.resultCode == RESULT_OK) {
ar.data?.handleGetContentResult(contentResolver)
?.firstOrNull()
?.uri?.let { importAppData2(false, it) }
}
private val arImportAppData = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.handleGetContentResult(contentResolver)
?.firstOrNull()
?.uri?.let { importAppData2(false, it) }
}
val arTimelineFont = activityResultHandler { ar ->
if (ar?.resultCode == RESULT_OK) {
ar.data?.let { handleFontResult(AppSettingItem.TIMELINE_FONT, it, "TimelineFont") }
}
val arTimelineFont = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.let { handleFontResult(AppSettingItem.TIMELINE_FONT, it, "TimelineFont") }
}
val arTimelineFontBold = activityResultHandler { ar ->
if (ar?.resultCode == RESULT_OK) {
ar.data?.let {
handleFontResult(
AppSettingItem.TIMELINE_FONT_BOLD,
it,
"TimelineFontBold"
)
}
val arTimelineFontBold = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.let {
handleFontResult(
AppSettingItem.TIMELINE_FONT_BOLD,
it,
"TimelineFontBold"
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arNoop.register(this, log)
arImportAppData.register(this, log)
arTimelineFont.register(this, log)
arTimelineFontBold.register(this, log)
arNoop.register(this)
arImportAppData.register(this)
arTimelineFont.register(this)
arTimelineFontBold.register(this)
requestWindowFeature(Window.FEATURE_NO_TITLE)
App1.setActivityTheme(this, noActionBar = true)
@ -1008,7 +1005,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
inner class AccountAdapter internal constructor() : BaseAdapter() {
internal val list = java.util.ArrayList<SavedAccount>()
internal val list = ArrayList<SavedAccount>()
init {
for (a in SavedAccount.loadAccountList(this@ActAppSetting)) {

View File

@ -19,7 +19,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import com.jrummyapps.android.colorpicker.ColorPickerDialog
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.*
import jp.juggler.util.*
import org.jetbrains.anko.textColor
@ -70,12 +71,10 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
private var lastImageUri: String? = null
private var lastImageBitmap: Bitmap? = null
private val arColumnBackgroundImage = activityResultHandler { ar ->
val data = ar?.data
if (data != null && ar.resultCode == RESULT_OK) {
data.handleGetContentResult(contentResolver)
.firstOrNull()?.uri?.let { updateBackground(it) }
}
private val arColumnBackgroundImage = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.handleGetContentResult(contentResolver)
?.firstOrNull()?.uri?.let { updateBackground(it) }
}
override fun onBackPressed() {
@ -91,7 +90,7 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arColumnBackgroundImage.register(this, log)
arColumnBackgroundImage.register(this)
App1.setActivityTheme(this)
initUI()

View File

@ -54,16 +54,12 @@ class ActHighlightWordEdit
private var bBusy = false
private val arNotificationSound = activityResultHandler { ar ->
val data = ar?.data
if (data != null && ar.resultCode == Activity.RESULT_OK) {
// RINGTONE_PICKERからの選択されたデータを取得する
val uri = data.extras?.get(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
if (uri is Uri) {
item.sound_uri = uri.toString()
item.sound_type = HighlightWord.SOUND_TYPE_CUSTOM
showSound()
}
private val arNotificationSound = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.decodeRingtonePickerResult()?.let { uri ->
item.sound_uri = uri.toString()
item.sound_type = HighlightWord.SOUND_TYPE_CUSTOM
showSound()
}
}
@ -78,7 +74,7 @@ class ActHighlightWordEdit
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arNotificationSound.register(this, log)
arNotificationSound.register(this)
App1.setActivityTheme(this)
initUI()

View File

@ -19,7 +19,6 @@ import com.woxthebox.draglistview.swipe.ListSwipeItem
import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.util.*
import java.lang.ref.WeakReference
import java.util.*
class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
@ -33,9 +32,10 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
private var lastRingtone: WeakReference<Ringtone>? = null
private val arEdit = activityResultHandler { ar ->
private val arEdit = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
try {
if (ar?.resultCode == RESULT_OK) loadData()
loadData()
} catch (ex: Throwable) {
errorEx(ex, "can't load data")
}
@ -48,7 +48,7 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arEdit.register(this, log)
arEdit.register(this)
App1.setActivityTheme(this)
initUI()
loadData()

View File

@ -117,21 +117,18 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
private val languageList = ArrayList<MyItem>()
private var loadingBusy: Boolean = false
private val arExport = activityResultHandler { }
private val arExport = ActivityResultHandler(log) { }
private val arImport = activityResultHandler { ar ->
val data = ar?.data
if (data != null && ar.resultCode == RESULT_OK) {
data.handleGetContentResult(contentResolver).firstOrNull()?.uri?.let {
import2(it)
}
}
private val arImport = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.handleGetContentResult(contentResolver)
?.firstOrNull()?.uri?.let { import2(it) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arExport.register(this, log)
arImport.register(this, log)
arExport.register(this)
arImport.register(this)
App1.setActivityTheme(this)
initUI()

View File

@ -220,105 +220,94 @@ class ActMain : AppCompatActivity(),
val viewPool = RecyclerView.RecycledViewPool()
val arColumnColor = activityResultHandler { ar ->
val data = ar?.data
if (data != null && ar.resultCode == Activity.RESULT_OK) {
appState.saveColumnList()
val idx = data.getIntExtra(ActColumnCustomize.EXTRA_COLUMN_INDEX, 0)
appState.column(idx)?.let {
val arColumnColor = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
appState.saveColumnList()
r.data?.intOrNull(ActColumnCustomize.EXTRA_COLUMN_INDEX)
?.let { appState.column(it) }
?.let {
it.fireColumnColor()
it.fireShowContent(
reason = "ActMain column color changed",
reset = true
)
}
updateColumnStrip()
}
updateColumnStrip()
}
val arLanguageFilter = activityResultHandler { ar ->
val data = ar?.data
if (data != null && ar.resultCode == Activity.RESULT_OK) {
appState.saveColumnList()
val idx = data.getIntExtra(ActLanguageFilter.EXTRA_COLUMN_INDEX, 0)
appState.column(idx)?.onLanguageFilterChanged()
}
val arLanguageFilter = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
appState.saveColumnList()
r.data?.intOrNull(ActLanguageFilter.EXTRA_COLUMN_INDEX)
?.let { appState.column(it) }
?.onLanguageFilterChanged()
}
val arNickname = activityResultHandler { ar ->
if (ar?.resultCode == Activity.RESULT_OK) {
updateColumnStrip()
appState.columnList.forEach { it.fireShowColumnHeader() }
}
val arNickname = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
updateColumnStrip()
appState.columnList.forEach { it.fireShowColumnHeader() }
}
val arAppSetting = activityResultHandler { ar ->
val arAppSetting = ActivityResultHandler(log) { r ->
Column.reloadDefaultColor(this, pref)
showFooterColor()
updateColumnStrip()
if (ar?.resultCode == RESULT_APP_DATA_IMPORT) {
ar.data?.data?.let { importAppData(it) }
if (r.resultCode == RESULT_APP_DATA_IMPORT) {
r.data?.data?.let { importAppData(it) }
}
}
val arAbout = activityResultHandler { ar ->
val data = ar?.data
if (data != null && ar.resultCode == Activity.RESULT_OK) {
data.getStringExtra(ActAbout.EXTRA_SEARCH)?.notEmpty()?.let { search ->
timeline(
defaultInsertPosition,
ColumnType.SEARCH,
args = arrayOf(search, true)
)
}
val arAbout = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.getStringExtra(ActAbout.EXTRA_SEARCH)?.notEmpty()?.let { search ->
timeline(
defaultInsertPosition,
ColumnType.SEARCH,
args = arrayOf(search, true)
)
}
}
val arAccountSetting = activityResultHandler { ar ->
val arAccountSetting = ActivityResultHandler(log) { r ->
updateColumnStrip()
appState.columnList.forEach { it.fireShowColumnHeader() }
when (ar?.resultCode) {
RESULT_OK -> ar.data?.data?.let { openBrowser(it) }
when (r.resultCode) {
RESULT_OK -> r.data?.data?.let { openBrowser(it) }
ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
ar.data?.getLongExtra(ActAccountSetting.EXTRA_DB_ID, -1L)
r.data?.getLongExtra(ActAccountSetting.EXTRA_DB_ID, -1L)
?.takeIf { it != -1L }
?.let { checkAccessToken2(it) }
}
}
val arColumnList = activityResultHandler { ar ->
val data = ar?.data
if (data != null && ar.resultCode == Activity.RESULT_OK) {
val order = data.getIntegerArrayListExtra(ActColumnList.EXTRA_ORDER)
if (order != null && isOrderChanged(order)) {
setColumnsOrder(order)
}
val select = data.getIntExtra(ActColumnList.EXTRA_SELECTION, -1)
if (select in 0 until appState.columnCount) {
scrollToColumn(select)
}
}
val arColumnList = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.getIntegerArrayListExtra(ActColumnList.EXTRA_ORDER)
?.takeIf { isOrderChanged(it) }
?.let { setColumnsOrder(it) }
r.data?.intOrNull(ActColumnList.EXTRA_SELECTION)
?.takeIf { it in 0 until appState.columnCount }
?.let { scrollToColumn(it) }
}
val arActText = activityResultHandler { ar ->
when (ar?.resultCode) {
ActText.RESULT_SEARCH_MSP -> searchFromActivityResult(ar.data, ColumnType.SEARCH_MSP)
ActText.RESULT_SEARCH_TS -> searchFromActivityResult(ar.data, ColumnType.SEARCH_TS)
val arActText = ActivityResultHandler(log) { r ->
when (r.resultCode) {
ActText.RESULT_SEARCH_MSP -> searchFromActivityResult(r.data, ColumnType.SEARCH_MSP)
ActText.RESULT_SEARCH_TS -> searchFromActivityResult(r.data, ColumnType.SEARCH_TS)
ActText.RESULT_SEARCH_NOTESTOCK -> searchFromActivityResult(
ar.data,
r.data,
ColumnType.SEARCH_NOTESTOCK
)
}
}
val arActPost = activityResultHandler { ar ->
ar?.data?.let { data ->
if (ar.resultCode == Activity.RESULT_OK) {
etQuickPost.setText("")
onCompleteActPost(data)
}
val arActPost = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.let { data ->
etQuickPost.setText("")
onCompleteActPost(data)
}
}
@ -330,15 +319,15 @@ class ActMain : AppCompatActivity(),
super.onCreate(savedInstanceState)
refActMain = WeakReference(this)
arColumnColor.register(this, log)
arLanguageFilter.register(this, log)
arNickname.register(this, log)
arAppSetting.register(this, log)
arAbout.register(this, log)
arAccountSetting.register(this, log)
arColumnList.register(this, log)
arActPost.register(this, log)
arActText.register(this, log)
arColumnColor.register(this)
arLanguageFilter.register(this)
arNickname.register(this)
arAppSetting.register(this)
arAbout.register(this)
arAccountSetting.register(this)
arColumnList.register(this)
arActPost.register(this)
arActText.register(this)
requestWindowFeature(Window.FEATURE_NO_TITLE)
App1.setActivityTheme(this, noActionBar = true)

View File

@ -7,7 +7,6 @@ import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.*
import android.os.Build
import android.os.Bundle
@ -16,8 +15,6 @@ import android.os.SystemClock
import android.view.View
import android.view.Window
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.Player.TimelineChangeReason
import com.google.android.exoplayer2.source.LoadEventInfo
@ -57,8 +54,6 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
internal const val DOWNLOAD_REPEAT_EXPIRE = 3000L
internal const val short_limit = 5000L
private const val PERMISSION_REQUEST_CODE = 1
internal const val EXTRA_IDX = "idx"
internal const val EXTRA_DATA = "data"
internal const val EXTRA_SERVICE_TYPE = "serviceType"
@ -231,6 +226,11 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
}
}
private val prDownload = PermissionRequester(
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
deniedId = R.string.missing_permission_to_access_media,
) { download(mediaList[idx]) }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
@ -247,6 +247,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prDownload.register(this)
requestWindowFeature(Window.FEATURE_NO_TITLE)
App1.setActivityTheme(this, noActionBar = true, forceDark = true)
@ -665,7 +666,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
}
private fun download(ta: TootAttachmentLike) {
if (!checkPermission()) return
if (!prDownload.checkOrLaunch()) return
val downLoadManager: DownloadManager = systemService(this)
?: error("missing DownloadManager system service")
@ -827,39 +828,6 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
}
}
private fun checkPermission(): Boolean {
val permissionCheck = ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
if (permissionCheck == PackageManager.PERMISSION_GRANTED) return true
if (Build.VERSION.SDK_INT >= 23) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE
)
} else {
showToast(true, R.string.missing_permission_to_access_media)
}
return false
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
if (requestCode == PERMISSION_REQUEST_CODE) {
when (permissions.indices.all { grantResults[it] == PackageManager.PERMISSION_GRANTED }) {
false -> showToast(true, R.string.missing_permission_to_access_media)
else -> download(mediaList[idx])
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
private fun mediaBackgroundDialog() {
val ad = ActionsDialog()
for (k in MediaBackgroundDrawable.Kind.values()) {

View File

@ -3,7 +3,6 @@ package jp.juggler.subwaytooter
import android.app.Activity
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Editable
@ -62,13 +61,10 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
private var notificationSoundUri: String? = null
private var loadingBusy = false
private val arNotificationSound = activityResultHandler { ar ->
if (ar?.resultCode == RESULT_OK) {
// RINGTONE_PICKERからの選択されたデータを取得する
val uri = ar.data?.extras?.get(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
if (uri is Uri) {
notificationSoundUri = uri.toString()
}
private val arNotificationSound = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.decodeRingtonePickerResult()?.let { uri ->
notificationSoundUri = uri.toString()
}
}
@ -79,7 +75,7 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arNotificationSound.register(this, log)
arNotificationSound.register(this)
App1.setActivityTheme(this)
val intent = intent

View File

@ -139,28 +139,23 @@ class ActPost : AppCompatActivity(),
var isPostComplete: Boolean = false
var scheduledStatus: TootScheduled? = null
// カスタムサムネイルを指定する添付メディア
var paThumbnailTarget: PostAttachment? = null
/////////////////////////////////////////////////////////////////////
val isMultiWindowPost: Boolean
get() = intent.getBooleanExtra(EXTRA_MULTI_WINDOW, false)
val arMushroom = activityResultHandler { ar ->
if (ar?.resultCode == RESULT_OK) {
ar.data?.getStringExtra("replace_key")
?.let { text ->
when (states.mushroomInput) {
0 -> applyMushroomText(views.etContent, text)
1 -> applyMushroomText(views.etContentWarning, text)
else -> for (i in 0..3) {
if (states.mushroomInput == i + 2) {
applyMushroomText(etChoices[i], text)
}
}
val arMushroom = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.getStringExtra("replace_key")?.let { text ->
when (states.mushroomInput) {
0 -> applyMushroomText(views.etContent, text)
1 -> applyMushroomText(views.etContentWarning, text)
else -> for (i in 0..3) {
if (states.mushroomInput == i + 2) {
applyMushroomText(etChoices[i], text)
}
}
}
}
}
@ -176,7 +171,7 @@ class ActPost : AppCompatActivity(),
attachmentUploader = AttachmentUploader(this, handler)
attachmentPicker = AttachmentPicker(this, this)
density = resources.displayMetrics.density
arMushroom.register(this, log)
arMushroom.register(this)
progressChannel = Channel(capacity = Channel.CONFLATED)
launchMain {
@ -288,15 +283,6 @@ class ActPost : AppCompatActivity(),
openBrowser(span.linkInfo.url)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
attachmentPicker.onRequestPermissionsResult(requestCode, permissions, grantResults)
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onPickAttachment(uri: Uri, mimeType: String?) {
addAttachment(uri, mimeType)
}
@ -315,8 +301,13 @@ class ActPost : AppCompatActivity(),
onPostAttachmentCompleteImpl(pa)
}
override fun onPickCustomThumbnail(src: GetContentResultEntry) {
onPickCustomThumbnailImpl(src)
override fun resumeCustomThumbnailTarget(id: String?): PostAttachment? {
id?: return null
return attachmentList.find{ it.attachment?.id?.toString() == id }
}
override fun onPickCustomThumbnail(pa: PostAttachment,src: GetContentResultEntry) {
onPickCustomThumbnailImpl(pa,src)
}
fun initUI() {

View File

@ -202,13 +202,14 @@ fun ActPost.performAttachmentClick(idx: Int) {
}
if (account?.isMastodon == true) {
when (pa.attachment?.type) {
TootAttachmentType.Audio, TootAttachmentType.GIFV, TootAttachmentType.Video ->
a.addAction(getString(R.string.custom_thumbnail)) {
openCustomThumbnail(pa)
}
else -> {
TootAttachmentType.Audio,
TootAttachmentType.GIFV,
TootAttachmentType.Video,
-> a.addAction(getString(R.string.custom_thumbnail)) {
attachmentPicker.openCustomThumbnail(pa)
}
else -> Unit
}
}
@ -309,19 +310,9 @@ fun ActPost.editAttachmentDescription(pa: PostAttachment) {
})
}
fun ActPost.openCustomThumbnail(pa: PostAttachment) {
paThumbnailTarget = pa
attachmentPicker.openCustomThumbnail()
}
fun ActPost.onPickCustomThumbnailImpl(src: GetContentResultEntry) {
val account = this.account
val pa = paThumbnailTarget
when {
account == null ->
showToast(false, R.string.account_select_please)
pa == null || !attachmentList.contains(pa) ->
showToast(true, "lost attachment information")
fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: GetContentResultEntry) {
when (val account = this.account) {
null -> showToast(false, R.string.account_select_please)
else -> launchMain {
val result = attachmentUploader.uploadCustomThumbnail(account, src, pa)
result?.error?.let { showToast(true, it) }

View File

@ -3,21 +3,16 @@ package jp.juggler.subwaytooter.util
import android.Manifest
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.kJson
import jp.juggler.util.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import java.util.*
import kotlinx.serialization.Serializable
class AttachmentPicker(
val activity: AppCompatActivity,
@ -25,14 +20,15 @@ class AttachmentPicker(
) {
companion object {
private val log = LogCategory("AttachmentPicker")
private val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
private const val PERMISSION_REQUEST_CODE = 1
}
// callback after media selected
interface Callback {
fun onPickAttachment(uri: Uri, mimeType: String? = null)
fun onPickCustomThumbnail(src: GetContentResultEntry)
fun onPickCustomThumbnail(pa:PostAttachment,src: GetContentResultEntry)
fun resumeCustomThumbnailTarget(id:String?): PostAttachment?
}
// actions after permission granted
@ -44,7 +40,7 @@ class AttachmentPicker(
@Serializable(with = UriSerializer::class)
var uriCameraImage: Uri? = null,
var afterPermission: AfterPermission = AfterPermission.Attachment,
var customThumbnailTargetId: String? = null,
)
private var states = States()
@ -52,49 +48,66 @@ class AttachmentPicker(
////////////////////////////////////////////////////////////////////////
// activity result handlers
private val arAttachmentChooser = activity.activityResultHandler { ar ->
if (ar?.resultCode == AppCompatActivity.RESULT_OK) {
ar.data?.handleGetContentResult(contentResolver)?.pickAll()
}
private val arAttachmentChooser = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.handleGetContentResult(activity.contentResolver)?.pickAll()
}
private val arCamera = activity.activityResultHandler { ar ->
if (ar?.resultCode == AppCompatActivity.RESULT_OK) {
// 画像のURL
when (val uri = ar.data?.data ?: states.uriCameraImage) {
null -> showToast(false, "missing image uri")
else -> callback.onPickAttachment(uri)
}
} else {
private val arCamera = ActivityResultHandler(log) { r ->
if (r.isNotOk) {
// 失敗したら DBからデータを削除
states.uriCameraImage?.let { uri ->
contentResolver.delete(uri, null, null)
activity.contentResolver.delete(uri, null, null)
states.uriCameraImage = null
}
} else {
// 画像のURL
when (val uri = r.data?.data ?: states.uriCameraImage) {
null -> activity.showToast(false, "missing image uri")
else -> callback.onPickAttachment(uri)
}
}
}
private val arCapture = activity.activityResultHandler { ar ->
if (ar?.resultCode == AppCompatActivity.RESULT_OK) {
ar.data?.data?.let { callback.onPickAttachment(it) }
}
private val arCapture = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data?.data?.let { callback.onPickAttachment(it) }
}
private val arCustomThumbnail = activity.activityResultHandler { ar ->
if (ar?.resultCode == AppCompatActivity.RESULT_OK) {
ar.data
?.handleGetContentResult(contentResolver)
?.firstOrNull()
?.let { callback.onPickCustomThumbnail(it) }
}
private val arCustomThumbnail = ActivityResultHandler(log) { r ->
if (r.isNotOk) return@ActivityResultHandler
r.data
?.handleGetContentResult(activity.contentResolver)
?.firstOrNull()
?.let {
callback.resumeCustomThumbnailTarget(states.customThumbnailTargetId)?.let { pa->
callback.onPickCustomThumbnail(pa,it)
}
}
}
private val prPickAttachment = PermissionRequester(
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
deniedId = R.string.missing_permission_to_access_media,
) { openPicker() }
private val prPickCustomThumbnail = PermissionRequester(
permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
deniedId = R.string.missing_permission_to_access_media,
) {
callback.resumeCustomThumbnailTarget(states.customThumbnailTargetId)
?.let{ openCustomThumbnail(it) }
}
init {
// must register all ARHs before onStart
arAttachmentChooser.register(activity, log)
arCamera.register(activity, log)
arCapture.register(activity, log)
arCustomThumbnail.register(activity, log)
prPickAttachment.register(activity)
prPickCustomThumbnail.register(activity)
arAttachmentChooser.register(activity)
arCamera.register(activity)
arCapture.register(activity)
arCustomThumbnail.register(activity)
}
////////////////////////////////////////////////////////////////////////
@ -107,53 +120,19 @@ class AttachmentPicker(
fun encodeState(): String {
val encoded = kJson.encodeToString(states)
val decoded = kJson.decodeFromString<States>(encoded)
log.d("encodeState: ${decoded.uriCameraImage},${decoded.afterPermission},$encoded")
log.d("encodeState: ${decoded.uriCameraImage},$encoded")
return encoded
}
fun restoreState(encoded: String) {
states = kJson.decodeFromString(encoded)
log.d("restoreState: ${states.uriCameraImage},${states.afterPermission},$encoded")
}
////////////////////////////////////////////////////////////////////////
// permission check
// (current implementation does not auto restart actions after got permission
// returns true if permission granted, false if not granted, (may request permissions)
private fun checkPermission(afterPermission: AfterPermission): Boolean {
states.afterPermission = afterPermission
if (permissions.all {
ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED
}) return true
if (Build.VERSION.SDK_INT >= 23) {
ActivityCompat.requestPermissions(activity, permissions, PERMISSION_REQUEST_CODE)
} else {
activity.showToast(true, R.string.missing_permission_to_access_media)
}
return false
}
fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode != PERMISSION_REQUEST_CODE) return
if ((permissions.indices).any { grantResults.elementAtOrNull(it) != PackageManager.PERMISSION_GRANTED }) {
activity.showToast(true, R.string.missing_permission_to_access_media)
return
}
when (states.afterPermission) {
AfterPermission.Attachment -> openPicker()
AfterPermission.CustomThumbnail -> openCustomThumbnail()
}
log.d("restoreState: ${states.uriCameraImage},$encoded")
}
////////////////////////////////////////////////////////////////////////
fun openPicker() {
if (!checkPermission(AfterPermission.Attachment)) return
if (!prPickAttachment.checkOrLaunch()) return
// permissionCheck = ContextCompat.checkSelfPermission( this, Manifest.permission.CAMERA );
// if( permissionCheck != PackageManager.PERMISSION_GRANTED ){
@ -209,8 +188,10 @@ class AttachmentPicker(
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
}
val newUri = activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
.also { states.uriCameraImage = it }
val newUri =
activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values)
.also { states.uriCameraImage = it }
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, newUri)
@ -238,13 +219,16 @@ class AttachmentPicker(
///////////////////////////////////////////////////////////////////////////////
// Mastodon's custom thumbnail
fun openCustomThumbnail() {
if (!checkPermission(AfterPermission.CustomThumbnail)) return
fun openCustomThumbnail(pa: PostAttachment) {
states.customThumbnailTargetId = pa.attachment?.id?.toString()
if (!prPickCustomThumbnail.checkOrLaunch()) return
// SAFのIntentで開く
try {
arCustomThumbnail.launch(
intentGetContent(false, activity.getString(R.string.pick_images), arrayOf("image/*"))
intentGetContent(false,
activity.getString(R.string.pick_images),
arrayOf("image/*"))
)
} catch (ex: Throwable) {
log.trace(ex)

View File

@ -0,0 +1,33 @@
package jp.juggler.util
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Bundle
/**
* API 33 でget() deprecatedになる
*/
fun Bundle.getRaw(key: String) = get(key)
fun Intent.decodeRingtonePickerResult() =
extras?.getRaw(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) as? Uri
fun Bundle.intOrNull(key: String) =
when (val v = getRaw(key)) {
null -> null
is Number -> v.toInt()
is String -> v.toIntOrNull()
else -> null
}
fun Bundle.longOrNull(key: String) =
when (val v = getRaw(key)) {
null -> null
is Number -> v.toLong()
is String -> v.toLongOrNull()
else -> null
}
fun Intent.intOrNull(key: String) = extras?.intOrNull(key)
fun Intent.longOrNull(key: String) = extras?.longOrNull(key)

View File

@ -0,0 +1,162 @@
package jp.juggler.util
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.provider.Settings
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import jp.juggler.subwaytooter.R
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resumeWithException
/**
* ActivityResultLauncherを使ってパーミッション要求とその結果の処理を行う
*/
class PermissionRequester(
/**
* 必要なパーミッションのリスト
*/
val permissions: List<String>,
/**
* 要求が拒否された場合に表示するメッセージのID
*/
@StringRes val deniedId: Int,
/**
* なぜ権限が必要なのか説明するメッセージのID
* デフォルトは0でこの場合はメッセージを出さない
*/
@StringRes val rationalId: Int = 0,
/**
* 権限が与えられた際に処理を再開するラムダ
* - ラムダの引数にこのPermissionRequester自身が渡される
*/
val onGrant: (PermissionRequester) -> Unit,
) : ActivityResultCallback<Map<String, Boolean>> {
companion object {
private val log = LogCategory("PermissionRequester")
}
private var launcher: ActivityResultLauncher<Array<String>>? = null
private var getContext: (() -> Context?)? = null
private val activity
get() = getContext?.invoke() as? FragmentActivity
// ActivityのonCreate()から呼び出す
fun register(activity: FragmentActivity) {
getContext = { activity }
launcher = activity.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
this,
)
}
// FragmentのonCreate()から呼び出す
fun register(fragment: Fragment) {
getContext = { fragment.activity ?: fragment.context }
launcher = fragment.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
this,
)
}
/**
* 実行時権限が全て揃っているならtrueを返す
* そうでなければ権限の要求を行いfalseを返す
*/
fun checkOrLaunch(): Boolean {
val activity = activity ?: error("missing activity.")
val listNotGranted = permissions.filter {
PackageManager.PERMISSION_GRANTED !=
ContextCompat.checkSelfPermission(activity, it)
}
if (listNotGranted.isEmpty()) return true
launchMain {
try {
if (Build.VERSION.SDK_INT < 23) {
activity.showToast(true, deniedId)
return@launchMain
}
val shouldShowRational = listNotGranted.any {
shouldShowRequestPermissionRationale(activity, it)
}
if (shouldShowRational && rationalId != 0) {
suspendCancellableCoroutine { cont ->
AlertDialog.Builder(activity)
.setMessage(rationalId)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
if (cont.isActive) cont.resumeWith(Result.success(Unit))
}
.setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException())
}
.create()
.also { dialog -> cont.invokeOnCancellation { dialog.dismissSafe() } }
.show()
}
}
launcher!!.launch(listNotGranted.toTypedArray())
} catch (ex: Throwable) {
if (ex !is CancellationException) {
activity.showToast(ex, "can't request permissions.")
}
}
}
return false
}
/**
* 権限要求の結果を処理する
* @param result パーミッション名それが許可されているなら真のマップ
*/
override fun onActivityResult(result: Map<String, Boolean>?) {
try {
result ?: error("missing result.")
val listNotGranted = result.entries.filter { !it.value }.map { it.key }
if (listNotGranted.isEmpty()) {
// すべて許可されている
onGrant(this)
return
}
// 許可されなかった。
val activity = activity ?: error("missing activity.")
AlertDialog.Builder(activity)
.setMessage(deniedId)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.setting) { _, _ ->
openAppSetting(activity)
}
.show()
} catch (ex: Throwable) {
log.trace(ex, "can't handle result.")
}
}
private fun openAppSetting(activity: FragmentActivity) {
try {
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
"package:${activity.packageName}".toUri()
).let { activity.startActivity(it) }
} catch (ex: Throwable) {
activity.showToast(ex, "openAppSetting failed.")
}
}
}

View File

@ -1,5 +1,6 @@
package jp.juggler.util
import android.app.Activity
import android.content.*
import android.content.res.ColorStateList
import android.content.res.TypedArray
@ -19,15 +20,14 @@ import android.util.SparseArray
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import jp.juggler.subwaytooter.R
import java.util.*
object UiUtils {
@ -95,7 +95,7 @@ fun createRoundDrawable(
radius: Float,
fillColor: Int? = null,
strokeColor: Int? = null,
strokeWidth: Int = 4
strokeWidth: Int = 4,
) =
GradientDrawable().apply {
cornerRadius = radius
@ -113,7 +113,7 @@ fun getAdaptiveRippleDrawableRound(
context: Context,
normalColor: Int,
pressedColor: Int,
roundNormal: Boolean = false
roundNormal: Boolean = false,
): Drawable {
val dp6 = context.resources.displayMetrics.density * 6f
return if (roundNormal) {
@ -137,7 +137,7 @@ fun getAdaptiveRippleDrawableRound(
private class ColorFilterCacheValue(
val filter: ColorFilter,
var lastUsed: Long
var lastUsed: Long,
)
private val colorFilterCache = SparseArray<ColorFilterCacheValue>()
@ -174,16 +174,16 @@ private fun createColorFilter(rgb: Int): ColorFilter {
private class ColoredDrawableCacheKey(
val drawableId: Int,
val rgb: Int,
val alpha: Int
val alpha: Int,
) {
override fun equals(other: Any?): Boolean {
return this === other || (
other is ColoredDrawableCacheKey &&
drawableId == other.drawableId &&
rgb == other.rgb &&
alpha == other.alpha
)
other is ColoredDrawableCacheKey &&
drawableId == other.drawableId &&
rgb == other.rgb &&
alpha == other.alpha
)
}
override fun hashCode(): Int {
@ -193,7 +193,7 @@ private class ColoredDrawableCacheKey(
private class ColoredDrawableCacheValue(
val drawable: Drawable,
var lastUsed: Long
var lastUsed: Long,
)
private val coloredDrawableCache = HashMap<ColoredDrawableCacheKey, ColoredDrawableCacheValue>()
@ -203,7 +203,7 @@ fun createColoredDrawable(
context: Context,
drawableId: Int,
color: Int,
alphaMultiplier: Float
alphaMultiplier: Float,
): Drawable {
val rgb = (color and 0xffffff) or Color.BLACK
val alpha = if (alphaMultiplier >= 1f) {
@ -248,7 +248,7 @@ fun setIconDrawableId(
imageView: ImageView,
drawableId: Int,
color: Int? = null,
alphaMultiplier: Float
alphaMultiplier: Float,
) {
if (color == null) {
// ImageViewにアイコンを設定する。デフォルトの色
@ -310,14 +310,14 @@ fun DialogInterface.dismissSafe() {
}
class CustomTextWatcher(
val callback: () -> Unit
val callback: () -> Unit,
) : TextWatcher {
override fun beforeTextChanged(
s: CharSequence,
start: Int,
count: Int,
after: Int
after: Int,
) {
}
@ -356,34 +356,36 @@ var View.isEnabledAlpha: Boolean
/////////////////////////////////////////////////
class ActivityResultHandler<A : ComponentActivity>(
val callback: A.(ActivityResult?) -> Unit
class ActivityResultHandler(
private val log: LogCategory,
private val callback: (ActivityResult) -> Unit,
) {
private lateinit var log: LogCategory
private lateinit var context: Context
private lateinit var launcher: ActivityResultLauncher<Intent>
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.launch(intent, options)
(launcher ?: error("ActivityResultHandler not registered."))
.launch(intent, options)
} catch (ex: Throwable) {
log.e(ex, "launch failed")
context.showToast(ex, "activity launch failed.")
context?.showToast(ex, "activity launch failed.")
}
// onCreate時に呼び出す
fun register(a: A, log: LogCategory) {
this.log = log
this.context = a.applicationContext
fun register(a: FragmentActivity) {
getContext = { a.applicationContext }
this.launcher = a.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { callback(a, it) }
) { callback(it) }
}
}
@Suppress("unused")
fun <A : ComponentActivity> A.activityResultHandler(callback: A.(ActivityResult?) -> Unit) =
ActivityResultHandler(callback)
val AppCompatActivity.isLiveActivity: Boolean
get() = !(isFinishing || isDestroyed)
val ActivityResult.isNotOk
get() = Activity.RESULT_OK != resultCode