通知チェック部分のコードをコルーチン対応にした。AppOpenerはいくつかのOSアクティビティを選択肢から除去するようになった。

This commit is contained in:
tateisu 2020-12-08 22:48:23 +09:00
parent 301dde36c0
commit 8818f25b6f
8 changed files with 3226 additions and 3185 deletions

View File

@ -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>

View File

@ -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
)
}
}

View File

@ -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))
}
}
})
}
}

View File

@ -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 -> "?"
}
}

View File

@ -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?> {

View File

@ -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) {

View File

@ -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を返すなら、それがベストなんだろう

View File

@ -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