detektで指摘された問題の対応
This commit is contained in:
parent
ab3b73a1fa
commit
b148c6dd03
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -206,6 +206,7 @@ fun openCustomTab(
|
|||
val tagInfo = url.findHashtagFromUrl()
|
||||
if (tagInfo != null) {
|
||||
activity.tagDialog(
|
||||
accessInfo,
|
||||
pos,
|
||||
url,
|
||||
Host.parse(tagInfo.second),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue