デバッグビルドのみアカウント設定にボタンを追加。通知表示の既読状況がおかしい場合にかるく修正する
This commit is contained in:
parent
307ddfa239
commit
fdd85d6a10
|
@ -99,6 +99,7 @@
|
|||
<w>noellabo</w>
|
||||
<w>noscript</w>
|
||||
<w>notestock</w>
|
||||
<w>noti</w>
|
||||
<w>noto</w>
|
||||
<w>nsfw</w>
|
||||
<w>okhttp</w>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
|
@ -35,7 +34,6 @@ import jp.juggler.util.*
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
|
@ -103,6 +101,8 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
|
||||
private lateinit var btnOpenBrowser : Button
|
||||
private lateinit var btnPushSubscription : Button
|
||||
private lateinit var btnPushSubscriptionNotForce : Button
|
||||
|
||||
private lateinit var cbNotificationMention : CheckBox
|
||||
private lateinit var cbNotificationBoost : CheckBox
|
||||
private lateinit var cbNotificationFavourite : CheckBox
|
||||
|
@ -279,6 +279,8 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
swMarkSensitive = findViewById(R.id.swMarkSensitive)
|
||||
btnOpenBrowser = findViewById(R.id.btnOpenBrowser)
|
||||
btnPushSubscription = findViewById(R.id.btnPushSubscription)
|
||||
btnPushSubscriptionNotForce= findViewById(R.id.btnPushSubscriptionNotForce)
|
||||
btnPushSubscriptionNotForce.vg(BuildConfig.DEBUG)
|
||||
cbNotificationMention = findViewById(R.id.cbNotificationMention)
|
||||
cbNotificationBoost = findViewById(R.id.cbNotificationBoost)
|
||||
cbNotificationFavourite = findViewById(R.id.cbNotificationFavourite)
|
||||
|
@ -331,6 +333,7 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
|
||||
btnOpenBrowser.setOnClickListener(this)
|
||||
btnPushSubscription.setOnClickListener(this)
|
||||
btnPushSubscriptionNotForce.setOnClickListener(this)
|
||||
btnAccessToken.setOnClickListener(this)
|
||||
btnInputAccessToken.setOnClickListener(this)
|
||||
btnAccountRemove.setOnClickListener(this)
|
||||
|
@ -495,7 +498,7 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
btnInputAccessToken.isEnabled = enabled
|
||||
btnVisibility.isEnabled = enabled
|
||||
btnPushSubscription.isEnabled = enabled
|
||||
|
||||
btnPushSubscriptionNotForce.isEnabled = enabled
|
||||
btnNotificationSoundEdit.isEnabled = Build.VERSION.SDK_INT < 26 && enabled
|
||||
btnNotificationSoundReset.isEnabled = Build.VERSION.SDK_INT < 26 && enabled
|
||||
btnNotificationStyleEdit.isEnabled = Build.VERSION.SDK_INT >= 26 && enabled
|
||||
|
@ -593,8 +596,9 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
R.id.btnLoadPreference -> performLoadPreference()
|
||||
R.id.btnVisibility -> performVisibility()
|
||||
R.id.btnOpenBrowser -> openBrowser("https://${account.apiHost.ascii}/")
|
||||
R.id.btnPushSubscription -> startTest()
|
||||
|
||||
R.id.btnPushSubscription -> startTest(force=true)
|
||||
R.id.btnPushSubscriptionNotForce-> startTest(force=false)
|
||||
|
||||
R.id.btnUserCustom -> ActNickname.open(
|
||||
this,
|
||||
account.acct,
|
||||
|
@ -1572,22 +1576,18 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
|
|||
)
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private fun startTest() {
|
||||
private fun startTest(force:Boolean) {
|
||||
val wps = PushSubscriptionHelper( applicationContext,account,verbose = true)
|
||||
|
||||
TootTaskRunner(this).run(account, object : TootTask {
|
||||
val wps = PushSubscriptionHelper(
|
||||
this@ActAccountSetting,
|
||||
account,
|
||||
verbose = true
|
||||
)
|
||||
|
||||
|
||||
override suspend fun background(client : TootApiClient) : TootApiResult? {
|
||||
return wps.updateSubscription(client, true)
|
||||
return wps.updateSubscription(client, force=force)
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result : TootApiResult?) {
|
||||
result ?: return
|
||||
val log = wps.log
|
||||
val log = wps.logString
|
||||
if(log.isNotEmpty()) {
|
||||
AlertDialog.Builder(this@ActAccountSetting)
|
||||
.setMessage(log)
|
||||
|
|
|
@ -484,11 +484,11 @@ class ActMain : AsyncActivity(), Column.Callback, View.OnClickListener,
|
|||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
log.w("onNewIntent: isResumed = isResumed")
|
||||
log.w("onNewIntent: isResumed=$isResumed")
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
log.d("onConfigurationChanged")
|
||||
log.w("onConfigurationChanged")
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (newConfig.screenHeightDp > 0 || newConfig.screenHeightDp > 0) {
|
||||
tabOnly { env -> resizeColumnWidth(env) }
|
||||
|
|
|
@ -701,6 +701,9 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
val isJobCancelled: Boolean
|
||||
get() = mJobCancelled_.get()
|
||||
|
||||
// 通知データインジェクションを行ったアカウント
|
||||
val injectedAccounts = HashSet<Long>()
|
||||
|
||||
constructor(jobService: JobService, params: JobParameters) {
|
||||
this.jobParams = params
|
||||
this.jobId = params.jobId
|
||||
|
@ -748,6 +751,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
favMuteSet = FavMute.acctSet
|
||||
|
||||
// タスクがあれば処理する
|
||||
|
||||
while (true) {
|
||||
if (isJobCancelled) throw JobCancelledException()
|
||||
val data = task_list.next(context) ?: break
|
||||
|
@ -831,7 +835,6 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
this.job = job
|
||||
this.taskId = taskId
|
||||
var process_db_id = -1L //
|
||||
|
||||
coroutineScope {
|
||||
try {
|
||||
|
@ -866,7 +869,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
// タスクによってはポーリング前にすることがある
|
||||
when (taskId) {
|
||||
TASK_DATA_INJECTED -> processInjectedData()
|
||||
TASK_DATA_INJECTED -> processInjectedData(job.injectedAccounts)
|
||||
|
||||
TASK_BOOT_COMPLETED -> NotificationTracking.resetPostAll()
|
||||
|
||||
|
@ -886,14 +889,14 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
val sa = SavedAccount.loadAccountByAcct(context, acct)
|
||||
if (sa != null) {
|
||||
NotificationCache.resetLastLoad(sa.db_id)
|
||||
process_db_id = sa.db_id
|
||||
job.injectedAccounts.add(sa.db_id)
|
||||
bDone = true
|
||||
}
|
||||
}
|
||||
if (!bDone) {
|
||||
for (sa in SavedAccount.loadByTag(context, tag)) {
|
||||
NotificationCache.resetLastLoad(sa.db_id)
|
||||
process_db_id = sa.db_id
|
||||
job.injectedAccounts.add(sa.db_id)
|
||||
bDone = true
|
||||
}
|
||||
}
|
||||
|
@ -949,25 +952,30 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
workerStatus = "make install id"
|
||||
|
||||
// インストールIDを生成する
|
||||
// インストールID生成時にSavedAccountテーブルを操作することがあるので
|
||||
// アカウントリストの取得より先に行う
|
||||
if (job.install_id == null) {
|
||||
workerStatus = "make install id"
|
||||
job.install_id = prepareInstallId(context, job)
|
||||
}
|
||||
|
||||
// アカウント別に処理スレッドを作る
|
||||
workerStatus = "create account thread"
|
||||
workerStatus = "create account threads"
|
||||
|
||||
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) }
|
||||
|
||||
if( job.injectedAccounts.isNotEmpty()){
|
||||
// 更新対象アカウントが限られているなら、そのdb_idだけ処理する
|
||||
job.injectedAccounts.forEach { db_id->
|
||||
SavedAccount.loadAccount(context, db_id)?.let { startForAccount(it) }
|
||||
}
|
||||
} else {
|
||||
// 全てのアカウントを処理する
|
||||
SavedAccount.loadAccountList(context).forEach { startForAccount(it) }
|
||||
|
@ -1062,6 +1070,8 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
// 未確認アカウントはチェック対象外
|
||||
if (!account.isConfirmed) return
|
||||
|
||||
log.d("${account.acct}: runSuspend start.")
|
||||
|
||||
client.account = account
|
||||
|
||||
val wps = PushSubscriptionHelper(context, account)
|
||||
|
@ -1083,7 +1093,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
wps.updateSubscription(client) ?: return // cancelled.
|
||||
|
||||
val wps_log = wps.log
|
||||
val wps_log = wps.logString
|
||||
if (wps_log.isNotEmpty())
|
||||
log.d("PushSubscriptionHelper: ${account.acct.pretty} $wps_log")
|
||||
|
||||
|
@ -1137,7 +1147,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
if (job.isJobCancelled) return
|
||||
tr.updateNotification()
|
||||
}
|
||||
|
||||
log.i("runSuspend complete normally.")
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex)
|
||||
} finally {
|
||||
|
@ -1156,22 +1166,19 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
|
||||
internal fun checkAccount() {
|
||||
|
||||
this.nr = NotificationTracking.load(account.db_id, trackingName)
|
||||
this.nr = NotificationTracking.load(account.acct.pretty, account.db_id, trackingName)
|
||||
|
||||
fun JsonObject.isMention()= when (parseNotificationType(account, this)) {
|
||||
TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
||||
|
||||
val jsonList = when (trackingType) {
|
||||
TrackingType.All -> cache.data
|
||||
TrackingType.Reply -> cache.data.filter {
|
||||
when (parseNotificationType(account, it)) {
|
||||
TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
TrackingType.NotReply -> cache.data.filter {
|
||||
!when (parseNotificationType(account, it)) {
|
||||
TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
TrackingType.Reply -> cache.data.filter { it.isMention()}
|
||||
TrackingType.NotReply -> cache.data.filter { !it.isMention()}
|
||||
}
|
||||
|
||||
// 新しい順に並んでいる。先頭から10件までを処理する。ただし処理順序は古い方から
|
||||
|
@ -1185,8 +1192,15 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
// 種別チェックより先に、cache中の最新のIDを「最後に表示した通知」に指定する
|
||||
// nid_show は通知タップ時に参照されるので、通知を表示する際は必ず更新・保存する必要がある
|
||||
// 種別チェックより優先する
|
||||
if (cache.sinceId != null) nr.nid_show = cache.sinceId
|
||||
nr.save()
|
||||
val latestId = cache.filterLatestId(account){
|
||||
when (trackingType) {
|
||||
TrackingType.Reply -> it.isMention()
|
||||
TrackingType.NotReply -> !it.isMention()
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
if (latestId != null) nr.nid_show = latestId
|
||||
nr.save(account.acct.pretty)
|
||||
}
|
||||
|
||||
private fun update_sub(src: JsonObject) {
|
||||
|
@ -1196,7 +1210,10 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
duplicate_check.add(id)
|
||||
|
||||
// タップ・削除した通知のIDと同じか古いなら対象外
|
||||
if (!id.isNewerThan(nr.nid_read)) return
|
||||
if (!id.isNewerThan(nr.nid_read)){
|
||||
log.d("update_sub: ${account.acct} skip old notification $id")
|
||||
return
|
||||
}
|
||||
|
||||
log.d("update_sub: found data that id=${id}, > read id ${nr.nid_read}")
|
||||
|
||||
|
@ -1232,7 +1249,7 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
else -> "${account.db_id}/$trackingName"
|
||||
}
|
||||
|
||||
val nt = NotificationTracking.load(account.db_id, trackingName)
|
||||
val nt = NotificationTracking.load(account.acct.pretty, account.db_id, trackingName)
|
||||
val dataList = dstListData
|
||||
val first = dataList.firstOrNull()
|
||||
if (first == null) {
|
||||
|
@ -1592,11 +1609,13 @@ class PollingWorker private constructor(contextArg: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun processInjectedData() {
|
||||
private fun processInjectedData( injectedAccounts:HashSet<Long>) {
|
||||
while (true) {
|
||||
val data = inject_queue.poll() ?: break
|
||||
val account = SavedAccount.loadAccount(context, data.account_db_id) ?: continue
|
||||
val list = data.list
|
||||
log.d("${account.acct} processInjectedData +${list.size}")
|
||||
if( list.isNotEmpty() ) injectedAccounts.add(account.db_id)
|
||||
NotificationCache(data.account_db_id).apply {
|
||||
load()
|
||||
inject(account, list)
|
||||
|
|
|
@ -44,6 +44,7 @@ class EntityId(val x : String) : Comparable<EntityId> {
|
|||
|
||||
fun from(cursor : Cursor, key : String) =
|
||||
cursor.getStringOrNull(key)?.decode()
|
||||
|
||||
}
|
||||
|
||||
private fun encode() : String {
|
||||
|
|
|
@ -14,42 +14,39 @@ import jp.juggler.util.*
|
|||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class NotificationCache(private val account_db_id : Long) {
|
||||
|
||||
private var id = - 1L
|
||||
|
||||
// サーバから通知を取得した時刻
|
||||
private var last_load : Long = 0
|
||||
|
||||
// 通知のリスト
|
||||
var data = ArrayList<JsonObject>()
|
||||
|
||||
// 次回以降の読み込み位置
|
||||
var sinceId : EntityId? = null
|
||||
|
||||
companion object : TableCompanion {
|
||||
|
||||
private val log = LogCategory("NotificationCache")
|
||||
|
||||
private const val table = "noti_cache"
|
||||
|
||||
private const val COL_ID = BaseColumns._ID
|
||||
|
||||
// アカウントDBの行ID。 サーバ側のIDではない
|
||||
private const val COL_ACCOUNT_DB_ID = "a"
|
||||
|
||||
// サーバから通知を取得した時刻
|
||||
private const val COL_LAST_LOAD = "l"
|
||||
|
||||
// サーバから最後に読んだデータ。既読は排除されてるかも
|
||||
private const val COL_DATA = "d"
|
||||
|
||||
// サーバから最後に読んだデータ。既読は排除されてるかも
|
||||
private const val COL_SINCE_ID = "si"
|
||||
|
||||
override fun onDBCreate(db : SQLiteDatabase) {
|
||||
|
||||
db.execSQL(
|
||||
class NotificationCache(private val account_db_id: Long) {
|
||||
|
||||
private var id = -1L
|
||||
|
||||
// サーバから通知を取得した時刻
|
||||
private var last_load: Long = 0
|
||||
|
||||
// 通知のリスト
|
||||
var data = ArrayList<JsonObject>()
|
||||
|
||||
companion object : TableCompanion {
|
||||
|
||||
private val log = LogCategory("NotificationCache")
|
||||
|
||||
private const val table = "noti_cache"
|
||||
|
||||
private const val COL_ID = BaseColumns._ID
|
||||
|
||||
// アカウントDBの行ID。 サーバ側のIDではない
|
||||
private const val COL_ACCOUNT_DB_ID = "a"
|
||||
|
||||
// サーバから通知を取得した時刻
|
||||
private const val COL_LAST_LOAD = "l"
|
||||
|
||||
// サーバから最後に読んだデータ。既読は排除されてるかも
|
||||
private const val COL_DATA = "d"
|
||||
|
||||
// サーバから最後に読んだデータ。既読は排除されてるかも
|
||||
private const val COL_SINCE_ID = "si" // 使わなくなった
|
||||
|
||||
override fun onDBCreate(db: SQLiteDatabase) {
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
create table if not exists $table
|
||||
($COL_ID INTEGER PRIMARY KEY
|
||||
|
@ -60,108 +57,110 @@ class NotificationCache(private val account_db_id : Long) {
|
|||
)
|
||||
"""
|
||||
)
|
||||
db.execSQL(
|
||||
db.execSQL(
|
||||
"create unique index if not exists ${table}_a on $table ($COL_ACCOUNT_DB_ID)"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDBUpgrade(db : SQLiteDatabase, oldVersion : Int, newVersion : Int) {
|
||||
if(oldVersion < 41 && newVersion >= 41) {
|
||||
onDBCreate(db)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private const val WHERE_AID = "$COL_ACCOUNT_DB_ID=?"
|
||||
|
||||
private const val KEY_TIME_CREATED_AT = "<>KEY_TIME_CREATED_AT"
|
||||
|
||||
fun resetLastLoad(db_id : Long) {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_LAST_LOAD, 0L)
|
||||
App1.database.update(table, cv, WHERE_AID, arrayOf(db_id.toString()))
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "resetLastLoad(db_id) failed.")
|
||||
}
|
||||
}
|
||||
|
||||
fun resetLastLoad() {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_LAST_LOAD, 0L)
|
||||
App1.database.update(table, cv, null, null)
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "resetLastLoad() failed.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getEntityOrderId(account : SavedAccount, src : JsonObject) : EntityId =
|
||||
if(account.isMisskey) {
|
||||
when(val created_at = src.string("createdAt")) {
|
||||
}
|
||||
|
||||
override fun onDBUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
if (oldVersion < 41 && newVersion >= 41) {
|
||||
onDBCreate(db)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private const val WHERE_AID = "$COL_ACCOUNT_DB_ID=?"
|
||||
|
||||
private const val KEY_TIME_CREATED_AT = "<>KEY_TIME_CREATED_AT"
|
||||
|
||||
fun resetLastLoad(db_id: Long) {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_LAST_LOAD, 0L)
|
||||
App1.database.update(table, cv, WHERE_AID, arrayOf(db_id.toString()))
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "resetLastLoad(db_id) failed.")
|
||||
}
|
||||
}
|
||||
|
||||
fun resetLastLoad() {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_LAST_LOAD, 0L)
|
||||
App1.database.update(table, cv, null, null)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "resetLastLoad() failed.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getEntityOrderId(account: SavedAccount, src: JsonObject): EntityId =
|
||||
if (account.isMisskey) {
|
||||
// 今のMisskeyはIDをIDとして使っても問題ないのだが、
|
||||
// ST的には既読チェックの値の内容が大幅に変わると困るのだった
|
||||
when (val created_at = src.string("createdAt")) {
|
||||
null -> EntityId.DEFAULT
|
||||
else -> EntityId(TootStatus.parseTime(created_at).toString())
|
||||
}
|
||||
} else {
|
||||
EntityId.mayDefault(src.string("id"))
|
||||
}
|
||||
|
||||
private fun makeNotificationUrl(
|
||||
accessInfo : SavedAccount,
|
||||
flags : Int,
|
||||
since_id : EntityId?
|
||||
else -> EntityId(TootStatus.parseTime(created_at).toString())
|
||||
}
|
||||
} else {
|
||||
EntityId.mayDefault(src.string("id"))
|
||||
}
|
||||
|
||||
private fun makeNotificationUrl(
|
||||
accessInfo: SavedAccount,
|
||||
flags: Int,
|
||||
since_id: EntityId?
|
||||
) = when {
|
||||
// MisskeyはsinceIdを指定すると未読範囲の古い方から読んでしまう?
|
||||
accessInfo.isMisskey -> "/api/i/notifications"
|
||||
|
||||
else -> {
|
||||
val sb = StringBuilder(Column.PATH_NOTIFICATIONS) // always contain "?limit=XX"
|
||||
|
||||
if(since_id != null) sb.append("&since_id=$since_id")
|
||||
|
||||
fun noBit(v : Int, mask : Int) = (v and mask) != mask
|
||||
|
||||
if(noBit(flags, 1)) sb.append("&exclude_types[]=reblog")
|
||||
if(noBit(flags, 2)) sb.append("&exclude_types[]=favourite")
|
||||
if(noBit(flags, 4)) sb.append("&exclude_types[]=follow")
|
||||
if(noBit(flags, 8)) sb.append("&exclude_types[]=mention")
|
||||
// if(noBit(flags,16)) /* mastodon has no reaction */
|
||||
if(noBit(flags, 32)) sb.append("&exclude_types[]=poll")
|
||||
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun parseNotificationTime(accessInfo : SavedAccount, src : JsonObject) : Long =
|
||||
when {
|
||||
accessInfo.isMisskey -> TootStatus.parseTime(src.string("createdAt"))
|
||||
else -> TootStatus.parseTime(src.string("created_at"))
|
||||
}
|
||||
|
||||
fun parseNotificationType(accessInfo : SavedAccount, src : JsonObject) : String =
|
||||
when {
|
||||
accessInfo.isMisskey -> src.string("type")
|
||||
else -> src.string("type")
|
||||
} ?: "?"
|
||||
|
||||
fun deleteCache(dbId : Long) {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_ACCOUNT_DB_ID, dbId)
|
||||
cv.put(COL_LAST_LOAD, 0L)
|
||||
cv.putNull(COL_DATA)
|
||||
App1.database.replaceOrThrow(table, null, cv)
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "deleteCache failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load into this object
|
||||
fun load() {
|
||||
try {
|
||||
App1.database.query(
|
||||
// MisskeyはsinceIdを指定すると未読範囲の古い方から読んでしまう?
|
||||
accessInfo.isMisskey -> "/api/i/notifications"
|
||||
|
||||
else -> {
|
||||
val sb = StringBuilder(Column.PATH_NOTIFICATIONS) // always contain "?limit=XX"
|
||||
|
||||
if (since_id != null) sb.append("&since_id=$since_id")
|
||||
|
||||
fun noBit(v: Int, mask: Int) = (v and mask) != mask
|
||||
|
||||
if (noBit(flags, 1)) sb.append("&exclude_types[]=reblog")
|
||||
if (noBit(flags, 2)) sb.append("&exclude_types[]=favourite")
|
||||
if (noBit(flags, 4)) sb.append("&exclude_types[]=follow")
|
||||
if (noBit(flags, 8)) sb.append("&exclude_types[]=mention")
|
||||
// if(noBit(flags,16)) /* mastodon has no reaction */
|
||||
if (noBit(flags, 32)) sb.append("&exclude_types[]=poll")
|
||||
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun parseNotificationTime(accessInfo: SavedAccount, src: JsonObject): Long =
|
||||
when {
|
||||
accessInfo.isMisskey -> TootStatus.parseTime(src.string("createdAt"))
|
||||
else -> TootStatus.parseTime(src.string("created_at"))
|
||||
}
|
||||
|
||||
fun parseNotificationType(accessInfo: SavedAccount, src: JsonObject): String =
|
||||
when {
|
||||
accessInfo.isMisskey -> src.string("type")
|
||||
else -> src.string("type")
|
||||
} ?: "?"
|
||||
|
||||
fun deleteCache(dbId: Long) {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_ACCOUNT_DB_ID, dbId)
|
||||
cv.put(COL_LAST_LOAD, 0L)
|
||||
cv.putNull(COL_DATA)
|
||||
App1.database.replaceOrThrow(table, null, cv)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "deleteCache failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load into this object
|
||||
fun load() {
|
||||
try {
|
||||
App1.database.query(
|
||||
table,
|
||||
null,
|
||||
WHERE_AID,
|
||||
|
@ -170,210 +169,211 @@ class NotificationCache(private val account_db_id : Long) {
|
|||
null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
if(cursor.moveToFirst()) {
|
||||
this.id = cursor.getLong(COL_ID)
|
||||
this.last_load = cursor.getLong(COL_LAST_LOAD)
|
||||
this.sinceId = EntityId.from(cursor, COL_SINCE_ID)
|
||||
|
||||
cursor.getStringOrNull(COL_DATA)?.decodeJsonArray()?.objectList()?.let {
|
||||
data.addAll(it)
|
||||
}
|
||||
} else {
|
||||
this.id = - 1
|
||||
this.last_load = 0L
|
||||
}
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex, "load failed.")
|
||||
}
|
||||
}
|
||||
|
||||
fun save() {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_ACCOUNT_DB_ID, account_db_id)
|
||||
cv.put(COL_LAST_LOAD, last_load)
|
||||
cv.put(COL_DATA, data.toJsonArray().toString())
|
||||
|
||||
when(val sinceId = sinceId) {
|
||||
null -> cv.putNull(COL_SINCE_ID)
|
||||
else -> sinceId.putTo(cv, COL_SINCE_ID)
|
||||
}
|
||||
|
||||
val rv = App1.database.replaceOrThrow(table, null, cv)
|
||||
if(rv != - 1L && id == - 1L) id = rv
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "save failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalize(account : SavedAccount) {
|
||||
|
||||
// 新しい順に並べる
|
||||
data.sortWith { a, b ->
|
||||
val la = a.optLong(KEY_TIME_CREATED_AT)
|
||||
val lb = b.optLong(KEY_TIME_CREATED_AT)
|
||||
when {
|
||||
la < lb -> 1
|
||||
la > lb -> - 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
val typeCount = HashMap<String, Int>()
|
||||
val it = data.iterator()
|
||||
val duplicateMap = HashSet<EntityId>()
|
||||
while(it.hasNext()) {
|
||||
val item = it.next()
|
||||
|
||||
val id = getEntityOrderId(account, item)
|
||||
if(id.isDefault) {
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
if(id.isNewerThan(sinceId)) {
|
||||
this.sinceId = id
|
||||
}
|
||||
|
||||
// skip duplicated
|
||||
if(duplicateMap.contains(id)) {
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
duplicateMap.add(id)
|
||||
|
||||
val type = parseNotificationType(account, item)
|
||||
|
||||
// 通知しないタイプなら取り除く
|
||||
if(! account.canNotificationShowing(type)) {
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
// 種類別に一定件数を保持する
|
||||
val count = 1 + (typeCount[type] ?: 0)
|
||||
if(count > 60) {
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
typeCount[type] = count
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun requestAsync(
|
||||
client : TootApiClient,
|
||||
account : SavedAccount,
|
||||
flags : Int,
|
||||
onError : (TootApiResult) -> Unit,
|
||||
isCancelled : () -> Boolean
|
||||
if (cursor.moveToFirst()) {
|
||||
this.id = cursor.getLong(COL_ID)
|
||||
this.last_load = cursor.getLong(COL_LAST_LOAD)
|
||||
|
||||
cursor.getStringOrNull(COL_DATA)?.decodeJsonArray()?.objectList()?.let {
|
||||
data.addAll(it)
|
||||
}
|
||||
} else {
|
||||
this.id = -1
|
||||
this.last_load = 0L
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex, "load failed.")
|
||||
}
|
||||
}
|
||||
|
||||
fun save() {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_ACCOUNT_DB_ID, account_db_id)
|
||||
cv.put(COL_LAST_LOAD, last_load)
|
||||
cv.put(COL_DATA, data.toJsonArray().toString())
|
||||
|
||||
val rv = App1.database.replaceOrThrow(table, null, cv)
|
||||
if (rv != -1L && id == -1L) id = rv
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "save failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalize(account: SavedAccount) {
|
||||
|
||||
// 新しい順に並べる
|
||||
data.sortWith { a, b ->
|
||||
val la = a.optLong(KEY_TIME_CREATED_AT)
|
||||
val lb = b.optLong(KEY_TIME_CREATED_AT)
|
||||
when {
|
||||
la < lb -> 1
|
||||
la > lb -> -1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
val typeCount = HashMap<String, Int>()
|
||||
val it = data.iterator()
|
||||
val duplicateMap = HashSet<EntityId>()
|
||||
while (it.hasNext()) {
|
||||
val item = it.next()
|
||||
|
||||
val id = getEntityOrderId(account, item)
|
||||
if (id.isDefault) {
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
// skip duplicated
|
||||
if (duplicateMap.contains(id)) {
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
duplicateMap.add(id)
|
||||
|
||||
val type = parseNotificationType(account, item)
|
||||
|
||||
// 通知しないタイプなら取り除く
|
||||
if (!account.canNotificationShowing(type)) {
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
// 種類別に一定件数を保持する
|
||||
val count = 1 + (typeCount[type] ?: 0)
|
||||
if (count > 60) {
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
typeCount[type] = count
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun requestAsync(
|
||||
client: TootApiClient,
|
||||
account: SavedAccount,
|
||||
flags: Int,
|
||||
onError: (TootApiResult) -> Unit,
|
||||
isCancelled: () -> Boolean
|
||||
) {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// 前回の更新から一定時刻が経過するまでは処理しない
|
||||
val remain = last_load + 120000L - now
|
||||
if(remain > 0) {
|
||||
log.d("skip request. wait ${remain}ms.")
|
||||
return
|
||||
}
|
||||
|
||||
this.last_load = now
|
||||
|
||||
val path = makeNotificationUrl(account, flags, this.sinceId)
|
||||
|
||||
try {
|
||||
for(nTry in 0 .. 3) {
|
||||
|
||||
if(isCancelled()) {
|
||||
log.d("cancelled.")
|
||||
return
|
||||
}
|
||||
|
||||
val result = if(account.isMisskey) {
|
||||
client.request(path, account.putMisskeyApiToken().toPostRequestBuilder())
|
||||
} else {
|
||||
client.request(path)
|
||||
}
|
||||
|
||||
if(result == null) {
|
||||
log.d("cancelled.")
|
||||
return
|
||||
}
|
||||
|
||||
val array = result.jsonArray
|
||||
if(array != null) {
|
||||
account.updateNotificationError(null)
|
||||
|
||||
// データをマージする
|
||||
array.objectList().forEach { item ->
|
||||
item[KEY_TIME_CREATED_AT] = parseNotificationTime(account, item)
|
||||
data.add(item)
|
||||
}
|
||||
|
||||
normalize(account)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.d("request error. ${result.error} ${result.requestInfo}")
|
||||
|
||||
account.updateNotificationError("${result.error} ${result.requestInfo}".trim())
|
||||
|
||||
onError(result)
|
||||
|
||||
// サーバからエラー応答が届いているならリトライしない
|
||||
val code = result.response?.code
|
||||
if(code != null && code in 200 until 600) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex, "request failed.")
|
||||
} finally {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
fun inject(account : SavedAccount, list : List<TootNotification>) {
|
||||
try {
|
||||
val jsonList = list.map { it.json }
|
||||
jsonList.forEach { item ->
|
||||
item[KEY_TIME_CREATED_AT] = parseNotificationTime(account, item)
|
||||
}
|
||||
data.addAll(jsonList)
|
||||
normalize(account)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex, "inject failed.")
|
||||
} finally {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
// fun updatePost(post_id : EntityId, post_time : Long) {
|
||||
// this.post_id = post_id
|
||||
// this.post_time = post_time
|
||||
// try {
|
||||
// val cv = ContentValues()
|
||||
// post_id.putTo(cv, COL_POST_ID)
|
||||
// cv.put(COL_POST_TIME, post_time)
|
||||
// val rows = App1.database.update(table, cv, WHERE_AID, arrayOf(account_db_id.toString()))
|
||||
// log.d(
|
||||
// "updatePost account_db_id=%s,post=%s,%s last_data=%s,update_rows=%s"
|
||||
// , account_db_id
|
||||
// , post_id
|
||||
// , post_time
|
||||
// , last_data?.length
|
||||
// , rows
|
||||
// )
|
||||
//
|
||||
// } catch(ex : Throwable) {
|
||||
// log.e(ex, "updatePost failed.")
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// 前回の更新から一定時刻が経過するまでは処理しない
|
||||
val remain = last_load + 120000L - now
|
||||
if (remain > 0) {
|
||||
log.d("${account.acct} skip request. wait ${remain}ms.")
|
||||
return
|
||||
}
|
||||
|
||||
this.last_load = now
|
||||
|
||||
// キャッシュ更新時は全データの最新データより新しいものを読みたい
|
||||
val newestId = data
|
||||
.mapNotNull { getEntityOrderId(account, it).takeIf { id -> !id.isDefault } }
|
||||
.reduceOrNull { a, b -> maxComparable(a, b) }
|
||||
|
||||
val path = makeNotificationUrl(account, flags, newestId)
|
||||
|
||||
try {
|
||||
for (nTry in 0..3) {
|
||||
|
||||
if (isCancelled()) {
|
||||
log.d("cancelled.")
|
||||
return
|
||||
}
|
||||
|
||||
val result = if (account.isMisskey) {
|
||||
client.request(path, account.putMisskeyApiToken().toPostRequestBuilder())
|
||||
} else {
|
||||
client.request(path)
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
log.d("cancelled.")
|
||||
return
|
||||
}
|
||||
|
||||
val array = result.jsonArray
|
||||
if (array != null) {
|
||||
account.updateNotificationError(null)
|
||||
|
||||
// データをマージする
|
||||
array.objectList().forEach { item ->
|
||||
item[KEY_TIME_CREATED_AT] = parseNotificationTime(account, item)
|
||||
data.add(item)
|
||||
}
|
||||
|
||||
normalize(account)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.d("request error. ${result.error} ${result.requestInfo}")
|
||||
|
||||
account.updateNotificationError("${result.error} ${result.requestInfo}".trim())
|
||||
|
||||
onError(result)
|
||||
|
||||
// サーバからエラー応答が届いているならリトライしない
|
||||
val code = result.response?.code
|
||||
if (code != null && code in 200 until 600) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex, "request failed.")
|
||||
} finally {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun filterLatestId(account: SavedAccount, predicate: (JsonObject) -> Boolean) =
|
||||
data
|
||||
.filter { predicate(it) }
|
||||
.mapNotNull { getEntityOrderId(account, it).takeIf { id -> !id.isDefault } }
|
||||
.reduceOrNull { a, b -> maxComparable(a, b) }
|
||||
|
||||
fun inject(account: SavedAccount, list: List<TootNotification>) {
|
||||
try {
|
||||
val jsonList = list.map { it.json }
|
||||
jsonList.forEach { item ->
|
||||
item[KEY_TIME_CREATED_AT] = parseNotificationTime(account, item)
|
||||
}
|
||||
data.addAll(jsonList)
|
||||
normalize(account)
|
||||
} catch (ex: Throwable) {
|
||||
log.trace(ex, "inject failed.")
|
||||
} finally {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
// fun updatePost(post_id : EntityId, post_time : Long) {
|
||||
// this.post_id = post_id
|
||||
// this.post_time = post_time
|
||||
// try {
|
||||
// val cv = ContentValues()
|
||||
// post_id.putTo(cv, COL_POST_ID)
|
||||
// cv.put(COL_POST_TIME, post_time)
|
||||
// val rows = App1.database.update(table, cv, WHERE_AID, arrayOf(account_db_id.toString()))
|
||||
// log.d(
|
||||
// "updatePost account_db_id=%s,post=%s,%s last_data=%s,update_rows=%s"
|
||||
// , account_db_id
|
||||
// , post_id
|
||||
// , post_time
|
||||
// , last_data?.length
|
||||
// , rows
|
||||
// )
|
||||
//
|
||||
// } catch(ex : Throwable) {
|
||||
// log.e(ex, "updatePost failed.")
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.api.entity.EntityId
|
|||
import jp.juggler.subwaytooter.api.entity.putMayNull
|
||||
import jp.juggler.util.LogCategory
|
||||
import jp.juggler.util.getLong
|
||||
import jp.juggler.util.minComparable
|
||||
|
||||
class NotificationTracking {
|
||||
|
||||
|
@ -20,7 +21,7 @@ class NotificationTracking {
|
|||
var post_id : EntityId? = null
|
||||
var post_time : Long = 0
|
||||
|
||||
fun save() {
|
||||
fun save(acct:String) {
|
||||
try {
|
||||
val cv = ContentValues()
|
||||
cv.put(COL_ACCOUNT_DB_ID, account_db_id)
|
||||
|
@ -32,12 +33,9 @@ class NotificationTracking {
|
|||
|
||||
val rv = App1.database.replaceOrThrow(table,null,cv)
|
||||
if( rv != -1L && id == -1L) id = rv
|
||||
|
||||
log.d(
|
||||
"save account_db_id=%s,nt=%s,post=%s,%s"
|
||||
, account_db_id
|
||||
, notificationType
|
||||
, post_id
|
||||
, post_time
|
||||
"${acct}/${notificationType} save. post=(${post_id},${post_time})"
|
||||
)
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "save failed.")
|
||||
|
@ -152,7 +150,7 @@ class NotificationTracking {
|
|||
|
||||
private const val WHERE_AID = "$COL_ACCOUNT_DB_ID=? and $COL_NOTIFICATION_TYPE=?"
|
||||
|
||||
fun load(account_db_id : Long,notificationType:String) : NotificationTracking {
|
||||
fun load(acct:String, account_db_id : Long,notificationType:String) : NotificationTracking {
|
||||
val dst = NotificationTracking()
|
||||
dst.account_db_id = account_db_id
|
||||
dst.notificationType = notificationType
|
||||
|
@ -168,20 +166,34 @@ class NotificationTracking {
|
|||
)?.use { cursor ->
|
||||
if(cursor.moveToFirst()) {
|
||||
dst.id = cursor.getLong(COL_ID)
|
||||
|
||||
dst.nid_show = EntityId.from(cursor, COL_NID_SHOW)
|
||||
dst.nid_read = EntityId.from(cursor, COL_NID_READ)
|
||||
|
||||
|
||||
dst.post_id = EntityId.from(cursor, COL_POST_ID)
|
||||
dst.post_time = cursor.getLong(COL_POST_TIME)
|
||||
|
||||
|
||||
val show = EntityId.from(cursor, COL_NID_SHOW)
|
||||
if( show == null){
|
||||
dst.nid_show = null
|
||||
dst.nid_read = null
|
||||
}else{
|
||||
dst.nid_show = show
|
||||
val read = EntityId.from(cursor, COL_NID_READ)
|
||||
if( read==null){
|
||||
dst.nid_read = null
|
||||
}else{
|
||||
val r2 = minComparable(show,read)
|
||||
dst.nid_read = r2
|
||||
if(r2 != read){
|
||||
log.e("${acct}/${notificationType} read>show! clip to $show")
|
||||
val cv = ContentValues()
|
||||
show.putTo(cv, COL_NID_READ) //変数名とキー名が異なるのに注意
|
||||
val where_args = arrayOf(account_db_id.toString(),notificationType)
|
||||
App1.database.update(table, cv, WHERE_AID, where_args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.d(
|
||||
"load account_db_id=%s,post=%s,%s,read=%s,show=%s"
|
||||
, account_db_id
|
||||
, dst.post_id
|
||||
, dst.post_time
|
||||
, dst.nid_read
|
||||
, dst.nid_show
|
||||
"${acct}/${notificationType} load. post=(${dst.post_id},${dst.post_time}), read=${dst.nid_read}, show=${dst.nid_show}"
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -213,13 +225,13 @@ class NotificationTracking {
|
|||
val nid_read = EntityId.from(cursor, COL_NID_READ)
|
||||
when {
|
||||
nid_show == null ->
|
||||
log.w("updateRead[$account_db_id,$notificationType]: nid_show is null.")
|
||||
log.e("updateRead[$account_db_id,$notificationType]: nid_show is null.")
|
||||
|
||||
nid_read != null && nid_read >= nid_show ->
|
||||
log.d("updateRead[$account_db_id,$notificationType]: nid_read already updated.")
|
||||
log.e("updateRead[$account_db_id,$notificationType]: nid_read already updated.")
|
||||
|
||||
else -> {
|
||||
log.w("updateRead[$account_db_id,$notificationType]: update nid_read as $nid_show...")
|
||||
log.e("updateRead[$account_db_id,$notificationType]: update nid_read as $nid_show...")
|
||||
val cv = ContentValues()
|
||||
nid_show.putTo(cv, COL_NID_READ) //変数名とキー名が異なるのに注意
|
||||
App1.database.update(table, cv, WHERE_AID, where_args)
|
||||
|
|
|
@ -293,10 +293,11 @@ class PostHelper(
|
|||
}
|
||||
|
||||
// 全ての確認を終えたらバックグラウンドでの処理を開始する
|
||||
last_post_task =
|
||||
WeakReference(TootTaskRunner(activity, progressSetupCallback = { progressDialog ->
|
||||
progressDialog.setCanceledOnTouchOutside(false)
|
||||
}
|
||||
last_post_task = WeakReference(
|
||||
TootTaskRunner(activity,
|
||||
progressSetupCallback = { progressDialog ->
|
||||
progressDialog.setCanceledOnTouchOutside(false)
|
||||
}
|
||||
).run(account, object : TootTask {
|
||||
|
||||
var status : TootStatus? = null
|
||||
|
@ -322,22 +323,21 @@ class PostHelper(
|
|||
// 元の投稿を削除する
|
||||
if(redraft_status_id != null) {
|
||||
result = if(account.isMisskey) {
|
||||
val params = account.putMisskeyApiToken(JsonObject()).apply {
|
||||
put("noteId", redraft_status_id)
|
||||
}
|
||||
client.request(
|
||||
"/api/notes/delete",
|
||||
params.toPostRequestBuilder()
|
||||
account.putMisskeyApiToken(JsonObject()).apply {
|
||||
put("noteId", redraft_status_id)
|
||||
}.toPostRequestBuilder()
|
||||
)
|
||||
} else {
|
||||
client.request(
|
||||
"/api/v1/statuses/$redraft_status_id",
|
||||
Request.Builder().delete()
|
||||
)
|
||||
|
||||
}
|
||||
log.d("delete redraft. result=$result")
|
||||
Thread.sleep(2000L)
|
||||
|
||||
} else if(scheduledId != null) {
|
||||
val r1 = client.request(
|
||||
"/api/v1/scheduled_statuses/$scheduledId",
|
||||
|
|
|
@ -14,12 +14,15 @@ import jp.juggler.util.*
|
|||
import okhttp3.Request
|
||||
|
||||
class PushSubscriptionHelper(
|
||||
val context: Context,
|
||||
val account: SavedAccount,
|
||||
val verbose: Boolean = false
|
||||
val context: Context,
|
||||
val account: SavedAccount,
|
||||
val verbose: Boolean = false
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LogCategory("PushSubscriptionHelper")
|
||||
|
||||
private const val ERROR_PREVENT_FREQUENTLY_CHECK =
|
||||
"prevent frequently subscription check."
|
||||
|
||||
|
@ -44,12 +47,12 @@ class PushSubscriptionHelper(
|
|||
account.notification_follow_request.booleanToInt(64) +
|
||||
account.notification_post.booleanToInt(128)
|
||||
|
||||
private val logBuffer = StringBuilder()
|
||||
private val logBuffer = StringBuilder()
|
||||
|
||||
val log: String
|
||||
get() = logBuffer.toString()
|
||||
val logString: String
|
||||
get() = logBuffer.toString()
|
||||
|
||||
private var subscribed: Boolean = false
|
||||
private var subscribed: Boolean = false
|
||||
|
||||
private fun addLog(s: String?) {
|
||||
if (s?.isNotEmpty() == true) {
|
||||
|
@ -71,10 +74,10 @@ class PushSubscriptionHelper(
|
|||
}
|
||||
|
||||
private suspend fun updateServerKey(
|
||||
client: TootApiClient,
|
||||
clientIdentifier: String,
|
||||
serverKey: String?
|
||||
): TootApiResult {
|
||||
client: TootApiClient,
|
||||
clientIdentifier: String,
|
||||
serverKey: String?
|
||||
): TootApiResult {
|
||||
|
||||
if (serverKey == null) {
|
||||
return TootApiResult(context.getString(R.string.push_notification_server_key_missing))
|
||||
|
@ -88,17 +91,17 @@ class PushSubscriptionHelper(
|
|||
|
||||
// サーバキーをアプリサーバに登録
|
||||
client.http(
|
||||
JsonObject().apply {
|
||||
put("client_id", clientIdentifier)
|
||||
put("server_key", serverKey)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("${PollingWorker.APP_SERVER}/webpushserverkey")
|
||||
.build()
|
||||
JsonObject().apply {
|
||||
put("client_id", clientIdentifier)
|
||||
put("server_key", serverKey)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("${PollingWorker.APP_SERVER}/webpushserverkey")
|
||||
.build()
|
||||
|
||||
).also{ result ->
|
||||
result.response?.let{ res ->
|
||||
when (res.code.also{ res.close()}) {
|
||||
).also { result ->
|
||||
result.response?.let { res ->
|
||||
when (res.code.also { res.close() }) {
|
||||
|
||||
200 -> {
|
||||
// 登録できたサーバーキーをアプリ内DBに保存
|
||||
|
@ -119,25 +122,25 @@ class PushSubscriptionHelper(
|
|||
|
||||
// アプリサーバにendpoint URLの変更を伝える
|
||||
private suspend fun registerEndpoint(
|
||||
client: TootApiClient,
|
||||
deviceId: String,
|
||||
endpoint: String
|
||||
): TootApiResult {
|
||||
client: TootApiClient,
|
||||
deviceId: String,
|
||||
endpoint: String
|
||||
): TootApiResult {
|
||||
|
||||
if (account.last_push_endpoint == endpoint) return TootApiResult()
|
||||
|
||||
return client.http(
|
||||
jsonObject {
|
||||
put("acct", account.acct.ascii)
|
||||
put("deviceId", deviceId)
|
||||
put("endpoint", endpoint)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("${PollingWorker.APP_SERVER}/webpushendpoint")
|
||||
.build()
|
||||
).also { result ->
|
||||
result.response?.let{ res->
|
||||
when (res.code.also{ res.close() }) {
|
||||
jsonObject {
|
||||
put("acct", account.acct.ascii)
|
||||
put("deviceId", deviceId)
|
||||
put("endpoint", endpoint)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("${PollingWorker.APP_SERVER}/webpushendpoint")
|
||||
.build()
|
||||
).also { result ->
|
||||
result.response?.let { res ->
|
||||
when (res.code.also { res.close() }) {
|
||||
in 200 until 300 -> {
|
||||
account.updateLastPushEndpoint(endpoint)
|
||||
}
|
||||
|
@ -196,17 +199,17 @@ class PushSubscriptionHelper(
|
|||
// 購読
|
||||
@Suppress("SpellCheckingInspection")
|
||||
return client.request(
|
||||
"/api/sw/register",
|
||||
account.putMisskeyApiToken().apply {
|
||||
put("endpoint", endpoint)
|
||||
put("auth", "iRdmDrOS6eK6xvG1H6KshQ")
|
||||
"/api/sw/register",
|
||||
account.putMisskeyApiToken().apply {
|
||||
put("endpoint", endpoint)
|
||||
put("auth", "iRdmDrOS6eK6xvG1H6KshQ")
|
||||
put(
|
||||
"publickey",
|
||||
"BBEUVi7Ehdzzpe_ZvlzzkQnhujNJuBKH1R0xYg7XdAKNFKQG9Gpm0TSGRGSuaU7LUFKX-uz8YW0hAshifDCkPuE"
|
||||
)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
)?.also { result ->
|
||||
.toPostRequestBuilder()
|
||||
)?.also { result ->
|
||||
val jsonObject = result.jsonObject
|
||||
if (jsonObject == null) {
|
||||
addLog("API error.")
|
||||
|
@ -214,10 +217,10 @@ class PushSubscriptionHelper(
|
|||
if (verbose) addLog(context.getString(R.string.push_subscription_updated))
|
||||
subscribed = true
|
||||
return updateServerKey(
|
||||
client,
|
||||
clientIdentifier,
|
||||
jsonObject.string("key") ?: "3q2+rw"
|
||||
)
|
||||
client,
|
||||
clientIdentifier,
|
||||
jsonObject.string("key") ?: "3q2+rw"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -231,32 +234,35 @@ class PushSubscriptionHelper(
|
|||
var r = client.request("/api/v1/push/subscription")
|
||||
var res = r?.response ?: return r // cancelled or missing response
|
||||
var subscription404 = false
|
||||
|
||||
if (res.code != 200) log.i("${account.acct}: check existing subscription: code=${res.code}")
|
||||
|
||||
when (res.code) {
|
||||
200 -> {
|
||||
if (r.error?.isNotEmpty() == true && r.jsonObject == null) {
|
||||
// Pleromaが200応答でもエラーHTMLを返す場合がある
|
||||
addLog(context.getString(R.string.instance_does_not_support_push_api_pleroma))
|
||||
return r
|
||||
}
|
||||
// たぶん購読が存在する
|
||||
}
|
||||
200 -> {
|
||||
if (r.error?.isNotEmpty() == true && r.jsonObject == null) {
|
||||
// Pleromaが200応答でもエラーHTMLを返す場合がある
|
||||
addLog(context.getString(R.string.instance_does_not_support_push_api_pleroma))
|
||||
return r
|
||||
}
|
||||
// たぶん購読が存在する
|
||||
}
|
||||
|
||||
404 -> {
|
||||
subscription404 = true
|
||||
// この時点では存在しないのが購読なのかAPIなのか分からない
|
||||
}
|
||||
404 -> {
|
||||
subscription404 = true
|
||||
// この時点では存在しないのが購読なのかAPIなのか分からない
|
||||
}
|
||||
|
||||
403 -> {
|
||||
// アクセストークンにpushスコープがない
|
||||
if (flags != 0 || verbose)
|
||||
addLog(context.getString(R.string.missing_push_scope))
|
||||
return r
|
||||
}
|
||||
403 -> {
|
||||
// アクセストークンにpushスコープがない
|
||||
if (flags != 0 || verbose)
|
||||
addLog(context.getString(R.string.missing_push_scope))
|
||||
return r
|
||||
}
|
||||
|
||||
in 400 until 500 -> {
|
||||
addLog(context.getString(R.string.instance_does_not_support_push_api_pleroma))
|
||||
return r
|
||||
}
|
||||
in 400 until 500 -> {
|
||||
addLog(context.getString(R.string.instance_does_not_support_push_api_pleroma))
|
||||
return r
|
||||
}
|
||||
|
||||
else -> {
|
||||
addLog("${res.request}")
|
||||
|
@ -267,15 +273,16 @@ class PushSubscriptionHelper(
|
|||
|
||||
val oldSubscription = parseItem(::TootPushSubscription, r.jsonObject)
|
||||
if (oldSubscription == null) {
|
||||
log.i("${account.acct}: oldSubscription is null")
|
||||
|
||||
val (ti, result) = TootInstance.get(client)
|
||||
val (ti, result) = TootInstance.get(client)
|
||||
ti ?: return result
|
||||
|
||||
// 2.4.0rc1 未満にはプッシュ購読APIはない
|
||||
if (!ti.versionGE(TootInstance.VERSION_2_4_0_rc1))
|
||||
return TootApiResult(
|
||||
context.getString(R.string.instance_does_not_support_push_api, ti.version)
|
||||
)
|
||||
context.getString(R.string.instance_does_not_support_push_api, ti.version)
|
||||
)
|
||||
|
||||
if (subscription404 && flags == 0) {
|
||||
when {
|
||||
|
@ -353,7 +360,7 @@ class PushSubscriptionHelper(
|
|||
|
||||
// 期待する購読アラートのリスト
|
||||
var alertsNew = newAlerts.entries
|
||||
.mapNotNull { if ((it.value as? Boolean) == true) it.key else null }
|
||||
.mapNotNull { pair -> pair.key.takeIf { pair.value == true } }
|
||||
.sorted()
|
||||
|
||||
// 両方に共通するアラートは除去する
|
||||
|
@ -362,30 +369,36 @@ class PushSubscriptionHelper(
|
|||
alertsNew = alertsNew.filter { !bothHave.contains(it) }
|
||||
|
||||
// サーバのバージョンを調べる前に、この時点でalertsが一致するか確認する
|
||||
if (alertsOld.joinToString(",") == alertsNew.joinToString(","))
|
||||
if (alertsOld.joinToString(",") == alertsNew.joinToString(",")) {
|
||||
log.i("${account.acct}: same alerts(1)")
|
||||
return makeSkipResult()
|
||||
}
|
||||
|
||||
// ここでサーバのバージョンによって対応が変わる
|
||||
val (ti, result) = TootInstance.get(client)
|
||||
val (ti, result) = TootInstance.get(client)
|
||||
ti ?: return result
|
||||
|
||||
// サーバが知らないアラート種別は比較対象から除去する
|
||||
fun Iterable<String>.knownOnly() = filter {
|
||||
when (it) {
|
||||
"follow", "mention", "favourite", "reblog" -> true
|
||||
"poll" -> ti.versionGE(TootInstance.VERSION_2_8_0_rc1)
|
||||
"follow_request" -> ti.versionGE(TootInstance.VERSION_3_1_0_rc1)
|
||||
"status" -> ti.versionGE(TootInstance.VERSION_3_3_0_rc1)
|
||||
else -> false // 未知のアラートの差異は比較しない
|
||||
"follow", "mention", "favourite", "reblog" -> true
|
||||
"poll" -> ti.versionGE(TootInstance.VERSION_2_8_0_rc1)
|
||||
"follow_request" -> ti.versionGE(TootInstance.VERSION_3_1_0_rc1)
|
||||
"status" -> ti.versionGE(TootInstance.VERSION_3_3_0_rc1)
|
||||
else -> {
|
||||
log.w("${account.acct}: unknown alert '$it'. server version='${ti.version}'")
|
||||
false // 未知のアラートの差異は比較しない
|
||||
}
|
||||
}
|
||||
}
|
||||
alertsOld = alertsOld.knownOnly()
|
||||
alertsNew = alertsNew.knownOnly()
|
||||
|
||||
return if (alertsOld.joinToString(",") == alertsNew.joinToString(",")){
|
||||
return if (alertsOld.joinToString(",") == alertsNew.joinToString(",")) {
|
||||
log.i("${account.acct}: same alerts(2)")
|
||||
makeSkipResult()
|
||||
}else {
|
||||
addLog("alerts not match. account=${account.acct.pretty} old=${alertsOld.sorted()}, new=${alertsNew.sorted()}")
|
||||
} else {
|
||||
addLog("${account.acct}: alerts not match. account=${account.acct.pretty} old=${alertsOld.sorted()}, new=${alertsNew.sorted()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@ -395,14 +408,14 @@ class PushSubscriptionHelper(
|
|||
|
||||
// アクセストークンの優先権を取得
|
||||
r = client.http(
|
||||
jsonObject {
|
||||
put("token_digest", tokenDigest)
|
||||
put("install_id", install_id)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("${PollingWorker.APP_SERVER}/webpushtokencheck")
|
||||
.build()
|
||||
)
|
||||
jsonObject {
|
||||
put("token_digest", tokenDigest)
|
||||
put("install_id", install_id)
|
||||
}
|
||||
.toPostRequestBuilder()
|
||||
.url("${PollingWorker.APP_SERVER}/webpushtokencheck")
|
||||
.build()
|
||||
)
|
||||
res = r.response ?: return r
|
||||
|
||||
if (res.code == 200) {
|
||||
|
@ -424,24 +437,24 @@ class PushSubscriptionHelper(
|
|||
res = r?.response ?: return r
|
||||
|
||||
when (res.code) {
|
||||
200 -> {
|
||||
if (verbose) addLog(context.getString(R.string.push_subscription_deleted))
|
||||
TootApiResult()
|
||||
}
|
||||
200 -> {
|
||||
if (verbose) addLog(context.getString(R.string.push_subscription_deleted))
|
||||
TootApiResult()
|
||||
}
|
||||
|
||||
404 -> {
|
||||
if (verbose) {
|
||||
addLog(context.getString(R.string.missing_push_api))
|
||||
r
|
||||
} else {
|
||||
TootApiResult()
|
||||
}
|
||||
}
|
||||
404 -> {
|
||||
if (verbose) {
|
||||
addLog(context.getString(R.string.missing_push_api))
|
||||
r
|
||||
} else {
|
||||
TootApiResult()
|
||||
}
|
||||
}
|
||||
|
||||
403 -> {
|
||||
addLog(context.getString(R.string.missing_push_scope))
|
||||
r
|
||||
}
|
||||
403 -> {
|
||||
addLog(context.getString(R.string.missing_push_scope))
|
||||
r
|
||||
}
|
||||
|
||||
else -> {
|
||||
addLog("${res.request}")
|
||||
|
@ -456,51 +469,51 @@ class PushSubscriptionHelper(
|
|||
@Suppress("SpellCheckingInspection")
|
||||
val params = JsonObject().apply {
|
||||
put("subscription", JsonObject().apply {
|
||||
put("endpoint", endpoint)
|
||||
put("keys", JsonObject().apply {
|
||||
put("endpoint", endpoint)
|
||||
put("keys", JsonObject().apply {
|
||||
put(
|
||||
"p256dh",
|
||||
"BBEUVi7Ehdzzpe_ZvlzzkQnhujNJuBKH1R0xYg7XdAKNFKQG9Gpm0TSGRGSuaU7LUFKX-uz8YW0hAshifDCkPuE"
|
||||
)
|
||||
put("auth", "iRdmDrOS6eK6xvG1H6KshQ")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
put("data", JsonObject().apply {
|
||||
put("alerts", newAlerts)
|
||||
})
|
||||
put("alerts", newAlerts)
|
||||
})
|
||||
}
|
||||
|
||||
r = client.request(
|
||||
"/api/v1/push/subscription",
|
||||
params.toPostRequestBuilder()
|
||||
) ?: return null
|
||||
"/api/v1/push/subscription",
|
||||
params.toPostRequestBuilder()
|
||||
) ?: return null
|
||||
|
||||
res = r.response ?: return r
|
||||
|
||||
when (res.code) {
|
||||
404 -> {
|
||||
addLog(context.getString(R.string.missing_push_api))
|
||||
r
|
||||
}
|
||||
404 -> {
|
||||
addLog(context.getString(R.string.missing_push_api))
|
||||
r
|
||||
}
|
||||
|
||||
403 -> {
|
||||
addLog(context.getString(R.string.missing_push_scope))
|
||||
r
|
||||
}
|
||||
403 -> {
|
||||
addLog(context.getString(R.string.missing_push_scope))
|
||||
r
|
||||
}
|
||||
|
||||
200 -> {
|
||||
val newSubscription = parseItem(::TootPushSubscription, r.jsonObject)
|
||||
?: return r.setError("parse error.")
|
||||
200 -> {
|
||||
val newSubscription = parseItem(::TootPushSubscription, r.jsonObject)
|
||||
?: return r.setError("parse error.")
|
||||
|
||||
subscribed = true
|
||||
if (verbose) addLog(context.getString(R.string.push_subscription_updated))
|
||||
subscribed = true
|
||||
if (verbose) addLog(context.getString(R.string.push_subscription_updated))
|
||||
|
||||
return updateServerKey(
|
||||
client,
|
||||
clientIdentifier,
|
||||
newSubscription.server_key
|
||||
)
|
||||
}
|
||||
return updateServerKey(
|
||||
client,
|
||||
clientIdentifier,
|
||||
newSubscription.server_key
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
addLog(r.jsonObject?.toString())
|
||||
|
@ -532,7 +545,7 @@ class PushSubscriptionHelper(
|
|||
if (error != null) addLog("$error $requestInfo".trimEnd())
|
||||
|
||||
// update error text on account table
|
||||
val log = log
|
||||
val log = logString
|
||||
when {
|
||||
|
||||
log.contains(ERROR_PREVENT_FREQUENTLY_CHECK) -> {
|
||||
|
|
|
@ -5,93 +5,91 @@ import android.graphics.Rect
|
|||
import android.util.AttributeSet
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import jp.juggler.util.LogCategory
|
||||
|
||||
class MyDrawerLayout : DrawerLayout {
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LogCategory("MyDrawerLayout")
|
||||
}
|
||||
|
||||
constructor(context : Context) :
|
||||
super(context)
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet?) :
|
||||
super(context, attrs)
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet?, defStyleAttr : Int) :
|
||||
super(context, attrs, defStyleAttr)
|
||||
|
||||
private var bottomExclusionWidth : Int = 0
|
||||
private var bottomExclusionHeight : Int = 0
|
||||
private val exclusionRects = listOf(Rect(), Rect(), Rect(), Rect())
|
||||
|
||||
override fun onLayout(changed : Boolean, l : Int, t : Int, r : Int, b : Int) {
|
||||
super.onLayout(changed, l, t, r, b)
|
||||
|
||||
// 画面下部の左右にはボタンがあるので、システムジェスチャーナビゲーションの対象外にする
|
||||
val w = r - l
|
||||
val h = b - t
|
||||
if(w > 0 && h > 0) {
|
||||
|
||||
log.d("onLayout $l,$t,$r,$b bottomExclusionSize=$bottomExclusionWidth,$bottomExclusionHeight")
|
||||
|
||||
exclusionRects[0].set(
|
||||
|
||||
companion object {
|
||||
// private val log = LogCategory("MyDrawerLayout")
|
||||
}
|
||||
|
||||
constructor(context: Context) :
|
||||
super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) :
|
||||
super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
|
||||
super(context, attrs, defStyleAttr)
|
||||
|
||||
private var bottomExclusionWidth: Int = 0
|
||||
private var bottomExclusionHeight: Int = 0
|
||||
private val exclusionRects = listOf(Rect(), Rect(), Rect(), Rect())
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
super.onLayout(changed, l, t, r, b)
|
||||
|
||||
// 画面下部の左右にはボタンがあるので、システムジェスチャーナビゲーションの対象外にする
|
||||
val w = r - l
|
||||
val h = b - t
|
||||
if (w > 0 && h > 0) {
|
||||
|
||||
// log.d("onLayout $l,$t,$r,$b bottomExclusionSize=$bottomExclusionWidth,$bottomExclusionHeight")
|
||||
|
||||
exclusionRects[0].set(
|
||||
0,
|
||||
h - bottomExclusionHeight * 2,
|
||||
0 + bottomExclusionWidth,
|
||||
h
|
||||
)
|
||||
|
||||
exclusionRects[1].set(
|
||||
|
||||
exclusionRects[1].set(
|
||||
w - bottomExclusionWidth,
|
||||
h - bottomExclusionHeight * 2,
|
||||
w,
|
||||
h
|
||||
)
|
||||
|
||||
exclusionRects[2].set(
|
||||
|
||||
exclusionRects[2].set(
|
||||
0,
|
||||
0,
|
||||
bottomExclusionWidth,
|
||||
(bottomExclusionHeight * 1.5f).toInt()
|
||||
)
|
||||
|
||||
exclusionRects[3].set(
|
||||
|
||||
exclusionRects[3].set(
|
||||
w - bottomExclusionWidth,
|
||||
0,
|
||||
w,
|
||||
(bottomExclusionHeight * 1.5).toInt()
|
||||
)
|
||||
|
||||
ViewCompat.setSystemGestureExclusionRects(this, exclusionRects)
|
||||
|
||||
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
}
|
||||
|
||||
// デバッグ用
|
||||
// val paint = Paint()
|
||||
// override fun dispatchDraw(canvas : Canvas?) {
|
||||
// super.dispatchDraw(canvas)
|
||||
//
|
||||
// canvas ?: return
|
||||
//
|
||||
// log.d("dispatchDraw")
|
||||
// for(rect in exclusionRects) {
|
||||
// paint.color = 0x40ff0000
|
||||
// canvas.drawRect(rect, paint)
|
||||
// }
|
||||
// }
|
||||
|
||||
fun setExclusionSize(sizeDp : Int) {
|
||||
val w = (sizeDp * 1.25f + 0.5f).toInt()
|
||||
val h = (sizeDp * 1.5f + 0.5f).toInt()
|
||||
|
||||
bottomExclusionWidth = w
|
||||
bottomExclusionHeight = h
|
||||
|
||||
}
|
||||
|
||||
ViewCompat.setSystemGestureExclusionRects(this, exclusionRects)
|
||||
|
||||
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
}
|
||||
|
||||
// デバッグ用
|
||||
// val paint = Paint()
|
||||
// override fun dispatchDraw(canvas : Canvas?) {
|
||||
// super.dispatchDraw(canvas)
|
||||
//
|
||||
// canvas ?: return
|
||||
//
|
||||
// log.d("dispatchDraw")
|
||||
// for(rect in exclusionRects) {
|
||||
// paint.color = 0x40ff0000
|
||||
// canvas.drawRect(rect, paint)
|
||||
// }
|
||||
// }
|
||||
|
||||
fun setExclusionSize(sizeDp: Int) {
|
||||
val w = (sizeDp * 1.25f + 0.5f).toInt()
|
||||
val h = (sizeDp * 1.5f + 0.5f).toInt()
|
||||
|
||||
bottomExclusionWidth = w
|
||||
bottomExclusionHeight = h
|
||||
|
||||
}
|
||||
}
|
|
@ -14,6 +14,9 @@ inline fun <reified T : Any> Any.castNotNull(): T = this as T
|
|||
inline fun <reified T> systemService(context: Context): T? =
|
||||
/* ContextCompat. */ ContextCompat.getSystemService(context, T::class.java)
|
||||
|
||||
fun<T:Comparable<T>> minComparable(a:T,b:T):T = if (a <= b) a else b
|
||||
fun<T:Comparable<T>> maxComparable(a:T,b:T):T = if (a >= b) a else b
|
||||
|
||||
//
|
||||
//object Utils {
|
||||
//
|
||||
|
|
|
@ -599,16 +599,21 @@
|
|||
android:text="@string/notification_type_post" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/setting_row_form">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPushSubscription"
|
||||
style="@style/setting_horizontal_stretch"
|
||||
style="@style/setting_row_button"
|
||||
android:ellipsize="start"
|
||||
android:text="@string/update_push_subscription"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/btnPushSubscriptionNotForce"
|
||||
style="@style/setting_row_button"
|
||||
android:ellipsize="start"
|
||||
android:text="@string/update_push_subscription_not_force"
|
||||
android:visibility="gone"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<View style="@style/setting_divider" />
|
||||
|
||||
|
|
|
@ -1074,4 +1074,5 @@
|
|||
|
||||
<string name="notestock" translatable="false">notestock</string>
|
||||
<string name="toot_search_notestock_of">notestock search \"%1$s\"</string>
|
||||
<string name="update_push_subscription_not_force" translatable="false">Update push subscription(not force)</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue