detektで指摘された問題の対応

This commit is contained in:
tateisu 2022-07-20 13:27:19 +09:00
parent ab3b73a1fa
commit b148c6dd03
44 changed files with 682 additions and 947 deletions

View File

@ -1513,13 +1513,11 @@ class ActAccountSetting : AppCompatActivity(),
return
}
launchMain {
runWithProgress(
"preparing image",
{ createOpener(uri, mimeType) },
{ updateCredential(propName, it) }
)
}
launchProgress(
"preparing image",
doInBackground = { createOpener(uri, mimeType) },
afterProc = { updateCredential(propName, it) }
)
}
private fun updatePushSubscription(force: Boolean) {

View File

@ -792,52 +792,50 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
@Suppress("BlockingMethodInNonBlockingContext")
fun exportAppData() {
val activity = this
launchMain {
runWithProgress(
"export app data",
doInBackground = {
val cacheDir = activity.cacheDir
launchProgress(
"export app data",
doInBackground = {
val cacheDir = activity.cacheDir
cacheDir.mkdir()
cacheDir.mkdir()
val file = File(
cacheDir,
"SubwayTooter.${android.os.Process.myPid()}.${android.os.Process.myTid()}.zip"
)
val file = File(
cacheDir,
"SubwayTooter.${android.os.Process.myPid()}.${android.os.Process.myTid()}.zip"
)
// ZipOutputStreamオブジェクトの作成
ZipOutputStream(FileOutputStream(file)).use { zipStream ->
// ZipOutputStreamオブジェクトの作成
ZipOutputStream(FileOutputStream(file)).use { zipStream ->
// アプリデータjson
zipStream.putNextEntry(ZipEntry("AppData.json"))
try {
val jw = JsonWriter(OutputStreamWriter(zipStream, "UTF-8"))
AppDataExporter.encodeAppData(activity, jw)
jw.flush()
} finally {
zipStream.closeEntry()
}
// カラム背景画像
val appState = App1.getAppState(activity)
for (column in appState.columnList) {
AppDataExporter.saveBackgroundImage(activity, zipStream, column)
}
// アプリデータjson
zipStream.putNextEntry(ZipEntry("AppData.json"))
try {
val jw = JsonWriter(OutputStreamWriter(zipStream, "UTF-8"))
AppDataExporter.encodeAppData(activity, jw)
jw.flush()
} finally {
zipStream.closeEntry()
}
file
},
afterProc = {
val uri = FileProvider.getUriForFile(activity, App1.FILE_PROVIDER_AUTHORITY, it)
val intent = Intent(Intent.ACTION_SEND)
intent.type = contentResolver.getType(uri)
intent.putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter app data")
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
arNoop.launch(intent)
// カラム背景画像
val appState = App1.getAppState(activity)
for (column in appState.columnList) {
AppDataExporter.saveBackgroundImage(activity, zipStream, column)
}
}
)
}
file
},
afterProc = {
val uri = FileProvider.getUriForFile(activity, App1.FILE_PROVIDER_AUTHORITY, it)
val intent = Intent(Intent.ACTION_SEND)
intent.type = contentResolver.getType(uri)
intent.putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter app data")
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
arNoop.launch(intent)
}
)
}
// open data picker
@ -1109,7 +1107,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
String.format(format, hours, minutes, tz.id, tz.displayName)
}
}
if (null == list.find { it.caption == caption }) {
if (list.none { it.caption == caption }) {
list.add(Item(id, caption, tz.rawOffset))
}
}

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.*
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.util.AsyncActivity
import jp.juggler.util.LogCategory
import jp.juggler.util.asciiPattern
@ -44,7 +45,7 @@ class ActDrawableList : AsyncActivity(), CoroutineScope {
val reSkipName =
"""^(abc_|avd_|btn_checkbox_|btn_radio_|googleg_|ic_keyboard_arrow_|ic_menu_arrow_|notification_|common_|emj_|cpv_|design_|exo_|mtrl_|ic_mtrl_)"""
.asciiPattern()
val list = withContext(Dispatchers.IO) {
val list = withContext(appDispatchers.io) {
R.drawable::class.java.fields
.mapNotNull {
val id = it.get(null) as? Int ?: return@mapNotNull null

View File

@ -22,7 +22,6 @@ import org.jetbrains.anko.textColor
import java.io.File
import java.io.FileOutputStream
import java.util.*
import kotlin.collections.ArrayList
class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
@ -212,7 +211,7 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
}
}
if (null == languageList.find { it.code == TootStatus.LANGUAGE_CODE_DEFAULT }) {
if (languageList.none { it.code == TootStatus.LANGUAGE_CODE_DEFAULT }) {
languageList.add(MyItem(TootStatus.LANGUAGE_CODE_DEFAULT, true))
}
@ -399,47 +398,45 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
@Suppress("BlockingMethodInNonBlockingContext")
private fun export() {
launchMain {
runWithProgress(
"export language filter",
doInBackground = {
val data = JsonObject().apply {
for (item in languageList) {
put(item.code, item.allow)
}
launchProgress(
"export language filter",
doInBackground = {
val data = JsonObject().apply {
for (item in languageList) {
put(item.code, item.allow)
}
.toString()
.encodeUTF8()
val cacheDir = this@ActLanguageFilter.cacheDir
cacheDir.mkdir()
val file = File(
cacheDir,
"SubwayTooter-language-filter.${Process.myPid()}.${Process.myTid()}.json"
)
FileOutputStream(file).use {
it.write(data)
}
file
},
afterProc = {
val uri = FileProvider.getUriForFile(
this@ActLanguageFilter,
App1.FILE_PROVIDER_AUTHORITY,
it
)
val intent = Intent(Intent.ACTION_SEND)
intent.type = contentResolver.getType(uri)
intent.putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter language filter data")
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
arExport.launch(intent)
}
)
}
.toString()
.encodeUTF8()
val cacheDir = this@ActLanguageFilter.cacheDir
cacheDir.mkdir()
val file = File(
cacheDir,
"SubwayTooter-language-filter.${Process.myPid()}.${Process.myTid()}.json"
)
FileOutputStream(file).use {
it.write(data)
}
file
},
afterProc = {
val uri = FileProvider.getUriForFile(
this@ActLanguageFilter,
App1.FILE_PROVIDER_AUTHORITY,
it
)
val intent = Intent(Intent.ACTION_SEND)
intent.type = contentResolver.getType(uri)
intent.putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter language filter data")
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
arExport.launch(intent)
}
)
}
private fun import() {
@ -448,22 +445,20 @@ class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
@Suppress("BlockingMethodInNonBlockingContext")
private fun import2(uri: Uri) {
launchMain {
runWithProgress(
"import language filter",
doInBackground = {
log.d("import2 type=${contentResolver.getType(uri)}")
try {
contentResolver.openInputStream(uri)!!.use {
it.readBytes().decodeUTF8().decodeJsonObject()
}
} catch (ex: Throwable) {
showToast(ex, "openInputStream failed.")
null
launchProgress(
"import language filter",
doInBackground = {
log.d("import2 type=${contentResolver.getType(uri)}")
try {
contentResolver.openInputStream(uri)!!.use {
it.readBytes().decodeUTF8().decodeJsonObject()
}
},
afterProc = { load(it) }
)
}
} catch (ex: Throwable) {
showToast(ex, "openInputStream failed.")
null
}
},
afterProc = { load(it) }
)
}
}

View File

@ -333,7 +333,7 @@ suspend fun Context.accountListWithFilter(
): MutableList<SavedAccount>? {
var resultList: MutableList<SavedAccount>? = null
runApiTask { client ->
coroutineScope {
supervisorScope {
resultList = SavedAccount.loadAccountList(this@accountListWithFilter)
.map {
async {

View File

@ -5,16 +5,18 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.addColumn
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootTag
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.dialog.ActionsDialog
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.subwaytooter.util.openCustomTab
import jp.juggler.util.encodePercent
import jp.juggler.util.launchMain
import java.util.*
import jp.juggler.util.*
private val log = LogCategory("Action_Tag")
fun ActMain.longClickTootTag(pos: Int, accessInfo: SavedAccount, item: TootTag) {
tagTimelineFromAccount(
@ -27,6 +29,7 @@ fun ActMain.longClickTootTag(pos: Int, accessInfo: SavedAccount, item: TootTag)
// ハッシュタグへの操作を選択する
fun ActMain.tagDialog(
accessInfo: SavedAccount?,
pos: Int,
url: String,
host: Host,
@ -36,55 +39,84 @@ fun ActMain.tagDialog(
) {
val tagWithSharp = "#$tagWithoutSharp"
val d = ActionsDialog()
.addAction(getString(R.string.open_hashtag_column)) {
tagTimelineFromAccount(
pos,
url,
host,
tagWithoutSharp
)
}
launchMain {
try {
// https://mastodon.juggler.jp/@tateisu/101865456016473337
// 一時的に使えなくする
if (whoAcct != null) {
d.addAction(
AcctColor.getStringWithNickname(
this,
R.string.open_hashtag_from_account,
whoAcct
)
) {
tagTimelineFromAccount(
pos,
"https://${whoAcct.host?.ascii}/@${whoAcct.username}/tagged/${tagWithoutSharp.encodePercent()}",
host,
tagWithoutSharp,
whoAcct
)
val d = ActionsDialog()
.addAction(getString(R.string.open_hashtag_column)) {
tagTimelineFromAccount(
pos,
url,
host,
tagWithoutSharp
)
}
// https://mastodon.juggler.jp/@tateisu/101865456016473337
// 一時的に使えなくする
if (whoAcct != null) {
d.addAction(
AcctColor.getStringWithNickname(
this@tagDialog,
R.string.open_hashtag_from_account,
whoAcct
)
) {
tagTimelineFromAccount(
pos,
"https://${whoAcct.host?.ascii}/@${whoAcct.username}/tagged/${tagWithoutSharp.encodePercent()}",
host,
tagWithoutSharp,
whoAcct
)
}
}
d.addAction(getString(R.string.open_in_browser)) { openCustomTab(url) }
.addAction(getString(R.string.quote_hashtag_of,
tagWithSharp)) { openPost("$tagWithSharp ") }
if (tagList != null && tagList.size > 1) {
val sb = StringBuilder()
for (s in tagList) {
if (sb.isNotEmpty()) sb.append(' ')
sb.append(s)
}
val tagAll = sb.toString()
d.addAction(
getString(
R.string.quote_all_hashtag_of,
tagAll
)
) { openPost("$tagAll ") }
}
val ti = TootInstance.getCached(accessInfo)
if (ti != null && accessInfo?.isMisskey == false) {
val result = runApiTask(accessInfo) { client ->
client.request("/api/v1/tags/${tagWithoutSharp.encodePercent()}")
}
val following = when {
result == null || result.error != null -> null
else -> result.jsonObject?.boolean("following")
}
val toggle = following?.let { !it }
if (toggle != null) {
val toggleCaption = when (toggle) {
true -> R.string.follow_hashtag_of
else -> R.string.unfollow_hashtag_of
}
d.addAction(getString(toggleCaption, tagWithSharp)) {
followHashTag(accessInfo, tagWithoutSharp, toggle)
}
}
}
d.show(this@tagDialog, tagWithSharp)
} catch (ex: Throwable) {
log.trace(ex)
}
}
d.addAction(getString(R.string.open_in_browser)) { openCustomTab(url) }
.addAction(getString(R.string.quote_hashtag_of, tagWithSharp)) { openPost("$tagWithSharp ") }
if (tagList != null && tagList.size > 1) {
val sb = StringBuilder()
for (s in tagList) {
if (sb.isNotEmpty()) sb.append(' ')
sb.append(s)
}
val tagAll = sb.toString()
d.addAction(
getString(
R.string.quote_all_hashtag_of,
tagAll
)
) { openPost("$tagAll ") }
}
d.show(this, tagWithSharp)
}
// 検索カラムからハッシュタグを選んだ場合、カラムのアカウントでハッシュタグを開く
@ -189,3 +221,30 @@ fun ActMain.tagTimelineFromAccount(
dialog.show(this, "#$tagWithoutSharp")
}
fun ActMain.followHashTag(
accessInfo: SavedAccount,
tagWithoutSharp: String,
isSet: Boolean,
) {
launchMain {
runApiTask(accessInfo) { client ->
client.request(
"/api/v1/tags/${tagWithoutSharp.encodePercent()}/${if (isSet) "follow" else "unfollow"}",
"".toFormRequestBody().toPost()
)
}?.let { result ->
when (val error = result.error) {
// 成功時はTagオブジェクトが返るが、使っていない
null -> showToast(
false,
when {
isSet -> R.string.follow_succeeded
else -> R.string.unfollow_succeeded
}
)
else -> showToast(true, error)
}
}
}
}

View File

@ -4,7 +4,6 @@ import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.column.fireShowColumnHeader
import jp.juggler.subwaytooter.pref.PrefL
import jp.juggler.subwaytooter.table.SavedAccount
import java.util.*
// デフォルトの投稿先アカウントを探す。アカウント選択が必要な状況ならnull
val ActMain.currentPostTarget: SavedAccount?
@ -34,7 +33,7 @@ val ActMain.currentPostTarget: SavedAccount?
break
}
// 既出でなければ追加する
if (null == accounts.find { it == a }) accounts.add(a)
if (accounts.none { it == a }) accounts.add(a)
} catch (ignored: Throwable) {
}
}
@ -50,7 +49,7 @@ val ActMain.currentPostTarget: SavedAccount?
fun ActMain.reloadAccountSetting(
newAccounts: ArrayList<SavedAccount> = SavedAccount.loadAccountList(
this
)
),
) {
for (column in appState.columnList) {
val a = column.accessInfo

View File

@ -10,7 +10,10 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.appsetting.AppDataExporter
import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.notification.setImportProtector
import jp.juggler.util.*
import jp.juggler.util.LogCategory
import jp.juggler.util.runOnMainLooper
import jp.juggler.util.launchProgress
import jp.juggler.util.showToast
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@ -21,129 +24,127 @@ private val log = LogCategory("ActMainImportAppData")
@WorkerThread
fun ActMain.importAppData(uri: Uri) {
launchMain {
// remove all columns
phoneOnly { env -> env.pager.adapter = null }
// remove all columns
phoneOnly { env -> env.pager.adapter = null }
appState.editColumnList(save = false) { list ->
list.forEach { it.dispose() }
list.clear()
}
appState.editColumnList(save = false) { list ->
list.forEach { it.dispose() }
list.clear()
}
phoneTab(
{ env -> env.pager.adapter = env.pagerAdapter },
{ env -> resizeColumnWidth(env) }
)
phoneTab(
{ env -> env.pager.adapter = env.pagerAdapter },
{ env -> resizeColumnWidth(env) }
)
updateColumnStrip()
updateColumnStrip()
runWithProgress(
"importing app data",
doInBackground = { progress ->
fun setProgressMessage(sv: String) = runOnMainLooper { progress.setMessageEx(sv) }
launchProgress(
"importing app data",
doInBackground = { progress ->
fun setProgressMessage(sv: String) = runOnMainLooper { progress.setMessageEx(sv) }
setProgressMessage("import data to local storage...")
setProgressMessage("import data to local storage...")
// アプリ内領域に一時ファイルを作ってコピーする
val cacheDir = cacheDir
cacheDir.mkdir()
val file = File(cacheDir, "SubwayTooter.${Process.myPid()}.${Process.myTid()}.tmp")
val copyBytes = contentResolver.openInputStream(uri)?.let { inStream ->
FileOutputStream(file).use { outStream ->
inStream.copyTo(outStream)
}
}
if (copyBytes == null) {
showToast(true, "openInputStream failed.")
return@runWithProgress null
// アプリ内領域に一時ファイルを作ってコピーする
val cacheDir = cacheDir
cacheDir.mkdir()
val file = File(cacheDir, "SubwayTooter.${Process.myPid()}.${Process.myTid()}.tmp")
val copyBytes = contentResolver.openInputStream(uri)?.let { inStream ->
FileOutputStream(file).use { outStream ->
inStream.copyTo(outStream)
}
}
if (copyBytes == null) {
showToast(true, "openInputStream failed.")
return@launchProgress null
}
// 通知サービスを止める
setProgressMessage("syncing notification poller…")
setImportProtector(this@importAppData, true)
// 通知サービスを止める
setProgressMessage("syncing notification poller…")
setImportProtector(this@importAppData, true)
// データを読み込む
setProgressMessage("reading app data...")
var newColumnList: ArrayList<Column>? = null
var zipEntryCount = 0
try {
ZipInputStream(FileInputStream(file)).use { zipStream ->
while (true) {
val entry = zipStream.nextEntry ?: break
++zipEntryCount
try {
//
val entryName = entry.name
if (entryName.endsWith(".json")) {
newColumnList = AppDataExporter.decodeAppData(
this@importAppData,
JsonReader(InputStreamReader(zipStream, "UTF-8"))
)
continue
}
if (AppDataExporter.restoreBackgroundImage(
this@importAppData,
newColumnList,
zipStream,
entryName
)
) {
continue
}
} finally {
zipStream.closeEntry()
// データを読み込む
setProgressMessage("reading app data...")
var newColumnList: ArrayList<Column>? = null
var zipEntryCount = 0
try {
ZipInputStream(FileInputStream(file)).use { zipStream ->
while (true) {
val entry = zipStream.nextEntry ?: break
++zipEntryCount
try {
//
val entryName = entry.name
if (entryName.endsWith(".json")) {
newColumnList = AppDataExporter.decodeAppData(
this@importAppData,
JsonReader(InputStreamReader(zipStream, "UTF-8"))
)
continue
}
if (AppDataExporter.restoreBackgroundImage(
this@importAppData,
newColumnList,
zipStream,
entryName
)
) {
continue
}
} finally {
zipStream.closeEntry()
}
}
} catch (ex: Throwable) {
log.trace(ex)
if (zipEntryCount != 0) {
showToast(ex, "importAppData failed.")
}
}
// zipではなかった場合、zipEntryがない状態になる。例外はPH-1では出なかったが、出ても問題ないようにする。
if (zipEntryCount == 0) {
InputStreamReader(FileInputStream(file), "UTF-8").use { inStream ->
newColumnList =
AppDataExporter.decodeAppData(this@importAppData, JsonReader(inStream))
}
} catch (ex: Throwable) {
log.trace(ex)
if (zipEntryCount != 0) {
showToast(ex, "importAppData failed.")
}
newColumnList
},
afterProc = {
// cancelled.
if (it == null) return@runWithProgress
try {
phoneOnly { env -> env.pager.adapter = null }
appState.editColumnList { list ->
list.clear()
list.addAll(it)
}
phoneTab(
{ env -> env.pager.adapter = env.pagerAdapter },
{ env -> resizeColumnWidth(env) }
)
updateColumnStrip()
} finally {
// 通知サービスをリスタート
setImportProtector(this@importAppData, false)
}
showToast(true, R.string.import_completed_please_restart_app)
finish()
},
preProc = {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
},
postProc = {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
)
}
// zipではなかった場合、zipEntryがない状態になる。例外はPH-1では出なかったが、出ても問題ないようにする。
if (zipEntryCount == 0) {
InputStreamReader(FileInputStream(file), "UTF-8").use { inStream ->
newColumnList =
AppDataExporter.decodeAppData(this@importAppData, JsonReader(inStream))
}
}
newColumnList
},
afterProc = {
// cancelled.
if (it == null) return@launchProgress
try {
phoneOnly { env -> env.pager.adapter = null }
appState.editColumnList { list ->
list.clear()
list.addAll(it)
}
phoneTab(
{ env -> env.pager.adapter = env.pagerAdapter },
{ env -> resizeColumnWidth(env) }
)
updateColumnStrip()
} finally {
// 通知サービスをリスタート
setImportProtector(this@importAppData, false)
}
showToast(true, R.string.import_completed_please_restart_app)
finish()
},
preProc = {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
},
postProc = {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
)
}

View File

@ -292,6 +292,10 @@ class SideMenuAdapter(
timeline(defaultInsertPosition, ColumnType.MISSKEY_ANTENNA_LIST)
},
Item(icon = R.drawable.ic_hashtag, title = R.string.followed_tags) {
timeline(defaultInsertPosition, ColumnType.FOLLOWED_HASHTAGS)
},
Item(icon = R.drawable.ic_search, title = R.string.search) {
timeline(defaultInsertPosition, ColumnType.SEARCH, args = arrayOf("", false))
},

View File

@ -135,11 +135,11 @@ fun ActPost.openDraftPicker() {
}
fun ActPost.restoreDraft(draft: JsonObject) {
launchMain {
val listWarning = ArrayList<String>()
var targetAccount: SavedAccount? = null
runWithProgress("restore from draft", doInBackground = { progress ->
val listWarning = ArrayList<String>()
var targetAccount: SavedAccount? = null
launchProgress(
"restore from draft",
doInBackground = { progress ->
fun isTaskCancelled() = !coroutineContext.isActive
var content = draft.string(DRAFT_CONTENT) ?: ""
@ -170,7 +170,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
} catch (ignored: JsonException) {
}
return@runWithProgress "OK"
return@launchProgress "OK"
}
targetAccount = account
@ -188,7 +188,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
// 返信ステータスが存在するかどうか
EntityId.from(draft, DRAFT_REPLY_ID)?.let { inReplyToId ->
val result = apiClient.request("/api/v1/statuses/$inReplyToId")
if (isTaskCancelled()) return@runWithProgress null
if (isTaskCancelled()) return@launchProgress null
if (result?.jsonObject == null) {
listWarning.add(getString(R.string.reply_to_in_draft_is_lost))
draft.remove(DRAFT_REPLY_ID)
@ -203,7 +203,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
var isSomeAttachmentRemoved = false
val it = tmpAttachmentList.iterator()
while (it.hasNext()) {
if (isTaskCancelled()) return@runWithProgress null
if (isTaskCancelled()) return@launchProgress null
val ta = TootAttachment.decodeJson(it.next())
if (checkExist(ta.url)) continue
it.remove()
@ -226,101 +226,100 @@ fun ActPost.restoreDraft(draft: JsonObject) {
"OK"
},
afterProc = { result ->
// cancelled.
if (result == null) return@runWithProgress
afterProc = { result ->
// cancelled.
if (result == null) return@launchProgress
val content = draft.string(DRAFT_CONTENT) ?: ""
val contentWarning = draft.string(DRAFT_CONTENT_WARNING) ?: ""
val contentWarningChecked = draft.optBoolean(DRAFT_CONTENT_WARNING_CHECK)
val nsfwChecked = draft.optBoolean(DRAFT_NSFW_CHECK)
val tmpAttachmentList = draft.jsonArray(DRAFT_ATTACHMENT_LIST)
val replyId = EntityId.from(draft, DRAFT_REPLY_ID)
val content = draft.string(DRAFT_CONTENT) ?: ""
val contentWarning = draft.string(DRAFT_CONTENT_WARNING) ?: ""
val contentWarningChecked = draft.optBoolean(DRAFT_CONTENT_WARNING_CHECK)
val nsfwChecked = draft.optBoolean(DRAFT_NSFW_CHECK)
val tmpAttachmentList = draft.jsonArray(DRAFT_ATTACHMENT_LIST)
val replyId = EntityId.from(draft, DRAFT_REPLY_ID)
val draftVisibility =
TootVisibility.parseSavedVisibility(draft.string(DRAFT_VISIBILITY))
val draftVisibility =
TootVisibility.parseSavedVisibility(draft.string(DRAFT_VISIBILITY))
val evEmoji = DecodeOptions(this@restoreDraft, decodeEmoji = true)
.decodeEmoji(content)
val evEmoji = DecodeOptions(this@restoreDraft, decodeEmoji = true)
.decodeEmoji(content)
views.etContent.setText(evEmoji)
views.etContent.setSelection(evEmoji.length)
views.etContentWarning.setText(contentWarning)
views.etContentWarning.setSelection(contentWarning.length)
views.cbContentWarning.isChecked = contentWarningChecked
views.cbNSFW.isChecked = nsfwChecked
if (draftVisibility != null) states.visibility = draftVisibility
views.etContent.setText(evEmoji)
views.etContent.setSelection(evEmoji.length)
views.etContentWarning.setText(contentWarning)
views.etContentWarning.setSelection(contentWarning.length)
views.cbContentWarning.isChecked = contentWarningChecked
views.cbNSFW.isChecked = nsfwChecked
if (draftVisibility != null) states.visibility = draftVisibility
views.cbQuote.isChecked = draft.optBoolean(DRAFT_QUOTE)
views.cbQuote.isChecked = draft.optBoolean(DRAFT_QUOTE)
val sv = draft.string(DRAFT_POLL_TYPE)
if (sv != null) {
views.spPollType.setSelection(min(1, sv.toPollTypeIndex()))
} else {
// old draft
val bv = draft.optBoolean(DRAFT_IS_ENQUETE, false)
views.spPollType.setSelection(if (bv) 1 else 0)
}
val sv = draft.string(DRAFT_POLL_TYPE)
if (sv != null) {
views.spPollType.setSelection(min(1, sv.toPollTypeIndex()))
} else {
// old draft
val bv = draft.optBoolean(DRAFT_IS_ENQUETE, false)
views.spPollType.setSelection(if (bv) 1 else 0)
}
views.cbMultipleChoice.isChecked = draft.optBoolean(DRAFT_POLL_MULTIPLE)
views.cbHideTotals.isChecked = draft.optBoolean(DRAFT_POLL_HIDE_TOTALS)
views.etExpireDays.setText(draft.optString(DRAFT_POLL_EXPIRE_DAY, "1"))
views.etExpireHours.setText(draft.optString(DRAFT_POLL_EXPIRE_HOUR, ""))
views.etExpireMinutes.setText(draft.optString(DRAFT_POLL_EXPIRE_MINUTE, ""))
views.cbMultipleChoice.isChecked = draft.optBoolean(DRAFT_POLL_MULTIPLE)
views.cbHideTotals.isChecked = draft.optBoolean(DRAFT_POLL_HIDE_TOTALS)
views.etExpireDays.setText(draft.optString(DRAFT_POLL_EXPIRE_DAY, "1"))
views.etExpireHours.setText(draft.optString(DRAFT_POLL_EXPIRE_HOUR, ""))
views.etExpireMinutes.setText(draft.optString(DRAFT_POLL_EXPIRE_MINUTE, ""))
val array = draft.jsonArray(DRAFT_ENQUETE_ITEMS)
if (array != null) {
var srcIndex = 0
for (et in etChoices) {
if (srcIndex < array.size) {
et.setText(array.optString(srcIndex))
++srcIndex
} else {
et.setText("")
}
val array = draft.jsonArray(DRAFT_ENQUETE_ITEMS)
if (array != null) {
var srcIndex = 0
for (et in etChoices) {
if (srcIndex < array.size) {
et.setText(array.optString(srcIndex))
++srcIndex
} else {
et.setText("")
}
}
if (targetAccount != null) selectAccount(targetAccount)
if (tmpAttachmentList?.isNotEmpty() == true) {
attachmentList.clear()
tmpAttachmentList.forEach {
if (it !is JsonObject) return@forEach
val pa = PostAttachment(TootAttachment.decodeJson(it))
attachmentList.add(pa)
}
}
if (replyId != null) {
states.inReplyToId = replyId
states.inReplyToText = draft.string(DRAFT_REPLY_TEXT)
states.inReplyToImage = draft.string(DRAFT_REPLY_IMAGE)
states.inReplyToUrl = draft.string(DRAFT_REPLY_URL)
}
showContentWarningEnabled()
showMediaAttachment()
showVisibility()
updateTextCount()
showReplyTo()
showPoll()
showQuotedRenote()
if (listWarning.isNotEmpty()) {
val sb = StringBuilder()
for (s in listWarning) {
if (sb.isNotEmpty()) sb.append("\n")
sb.append(s)
}
AlertDialog.Builder(this@restoreDraft)
.setMessage(sb)
.setNeutralButton(R.string.close, null)
.show()
}
}
)
}
if (targetAccount != null) selectAccount(targetAccount)
if (tmpAttachmentList?.isNotEmpty() == true) {
attachmentList.clear()
tmpAttachmentList.forEach {
if (it !is JsonObject) return@forEach
val pa = PostAttachment(TootAttachment.decodeJson(it))
attachmentList.add(pa)
}
}
if (replyId != null) {
states.inReplyToId = replyId
states.inReplyToText = draft.string(DRAFT_REPLY_TEXT)
states.inReplyToImage = draft.string(DRAFT_REPLY_IMAGE)
states.inReplyToUrl = draft.string(DRAFT_REPLY_URL)
}
showContentWarningEnabled()
showMediaAttachment()
showVisibility()
updateTextCount()
showReplyTo()
showPoll()
showQuotedRenote()
if (listWarning.isNotEmpty()) {
val sb = StringBuilder()
for (s in listWarning) {
if (sb.isNotEmpty()) sb.append("\n")
sb.append(s)
}
AlertDialog.Builder(this@restoreDraft)
.setMessage(sb)
.setNeutralButton(R.string.close, null)
.show()
}
}
)
}
fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String) {

View File

@ -6,10 +6,16 @@ import android.os.SystemClock
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.dialog.ProgressDialogEx
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import kotlinx.coroutines.*
import java.lang.Runnable
import jp.juggler.util.clip
import jp.juggler.util.dismissSafe
import jp.juggler.util.isMainThread
import jp.juggler.util.withCaption
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.supervisorScope
import java.lang.ref.WeakReference
import java.text.NumberFormat
@ -52,7 +58,7 @@ private class TootTaskRunner(
progressSetup: (progress: ProgressDialogEx) -> Unit = ApiTask.defaultProgressSetupCallback,
backgroundBlock: suspend A.(client: TootApiClient) -> TootApiResult?,
): TootApiResult? {
if (!isMainThread) error("runApiTask: not main thread")
if (!isMainThread) error("runApiTask: must main thread")
val runner = TootTaskRunner(
context = context,
progressStyle = progressStyle,
@ -62,21 +68,21 @@ private class TootTaskRunner(
return runner.run {
accessInfo?.let { client.account = it }
apiHost?.let { client.apiHost = it }
withContext(SupervisorJob() + Dispatchers.Main) {
try {
openProgress()
asyncIO {
try {
openProgress()
supervisorScope {
async(appDispatchers.io) {
backgroundBlock(context, client)
}.also {
task = it
}.await()
} catch (ignored: CancellationException) {
null
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("error"))
} finally {
dismissProgress()
}
} catch (ignored: CancellationException) {
null
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("error"))
} finally {
dismissProgress()
}
}
}
@ -218,7 +224,13 @@ suspend fun <A : Context> A.runApiTask(
progressPrefix: String? = null,
progressSetup: (progress: ProgressDialogEx) -> Unit = ApiTask.defaultProgressSetupCallback,
backgroundBlock: suspend A.(client: TootApiClient) -> TootApiResult?,
) = TootTaskRunner.runApiTask(this, accessInfo, null, progressStyle, progressPrefix, progressSetup, backgroundBlock)
) = TootTaskRunner.runApiTask(this,
accessInfo,
null,
progressStyle,
progressPrefix,
progressSetup,
backgroundBlock)
suspend fun <A : Context> A.runApiTask(
apiHost: Host,
@ -226,11 +238,23 @@ suspend fun <A : Context> A.runApiTask(
progressPrefix: String? = null,
progressSetup: (progress: ProgressDialogEx) -> Unit = ApiTask.defaultProgressSetupCallback,
backgroundBlock: suspend A.(client: TootApiClient) -> TootApiResult?,
) = TootTaskRunner.runApiTask(this, null, apiHost, progressStyle, progressPrefix, progressSetup, backgroundBlock)
) = TootTaskRunner.runApiTask(this,
null,
apiHost,
progressStyle,
progressPrefix,
progressSetup,
backgroundBlock)
suspend fun <A : Context> A.runApiTask(
progressStyle: Int = ApiTask.PROGRESS_SPINNER,
progressPrefix: String? = null,
progressSetup: (progress: ProgressDialogEx) -> Unit = ApiTask.defaultProgressSetupCallback,
backgroundBlock: suspend A.(client: TootApiClient) -> TootApiResult?,
) = TootTaskRunner.runApiTask(this, null, null, progressStyle, progressPrefix, progressSetup, backgroundBlock)
) = TootTaskRunner.runApiTask(this,
null,
null,
progressStyle,
progressPrefix,
progressSetup,
backgroundBlock)

View File

@ -42,7 +42,7 @@ class TootAttachment : TootAttachmentLike {
private fun guessMediaTypeByUrl(src: String?): TootAttachmentType? {
val uri = src.mayUri() ?: return null
if (ext_audio.find { uri.path?.endsWith(it) == true } != null) {
if (ext_audio.any { uri.path?.endsWith(it) == true }) {
return TootAttachmentType.Audio
}

View File

@ -11,7 +11,7 @@ open class TootTag constructor(
// The hashtag, not including the preceding #
val name: String,
val type: TagType = TagType.Tag,
var type: TagType = TagType.Tag,
// The URL of the hashtag. may null if generated from TootContext
val url: String? = null,
@ -22,11 +22,12 @@ open class TootTag constructor(
// Mastodon /api/v2/search provides history.
val history: ArrayList<History>? = null,
) : TimelineItem() {
) : TimelineItem() {
enum class TagType {
Tag,
TrendLink
TrendLink,
FollowedTags,
}
val countDaily: Int

View File

@ -191,7 +191,7 @@ fun Column.removeNotifications() {
duplicateMap.clear()
fireShowContent(reason = "removeNotifications", reset = true)
EndlessScope.launch {
EmptyScope.launch {
try {
onNotificationCleared(context, accessInfo.db_id)
} catch (ex: Throwable) {

View File

@ -11,9 +11,12 @@ import jp.juggler.subwaytooter.api.entity.TimelineItem
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.parseList
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
enum class ColumnTaskType(val marker: Char) {
@ -25,7 +28,7 @@ enum class ColumnTaskType(val marker: Char) {
abstract class ColumnTask(
val column: Column,
val ctType: ColumnTaskType
val ctType: ColumnTaskType,
) {
val ctStarted = AtomicBoolean(false)
@ -79,7 +82,7 @@ abstract class ColumnTask(
internal suspend fun getAnnouncements(
client: TootApiClient,
force: Boolean = false
force: Boolean = false,
): TootApiResult? {
// announcements is shown only mastodon home timeline, not pseudo.
if (isMastodon && !isPseudo) {
@ -126,7 +129,7 @@ abstract class ColumnTask(
fun start() {
job = launchMain {
val result = try {
withContext(Dispatchers.IO) { background() }
withContext(appDispatchers.io) { background() }
} catch (ignored: CancellationException) {
null // キャンセルされたらresult==nullとする
} catch (ex: Throwable) {

View File

@ -863,6 +863,15 @@ class ColumnTask_Loading(
return result
}
suspend fun getFollowedHashtags(client: TootApiClient): TootApiResult? {
val result = client.request("/api/v1/followed_tags")
val src = parser.tagList(result?.jsonArray)
.onEach { it.type = TootTag.TagType.FollowedTags }
listTmp = addAll(listTmp, src)
column.saveRange(bBottom = true, bTop = true, result = result, list = src)
return result
}
suspend fun getListList(
client: TootApiClient,
pathBase: String,

View File

@ -1,7 +1,9 @@
package jp.juggler.subwaytooter.column
import android.os.SystemClock
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.DedupMode
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
@ -1171,4 +1173,14 @@ class ColumnTask_Refresh(
column.saveRange(bBottom, !bBottom, result, src)
return result
}
suspend fun getFollowedHashtags(client: TootApiClient): TootApiResult? {
val path = column.addRange(bBottom = bBottom, "/api/v1/followed_tags")
val result = client.request(path)
val src = parser.tagList(result?.jsonArray)
.onEach { it.type = TootTag.TagType.FollowedTags }
listTmp = addAll(listTmp, src)
column.saveRange(bBottom = bBottom, bTop = !bBottom, result = result, list = src)
return result
}
}

View File

@ -2038,8 +2038,30 @@ enum class ColumnType(
canStreamingMisskey = streamingTypeNo,
),
FOLLOWED_HASHTAGS(
46,
iconId = { R.drawable.ic_hashtag },
name1 = { it.getString(R.string.followed_tags) },
bAllowPseudo = false,
bAllowMisskey = false,
loading = { client ->
getFollowedHashtags(client)
},
refresh = { client ->
getFollowedHashtags(client)
},
canAutoRefresh = false,
canStreamingMastodon = streamingTypeNo,
canStreamingMisskey = streamingTypeNo,
),
;
private fun getFollowedHashtags(client: TootApiClient) {
}
init {
val old = Column.typeMap[id]
if (id > 0 && old != null) error("ColumnType: duplicate id $id. name=$name, ${old.name}")

View File

@ -6,13 +6,13 @@ import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirec
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.closePopup
import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.util.endPadding
import jp.juggler.subwaytooter.util.startPadding
import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import org.jetbrains.anko.backgroundColor
@ -60,7 +60,7 @@ fun ColumnViewHolder.loadBackgroundImage(iv: ImageView, url: String?) {
// 非同期処理を開始
lastImageTask = launchMain {
val bitmap = try {
withContext(Dispatchers.IO) {
withContext(appDispatchers.io) {
try {
createResizedBitmap(
activity,

View File

@ -15,9 +15,12 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actpost.DRAFT_CONTENT
import jp.juggler.subwaytooter.actpost.DRAFT_CONTENT_WARNING
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.table.PostDraft
import jp.juggler.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
class DlgDraftPicker : AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener,
DialogInterface.OnDismissListener {
@ -119,7 +122,7 @@ class DlgDraftPicker : AdapterView.OnItemClickListener, AdapterView.OnItemLongCl
task = launchMain {
val cursor = try {
withContext(Dispatchers.IO) {
withContext(appDispatchers.io) {
PostDraft.createCursor()
} ?: error("cursor is null")
} catch (ignored: CancellationException) {

View File

@ -185,7 +185,7 @@ class DlgListMember(
if (whoLocal != null) {
forEach { list ->
list.isRegistered =
null != list.userIds?.find { it == whoLocal.id }
list.userIds?.any { it == whoLocal.id } ?: false
}
}
}

View File

@ -8,8 +8,7 @@ import android.widget.ImageView
import android.widget.TextView
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
import jp.juggler.util.launchMain
import jp.juggler.util.runWithProgress
import jp.juggler.util.launchProgress
import net.glxn.qrgen.android.QRCode
@SuppressLint("StaticFieldLeak")
@ -23,22 +22,20 @@ object DlgQRCode {
activity: ActMain,
size: Int,
url: String,
callback: QrCodeCallback
callback: QrCodeCallback,
) {
launchMain {
activity.runWithProgress(
"making QR code",
progressInitializer = {
it.setMessageEx(activity.getString(R.string.generating_qr_code))
},
doInBackground = {
QRCode.from(url).withSize(size, size).bitmap()
},
afterProc = {
if (it != null) callback.onQrCode(it)
},
)
}
activity.launchProgress(
"making QR code",
progressInitializer = {
it.setMessageEx(activity.getString(R.string.generating_qr_code))
},
doInBackground = {
QRCode.from(url).withSize(size, size).bitmap()
},
afterProc = {
if (it != null) callback.onQrCode(it)
},
)
}
fun open(activity: ActMain, message: CharSequence, url: String) {

View File

@ -728,7 +728,7 @@ private class EmojiPicker(
else -> log.w("handleTouch else $ev")
}
} catch (ex: Throwable) {
log.w("handleTouch failed. ev=$ev, wasIntercept=$wasIntercept")
log.trace(ex, "handleTouch failed. ev=$ev, wasIntercept=$wasIntercept")
wasIntercept
}

View File

@ -0,0 +1,21 @@
package jp.juggler.subwaytooter.global
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainCoroutineDispatcher
@Suppress("VariableNaming")
interface AppDispatchers {
val main: MainCoroutineDispatcher
val io: CoroutineDispatcher
val default: CoroutineDispatcher
val unconfined: CoroutineDispatcher
}
@Suppress("InjectDispatcher")
class AppDispatchersImpl : AppDispatchers {
override val main = Dispatchers.Main
override val io = Dispatchers.IO
override val default = Dispatchers.Default
override val unconfined = Dispatchers.Unconfined
}

View File

@ -9,13 +9,11 @@ import org.koin.core.context.startKoin
import org.koin.dsl.module
import org.koin.mp.KoinPlatformTools
val appDatabase by lazy {
getKoin().get<AppDatabaseHolder>().database
}
val appDatabase by lazy { getKoin().get<AppDatabaseHolder>().database }
val appPref by lazy {
getKoin().get<AppPrefHolder>().pref
}
val appPref by lazy { getKoin().get<AppPrefHolder>().pref }
val appDispatchers by lazy { getKoin().get<AppDispatchers>() }
fun getKoin(): Koin = KoinPlatformTools.defaultContext().get()
@ -44,6 +42,9 @@ object Global {
log.i("AppDatabaseHolderImpl: context=$context")
AppDatabaseHolderImpl(context)
}
single<AppDispatchers> {
AppDispatchersImpl()
}
})
}
getKoin().get<AppDatabaseHolder>().afterGlobalPrepare()

View File

@ -263,10 +263,21 @@ private fun ItemViewHolder.clickAvatar(pos: Int, longClick: Boolean = false) {
private fun ItemViewHolder.clickTag(pos: Int, item: TimelineItem?) {
with(activity) {
when (item) {
is TootTag -> if (item.type == TootTag.TagType.TrendLink) {
openCustomTab(item.url)
} else {
tagTimeline(pos, accessInfo, item.name)
is TootTag -> when (item.type) {
TootTag.TagType.Tag ->
tagTimeline(pos, accessInfo, item.name)
TootTag.TagType.FollowedTags -> {
val host = accessInfo.apiHost
tagDialog(accessInfo,
pos,
item.url!!,
host,
item.name,
tagList = null,
whoAcct = null)
}
TootTag.TagType.TrendLink ->
openCustomTab(item.url)
}
is TootSearchGap -> column.startGap(item, isHead = true)
is TootConversationSummary -> clickConversation(

View File

@ -8,11 +8,13 @@ import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatTextView
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TootPolls
import jp.juggler.subwaytooter.api.entity.TootPollsChoice
import jp.juggler.subwaytooter.api.entity.TootPollsType
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.isSearchColumn
import jp.juggler.subwaytooter.drawable.PollPlotDrawable
import jp.juggler.subwaytooter.table.SavedAccount
@ -66,7 +68,7 @@ fun ItemViewHolder.makeEnqueteChoiceView(
enquete: TootPolls,
canVote: Boolean,
i: Int,
item: TootPollsChoice
item: TootPollsChoice,
) {
val text = when (enquete.pollType) {
@ -213,7 +215,7 @@ fun ItemViewHolder.makeEnqueteFooterFriendsNico(enquete: TootPolls) {
fun ItemViewHolder.makeEnqueteFooterMastodon(
status: TootStatus,
enquete: TootPolls,
canVote: Boolean
canVote: Boolean,
) {
val density = activity.density
@ -281,7 +283,7 @@ fun ItemViewHolder.onClickEnqueteChoice(
enquete: TootPolls,
context: Context,
accessInfo: SavedAccount,
idx: Int
idx: Int,
) {
if (enquete.ownVoted) {
context.showToast(false, R.string.already_voted)
@ -387,7 +389,7 @@ fun ItemViewHolder.sendMultiple(
status: TootStatus,
enquete: TootPolls,
context: Context,
accessInfo: SavedAccount
accessInfo: SavedAccount,
) {
val now = System.currentTimeMillis()
if (now >= enquete.expired_at) {
@ -395,7 +397,7 @@ fun ItemViewHolder.sendMultiple(
return
}
if (enquete.items?.find { it.checked } == null) {
if (enquete.items?.any { it.checked } != true) {
context.showToast(false, R.string.polls_choice_not_selected)
return
}

View File

@ -12,10 +12,13 @@ import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.annotation.StringRes
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.Styler
import jp.juggler.subwaytooter.actmain.closePopup
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.appendColorShadeIcon
import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.getAcctColor
@ -417,13 +420,16 @@ fun ItemViewHolder.showSearchTag(tag: TootTag) {
tvTrendTagCount.text = "${tag.countDaily}(${tag.countWeekly})"
cvTagHistory.setHistory(tag.history)
if (tag.type == TootTag.TagType.TrendLink) {
tvTrendTagName.text = tag.url?.ellipsizeDot3(256)
tvTrendTagDesc.text = tag.name + "\n" + tag.description
} else {
tvTrendTagName.text = "#${tag.name}"
tvTrendTagDesc.text =
activity.getString(R.string.people_talking, tag.accountDaily, tag.accountWeekly)
when (tag.type) {
TootTag.TagType.TrendLink -> {
tvTrendTagName.text = tag.url?.ellipsizeDot3(256)
tvTrendTagDesc.text = tag.name + "\n" + tag.description
}
else -> {
tvTrendTagName.text = "#${tag.name.ellipsizeDot3(256)}"
tvTrendTagDesc.text =
activity.getString(R.string.people_talking, tag.accountDaily, tag.accountWeekly)
}
}
} else {
llSearchTag.visibility = View.VISIBLE

View File

@ -6,11 +6,11 @@ import android.content.Intent
import android.os.IBinder
import android.os.SystemClock
import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.notification.CheckerWakeLocks.Companion.checkerWakeLocks
import jp.juggler.util.EndlessScope
import jp.juggler.util.EmptyScope
import jp.juggler.util.LogCategory
import jp.juggler.util.launchMain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.launch
@ -84,7 +84,7 @@ class ForegroundPollingService : Service() {
}
init {
EndlessScope.launch(Dispatchers.Default) {
EmptyScope.launch(appDispatchers.default) {
var lastStartId = 0
while (true) {
try {

View File

@ -9,6 +9,7 @@ import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.notification.CheckerWakeLocks.Companion.checkerWakeLocks
import jp.juggler.subwaytooter.notification.MessageNotification.getMessageNotifications
import jp.juggler.subwaytooter.notification.MessageNotification.removeMessageNotification
@ -213,7 +214,7 @@ class PollingChecker(
return
}
withContext(Dispatchers.Default + checkJob) {
withContext(appDispatchers.default + checkJob) {
if (importProtector.get()) {
log.w("aborted by importProtector.")
return@withContext
@ -294,9 +295,9 @@ class PollingChecker(
cache = NotificationCache(account.db_id).apply {
load()
if( account.isMisskey && ! PrefB.bpMisskeyNotificationCheck() ){
if (account.isMisskey && !PrefB.bpMisskeyNotificationCheck()) {
log.d("skip misskey server. ${account.acct}")
}else{
} else {
requestAsync(
client,
account,

View File

@ -11,6 +11,7 @@ import com.google.firebase.messaging.FirebaseMessaging
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.notification.MessageNotification.removeMessageNotification
import jp.juggler.subwaytooter.notification.ServerTimeoutNotification.createServerTimeoutNotification
import jp.juggler.subwaytooter.pref.PrefDevice
@ -19,7 +20,10 @@ import jp.juggler.subwaytooter.table.NotificationTracking
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.PrivacyPolicyChecker
import jp.juggler.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.tasks.await
import okhttp3.Request
@ -164,7 +168,7 @@ suspend fun cancelAllWorkAndJoin(context: Context) {
}
fun restartAllWorker(context: Context) {
EndlessScope.launch {
EmptyScope.launch {
try {
if (importProtector.get()) {
log.w("restartAllWorker: abort by importProtector.")
@ -215,7 +219,7 @@ fun checkNotificationImmediate(
accountDbId: Long,
injectData: List<TootNotification> = emptyList(),
) {
EndlessScope.launch {
EmptyScope.launch {
try {
if (importProtector.get()) {
log.w("checkNotificationImmediate: abort by importProtector.")
@ -293,7 +297,7 @@ suspend fun checkNoticifationAll(
SavedAccount.loadAccountList(context).mapNotNull { sa ->
when {
sa.isPseudo || !sa.isConfirmed -> null
else -> EndlessScope.launch(Dispatchers.Default) {
else -> EmptyScope.launch(appDispatchers.default) {
try {
PollingChecker(
context = context,
@ -335,7 +339,7 @@ suspend fun checkNoticifationAll(
* メイン画面のonCreate時に全ての通知をチェックする
*/
fun checkNotificationImmediateAll(context: Context, onlySubscription: Boolean = false) {
EndlessScope.launch {
EmptyScope.launch {
try {
if (importProtector.get()) {
log.w("checkNotificationImmediateAll: abort by importProtector.")

View File

@ -95,8 +95,8 @@ class PollingWorker2(
CheckerNotification.showMessage(applicationContext, text) {
try {
setForeground(ForegroundInfo(NOTIFICATION_ID_POLLING_WORKER, it))
}catch(ex:Throwable){
log.e(ex,"showMessage failed.")
} catch (ex: Throwable) {
log.e(ex, "showMessage failed.")
}
}

View File

@ -118,7 +118,6 @@ class NotificationCache(private val account_db_id: Long) {
if (noBit(flags, 8)) append("&exclude_types[]=mention")
// if(noBit(flags,16)) /* mastodon has no reaction */
if (noBit(flags, 32)) append("&exclude_types[]=poll")
}.toString()
fun parseNotificationTime(accessInfo: SavedAccount, src: JsonObject): Long =

View File

@ -206,6 +206,7 @@ fun openCustomTab(
val tagInfo = url.findHashtagFromUrl()
if (tagInfo != null) {
activity.tagDialog(
accessInfo,
pos,
url,
Host.parse(tagInfo.second),

View File

@ -2,8 +2,8 @@ package jp.juggler.subwaytooter.util
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.global.appDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext
@ -12,7 +12,7 @@ abstract class AsyncActivity : AppCompatActivity(), CoroutineScope {
private lateinit var activityJob: Job
override val coroutineContext: CoroutineContext
get() = activityJob + Dispatchers.Main
get() = activityJob + appDispatchers.main
override fun onCreate(savedInstanceState: Bundle?) {
activityJob = Job()
@ -21,6 +21,6 @@ abstract class AsyncActivity : AppCompatActivity(), CoroutineScope {
override fun onDestroy() {
super.onDestroy()
(activityJob + Dispatchers.Default).cancel()
(activityJob + appDispatchers.default).cancel()
}
}

View File

@ -13,10 +13,10 @@ import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import jp.juggler.util.VideoInfo.Companion.videoInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay
@ -212,7 +212,7 @@ class AttachmentUploader(
}
val result = try {
if (request.pa.isCancelled) continue
withContext(request.pa.job + Dispatchers.IO) {
withContext(request.pa.job + appDispatchers.io) {
request.upload()
}
} catch (ex: Throwable) {
@ -220,7 +220,7 @@ class AttachmentUploader(
}
try {
request.pa.progress = ""
withContext(Dispatchers.Main) {
withContext(appDispatchers.main) {
handleResult(request, result)
}
} catch (ex: Throwable) {

View File

@ -298,7 +298,7 @@ object EmojiDecoder {
var start = i
loop@ while (i < end) {
val c = s.codePointAt(i)
if (c == codepointColon && null == urlList.find { i in it }) break@loop
if (c == codepointColon && urlList.none { i in it }) break@loop
i += Character.charCount(c)
}
if (i > start) callback.onString(s.substring(start, i))

View File

@ -1,431 +0,0 @@
package jp.juggler.subwaytooter.view
import android.content.Context
import android.database.DataSetObservable
import android.database.DataSetObserver
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.Filter
import android.widget.Filterable
import android.widget.FrameLayout
import android.widget.GridView
import android.widget.ListAdapter
import android.widget.WrapperListAdapter
import java.util.ArrayList
class HeaderGridView : GridView {
private inner class FullWidthFixedViewLayout(context: Context) : FrameLayout(context) {
override fun onMeasure(widthMeasureSpecArg: Int, heightMeasureSpec: Int) {
val widthMeasureSpec = MeasureSpec.makeMeasureSpec(
contentWidth,
MeasureSpec.getMode(widthMeasureSpecArg)
)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
class Header(
val rangeStart: Int,
val rangeLength: Int,
val itemHeight: Int,
val view: View,
val viewContainer: ViewGroup,
val data: Any?,
val isSelectable: Boolean,
) {
var rowStart = 0
var rowLength = 0 // includes header itself
}
companion object {
@Suppress("unused")
private val TAG = "HeaderGridView"
private fun areAllListInfosSelectable(infos: ArrayList<Header>?): Boolean {
val hasNotSelectable = null != infos?.find { !it.isSelectable }
return !hasNotSelectable
}
}
val contentWidth: Int
get() = measuredWidth - paddingLeft - paddingRight
private val headers = ArrayList<Header>()
private var willCalculateRows = true
private var lastNumColumns = -1
private var rowEnd = 0
private fun updateRows(): Boolean {
if (!willCalculateRows) return true
val numColumns = numColumns
if (numColumns < 1) return false
willCalculateRows = false
var row = 0
for (header in headers) {
header.rowStart = row
val rowLength = 1 + (header.rangeLength + numColumns - 1) / numColumns
header.rowLength = rowLength
row += rowLength
}
rowEnd = row
lastNumColumns = numColumns
return true
}
private fun findHeader(pos: Int): Pair<Header, Int> {
val row = pos / lastNumColumns
var start = 0
var end = headers.size
while (end - start > 0) {
val mid = (start + end) shr 1
val header = headers[mid]
when {
row < header.rowStart -> end = mid
row >= header.rowStart + header.rowLength -> start = mid + 1
else -> {
val offset = pos - header.rowStart * lastNumColumns
return Pair(header, offset)
}
}
}
throw ArrayIndexOutOfBoundsException("pos=$pos,row=$row,start=$start,end=$end,headers.size=${headers.size}")
}
fun findListItemIndex(position: Int): Int {
return if (adapter is HeaderViewGridAdapter) {
if (!updateRows()) return -1
val (header, idx) = findHeader(position)
val offset = idx - lastNumColumns
if (offset in (0 until header.rangeLength)) {
offset + header.rangeStart
} else {
-1
}
} else {
position
}
}
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet, defStyle: Int) :
super(context, attrs, defStyle) {
init()
}
override fun setClipChildren(clipChildren: Boolean) {
// Ignore, since the header rows depend on not being clipped
}
private fun init() {
super.setClipChildren(false)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val oldNumColumns = numColumns
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val numColumns = numColumns
if (numColumns != oldNumColumns) {
(adapter as? HeaderViewGridAdapter)?.update()
}
}
/**
* Add a fixed view to appear at the top of the grid. If addHeaderView is
* called more than once, the views will appear in the order they were
* added. Views added using this call can take focus if they want.
*
*
* NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
* the supplied cursor with one that will also account for header views.
*
* @param v The view to add.
* @param data Data to associate with this view
* @param isSelectable whether the item is selectable
*/
@JvmOverloads
@Suppress("unused")
fun addHeaderView(
rangeStart: Int,
rangeLength: Int,
itemHeight: Int,
v: View,
data: Any? = null,
isSelectable: Boolean = true,
) {
if (adapter != null && adapter !is HeaderViewGridAdapter) {
error("Cannot add header view to grid -- setAdapter has already been called.")
}
headers.add(
Header(
rangeStart = rangeStart,
rangeLength = rangeLength,
itemHeight = itemHeight,
view = v,
viewContainer = FullWidthFixedViewLayout(context)
.apply {
try {
// remove from old parent
(v.parent as? ViewGroup)?.removeView(v)
} catch (ex: Throwable) {
Log.w(TAG, "can't remove from old parent", ex)
}
addView(v)
},
data = data,
isSelectable = isSelectable
)
)
(adapter as? HeaderViewGridAdapter)?.update()
}
/**
* Removes a previously-added header view.
*
* @param v The view to remove
* @return true if the view was removed, false if the view was not a header
* view
*/
@Suppress("unused")
fun removeHeaderView(v: View): Boolean {
willCalculateRows = true
val it = headers.iterator()
while (it.hasNext()) {
val info = it.next()
if (info.view === v) {
it.remove()
(adapter as? HeaderViewGridAdapter)?.update()
return true
}
}
return false
}
fun reset() {
headers.clear()
adapter = null
}
override fun setAdapter(adapter: ListAdapter?) {
if (adapter != null && headers.size > 0) {
willCalculateRows = true
super.setAdapter(HeaderViewGridAdapter(adapter))
} else {
super.setAdapter(adapter)
}
}
/**
* ListAdapter used when a HeaderGridView has header views. This ListAdapter
* wraps another one and also keeps track of the header views and their
* associated data objects.
*
* This is intended as a base class; you will probably not need to
* use this class directly in your own code.
*/
private inner class HeaderViewGridAdapter(private val mAdapter: ListAdapter) :
WrapperListAdapter, Filterable {
// This is used to notify the container of updates relating to number of columns
// or headers changing, which changes the number of placeholders needed
private val mDataSetObservable = DataSetObservable()
var mAreAllFixedViewsSelectable: Boolean = false
private val mIsFilterable: Boolean
init {
mIsFilterable = mAdapter is Filterable
mAreAllFixedViewsSelectable = areAllListInfosSelectable(headers)
}
override fun isEmpty(): Boolean {
return (mAdapter.isEmpty) && headers.size == 0
}
override fun areAllItemsEnabled(): Boolean {
return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled()
}
override fun hasStableIds(): Boolean {
return mAdapter.hasStableIds()
}
override fun registerDataSetObserver(observer: DataSetObserver) {
mDataSetObservable.registerObserver(observer)
mAdapter.registerDataSetObserver(observer)
}
override fun unregisterDataSetObserver(observer: DataSetObserver) {
mDataSetObservable.unregisterObserver(observer)
mAdapter.unregisterDataSetObserver(observer)
}
override fun getFilter(): Filter? {
return if (mIsFilterable) {
(mAdapter as Filterable).filter
} else null
}
override fun getWrappedAdapter(): ListAdapter {
return mAdapter
}
fun update() {
willCalculateRows = true
mAreAllFixedViewsSelectable = areAllListInfosSelectable(headers)
mDataSetObservable.notifyChanged()
}
override fun getCount(): Int {
if (!updateRows()) return 0
return rowEnd * lastNumColumns
}
override fun isEnabled(position: Int): Boolean {
if (!updateRows()) return false
val (header, idx) = findHeader(position)
return when {
// Header
idx == 0 -> header.isSelectable
// right of header
idx < lastNumColumns -> false
// data
else -> {
val offset = idx - lastNumColumns
if (offset in (0 until header.rangeLength)) {
mAdapter.isEnabled(offset + header.rangeStart)
} else {
false
}
}
}
}
override fun getItem(position: Int): Any? {
if (!updateRows()) return null
val (header, idx) = findHeader(position)
return when {
// Header
idx == 0 -> header.data
// right of header
idx < lastNumColumns -> null
// data
else -> {
val offset = idx - lastNumColumns
if (offset in (0 until header.rangeLength)) {
mAdapter.getItem(offset + header.rangeStart)
} else {
null
}
}
}
}
override fun getItemId(position: Int): Long {
if (!updateRows()) return -1L
val (header, idx) = findHeader(position)
return when {
// Header, right of header
idx < lastNumColumns -> -1L
// data
else -> {
val offset = idx - lastNumColumns
if (offset in (0 until header.rangeLength)) {
mAdapter.getItemId(offset + header.rangeStart)
} else {
-1L
}
}
}
}
override fun getViewTypeCount(): Int {
return mAdapter.viewTypeCount + 1
}
override fun getItemViewType(position: Int): Int {
if (!updateRows()) error("view required before layout")
val (header, idx) = findHeader(position)
return when {
// Header
idx == 0 -> AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER
// right of header
// Placeholders get the last view type number
idx < lastNumColumns -> mAdapter.viewTypeCount
// data
else -> {
val offset = idx - lastNumColumns
if (offset in (0 until header.rangeLength)) {
mAdapter.getItemViewType(offset + header.rangeStart)
} else {
// Placeholders get the last view type number
mAdapter.viewTypeCount
}
}
}
}
override fun getView(position: Int, convertViewArg: View?, parent: ViewGroup): View? {
if (!updateRows()) error("view required before layout.")
val (header, idx) = findHeader(position)
return when {
// Header
idx == 0 -> header.viewContainer
// right of header
// Placeholders get the last view type number
idx < lastNumColumns -> (convertViewArg ?: View(parent.context))
.apply {
// We need to do this because GridView uses the height of the last item
// in a row to determine the height for the entire row.
visibility = View.INVISIBLE
minimumHeight = header.viewContainer.height
}
// data
else -> {
val offset = idx - lastNumColumns
if (offset in (0 until header.rangeLength)) {
mAdapter.getView(offset + header.rangeStart, convertViewArg, parent)
} else {
// Placeholders get the last view type number
(convertViewArg ?: View(parent.context))
.apply {
// We need to do this because GridView uses the height of the last item
// in a row to determine the height for the entire row.
visibility = View.INVISIBLE
minimumHeight = header.itemHeight
}
}
}
}
}
}
}

View File

@ -16,17 +16,17 @@ import java.util.*
class OutsideDrawerLayout : LinearLayout {
constructor(context: Context) :
super(context) {
super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet) :
super(context, attrs) {
super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
super(context, attrs, defStyleAttr) {
super(context, attrs, defStyleAttr) {
init()
}
@ -41,8 +41,8 @@ class OutsideDrawerLayout : LinearLayout {
parent: ViewGroup,
descendant: View,
left: Int,
top: Int
) -> Unit
top: Int,
) -> Unit,
)
private val callbackList = LinkedList<Callback>()
@ -54,12 +54,12 @@ class OutsideDrawerLayout : LinearLayout {
parent: ViewGroup,
descendant: View,
left: Int,
top: Int
) -> Unit
top: Int,
) -> Unit,
) {
if (null == callbackList.find { it.view == view && it.draw == draw }) callbackList.add(
Callback(view, draw)
)
if (callbackList.none { it.view == view && it.draw == draw }) {
callbackList.add(Callback(view, draw))
}
}
@Suppress("unused")

View File

@ -3,6 +3,7 @@ package jp.juggler.util
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import jp.juggler.subwaytooter.dialog.ProgressDialogEx
import jp.juggler.subwaytooter.global.appDispatchers
import kotlinx.coroutines.*
import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
@ -15,7 +16,7 @@ val <T : Any> T.wrapWeakReference: WeakReference<T>
// kotlinx.coroutines 1.5.0 で GlobalScopeがdeprecated になったが、
// プロセスが生きてる間ずっと動いててほしいものや特にキャンセルのタイミングがないコルーチンでは使い続けたい
object EndlessScope : CoroutineScope {
object EmptyScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
@ -23,18 +24,22 @@ object EndlessScope : CoroutineScope {
// メインスレッド上で動作するコルーチンを起動して、終了を待たずにリターンする。
// 起動されたアクティビティのライフサイクルに関わらず中断しない。
fun launchMain(block: suspend CoroutineScope.() -> Unit): Job =
EndlessScope.launch(context = Dispatchers.Main.immediate) {
EmptyScope.launch(context = appDispatchers.main.immediate) {
try {
block()
} catch (ex: CancellationException) {
log.trace(ex, "launchMain: cancelled.")
} catch (ex: Throwable) {
if (ex is CancellationException) {
log.w("lainchMain cancelled.")
} else {
log.trace(ex, "launchMain failed.")
}
}
}
// Default Dispatcherで動作するコルーチンを起動して、終了を待たずにリターンする。
// 起動されたアクティビティのライフサイクルに関わらず中断しない。
fun launchDefault(block: suspend CoroutineScope.() -> Unit): Job =
EndlessScope.launch(context = Dispatchers.Default) {
EmptyScope.launch(context = appDispatchers.default) {
try {
block()
} catch (ex: CancellationException) {
@ -45,7 +50,7 @@ fun launchDefault(block: suspend CoroutineScope.() -> Unit): Job =
// IOスレッド上で動作するコルーチンを起動して、終了を待たずにリターンする。
// 起動されたアクティビティのライフサイクルに関わらず中断しない。
fun launchIO(block: suspend CoroutineScope.() -> Unit): Job =
EndlessScope.launch(context = Dispatchers.IO) {
EmptyScope.launch(context = appDispatchers.io) {
try {
block()
} catch (ex: CancellationException) {
@ -53,17 +58,10 @@ fun launchIO(block: suspend CoroutineScope.() -> Unit): Job =
}
}
// IOスレッド上で動作するコルーチンを起動して、終了を待たずにリターンする。
// 起動されたアクティビティのライフサイクルに関わらず中断しない。
// asyncの場合キャンセル例外のキャッチは呼び出し側で行う必要がある
@Suppress("DeferredIsResult")
fun <T : Any?> asyncIO(block: suspend CoroutineScope.() -> T): Deferred<T> =
EndlessScope.async(block = block, context = Dispatchers.IO)
fun AppCompatActivity.launchAndShowError(
errorCaption: String? = null,
block: suspend CoroutineScope.() -> Unit,
): Job = lifecycleScope.launch() {
): Job = lifecycleScope.launch {
try {
block()
} catch (ex: Throwable) {
@ -73,7 +71,7 @@ fun AppCompatActivity.launchAndShowError(
/////////////////////////////////////////////////////////////////////////
suspend fun <T : Any?> AppCompatActivity.runWithProgress(
fun <T : Any?> AppCompatActivity.launchProgress(
caption: String,
doInBackground: suspend CoroutineScope.(ProgressDialogEx) -> T,
afterProc: suspend CoroutineScope.(result: T) -> Unit = {},
@ -81,46 +79,37 @@ suspend fun <T : Any?> AppCompatActivity.runWithProgress(
preProc: suspend CoroutineScope.() -> Unit = {},
postProc: suspend CoroutineScope.() -> Unit = {},
) {
coroutineScope {
if (!isMainThread) error("runWithProgress: not main thread.")
val progress = ProgressDialogEx(this@runWithProgress)
val task = async(Dispatchers.IO) {
doInBackground(progress)
}
launch(Dispatchers.Main) {
val activity = this
EmptyScope.launch(Dispatchers.Main.immediate) {
val progress = ProgressDialogEx(activity)
try {
progress.setCancelable(true)
progress.isIndeterminateEx = true
progress.setMessageEx("$caption")
progressInitializer(progress)
try {
preProc()
} catch (ex: Throwable) {
log.trace(ex)
}
progress.setCancelable(true)
progress.setOnCancelListener { task.cancel() }
progress.isIndeterminateEx = true
progress.setMessageEx("$caption")
progressInitializer(progress)
progress.show()
val result = supervisorScope {
val task = async(appDispatchers.io) {
doInBackground(progress)
}
progress.setOnCancelListener { task.cancel() }
progress.show()
task.await()
}
if (result != null) afterProc(result)
} catch (ex: Throwable) {
log.trace(ex)
showToast(ex, "$caption failed.")
} finally {
progress.dismissSafe()
try {
val result = try {
task.await()
} catch (ignored: CancellationException) {
null
}
if (result != null) afterProc(result)
postProc()
} catch (ex: Throwable) {
showToast(ex, "$caption failed.")
} finally {
progress.dismissSafe()
try {
postProc()
} catch (ex: Throwable) {
log.trace(ex)
}
log.trace(ex)
}
}
}

View File

@ -5,6 +5,7 @@ import com.otaliastudios.transcoder.TranscoderListener
import com.otaliastudios.transcoder.common.Size
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
import jp.juggler.subwaytooter.global.appDispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
@ -74,7 +75,7 @@ suspend fun transcodeVideo(
resizeConfig: MovieResizeConfig,
onProgress: (Float) -> Unit,
): File = try {
withContext(Dispatchers.IO) {
withContext(appDispatchers.io) {
if (!resizeConfig.isTranscodeRequired(info)) {
log.i("transcodeVideo: isTranscodeRequired returns false.")
return@withContext inFile

View File

@ -10,7 +10,6 @@ import android.webkit.MimeTypeMap
import androidx.annotation.RawRes
import okhttp3.internal.closeQuietly
import java.io.InputStream
import java.util.*
// internal object StorageUtils{
//
@ -294,7 +293,7 @@ fun Intent.handleGetContentResult(contentResolver: ContentResolver): ArrayList<G
// 複数選択
this.clipData?.let { clipData ->
(0 until clipData.itemCount).mapNotNull { clipData.getItemAt(it)?.uri }.forEach { uri ->
if (null == urlList.find { it.uri == uri }) {
if (urlList.none { it.uri == uri }) {
urlList.add(GetContentResultEntry(uri))
}
}

View File

@ -1151,4 +1151,7 @@
<string name="content">本文</string>
<string name="enable_misskey_notification_check">Misskeyサーバで通知チェックを行う(不安定)</string>
<string name="old_devices_warning">古い端末のサポート終了</string>
<string name="followed_tags">フォロー中のハッシュタグ</string>
<string name="follow_hashtag_of">\"%1$s\"のフォロー</string>
<string name="unfollow_hashtag_of">\"%1$s\"のフォロー解除</string>
</resources>

View File

@ -1160,4 +1160,7 @@
<string name="content">Content</string>
<string name="enable_misskey_notification_check">Enable notification check for Misskey server (unstable)</string>
<string name="old_devices_warning">End of support for older devices</string>
<string name="followed_tags">Followed hashtags</string>
<string name="follow_hashtag_of">Follow %1$s</string>
<string name="unfollow_hashtag_of">Unfollow %1$s</string>
</resources>