From c1afdbc0abfdbe1e43c703d65ca16b0b3a0c551c Mon Sep 17 00:00:00 2001 From: tateisu Date: Sat, 6 Aug 2022 07:52:31 +0900 Subject: [PATCH] =?UTF-8?q?requestPermissions=E3=82=92ActivityResultContra?= =?UTF-8?q?cts.RequestMultiplePermissions=E3=81=AB=E7=A7=BB=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/jp/juggler/apng/GifDecoder.kt | 2 +- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 2 +- .../juggler/subwaytooter/ActAccountSetting.kt | 141 +++++++-------- .../jp/juggler/subwaytooter/ActAppSetting.kt | 47 +++-- .../subwaytooter/ActColumnCustomize.kt | 15 +- .../subwaytooter/ActHighlightWordEdit.kt | 18 +- .../subwaytooter/ActHighlightWordList.kt | 8 +- .../juggler/subwaytooter/ActLanguageFilter.kt | 17 +- .../java/jp/juggler/subwaytooter/ActMain.kt | 129 +++++++------- .../jp/juggler/subwaytooter/ActMediaViewer.kt | 46 +---- .../jp/juggler/subwaytooter/ActNickname.kt | 14 +- .../java/jp/juggler/subwaytooter/ActPost.kt | 45 ++--- .../subwaytooter/actpost/ActPostAttachment.kt | 29 ++-- .../subwaytooter/util/AttachmentPicker.kt | 142 +++++++-------- app/src/main/java/jp/juggler/util/Compat.kt | 33 ++++ .../jp/juggler/util/PermissionRequester.kt | 162 ++++++++++++++++++ app/src/main/java/jp/juggler/util/UiUtils.kt | 64 +++---- 18 files changed, 502 insertions(+), 415 deletions(-) create mode 100644 app/src/main/java/jp/juggler/util/Compat.kt create mode 100644 app/src/main/java/jp/juggler/util/PermissionRequester.kt diff --git a/apng/src/main/java/jp/juggler/apng/GifDecoder.kt b/apng/src/main/java/jp/juggler/apng/GifDecoder.kt index f65d7a98..0970eb47 100644 --- a/apng/src/main/java/jp/juggler/apng/GifDecoder.kt +++ b/apng/src/main/java/jp/juggler/apng/GifDecoder.kt @@ -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) { diff --git a/app/build.gradle b/app/build.gradle index 205d3796..e6448a66 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ccd1ddb..4118255f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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"> + - 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, - 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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt index 9d7286a2..5d563d0a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt @@ -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() + internal val list = ArrayList() init { for (a in SavedAccount.loadAccountList(this@ActAppSetting)) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt b/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt index 38d256f0..6b27e0c5 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt @@ -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() diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.kt b/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.kt index c547fb08..50e1a363 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.kt @@ -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() diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordList.kt b/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordList.kt index 1352f5f0..6defa486 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordList.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordList.kt @@ -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? = 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() diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActLanguageFilter.kt b/app/src/main/java/jp/juggler/subwaytooter/ActLanguageFilter.kt index c306d88e..3096b2ab 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActLanguageFilter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActLanguageFilter.kt @@ -117,21 +117,18 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener { private val languageList = ArrayList() 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() diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt index 59257c93..deaf04ed 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt index d3ffed11..55f99ab3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt @@ -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, - 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()) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActNickname.kt b/app/src/main/java/jp/juggler/subwaytooter/ActNickname.kt index d5bd0e90..a26acdee 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActNickname.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActNickname.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index feb81aa4..3f5137b3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -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, - 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() { 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 ba2db687..91179bbc 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt @@ -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) } 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 e4684a5a..010f86f9 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt @@ -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(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, 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) diff --git a/app/src/main/java/jp/juggler/util/Compat.kt b/app/src/main/java/jp/juggler/util/Compat.kt new file mode 100644 index 00000000..879703c7 --- /dev/null +++ b/app/src/main/java/jp/juggler/util/Compat.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/util/PermissionRequester.kt b/app/src/main/java/jp/juggler/util/PermissionRequester.kt new file mode 100644 index 00000000..e2f600c5 --- /dev/null +++ b/app/src/main/java/jp/juggler/util/PermissionRequester.kt @@ -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, + + /** + * 要求が拒否された場合に表示するメッセージのID + */ + @StringRes val deniedId: Int, + + /** + * なぜ権限が必要なのか説明するメッセージのID。 + * デフォルトは0で、この場合はメッセージを出さない。 + */ + @StringRes val rationalId: Int = 0, + + /** + * 権限が与えられた際に処理を再開するラムダ + * - ラムダの引数にこのPermissionRequester自身が渡される + */ + val onGrant: (PermissionRequester) -> Unit, +) : ActivityResultCallback> { + companion object { + private val log = LogCategory("PermissionRequester") + } + + private var launcher: ActivityResultLauncher>? = 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?) { + 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.") + } + } +} diff --git a/app/src/main/java/jp/juggler/util/UiUtils.kt b/app/src/main/java/jp/juggler/util/UiUtils.kt index 934bc319..d380f356 100644 --- a/app/src/main/java/jp/juggler/util/UiUtils.kt +++ b/app/src/main/java/jp/juggler/util/UiUtils.kt @@ -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() @@ -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() @@ -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( - 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 + 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.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.activityResultHandler(callback: A.(ActivityResult?) -> Unit) = - ActivityResultHandler(callback) - val AppCompatActivity.isLiveActivity: Boolean get() = !(isFinishing || isDestroyed) + +val ActivityResult.isNotOk + get() = Activity.RESULT_OK != resultCode