デバッグビルドのみアカウント設定にボタンを追加。通知表示の既読状況がおかしい場合にかるく修正する

This commit is contained in:
tateisu 2020-12-11 08:25:55 +09:00
parent 307ddfa239
commit fdd85d6a10
13 changed files with 673 additions and 620 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.")
// }
//
// }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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