通知チェック部分のコードをコルーチン対応にした。AppOpenerはいくつかのOSアクティビティを選択肢から除去するようになった。
This commit is contained in:
parent
301dde36c0
commit
8818f25b6f
|
@ -84,6 +84,7 @@
|
|||
<w>mastodonsearch</w>
|
||||
<w>mimumedon</w>
|
||||
<w>misskey</w>
|
||||
<w>misskeyclientproto</w>
|
||||
<w>miyon</w>
|
||||
<w>mpeg</w>
|
||||
<w>mpga</w>
|
||||
|
@ -126,6 +127,7 @@
|
|||
<w>styler</w>
|
||||
<w>subwaytooter</w>
|
||||
<w>swipy</w>
|
||||
<w>systemui</w>
|
||||
<w>taisaku</w>
|
||||
<w>tateisu</w>
|
||||
<w>tbody</w>
|
||||
|
|
|
@ -174,7 +174,7 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
lateinit var app_state: AppState
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// 変更しない変数
|
||||
// 読み取り専用のプロパティ
|
||||
|
||||
val follow_complete_callback: () -> Unit = {
|
||||
showToast(false, R.string.follow_succeeded)
|
||||
|
@ -229,85 +229,71 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
private val link_click_listener: (View, MyClickableSpan) -> Unit = { viewClicked, span ->
|
||||
|
||||
val linkInfo = span.linkInfo
|
||||
var view = viewClicked
|
||||
// ビュー階層を下から辿って文脈を取得する
|
||||
var column: Column? = null
|
||||
var whoRef: TootAccountRef? = null
|
||||
|
||||
while(true) {
|
||||
val tag = view.tag
|
||||
if(tag is ItemViewHolder) {
|
||||
var view = viewClicked
|
||||
loop@ while (true) {
|
||||
when (val tag = view.tag) {
|
||||
is ItemViewHolder -> {
|
||||
column = tag.column
|
||||
whoRef = tag.getAccount()
|
||||
|
||||
break
|
||||
} else if(tag is ViewHolderItem) {
|
||||
break@loop
|
||||
}
|
||||
is ViewHolderItem -> {
|
||||
column = tag.ivh.column
|
||||
whoRef = tag.ivh.getAccount()
|
||||
break
|
||||
} else if(tag is ColumnViewHolder) {
|
||||
break@loop
|
||||
}
|
||||
is ColumnViewHolder -> {
|
||||
column = tag.column
|
||||
whoRef = null
|
||||
break
|
||||
} else if(tag is ViewHolderHeaderBase) {
|
||||
break@loop
|
||||
}
|
||||
is ViewHolderHeaderBase -> {
|
||||
column = tag.column
|
||||
whoRef = tag.getAccount()
|
||||
break
|
||||
} else if(tag is TabletColumnViewHolder) {
|
||||
break@loop
|
||||
}
|
||||
is TabletColumnViewHolder -> {
|
||||
column = tag.columnViewHolder.column
|
||||
break
|
||||
} else {
|
||||
val parent = view.parent
|
||||
if(parent is View) {
|
||||
view = parent
|
||||
} else {
|
||||
break
|
||||
break@loop
|
||||
}
|
||||
else -> when (val parent = view.parent) {
|
||||
is View -> view = parent
|
||||
else -> break@loop
|
||||
}
|
||||
}
|
||||
}
|
||||
val pos = nextPosition(column)
|
||||
val access_info = column?.access_info
|
||||
|
||||
var tag_list : ArrayList<String>? = null
|
||||
|
||||
val hashtagList = ArrayList<String>().apply {
|
||||
try {
|
||||
val cs = (viewClicked as TextView).text
|
||||
val cs = viewClicked.cast<TextView>()?.text
|
||||
if (cs is Spannable) {
|
||||
for (s in cs.getSpans(0, cs.length, MyClickableSpan::class.java)) {
|
||||
val li = s.linkInfo
|
||||
val pair = li.url.findHashtagFromUrl()
|
||||
if(pair != null) {
|
||||
if(tag_list == null) tag_list = ArrayList()
|
||||
tag_list.add(if(li.text.startsWith('#')) li.text else "#${pair.first}")
|
||||
}
|
||||
if (pair != null) add(if (li.text.startsWith('#')) li.text else "#${pair.first}")
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
|
||||
val linkInfo = span.linkInfo
|
||||
|
||||
openCustomTab(
|
||||
this@ActMain,
|
||||
pos,
|
||||
this,
|
||||
nextPosition(column),
|
||||
linkInfo.url,
|
||||
accessInfo = access_info,
|
||||
tagList = tag_list,
|
||||
accessInfo = column?.access_info,
|
||||
tagList = hashtagList.notEmpty(),
|
||||
whoRef = whoRef,
|
||||
linkInfo = linkInfo
|
||||
)
|
||||
}
|
||||
|
||||
private fun showQuickTootVisibility() {
|
||||
btnQuickTootMenu.imageResource =
|
||||
when(val resId = Styler.getVisibilityIconId(false, quickTootVisibility)) {
|
||||
R.drawable.ic_question -> R.drawable.ic_description
|
||||
else -> resId
|
||||
}
|
||||
}
|
||||
|
||||
private fun performQuickTootMenu() {
|
||||
dlgQuickTootMenu.toggle()
|
||||
}
|
||||
|
||||
private val dlgQuickTootMenu = DlgQuickTootMenu(this, object : DlgQuickTootMenu.Callback {
|
||||
|
||||
|
@ -339,9 +325,6 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
val viewPool = RecyclerView.RecycledViewPool()
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// 読み取り専用のプロパティ
|
||||
|
||||
override val isActivityStart: Boolean
|
||||
get() = isStart_
|
||||
|
||||
|
@ -825,10 +808,8 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
drawer.openDrawer(GravityCompat.START)
|
||||
}
|
||||
|
||||
R.id.btnToot -> Action_Account.openPost(this@ActMain)
|
||||
|
||||
R.id.btnToot -> Action_Account.openPost(this)
|
||||
R.id.btnQuickToot -> performQuickPost(null)
|
||||
|
||||
R.id.btnQuickTootMenu -> performQuickTootMenu()
|
||||
}
|
||||
}
|
||||
|
@ -873,6 +854,18 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
return defaultInsertPosition
|
||||
}
|
||||
|
||||
private fun showQuickTootVisibility() {
|
||||
btnQuickTootMenu.imageResource =
|
||||
when (val resId = Styler.getVisibilityIconId(false, quickTootVisibility)) {
|
||||
R.drawable.ic_question -> R.drawable.ic_description
|
||||
else -> resId
|
||||
}
|
||||
}
|
||||
|
||||
private fun performQuickTootMenu() {
|
||||
dlgQuickTootMenu.toggle()
|
||||
}
|
||||
|
||||
private fun refreshAfterPost() {
|
||||
val posted_acct = this.posted_acct
|
||||
val posted_status_id = this.posted_status_id
|
||||
|
@ -1095,15 +1088,14 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
REQUEST_CODE_ACCOUNT_SETTING -> {
|
||||
updateColumnStrip()
|
||||
|
||||
for(column in app_state.column_list) {
|
||||
column.fireShowColumnHeader()
|
||||
}
|
||||
app_state.column_list.forEach { it.fireShowColumnHeader() }
|
||||
|
||||
if(resultCode == Activity.RESULT_OK && data != null) {
|
||||
openBrowser(data.data)
|
||||
} else if(resultCode == ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN && data != null) {
|
||||
val db_id = data.getLongExtra(ActAccountSetting.EXTRA_DB_ID, - 1L)
|
||||
checkAccessToken2(db_id)
|
||||
when (resultCode) {
|
||||
RESULT_OK -> data?.data?.let { openBrowser(it) }
|
||||
|
||||
ActAccountSetting.RESULT_INPUT_ACCESS_TOKEN ->
|
||||
data?.getLongExtra(ActAccountSetting.EXTRA_DB_ID, -1L)
|
||||
?.takeIf { it != -1L }?.let { checkAccessToken2(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1113,43 +1105,14 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
updateColumnStrip()
|
||||
|
||||
if (resultCode == RESULT_APP_DATA_IMPORT) {
|
||||
importAppData(data?.data)
|
||||
data?.data?.let { importAppData(it) }
|
||||
}
|
||||
}
|
||||
|
||||
REQUEST_CODE_TEXT -> when (resultCode) {
|
||||
ActText.RESULT_SEARCH_MSP -> {
|
||||
val text = data?.getStringExtra(Intent.EXTRA_TEXT) ?: ""
|
||||
addColumn(
|
||||
false,
|
||||
defaultInsertPosition,
|
||||
SavedAccount.na,
|
||||
ColumnType.SEARCH_MSP,
|
||||
text
|
||||
)
|
||||
}
|
||||
|
||||
ActText.RESULT_SEARCH_TS -> {
|
||||
val text = data?.getStringExtra(Intent.EXTRA_TEXT) ?: ""
|
||||
addColumn(
|
||||
false,
|
||||
defaultInsertPosition,
|
||||
SavedAccount.na,
|
||||
ColumnType.SEARCH_TS,
|
||||
text
|
||||
)
|
||||
}
|
||||
|
||||
ActText.RESULT_SEARCH_NOTESTOCK -> {
|
||||
val text = data?.getStringExtra(Intent.EXTRA_TEXT) ?: ""
|
||||
addColumn(
|
||||
false,
|
||||
defaultInsertPosition,
|
||||
SavedAccount.na,
|
||||
ColumnType.SEARCH_NOTESTOCK,
|
||||
text
|
||||
)
|
||||
}
|
||||
ActText.RESULT_SEARCH_MSP -> searchFromActivityResult(data, ColumnType.SEARCH_MSP)
|
||||
ActText.RESULT_SEARCH_TS -> searchFromActivityResult(data, ColumnType.SEARCH_TS)
|
||||
ActText.RESULT_SEARCH_NOTESTOCK -> searchFromActivityResult(data, ColumnType.SEARCH_NOTESTOCK)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1725,9 +1688,10 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
when (uri.scheme) {
|
||||
"subwaytooter", "misskeyclientproto" -> return try {
|
||||
handleOAuth2CallbackUri(uri)
|
||||
handleCustomSchemaUri(uri)
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
showToast(ex, "handleCustomSchemaUri failed.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1852,49 +1816,37 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
}
|
||||
|
||||
private fun handleOAuth2CallbackUri(uri : Uri) {
|
||||
|
||||
// 通知タップ
|
||||
// subwaytooter://notification_click/?db_id=(db_id)
|
||||
val dataIdString = uri.getQueryParameter("db_id")
|
||||
if(dataIdString != null) {
|
||||
private fun handleNotificationClick(uri: Uri, dataIdString: String) {
|
||||
try {
|
||||
val account = dataIdString.toLongOrNull()?.let { SavedAccount.loadAccount(this, it) }
|
||||
if (account == null) {
|
||||
showToast(true, "handleNotificationClick: missing SavedAccount. id=$dataIdString")
|
||||
return
|
||||
}
|
||||
|
||||
PollingWorker.queueNotificationClicked(this, uri)
|
||||
|
||||
try {
|
||||
val dataId = dataIdString.toLong()
|
||||
val account = SavedAccount.loadAccount(this@ActMain, dataId)
|
||||
if(account != null) {
|
||||
var column = app_state.column_list.firstOrNull {
|
||||
it.type == ColumnType.NOTIFICATIONS
|
||||
&& account == it.access_info
|
||||
&& ! it.system_notification_not_related
|
||||
}
|
||||
if(column != null) {
|
||||
val index = app_state.column_list.indexOf(column)
|
||||
scrollToColumn(index)
|
||||
} else {
|
||||
column = addColumn(
|
||||
val column = app_state.column_list.firstOrNull {
|
||||
it.type == ColumnType.NOTIFICATIONS &&
|
||||
it.access_info == account &&
|
||||
!it.system_notification_not_related
|
||||
}?.also {
|
||||
scrollToColumn(app_state.column_list.indexOf(it))
|
||||
} ?: addColumn(
|
||||
true,
|
||||
defaultInsertPosition,
|
||||
account,
|
||||
ColumnType.NOTIFICATIONS
|
||||
)
|
||||
}
|
||||
|
||||
// 通知を読み直す
|
||||
if(! column.bInitialLoading) {
|
||||
column.startLoading()
|
||||
}
|
||||
}
|
||||
if (!column.bInitialLoading) column.startLoading()
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// OAuth2 認証コールバック
|
||||
// subwaytooter://oauth(\d*)/?...
|
||||
private fun handleOAuth2Callback(uri: Uri) {
|
||||
TootTaskRunner(this@ActMain).run(object : TootTask {
|
||||
|
||||
var ta: TootAccount? = null
|
||||
|
@ -1911,20 +1863,23 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
// Misskey 認証コールバック
|
||||
val token = uri.getQueryParameter("token")
|
||||
if(token?.isEmpty() != false) {
|
||||
if (token.isNullOrBlank())
|
||||
return TootApiResult("missing token in callback URL")
|
||||
}
|
||||
val prefDevice = PrefDevice.prefDevice(this@ActMain)
|
||||
|
||||
val db_id = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, - 1L)
|
||||
val prefDevice = PrefDevice.prefDevice(this@ActMain)
|
||||
|
||||
val instance = Host.parse(
|
||||
prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE, null)
|
||||
?: return TootApiResult("missing instance name.")
|
||||
)
|
||||
|
||||
if(db_id != - 1L) {
|
||||
try {
|
||||
when (val db_id = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID, -1L)) {
|
||||
|
||||
// new registration
|
||||
-1L -> client.apiHost = instance
|
||||
|
||||
// update access token
|
||||
else -> try {
|
||||
val sa = SavedAccount.loadAccount(this@ActMain, db_id)
|
||||
?: return TootApiResult("missing account db_id=$db_id")
|
||||
this.sa = sa
|
||||
|
@ -1933,8 +1888,6 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
log.trace(ex)
|
||||
return TootApiResult(ex.withCaption("invalid state"))
|
||||
}
|
||||
} else {
|
||||
client.apiHost = instance
|
||||
}
|
||||
|
||||
val (ti, r2) = TootInstance.get(client)
|
||||
|
@ -1942,17 +1895,20 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
this.ti = ti
|
||||
this.host = instance
|
||||
val client_name = Pref.spClientName(this@ActMain)
|
||||
val result =
|
||||
client.authentication2Misskey(client_name, token, ti.misskeyVersion)
|
||||
this.ta = TootParser(
|
||||
|
||||
val parser = TootParser(
|
||||
this@ActMain,
|
||||
linkHelper = LinkHelper.create(
|
||||
instance,
|
||||
misskeyVersion = ti.misskeyVersion
|
||||
)
|
||||
).account(result?.jsonObject)
|
||||
return result
|
||||
)
|
||||
|
||||
return client.authentication2Misskey(
|
||||
Pref.spClientName(this@ActMain),
|
||||
token,
|
||||
ti.misskeyVersion
|
||||
)?.also { this.ta = parser.account(it.jsonObject) }
|
||||
|
||||
} else {
|
||||
// Mastodon 認証コールバック
|
||||
|
@ -1962,28 +1918,27 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
// ?error=access_denied
|
||||
// &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
|
||||
// &state=db%3A3
|
||||
val error = uri.getQueryParameter("error_description")
|
||||
if(error?.isNotEmpty() == true) {
|
||||
return TootApiResult(error)
|
||||
}
|
||||
val error = uri.getQueryParameter("error")
|
||||
val error_description = uri.getQueryParameter("error_description")
|
||||
if (error != null || error_description != null)
|
||||
return TootApiResult(error_description.notBlank() ?: error.notBlank()
|
||||
?: "?")
|
||||
|
||||
// subwaytooter://oauth(\d*)/
|
||||
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
|
||||
// &state=host%3Amastodon.juggler.jp
|
||||
|
||||
val code = uri.getQueryParameter("code")
|
||||
if(code?.isEmpty() != false) {
|
||||
return TootApiResult("missing code in callback url.")
|
||||
}
|
||||
|
||||
val sv = uri.getQueryParameter("state")
|
||||
if(sv?.isEmpty() != false) {
|
||||
|
||||
if (code.isNullOrBlank())
|
||||
return TootApiResult("missing code in callback url.")
|
||||
|
||||
if (sv.isNullOrBlank())
|
||||
return TootApiResult("missing state in callback url.")
|
||||
}
|
||||
|
||||
for (param in sv.split(",")) {
|
||||
when {
|
||||
|
||||
param.startsWith("db:") -> try {
|
||||
val dataId = param.substring(3).toLong(10)
|
||||
val sa = SavedAccount.loadAccount(this@ActMain, dataId)
|
||||
|
@ -2000,11 +1955,9 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
client.apiHost = host
|
||||
}
|
||||
|
||||
else -> {
|
||||
// ignore other parameter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val instance = client.apiHost
|
||||
?: return TootApiResult("missing instance in callback url.")
|
||||
|
@ -2014,15 +1967,17 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
this.ti = ti
|
||||
this.host = instance
|
||||
val client_name = Pref.spClientName(this@ActMain)
|
||||
val result = client.authentication2(client_name, code)
|
||||
this.ta = TootParser(
|
||||
|
||||
val parser = TootParser(
|
||||
this@ActMain,
|
||||
linkHelper = LinkHelper.create(instance)
|
||||
).account(result?.jsonObject)
|
||||
return result
|
||||
}
|
||||
)
|
||||
|
||||
return client.authentication2(
|
||||
Pref.spClientName(this@ActMain),
|
||||
code
|
||||
)?.also { this.ta = parser.account(it.jsonObject) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleResult(result: TootApiResult?) {
|
||||
|
@ -2042,6 +1997,18 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
})
|
||||
}
|
||||
|
||||
private fun handleCustomSchemaUri(uri: Uri) {
|
||||
val dataIdString = uri.getQueryParameter("db_id")
|
||||
if (dataIdString != null) {
|
||||
// subwaytooter://notification_click/?db_id=(db_id)
|
||||
handleNotificationClick(uri, dataIdString)
|
||||
} else {
|
||||
// OAuth2 認証コールバック
|
||||
// subwaytooter://oauth(\d*)/?...
|
||||
handleOAuth2Callback(uri)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun afterAccountVerify(
|
||||
result: TootApiResult?,
|
||||
ta: TootAccount?,
|
||||
|
@ -2049,22 +2016,21 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
ti: TootInstance?,
|
||||
host: Host?
|
||||
): Boolean {
|
||||
result ?: return false
|
||||
|
||||
val jsonObject = result?.jsonObject
|
||||
val token_info = result?.tokenInfo
|
||||
val error = result?.error
|
||||
val jsonObject = result.jsonObject
|
||||
val token_info = result.tokenInfo
|
||||
val error = result.error
|
||||
|
||||
when {
|
||||
result == null -> {
|
||||
// cancelled.
|
||||
}
|
||||
|
||||
error != null ->
|
||||
showToast(true, "${result.error} ${result.requestInfo}".trim())
|
||||
|
||||
token_info == null -> showToast(true, "can't get access token.")
|
||||
token_info == null ->
|
||||
showToast(true, "can't get access token.")
|
||||
|
||||
jsonObject == null -> showToast(true, "can't parse json response.")
|
||||
jsonObject == null ->
|
||||
showToast(true, "can't parse json response.")
|
||||
|
||||
// 自分のユーザネームを取れなかった
|
||||
// …普通はエラーメッセージが設定されてるはずだが
|
||||
|
@ -2072,16 +2038,10 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
// アクセストークン更新時
|
||||
// インスタンスは同じだと思うが、ユーザ名が異なる可能性がある
|
||||
sa != null ->
|
||||
if(sa.username != ta.username) {
|
||||
sa != null -> if (sa.username != ta.username) {
|
||||
showToast(true, R.string.user_name_not_match)
|
||||
} else {
|
||||
showToast(
|
||||
|
||||
false,
|
||||
R.string.access_token_updated_for,
|
||||
sa.acct.pretty
|
||||
)
|
||||
showToast(false, R.string.access_token_updated_for, sa.acct.pretty)
|
||||
|
||||
// DBの情報を更新する
|
||||
sa.updateTokenInfo(token_info)
|
||||
|
@ -2090,11 +2050,9 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
reloadAccountSetting()
|
||||
|
||||
// 自動でリロードする
|
||||
for(it in app_state.column_list) {
|
||||
if(it.access_info == sa) {
|
||||
it.startLoading()
|
||||
}
|
||||
}
|
||||
app_state.column_list
|
||||
.filter { it.access_info == sa }
|
||||
.forEach { it.startLoading() }
|
||||
|
||||
// 通知の更新が必要かもしれない
|
||||
PushSubscriptionHelper.clearLastCheck(sa)
|
||||
|
@ -2636,8 +2594,7 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private fun importAppData(uri : Uri?) {
|
||||
uri ?: return
|
||||
private fun importAppData(uri: Uri) {
|
||||
|
||||
// remove all columns
|
||||
phoneOnly { env -> env.pager.adapter = null }
|
||||
|
@ -2914,4 +2871,14 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
dialog.show()
|
||||
}
|
||||
|
||||
private fun searchFromActivityResult(data: Intent?, columnType: ColumnType) =
|
||||
data?.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
addColumn(
|
||||
false,
|
||||
defaultInsertPosition,
|
||||
SavedAccount.na,
|
||||
columnType,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat
|
|||
|
||||
import jp.juggler.util.LogCategory
|
||||
import jp.juggler.subwaytooter.util.NotificationHelper
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PollingForegrounder : IntentService("PollingForegrounder") {
|
||||
|
||||
|
@ -89,17 +90,17 @@ class PollingForegrounder : IntentService("PollingForegrounder") {
|
|||
|
||||
override fun onHandleIntent(intent : Intent?) {
|
||||
if(intent == null) return
|
||||
runBlocking {
|
||||
val tag = intent.getStringExtra(PollingWorker.EXTRA_TAG)
|
||||
val context = applicationContext
|
||||
PollingWorker.handleFCMMessage(this, tag, object : PollingWorker.JobStatusCallback {
|
||||
override fun onStatus(sv : String) {
|
||||
if(sv.isNotEmpty() && sv != last_status) {
|
||||
log.d("onStatus %s", sv)
|
||||
PollingWorker.handleFCMMessage(context, tag) { sv ->
|
||||
if (sv.isEmpty() || sv==last_status) return@handleFCMMessage
|
||||
// 状況が変化したらログと通知領域に出力する
|
||||
last_status = sv
|
||||
log.d("onStatus %s", sv)
|
||||
startForeground(NOTIFICATION_ID_FOREGROUNDER, createNotification(context, sv))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import android.net.ConnectivityManager
|
|||
import android.net.Uri
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.PowerManager
|
||||
import android.os.SystemClock
|
||||
import android.service.notification.StatusBarNotification
|
||||
|
@ -24,6 +23,7 @@ import androidx.core.content.ContextCompat
|
|||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import jp.juggler.subwaytooter.api.TootApiCallback
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.table.*
|
||||
|
@ -31,7 +31,9 @@ import jp.juggler.subwaytooter.table.NotificationCache.Companion.getEntityOrderI
|
|||
import jp.juggler.subwaytooter.table.NotificationCache.Companion.parseNotificationType
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
import jp.juggler.util.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
|
@ -41,16 +43,12 @@ import java.util.*
|
|||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class PollingWorker private constructor(contextArg: Context) {
|
||||
|
||||
interface JobStatusCallback {
|
||||
|
||||
fun onStatus(sv: String)
|
||||
}
|
||||
|
||||
enum class TrackingType(val str: String) {
|
||||
All("all"),
|
||||
Reply("reply"),
|
||||
|
@ -68,6 +66,14 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
}
|
||||
|
||||
internal class Data(val access_info: SavedAccount, val notification: TootNotification)
|
||||
|
||||
internal class InjectData {
|
||||
|
||||
var account_db_id: Long = 0
|
||||
val list = ArrayList<TootNotification>()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
internal val log = LogCategory("PollingWorker")
|
||||
|
@ -153,7 +159,6 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
}
|
||||
|
||||
|
||||
|
||||
// インストールIDを生成する前に、各データの通知登録キャッシュをクリアする
|
||||
// トークンがまだ生成されていない場合、このメソッドは null を返します。
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
|
@ -371,29 +376,43 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
addTask(context, true, TASK_PACKAGE_REPLACED, null)
|
||||
}
|
||||
|
||||
internal val job_status = AtomicReference<String>(null)
|
||||
private val job_status = AtomicReference<String>(null)
|
||||
|
||||
fun handleFCMMessage(context: Context, tag: String?, callback: JobStatusCallback) {
|
||||
private var workerStatus: String
|
||||
get() = job_status.get()
|
||||
set(x) {
|
||||
log.d("workerStatus:$x")
|
||||
job_status.set(x)
|
||||
}
|
||||
|
||||
// IntentServiceが作ったスレッドから呼ばれる
|
||||
suspend fun handleFCMMessage(
|
||||
context: Context,
|
||||
tag: String?,
|
||||
progress: (String) -> Unit
|
||||
) {
|
||||
log.d("handleFCMMessage: start. tag=$tag")
|
||||
|
||||
val time_start = SystemClock.elapsedRealtime()
|
||||
|
||||
callback.onStatus("=>")
|
||||
// この呼出でIntentServiceがstartForegroundする
|
||||
progress("=>")
|
||||
|
||||
// タスクを追加
|
||||
val data = JsonObject().apply {
|
||||
try {
|
||||
putNotNull(EXTRA_TAG, tag)
|
||||
task_list.addLast(
|
||||
context,
|
||||
true,
|
||||
JsonObject().apply {
|
||||
this[EXTRA_TASK_ID] = TASK_FCM_MESSAGE
|
||||
} catch (_: JsonException) {
|
||||
}
|
||||
if (tag != null) this[EXTRA_TAG] = tag
|
||||
}
|
||||
)
|
||||
|
||||
task_list.addLast(context, true, data)
|
||||
|
||||
callback.onStatus("==>")
|
||||
progress("==>")
|
||||
|
||||
// 疑似ジョブを開始
|
||||
val pw = getInstance(context)
|
||||
|
||||
pw.addJobFCM()
|
||||
|
||||
// 疑似ジョブが終了するまで待機する
|
||||
|
@ -407,49 +426,35 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
)
|
||||
break
|
||||
}
|
||||
|
||||
// ジョブの状況を通知する
|
||||
var sv: String? = job_status.get()
|
||||
if (sv == null) sv = "(null)"
|
||||
callback.onStatus(sv)
|
||||
progress(job_status.get() ?: "(null)")
|
||||
|
||||
// 少し待機
|
||||
try {
|
||||
Thread.sleep(50L)
|
||||
} catch (ex: InterruptedException) {
|
||||
log.e(ex, "handleFCMMessage: blocking is interrupted.")
|
||||
break
|
||||
delay(50L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val context: Context
|
||||
private val appState: AppState
|
||||
internal val handler: Handler
|
||||
internal val pref: SharedPreferences
|
||||
private val connectivityManager: ConnectivityManager
|
||||
internal val notification_manager: NotificationManager
|
||||
internal val scheduler: JobScheduler
|
||||
private val power_manager: PowerManager?
|
||||
internal val power_lock: PowerManager.WakeLock
|
||||
private val power_lock: PowerManager.WakeLock
|
||||
private val wifi_manager: WifiManager?
|
||||
internal val wifi_lock: WifiManager.WifiLock
|
||||
|
||||
private var worker: Worker
|
||||
private val wifi_lock: WifiManager.WifiLock
|
||||
|
||||
internal val job_list = LinkedList<JobItem>()
|
||||
|
||||
internal class Data(val access_info: SavedAccount, val notification: TootNotification)
|
||||
|
||||
internal class InjectData {
|
||||
|
||||
var account_db_id: Long = 0
|
||||
val list = ArrayList<TootNotification>()
|
||||
}
|
||||
private val workerJob: Job
|
||||
private val workerNotifier = Channel<Unit>(capacity = Channel.CONFLATED)
|
||||
|
||||
init {
|
||||
log.d("ctor")
|
||||
log.d("init")
|
||||
|
||||
val context = contextArg.applicationContext
|
||||
|
||||
|
@ -457,9 +462,8 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
// クラッシュレポートによると App1.onCreate より前にここを通る場合がある
|
||||
// データベースへアクセスできるようにする
|
||||
this.appState = App1.prepare(context, "PollingWorker.ctor()")
|
||||
this.appState = App1.prepare(context, "PollingWorker.init")
|
||||
this.pref = App1.pref
|
||||
this.handler = appState.handler
|
||||
|
||||
this.connectivityManager = systemService(context)
|
||||
?: error("missing ConnectivityManager system service")
|
||||
|
@ -496,18 +500,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
wifi_lock.setReferenceCounted(false)
|
||||
|
||||
//
|
||||
worker = Worker()
|
||||
worker.start()
|
||||
}
|
||||
|
||||
inner class Worker : WorkerBase() {
|
||||
|
||||
val bThreadCancelled = AtomicBoolean(false)
|
||||
|
||||
override fun cancel() {
|
||||
bThreadCancelled.set(true)
|
||||
notifyEx()
|
||||
workerJob = GlobalScope.launch(Dispatchers.Default) { worker() }
|
||||
}
|
||||
|
||||
@SuppressLint("WakelockTimeout")
|
||||
|
@ -547,48 +540,47 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
log.d("worker thread start.")
|
||||
job_status.set("worker thread start.")
|
||||
while (!bThreadCancelled.get()) {
|
||||
private suspend fun worker() {
|
||||
workerStatus = "worker start."
|
||||
try {
|
||||
val item: JobItem? = synchronized(job_list) {
|
||||
suspend fun isActive() = coroutineContext[Job]?.isActive == true
|
||||
while (isActive()) {
|
||||
while (true) {
|
||||
handleJobItem(synchronized(job_list) {
|
||||
for (ji in job_list) {
|
||||
if (bThreadCancelled.get()) break
|
||||
if (ji.mJobCancelled_.get()) continue
|
||||
if (ji.mWorkerAttached.compareAndSet(false, true)) {
|
||||
return@synchronized ji
|
||||
}
|
||||
}
|
||||
null
|
||||
} ?: break)
|
||||
}
|
||||
try {
|
||||
workerNotifier.receive()
|
||||
} catch (ex: ClosedReceiveChannelException) {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
workerStatus = "worker end."
|
||||
}
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
job_status.set("no job to run.")
|
||||
waitEx(86400000L)
|
||||
continue
|
||||
}
|
||||
|
||||
job_status.set("start job " + item.jobId)
|
||||
private suspend fun handleJobItem(item: JobItem) {
|
||||
try {
|
||||
workerStatus = "start job ${item.jobId}"
|
||||
acquirePowerLock()
|
||||
try {
|
||||
item.refWorker.set(this@Worker)
|
||||
item.run()
|
||||
} finally {
|
||||
job_status.set("end job " + item.jobId)
|
||||
item.refWorker.set(null)
|
||||
releasePowerLock()
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
}
|
||||
job_status.set("worker thread end.")
|
||||
log.d("worker thread end.")
|
||||
} finally {
|
||||
workerStatus = "end job ${item.jobId}"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -612,7 +604,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
}
|
||||
|
||||
// JobService#onStartJob から呼ばれる
|
||||
fun onStartJob(jobService: JobService, params: JobParameters): Boolean {
|
||||
suspend fun onStartJob(jobService: JobService, params: JobParameters): Boolean {
|
||||
val item = JobItem(jobService, params)
|
||||
addJob(item, true)
|
||||
return true
|
||||
|
@ -631,11 +623,11 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
}
|
||||
|
||||
// FCMメッセージイベントから呼ばれる
|
||||
private fun addJobFCM() {
|
||||
private suspend fun addJobFCM() {
|
||||
addJob(JobItem(JOB_FCM), false)
|
||||
}
|
||||
|
||||
private fun addJob(item: JobItem, bRemoveOld: Boolean) {
|
||||
private suspend fun addJob(item: JobItem, bRemoveOld: Boolean) {
|
||||
val jobId = item.jobId
|
||||
|
||||
// 同じジョブ番号がジョブリストにあるか?
|
||||
|
@ -656,7 +648,8 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
job_list.add(item)
|
||||
}
|
||||
|
||||
worker.notifyEx()
|
||||
workerNotifier.send(Unit)
|
||||
|
||||
}
|
||||
|
||||
// JobService#onStopJob から呼ばれる
|
||||
|
@ -705,14 +698,9 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
var current_call: Call? = null
|
||||
|
||||
val refWorker = AtomicReference<Worker>(null)
|
||||
|
||||
val isJobCancelled: Boolean
|
||||
get() {
|
||||
if (mJobCancelled_.get()) return true
|
||||
val worker = refWorker.get()
|
||||
return worker != null && worker.bThreadCancelled.get()
|
||||
}
|
||||
get() = mJobCancelled_.get() || workerJob.isCancelled
|
||||
|
||||
constructor(jobService: JobService, params: JobParameters) {
|
||||
this.jobParams = params
|
||||
|
@ -726,45 +714,34 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
this.refJobService = null
|
||||
}
|
||||
|
||||
fun notifyWorkerThread() {
|
||||
val worker = refWorker.get()
|
||||
worker?.notifyEx()
|
||||
}
|
||||
|
||||
fun waitWorkerThread(ms: Long) {
|
||||
val worker = refWorker.get()
|
||||
worker?.waitEx(ms)
|
||||
}
|
||||
|
||||
fun cancel(bReschedule: Boolean) {
|
||||
mJobCancelled_.set(true)
|
||||
mReschedule.set(bReschedule)
|
||||
current_call?.cancel()
|
||||
notifyWorkerThread()
|
||||
runBlocking { workerNotifier.send(Unit) }
|
||||
}
|
||||
|
||||
fun run() {
|
||||
|
||||
job_status.set("job start.")
|
||||
suspend fun run() = coroutineScope {
|
||||
workerStatus = "job start."
|
||||
try {
|
||||
log.d("(JobItem.run jobId=${jobId}")
|
||||
if (isJobCancelled) throw JobCancelledException()
|
||||
|
||||
job_status.set("check network status..")
|
||||
workerStatus = "check network status.."
|
||||
|
||||
val net_wait_start = SystemClock.elapsedRealtime()
|
||||
var connectionState: String? = null
|
||||
try {
|
||||
withTimeout(10000L) {
|
||||
while (true) {
|
||||
val connectionState = App1.getAppState(context, "PollingWorker.JobItem.run()")
|
||||
.networkTracker.connectionState
|
||||
?: break
|
||||
if (isJobCancelled) throw JobCancelledException()
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val delta = now - net_wait_start
|
||||
if (delta >= 10000L) {
|
||||
log.d("network state timeout. $connectionState")
|
||||
break
|
||||
connectionState = App1.getAppState(context, "PollingWorker.JobItem.run()")
|
||||
.networkTracker.connectionState
|
||||
?: break // null if connected
|
||||
delay(333L)
|
||||
}
|
||||
waitWorkerThread(333L)
|
||||
}
|
||||
} catch (ex: TimeoutCancellationException) {
|
||||
log.d("network state timeout. $connectionState")
|
||||
}
|
||||
|
||||
muted_app = MutedApp.nameSet
|
||||
|
@ -783,7 +760,8 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
// タスクがなかった場合でも定期実行ジョブからの実行ならポーリングを行う
|
||||
TaskRunner().runTask(this@JobItem, TASK_POLLING, JsonObject())
|
||||
}
|
||||
job_status.set("make next schedule.")
|
||||
|
||||
workerStatus = "make next schedule."
|
||||
|
||||
log.d("pollingComplete=${bPollingComplete},isJobCancelled=${isJobCancelled},bPollingRequired=${bPollingRequired.get()}")
|
||||
|
||||
|
@ -809,33 +787,32 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
log.trace(ex)
|
||||
log.e(ex, "job execution failed.")
|
||||
} finally {
|
||||
job_status.set("job finished.")
|
||||
workerStatus = "job finished."
|
||||
}
|
||||
// ジョブ終了報告
|
||||
if (!isJobCancelled) {
|
||||
handler.post(Runnable {
|
||||
if (isJobCancelled) return@Runnable
|
||||
|
||||
log.d(")JobItem.run jobId=${jobId}, cancel=${isJobCancelled}")
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
if (isJobCancelled) return@launch
|
||||
|
||||
synchronized(job_list) {
|
||||
job_list.remove(this@JobItem)
|
||||
}
|
||||
|
||||
refJobService?.get()?.let { jobService ->
|
||||
try {
|
||||
val jobService = refJobService?.get()
|
||||
if (jobService != null) {
|
||||
// ジョブ終了報告
|
||||
val willReschedule = mReschedule.get()
|
||||
log.d("sending jobFinished. willReschedule=$willReschedule")
|
||||
jobService.jobFinished(jobParams, willReschedule)
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex, "jobFinished failed(1).")
|
||||
}
|
||||
})
|
||||
}
|
||||
log.d(")JobItem.run jobId=${jobId}, cancel=${isJobCancelled}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun TrackingType.trackingTypeName() = when (this) {
|
||||
TrackingType.NotReply -> NotificationHelper.TRACKING_NAME_DEFAULT
|
||||
|
@ -850,16 +827,15 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
val error_instance = ArrayList<String>()
|
||||
|
||||
fun runTask(job: JobItem, taskId: Int, taskData: JsonObject) {
|
||||
try {
|
||||
log.d("(runTask: taskId=${taskId}")
|
||||
job_status.set("start task $taskId")
|
||||
suspend fun runTask(job: JobItem, taskId: Int, taskData: JsonObject) {
|
||||
workerStatus = "start task $taskId"
|
||||
|
||||
this.job = job
|
||||
this.taskId = taskId
|
||||
|
||||
var process_db_id = -1L //
|
||||
|
||||
coroutineScope {
|
||||
try {
|
||||
when (taskId) {
|
||||
TASK_APP_DATA_IMPORT_BEFORE -> {
|
||||
scheduler.cancelAll()
|
||||
|
@ -873,7 +849,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
}
|
||||
mBusyAppDataImportBefore.set(false)
|
||||
return
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
TASK_APP_DATA_IMPORT_AFTER -> {
|
||||
|
@ -886,7 +862,8 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
}
|
||||
|
||||
// アプリデータのインポート処理がビジーな間、他のジョブは実行されない
|
||||
if (mBusyAppDataImportBefore.get() || mBusyAppDataImportAfter.get()) return
|
||||
if (mBusyAppDataImportBefore.get() || mBusyAppDataImportAfter.get())
|
||||
return@coroutineScope
|
||||
|
||||
// タスクによってはポーリング前にすることがある
|
||||
when (taskId) {
|
||||
|
@ -945,7 +922,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
if (db_id != null) {
|
||||
NotificationTracking.updateRead(db_id, typeName)
|
||||
}
|
||||
return
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
TASK_NOTIFICATION_CLICK -> {
|
||||
|
@ -969,42 +946,45 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
// DB更新処理
|
||||
NotificationTracking.updateRead(db_id, typeName)
|
||||
}
|
||||
return
|
||||
return@coroutineScope
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
job_status.set("make install id")
|
||||
workerStatus = "make install id"
|
||||
|
||||
// インストールIDを生成する
|
||||
// インストールID生成時にSavedAccountテーブルを操作することがあるので
|
||||
// アカウントリストの取得より先に行う
|
||||
if (job.install_id == null) {
|
||||
job.install_id = runBlocking { prepareInstallId(context, job) }
|
||||
job.install_id = prepareInstallId(context, job)
|
||||
}
|
||||
|
||||
// アカウント別に処理スレッドを作る
|
||||
job_status.set("create account thread")
|
||||
val thread_list = LinkedList<AccountThread>()
|
||||
for (_a in SavedAccount.loadAccountList(context)) {
|
||||
if (_a.isPseudo) continue
|
||||
if (process_db_id != -1L && _a.db_id != process_db_id) continue
|
||||
val t = AccountThread(_a)
|
||||
thread_list.add(t)
|
||||
t.start()
|
||||
workerStatus = "create account thread"
|
||||
val thread_list = LinkedList<AccountRunner>()
|
||||
suspend fun startForAccount(_a: SavedAccount) {
|
||||
if (_a.isPseudo) return
|
||||
thread_list.add(AccountRunner(_a).apply { start() })
|
||||
}
|
||||
if (process_db_id != -1L) {
|
||||
// process_db_id が指定されているなら、そのdb_idだけ処理する
|
||||
SavedAccount.loadAccount(context, process_db_id)?.let { startForAccount(it) }
|
||||
} else {
|
||||
// 全てのアカウントを処理する
|
||||
SavedAccount.loadAccountList(context).forEach { startForAccount(it) }
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// 同じホスト名が重複しないようにSetに集める
|
||||
val liveSet = TreeSet<Host>()
|
||||
for (t in thread_list) {
|
||||
if (!t.isAlive) continue
|
||||
if (!t.isActive) continue
|
||||
if (job.isJobCancelled) t.cancel()
|
||||
liveSet.add(t.account.apiHost)
|
||||
}
|
||||
if (liveSet.isEmpty()) break
|
||||
|
||||
job_status.set("waiting " + liveSet.joinToString(", ") { it.pretty })
|
||||
job.waitWorkerThread(if (job.isJobCancelled) 100L else 1000L)
|
||||
workerStatus = "waiting ${liveSet.joinToString(", ") { it.pretty }}"
|
||||
delay(if (job.isJobCancelled) 100L else 1000L)
|
||||
}
|
||||
|
||||
synchronized(error_instance) {
|
||||
|
@ -1017,43 +997,63 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
log.trace(ex, "task execution failed.")
|
||||
} finally {
|
||||
log.d(")runTask: taskId=$taskId")
|
||||
job_status.set("end task $taskId")
|
||||
workerStatus = "end task $taskId"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AccountThread(
|
||||
val account: SavedAccount
|
||||
) : Thread(), CurrentCallCallback {
|
||||
internal inner class AccountRunner(val account: SavedAccount) {
|
||||
|
||||
private var current_call: Call? = null
|
||||
private var suspendJob: Job? = null
|
||||
|
||||
private lateinit var parser: TootParser
|
||||
|
||||
private lateinit var cache: NotificationCache
|
||||
|
||||
private var currentCall: WeakReference<Call>? = null
|
||||
|
||||
///////////////////
|
||||
|
||||
val isActive: Boolean
|
||||
get() = suspendJob?.isActive ?: true
|
||||
|
||||
private val onCallCreated: (Call) -> Unit =
|
||||
{ currentCall = WeakReference(it) }
|
||||
|
||||
private val client = TootApiClient(context, callback = object : TootApiCallback {
|
||||
override val isApiCancelled: Boolean
|
||||
get() = job.isJobCancelled
|
||||
})
|
||||
get() = job.isJobCancelled || (suspendJob?.isCancelled == true)
|
||||
}).apply {
|
||||
currentCallCallback = onCallCreated
|
||||
}
|
||||
|
||||
private val favMuteSet: HashSet<Acct> get() = job.favMuteSet
|
||||
private lateinit var parser: TootParser
|
||||
private lateinit var cache: NotificationCache
|
||||
|
||||
init {
|
||||
client.currentCallCallback = this
|
||||
}
|
||||
|
||||
override fun onCallCreated(call: Call) {
|
||||
current_call = call
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
try {
|
||||
current_call?.cancel()
|
||||
currentCall?.get()?.cancel()
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun run() = runBlocking { runSuspend() }
|
||||
|
||||
suspend fun start() {
|
||||
coroutineScope {
|
||||
this@AccountRunner.suspendJob = launch(Dispatchers.IO) {
|
||||
runSuspend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val onError: (TootApiResult) -> Unit = { result ->
|
||||
val sv = result.error
|
||||
if (sv?.contains("Timeout") == true && !account.dont_show_timeout) {
|
||||
synchronized(error_instance) {
|
||||
if (!error_instance.any { it == sv }) error_instance.add(sv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runSuspend() {
|
||||
try {
|
||||
|
@ -1099,32 +1099,15 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
this.cache = NotificationCache(account.db_id).apply {
|
||||
load()
|
||||
request(
|
||||
requestAsync(
|
||||
client,
|
||||
account,
|
||||
wps.flags,
|
||||
onError = { result ->
|
||||
val sv = result.error
|
||||
if (sv?.contains("Timeout") == true && !account.dont_show_timeout) {
|
||||
synchronized(error_instance) {
|
||||
var bFound = false
|
||||
for (x in error_instance) {
|
||||
if (x == sv) {
|
||||
bFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!bFound) {
|
||||
error_instance.add(sv)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isCancelled = {
|
||||
job.isJobCancelled
|
||||
}
|
||||
onError = onError,
|
||||
isCancelled = { job.isJobCancelled }
|
||||
)
|
||||
}
|
||||
|
||||
if (job.isJobCancelled) return
|
||||
|
||||
this.parser = TootParser(context, account)
|
||||
|
@ -1159,7 +1142,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
} finally {
|
||||
job.notifyWorkerThread()
|
||||
workerNotifier.send(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1244,6 +1227,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
internal fun updateNotification() {
|
||||
|
||||
|
||||
val notification_tag = when (trackingName) {
|
||||
"" -> "${account.db_id}/_"
|
||||
else -> "${account.db_id}/$trackingName"
|
||||
|
@ -1548,7 +1532,9 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getNotificationLine(item: Data): String {
|
||||
|
||||
val name = when (Pref.bpShowAcctInSystemNotification(pref)) {
|
||||
false -> item.notification.accountRef?.decoded_display_name
|
||||
|
||||
|
@ -1561,48 +1547,49 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
}
|
||||
}
|
||||
} ?: "?"
|
||||
return when (item.notification.type) {
|
||||
|
||||
return "- " + when (item.notification.type) {
|
||||
TootNotification.TYPE_MENTION,
|
||||
TootNotification.TYPE_REPLY ->
|
||||
"- " + context.getString(R.string.display_name_replied_by, name)
|
||||
context.getString(R.string.display_name_replied_by, name)
|
||||
|
||||
TootNotification.TYPE_RENOTE,
|
||||
TootNotification.TYPE_REBLOG ->
|
||||
"- " + context.getString(R.string.display_name_boosted_by, name)
|
||||
context.getString(R.string.display_name_boosted_by, name)
|
||||
|
||||
TootNotification.TYPE_QUOTE ->
|
||||
"- " + context.getString(R.string.display_name_quoted_by, name)
|
||||
context.getString(R.string.display_name_quoted_by, name)
|
||||
|
||||
TootNotification.TYPE_STATUS ->
|
||||
"- " + context.getString(R.string.display_name_posted_by, name)
|
||||
context.getString(R.string.display_name_posted_by, name)
|
||||
|
||||
TootNotification.TYPE_FOLLOW ->
|
||||
"- " + context.getString(R.string.display_name_followed_by, name)
|
||||
context.getString(R.string.display_name_followed_by, name)
|
||||
|
||||
TootNotification.TYPE_UNFOLLOW ->
|
||||
"- " + context.getString(R.string.display_name_unfollowed_by, name)
|
||||
context.getString(R.string.display_name_unfollowed_by, name)
|
||||
|
||||
TootNotification.TYPE_FAVOURITE ->
|
||||
"- " + context.getString(R.string.display_name_favourited_by, name)
|
||||
context.getString(R.string.display_name_favourited_by, name)
|
||||
|
||||
TootNotification.TYPE_REACTION ->
|
||||
"- " + context.getString(R.string.display_name_reaction_by, name)
|
||||
context.getString(R.string.display_name_reaction_by, name)
|
||||
|
||||
TootNotification.TYPE_VOTE,
|
||||
TootNotification.TYPE_POLL_VOTE_MISSKEY ->
|
||||
"- " + context.getString(R.string.display_name_voted_by, name)
|
||||
context.getString(R.string.display_name_voted_by, name)
|
||||
|
||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY ->
|
||||
"- " + context.getString(R.string.display_name_follow_request_by, name)
|
||||
context.getString(R.string.display_name_follow_request_by, name)
|
||||
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY ->
|
||||
"- " + context.getString(R.string.display_name_follow_request_accepted_by, name)
|
||||
context.getString(R.string.display_name_follow_request_accepted_by, name)
|
||||
|
||||
TootNotification.TYPE_POLL ->
|
||||
"- " + context.getString(R.string.end_of_polling_from, name)
|
||||
context.getString(R.string.end_of_polling_from, name)
|
||||
|
||||
else -> "- " + "?"
|
||||
else -> "?"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package jp.juggler.subwaytooter.api
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.table.ClientInfo
|
||||
|
@ -13,36 +12,10 @@ import java.util.*
|
|||
|
||||
class TootApiClient(
|
||||
internal val context: Context,
|
||||
internal val httpClient: SimpleHttpClient = SimpleHttpClientImpl(
|
||||
context,
|
||||
App1.ok_http_client
|
||||
),
|
||||
internal val httpClient: SimpleHttpClient =
|
||||
SimpleHttpClientImpl(context,App1.ok_http_client),
|
||||
internal val callback: TootApiCallback
|
||||
) {
|
||||
|
||||
// 認証に関する設定を保存する
|
||||
internal val pref: SharedPreferences
|
||||
|
||||
// インスタンスのホスト名
|
||||
var apiHost: Host? = null
|
||||
|
||||
// アカウントがある場合に使用する
|
||||
var account: SavedAccount? = null
|
||||
set(value) {
|
||||
apiHost = value?.apiHost
|
||||
field = value
|
||||
}
|
||||
|
||||
var currentCallCallback: CurrentCallCallback?
|
||||
get() = httpClient.currentCallCallback
|
||||
set(value) {
|
||||
httpClient.currentCallCallback = value
|
||||
}
|
||||
|
||||
init {
|
||||
pref = context.pref()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LogCategory("TootApiClient")
|
||||
|
@ -222,6 +195,25 @@ class TootApiClient(
|
|||
|
||||
}
|
||||
|
||||
// 認証に関する設定を保存する
|
||||
internal val pref = context.pref()
|
||||
|
||||
// インスタンスのホスト名
|
||||
var apiHost: Host? = null
|
||||
|
||||
// アカウントがある場合に使用する
|
||||
var account: SavedAccount? = null
|
||||
set(value) {
|
||||
apiHost = value?.apiHost
|
||||
field = value
|
||||
}
|
||||
|
||||
var currentCallCallback: (Call) -> Unit
|
||||
get() = httpClient.onCallCreated
|
||||
set(value) {
|
||||
httpClient.onCallCreated = value
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
internal val isApiCancelled: Boolean
|
||||
get() = callback.isApiCancelled
|
||||
|
@ -277,6 +269,46 @@ class TootApiClient(
|
|||
}
|
||||
}
|
||||
|
||||
// リクエストをokHttpに渡してレスポンスを取得する
|
||||
private suspend inline fun sendRequestAsync(
|
||||
result: TootApiResult,
|
||||
progressPath: String? = null,
|
||||
tmpOkhttpClient: OkHttpClient? = null,
|
||||
block: () -> Request
|
||||
): Boolean {
|
||||
return try {
|
||||
result.response = null
|
||||
result.bodyString = null
|
||||
result.data = null
|
||||
|
||||
val request = block()
|
||||
|
||||
result.requestInfo = "${request.method} ${progressPath ?: request.url.encodedPath}"
|
||||
|
||||
callback.publishApiProgress(
|
||||
context.getString(
|
||||
R.string.request_api, request.method, progressPath ?: request.url.encodedPath
|
||||
)
|
||||
)
|
||||
|
||||
val response = httpClient.getResponseAsync(request, tmpOkhttpClient = tmpOkhttpClient)
|
||||
result.response = response
|
||||
|
||||
null == result.error
|
||||
|
||||
} catch (ex: Throwable) {
|
||||
result.setError(
|
||||
"${result.caption}: ${
|
||||
ex.withCaption(
|
||||
context.resources,
|
||||
R.string.network_error
|
||||
)
|
||||
}"
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// レスポンスがエラーかボディがカラならエラー状態を設定する
|
||||
// 例外を出すかも
|
||||
internal fun readBodyString(
|
||||
|
@ -380,7 +412,6 @@ class TootApiClient(
|
|||
try {
|
||||
readBodyBytes(result, progressPath, jsonErrorParser)
|
||||
?: return if (isApiCancelled) null else result
|
||||
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
result.error =
|
||||
|
@ -511,6 +542,39 @@ class TootApiClient(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun requestAsync(
|
||||
path: String,
|
||||
request_builder: Request.Builder = Request.Builder()
|
||||
): TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(apiHost?.pretty)
|
||||
if (result.error != null) return result
|
||||
|
||||
val account = this.account // may null
|
||||
|
||||
try {
|
||||
if (!sendRequestAsync(result) {
|
||||
|
||||
log.d("request: $path")
|
||||
|
||||
request_builder.url("https://${apiHost?.ascii}$path")
|
||||
|
||||
val access_token = account?.getAccessToken()
|
||||
if (access_token?.isNotEmpty() == true) {
|
||||
request_builder.header("Authorization", "Bearer $access_token")
|
||||
}
|
||||
|
||||
request_builder.build()
|
||||
|
||||
}) return result
|
||||
|
||||
return parseJson(result)
|
||||
} finally {
|
||||
val error = result.error
|
||||
if (error != null) log.d("error: $error")
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// misskey authentication
|
||||
|
||||
|
@ -1166,10 +1230,9 @@ class TootApiClient(
|
|||
////////////////////////////////////////////////////////////////////////
|
||||
// JSONデータ以外を扱うリクエスト
|
||||
|
||||
fun http(req: Request): TootApiResult? {
|
||||
fun http(req: Request): TootApiResult {
|
||||
val result = TootApiResult.makeWithCaption(req.url.host)
|
||||
if (result.error != null) return result
|
||||
|
||||
sendRequest(result, progressPath = null) { req }
|
||||
return result
|
||||
}
|
||||
|
@ -1186,10 +1249,7 @@ class TootApiClient(
|
|||
// 疑似アカウントでステータスURLからステータスIDを取得するためにHTMLを取得する
|
||||
fun getHttp(url: String):TootApiResult?{
|
||||
val result = http(Request.Builder().url(url).build())
|
||||
if (result != null && result.error == null) {
|
||||
parseString(result)
|
||||
}
|
||||
return result
|
||||
return if (result.error != null) result else parseString(result)
|
||||
}
|
||||
|
||||
fun getHttpBytes(url: String): Pair<TootApiResult?, ByteArray?> {
|
||||
|
|
|
@ -262,7 +262,7 @@ class NotificationCache(private val account_db_id : Long) {
|
|||
|
||||
}
|
||||
|
||||
fun request(
|
||||
suspend fun requestAsync(
|
||||
client : TootApiClient,
|
||||
account : SavedAccount,
|
||||
flags : Int,
|
||||
|
@ -291,9 +291,9 @@ class NotificationCache(private val account_db_id : Long) {
|
|||
}
|
||||
|
||||
val result = if(account.isMisskey) {
|
||||
client.request(path, account.putMisskeyApiToken().toPostRequestBuilder())
|
||||
client.requestAsync(path, account.putMisskeyApiToken().toPostRequestBuilder())
|
||||
} else {
|
||||
client.request(path)
|
||||
client.requestAsync(path)
|
||||
}
|
||||
|
||||
if(result == null) {
|
||||
|
|
|
@ -48,9 +48,21 @@ private fun Activity.startActivityExcludeMyApp(
|
|||
val myName = packageName
|
||||
|
||||
val filter: (ResolveInfo) -> Boolean = {
|
||||
it.activityInfo.packageName != myName &&
|
||||
it.activityInfo.exported &&
|
||||
-1 == it.activityInfo.packageName.indexOf("com.huawei.android.internal")
|
||||
when{
|
||||
it.activityInfo.packageName == myName -> false
|
||||
!it.activityInfo.exported -> false
|
||||
|
||||
// Huaweiの謎Activityのせいでうまく働かないことがある
|
||||
-1 != it.activityInfo.packageName.indexOf("com.huawei.android.internal") -> false
|
||||
|
||||
// 標準アプリが設定されていない場合、アプリを選択するためのActivityが出てくる場合がある
|
||||
it.activityInfo.packageName == "android" -> false
|
||||
it.activityInfo.javaClass.name.startsWith( "com.android.internal") -> false
|
||||
it.activityInfo.javaClass.name.startsWith("com.android.systemui") -> false
|
||||
|
||||
// たぶんChromeとかfirefoxとか
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
// resolveActivity がこのアプリ以外のActivityを返すなら、それがベストなんだろう
|
||||
|
|
|
@ -4,22 +4,25 @@ import android.content.Context
|
|||
import okhttp3.*
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.util.LogCategory
|
||||
import ru.gildor.coroutines.okhttp.await
|
||||
|
||||
// okhttpそのままだとモックしづらいので
|
||||
// リクエストを投げてレスポンスを得る部分をインタフェースにまとめる
|
||||
|
||||
interface CurrentCallCallback {
|
||||
fun onCallCreated(call : Call)
|
||||
}
|
||||
|
||||
interface SimpleHttpClient {
|
||||
var currentCallCallback : CurrentCallCallback?
|
||||
|
||||
var onCallCreated: (Call) -> Unit
|
||||
|
||||
fun getResponse(
|
||||
request: Request,
|
||||
tmpOkhttpClient: OkHttpClient? = null
|
||||
): Response
|
||||
|
||||
suspend fun getResponseAsync(
|
||||
request: Request,
|
||||
tmpOkhttpClient: OkHttpClient? = null
|
||||
): Response
|
||||
|
||||
fun getWebSocket(
|
||||
request: Request,
|
||||
webSocketListener: WebSocketListener
|
||||
|
@ -31,12 +34,11 @@ class SimpleHttpClientImpl(
|
|||
private val okHttpClient: OkHttpClient
|
||||
) : SimpleHttpClient {
|
||||
|
||||
|
||||
companion object {
|
||||
val log = LogCategory("SimpleHttpClientImpl")
|
||||
}
|
||||
|
||||
override var currentCallCallback : CurrentCallCallback? = null
|
||||
override var onCallCreated: (Call) -> Unit = {}
|
||||
|
||||
override fun getResponse(
|
||||
request: Request,
|
||||
|
@ -44,10 +46,20 @@ class SimpleHttpClientImpl(
|
|||
): Response {
|
||||
App1.getAppState(context).networkTracker.checkNetworkState()
|
||||
val call = (tmpOkhttpClient ?: this.okHttpClient).newCall(request)
|
||||
currentCallCallback?.onCallCreated(call)
|
||||
onCallCreated(call)
|
||||
return call.execute()
|
||||
}
|
||||
|
||||
override suspend fun getResponseAsync(
|
||||
request: Request,
|
||||
tmpOkhttpClient: OkHttpClient?
|
||||
): Response {
|
||||
App1.getAppState(context).networkTracker.checkNetworkState()
|
||||
val call = (tmpOkhttpClient ?: this.okHttpClient).newCall(request)
|
||||
onCallCreated(call)
|
||||
return call.await()
|
||||
}
|
||||
|
||||
override fun getWebSocket(
|
||||
request: Request,
|
||||
webSocketListener: WebSocketListener
|
||||
|
|
Loading…
Reference in New Issue