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は最後まで読まないとフレーム数が分からない // GIFは最後まで読まないとフレーム数が分からない
if(frames.isEmpty()) throw error("there is no frame.") if(frames.isEmpty()) error("there is no frame.")
callback.onGifHeader(header) callback.onGifHeader(header)
callback.onGifAnimationInfo(header, animationControl) callback.onGifAnimationInfo(header, animationControl)
for(frame in frames) { for(frame in frames) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import android.content.ClipData
import android.content.ClipDescription import android.content.ClipDescription
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.* import android.graphics.*
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -16,8 +15,6 @@ import android.os.SystemClock
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import androidx.appcompat.app.AppCompatActivity 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.*
import com.google.android.exoplayer2.Player.TimelineChangeReason import com.google.android.exoplayer2.Player.TimelineChangeReason
import com.google.android.exoplayer2.source.LoadEventInfo 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 DOWNLOAD_REPEAT_EXPIRE = 3000L
internal const val short_limit = 5000L internal const val short_limit = 5000L
private const val PERMISSION_REQUEST_CODE = 1
internal const val EXTRA_IDX = "idx" internal const val EXTRA_IDX = "idx"
internal const val EXTRA_DATA = "data" internal const val EXTRA_DATA = "data"
internal const val EXTRA_SERVICE_TYPE = "serviceType" 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) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
@ -247,6 +247,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
prDownload.register(this)
requestWindowFeature(Window.FEATURE_NO_TITLE) requestWindowFeature(Window.FEATURE_NO_TITLE)
App1.setActivityTheme(this, noActionBar = true, forceDark = true) App1.setActivityTheme(this, noActionBar = true, forceDark = true)
@ -665,7 +666,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
} }
private fun download(ta: TootAttachmentLike) { private fun download(ta: TootAttachmentLike) {
if (!checkPermission()) return if (!prDownload.checkOrLaunch()) return
val downLoadManager: DownloadManager = systemService(this) val downLoadManager: DownloadManager = systemService(this)
?: error("missing DownloadManager system service") ?: 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() { private fun mediaBackgroundDialog() {
val ad = ActionsDialog() val ad = ActionsDialog()
for (k in MediaBackgroundDrawable.Kind.values()) { for (k in MediaBackgroundDrawable.Kind.values()) {

View File

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

View File

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

View File

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

View File

@ -3,21 +3,16 @@ package jp.juggler.subwaytooter.util
import android.Manifest import android.Manifest
import android.content.ContentValues import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.dialog.ActionsDialog import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.kJson import jp.juggler.subwaytooter.kJson
import jp.juggler.util.* import jp.juggler.util.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import java.util.*
import kotlinx.serialization.Serializable
class AttachmentPicker( class AttachmentPicker(
val activity: AppCompatActivity, val activity: AppCompatActivity,
@ -25,14 +20,15 @@ class AttachmentPicker(
) { ) {
companion object { companion object {
private val log = LogCategory("AttachmentPicker") private val log = LogCategory("AttachmentPicker")
private val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
private const val PERMISSION_REQUEST_CODE = 1 private const val PERMISSION_REQUEST_CODE = 1
} }
// callback after media selected // callback after media selected
interface Callback { interface Callback {
fun onPickAttachment(uri: Uri, mimeType: String? = null) 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 // actions after permission granted
@ -44,7 +40,7 @@ class AttachmentPicker(
@Serializable(with = UriSerializer::class) @Serializable(with = UriSerializer::class)
var uriCameraImage: Uri? = null, var uriCameraImage: Uri? = null,
var afterPermission: AfterPermission = AfterPermission.Attachment, var customThumbnailTargetId: String? = null,
) )
private var states = States() private var states = States()
@ -52,49 +48,66 @@ class AttachmentPicker(
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
// activity result handlers // activity result handlers
private val arAttachmentChooser = activity.activityResultHandler { ar -> private val arAttachmentChooser = ActivityResultHandler(log) { r ->
if (ar?.resultCode == AppCompatActivity.RESULT_OK) { if (r.isNotOk) return@ActivityResultHandler
ar.data?.handleGetContentResult(contentResolver)?.pickAll() r.data?.handleGetContentResult(activity.contentResolver)?.pickAll()
}
} }
private val arCamera = activity.activityResultHandler { ar -> private val arCamera = ActivityResultHandler(log) { r ->
if (ar?.resultCode == AppCompatActivity.RESULT_OK) { if (r.isNotOk) {
// 画像のURL
when (val uri = ar.data?.data ?: states.uriCameraImage) {
null -> showToast(false, "missing image uri")
else -> callback.onPickAttachment(uri)
}
} else {
// 失敗したら DBからデータを削除 // 失敗したら DBからデータを削除
states.uriCameraImage?.let { uri -> states.uriCameraImage?.let { uri ->
contentResolver.delete(uri, null, null) activity.contentResolver.delete(uri, null, null)
states.uriCameraImage = 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 -> private val arCapture = ActivityResultHandler(log) { r ->
if (ar?.resultCode == AppCompatActivity.RESULT_OK) { if (r.isNotOk) return@ActivityResultHandler
ar.data?.data?.let { callback.onPickAttachment(it) } r.data?.data?.let { callback.onPickAttachment(it) }
}
} }
private val arCustomThumbnail = activity.activityResultHandler { ar -> private val arCustomThumbnail = ActivityResultHandler(log) { r ->
if (ar?.resultCode == AppCompatActivity.RESULT_OK) { if (r.isNotOk) return@ActivityResultHandler
ar.data r.data
?.handleGetContentResult(contentResolver) ?.handleGetContentResult(activity.contentResolver)
?.firstOrNull() ?.firstOrNull()
?.let { callback.onPickCustomThumbnail(it) } ?.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 { init {
// must register all ARHs before onStart // must register all ARHs before onStart
arAttachmentChooser.register(activity, log) prPickAttachment.register(activity)
arCamera.register(activity, log) prPickCustomThumbnail.register(activity)
arCapture.register(activity, log) arAttachmentChooser.register(activity)
arCustomThumbnail.register(activity, log) arCamera.register(activity)
arCapture.register(activity)
arCustomThumbnail.register(activity)
} }
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
@ -107,53 +120,19 @@ class AttachmentPicker(
fun encodeState(): String { fun encodeState(): String {
val encoded = kJson.encodeToString(states) val encoded = kJson.encodeToString(states)
val decoded = kJson.decodeFromString<States>(encoded) val decoded = kJson.decodeFromString<States>(encoded)
log.d("encodeState: ${decoded.uriCameraImage},${decoded.afterPermission},$encoded") log.d("encodeState: ${decoded.uriCameraImage},$encoded")
return encoded return encoded
} }
fun restoreState(encoded: String) { fun restoreState(encoded: String) {
states = kJson.decodeFromString(encoded) states = kJson.decodeFromString(encoded)
log.d("restoreState: ${states.uriCameraImage},${states.afterPermission},$encoded") log.d("restoreState: ${states.uriCameraImage},$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()
}
} }
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
fun openPicker() { fun openPicker() {
if (!checkPermission(AfterPermission.Attachment)) return if (!prPickAttachment.checkOrLaunch()) return
// permissionCheck = ContextCompat.checkSelfPermission( this, Manifest.permission.CAMERA ); // permissionCheck = ContextCompat.checkSelfPermission( this, Manifest.permission.CAMERA );
// if( permissionCheck != PackageManager.PERMISSION_GRANTED ){ // if( permissionCheck != PackageManager.PERMISSION_GRANTED ){
@ -209,8 +188,10 @@ class AttachmentPicker(
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
} }
val newUri = activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) val newUri =
.also { states.uriCameraImage = it } activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values)
.also { states.uriCameraImage = it }
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, newUri) putExtra(MediaStore.EXTRA_OUTPUT, newUri)
@ -238,13 +219,16 @@ class AttachmentPicker(
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Mastodon's custom thumbnail // Mastodon's custom thumbnail
fun openCustomThumbnail() { fun openCustomThumbnail(pa: PostAttachment) {
if (!checkPermission(AfterPermission.CustomThumbnail)) return states.customThumbnailTargetId = pa.attachment?.id?.toString()
if (!prPickCustomThumbnail.checkOrLaunch()) return
// SAFのIntentで開く // SAFのIntentで開く
try { try {
arCustomThumbnail.launch( 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) { } catch (ex: Throwable) {
log.trace(ex) 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 package jp.juggler.util
import android.app.Activity
import android.content.* import android.content.*
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.TypedArray import android.content.res.TypedArray
@ -19,15 +20,14 @@ import android.util.SparseArray
import android.view.View import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import java.util.*
object UiUtils { object UiUtils {
@ -95,7 +95,7 @@ fun createRoundDrawable(
radius: Float, radius: Float,
fillColor: Int? = null, fillColor: Int? = null,
strokeColor: Int? = null, strokeColor: Int? = null,
strokeWidth: Int = 4 strokeWidth: Int = 4,
) = ) =
GradientDrawable().apply { GradientDrawable().apply {
cornerRadius = radius cornerRadius = radius
@ -113,7 +113,7 @@ fun getAdaptiveRippleDrawableRound(
context: Context, context: Context,
normalColor: Int, normalColor: Int,
pressedColor: Int, pressedColor: Int,
roundNormal: Boolean = false roundNormal: Boolean = false,
): Drawable { ): Drawable {
val dp6 = context.resources.displayMetrics.density * 6f val dp6 = context.resources.displayMetrics.density * 6f
return if (roundNormal) { return if (roundNormal) {
@ -137,7 +137,7 @@ fun getAdaptiveRippleDrawableRound(
private class ColorFilterCacheValue( private class ColorFilterCacheValue(
val filter: ColorFilter, val filter: ColorFilter,
var lastUsed: Long var lastUsed: Long,
) )
private val colorFilterCache = SparseArray<ColorFilterCacheValue>() private val colorFilterCache = SparseArray<ColorFilterCacheValue>()
@ -174,16 +174,16 @@ private fun createColorFilter(rgb: Int): ColorFilter {
private class ColoredDrawableCacheKey( private class ColoredDrawableCacheKey(
val drawableId: Int, val drawableId: Int,
val rgb: Int, val rgb: Int,
val alpha: Int val alpha: Int,
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return this === other || ( return this === other || (
other is ColoredDrawableCacheKey && other is ColoredDrawableCacheKey &&
drawableId == other.drawableId && drawableId == other.drawableId &&
rgb == other.rgb && rgb == other.rgb &&
alpha == other.alpha alpha == other.alpha
) )
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -193,7 +193,7 @@ private class ColoredDrawableCacheKey(
private class ColoredDrawableCacheValue( private class ColoredDrawableCacheValue(
val drawable: Drawable, val drawable: Drawable,
var lastUsed: Long var lastUsed: Long,
) )
private val coloredDrawableCache = HashMap<ColoredDrawableCacheKey, ColoredDrawableCacheValue>() private val coloredDrawableCache = HashMap<ColoredDrawableCacheKey, ColoredDrawableCacheValue>()
@ -203,7 +203,7 @@ fun createColoredDrawable(
context: Context, context: Context,
drawableId: Int, drawableId: Int,
color: Int, color: Int,
alphaMultiplier: Float alphaMultiplier: Float,
): Drawable { ): Drawable {
val rgb = (color and 0xffffff) or Color.BLACK val rgb = (color and 0xffffff) or Color.BLACK
val alpha = if (alphaMultiplier >= 1f) { val alpha = if (alphaMultiplier >= 1f) {
@ -248,7 +248,7 @@ fun setIconDrawableId(
imageView: ImageView, imageView: ImageView,
drawableId: Int, drawableId: Int,
color: Int? = null, color: Int? = null,
alphaMultiplier: Float alphaMultiplier: Float,
) { ) {
if (color == null) { if (color == null) {
// ImageViewにアイコンを設定する。デフォルトの色 // ImageViewにアイコンを設定する。デフォルトの色
@ -310,14 +310,14 @@ fun DialogInterface.dismissSafe() {
} }
class CustomTextWatcher( class CustomTextWatcher(
val callback: () -> Unit val callback: () -> Unit,
) : TextWatcher { ) : TextWatcher {
override fun beforeTextChanged( override fun beforeTextChanged(
s: CharSequence, s: CharSequence,
start: Int, start: Int,
count: Int, count: Int,
after: Int after: Int,
) { ) {
} }
@ -356,34 +356,36 @@ var View.isEnabledAlpha: Boolean
///////////////////////////////////////////////// /////////////////////////////////////////////////
class ActivityResultHandler<A : ComponentActivity>( class ActivityResultHandler(
val callback: A.(ActivityResult?) -> Unit private val log: LogCategory,
private val callback: (ActivityResult) -> Unit,
) { ) {
private lateinit var log: LogCategory private var launcher: ActivityResultLauncher<Intent>? = null
private lateinit var context: Context private var getContext: (() -> Context?)? = null
private lateinit var launcher: ActivityResultLauncher<Intent>
private val context
get() = getContext?.invoke()
// startForActivityResultの代わりに呼び出す // startForActivityResultの代わりに呼び出す
fun launch(intent: Intent, options: ActivityOptionsCompat? = null) = try { fun launch(intent: Intent, options: ActivityOptionsCompat? = null) = try {
launcher.launch(intent, options) (launcher ?: error("ActivityResultHandler not registered."))
.launch(intent, options)
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "launch failed") log.e(ex, "launch failed")
context.showToast(ex, "activity launch failed.") context?.showToast(ex, "activity launch failed.")
} }
// onCreate時に呼び出す // onCreate時に呼び出す
fun register(a: A, log: LogCategory) { fun register(a: FragmentActivity) {
this.log = log getContext = { a.applicationContext }
this.context = a.applicationContext
this.launcher = a.registerForActivityResult( this.launcher = a.registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { callback(a, it) } ) { callback(it) }
} }
} }
@Suppress("unused")
fun <A : ComponentActivity> A.activityResultHandler(callback: A.(ActivityResult?) -> Unit) =
ActivityResultHandler(callback)
val AppCompatActivity.isLiveActivity: Boolean val AppCompatActivity.isLiveActivity: Boolean
get() = !(isFinishing || isDestroyed) get() = !(isFinishing || isDestroyed)
val ActivityResult.isNotOk
get() = Activity.RESULT_OK != resultCode