SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/ActPushMessageList.kt

257 lines
9.9 KiB
Kotlin

package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import jp.juggler.subwaytooter.api.dialogOrToast
import jp.juggler.subwaytooter.databinding.ActPushMessageListBinding
import jp.juggler.subwaytooter.databinding.LvPushMessageBinding
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.runInProgress
import jp.juggler.subwaytooter.push.PushMessageIconColor
import jp.juggler.subwaytooter.push.iconColor
import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.table.PushMessage
import jp.juggler.subwaytooter.table.daoAccountNotificationStatus
import jp.juggler.subwaytooter.table.daoPushMessage
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.permissionSpecNotification
import jp.juggler.subwaytooter.util.requester
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.encodeBase64Url
import jp.juggler.util.data.notBlank
import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory
import jp.juggler.util.os.saveToDownload
import jp.juggler.util.time.formatLocalTime
import jp.juggler.util.ui.setNavigationBack
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.PrintWriter
class ActPushMessageList : AppCompatActivity() {
companion object {
private val log = LogCategory("ActPushMessageList")
}
private val views by lazy {
ActPushMessageListBinding.inflate(layoutInflater)
}
private val listAdapter = MyAdapter()
private val layoutManager by lazy {
LinearLayoutManager(this)
}
private val prNotification = permissionSpecNotification.requester {
// 特に何もしない
}
private val acctMap by lazy {
daoSavedAccount.loadRealAccounts().associateBy { it.acct }
}
override fun onCreate(savedInstanceState: Bundle?) {
prNotification.register(this)
prNotification.checkOrLaunch()
super.onCreate(savedInstanceState)
App1.setActivityTheme(this)
setContentView(views.root)
setSupportActionBar(views.toolbar)
setNavigationBack(views.toolbar)
views.rvMessages.also {
val dividerItemDecoration = DividerItemDecoration(
this,
LinearLayout.VERTICAL,
)
it.addItemDecoration(dividerItemDecoration)
it.adapter = listAdapter
it.layoutManager = layoutManager
}
lifecycleScope.launch {
PushMessage.flowDataChanged.collect {
try {
listAdapter.items = withContext(AppDispatchers.IO) {
daoPushMessage.listAll()
}
} catch (ex: Throwable) {
log.e(ex, "load failed.")
}
}
}
}
fun itemActions(pm: PushMessage) {
launchAndShowError {
actionsDialog {
action(getString(R.string.push_message_re_decode)) {
pushRepo.reprocess(pm)
}
action(getString(R.string.push_message_save_to_download_folder)) {
export(pm)
}
action(getString(R.string.push_message_save_to_download_folder_with_secret_key)) {
export(pm, exportKeys = true)
}
}
}
}
/**
* エクスポート、というか端末のダウンロードフォルダに保存する
*/
private suspend fun export(pm: PushMessage, exportKeys: Boolean = false) {
val path = runInProgress {
withContext(AppDispatchers.DEFAULT) {
saveToDownload(
displayName = "PushMessageDump-${pm.id}.txt",
) { PrintWriter(it).apply { dumpMessage(pm, exportKeys) }.flush() }
}
}
if (!path.isNullOrEmpty()) {
dialogOrToast(getString(R.string.saved_to, path))
}
}
private fun PrintWriter.dumpMessage(pm: PushMessage, exportKeys: Boolean) {
println("timestamp: ${pm.timestamp.formatLocalTime()}")
println("timeSave: ${pm.timeSave.formatLocalTime()}")
println("timeDismiss: ${pm.timeDismiss.formatLocalTime()}")
println("to: ${pm.loginAcct}")
println("type: ${pm.notificationType}")
println("id: ${pm.notificationId}")
println("text: ${pm.textExpand}")
println("formatJson=${pm.formatJson.toString(1, sort = true)}")
println("messageJson=${pm.messageJson?.toString(1, sort = true)}")
println("dataSize: ${pm.rawBody?.size}")
if (exportKeys) {
val acct = pm.loginAcct
if (acct == null) {
println("!!secret key is not exported because missing recepients acct.")
} else {
val status = daoAccountNotificationStatus.load(acct)
if (status == null) {
println("!!secret key is not exported because missing status for acct $acct .")
} else {
println("receiverPrivateBytes=${status.pushKeyPrivate?.encodeBase64Url()}")
println("receiverPublicBytes=${status.pushKeyPublic?.encodeBase64Url()}")
println("senderPublicBytes=${status.pushServerKey?.encodeBase64Url()}")
println("authSecret=${status.pushAuthSecret?.encodeBase64Url()}")
}
}
}
println("headerJson=${pm.headerJson}")
println("rawBody=${pm.rawBody?.encodeBase64Url()}")
}
private val tintIconMap = HashMap<String, Drawable>()
fun tintIcon(pm: PushMessage, ic: PushMessageIconColor) =
tintIconMap.getOrPut("${ic.name}-${pm.loginAcct}") {
val context = this
val src = ContextCompat.getDrawable(context, ic.iconId)!!
DrawableCompat.wrap(src).also { d ->
val a = acctMap[pm.loginAcct]
val c = ic.colorRes.notZero()?.let { ContextCompat.getColor(context, it) }
?: a?.notificationAccentColor?.notZero()
?: ContextCompat.getColor(this, R.color.colorOsNotificationAccent)
DrawableCompat.setTint(d, c)
}
}
@SuppressLint("SetTextI18n")
private inner class MyViewHolder(
parent: ViewGroup,
val views: LvPushMessageBinding =
LvPushMessageBinding.inflate(layoutInflater, parent, false),
) : RecyclerView.ViewHolder(views.root) {
var lastItem: PushMessage? = null
init {
views.root.setOnClickListener { lastItem?.let { itemActions(it) } }
}
fun bind(pm: PushMessage?) {
pm ?: return
lastItem = pm
Glide.with(views.ivSmall)
.load(pm.iconSmall)
.error(tintIcon(pm, pm.iconColor()))
.into(views.ivSmall)
Glide.with(views.ivLarge)
.load(pm.iconLarge)
.into(views.ivLarge)
views.tvText.text = arrayOf(
"when: ${pm.timestamp.formatLocalTime()}",
pm.timeDismiss.takeIf { it > 0L }?.let { "既読: ${it.formatLocalTime()}" },
"to: ${pm.loginAcct}",
"type: ${pm.notificationType}",
"id: ${pm.notificationId}",
"dataSize: ${pm.rawBody?.size}",
pm.textExpand,
pm.formatError?.let { "error: $it" },
).mapNotNull { it.notBlank() }.joinToString("\n")
}
}
private inner class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
var items: List<PushMessage> = emptyList()
set(value) {
val oldScrollPos = layoutManager.findFirstVisibleItemPosition()
.takeIf { it != RecyclerView.NO_POSITION }
val oldItems = field
field = value
DiffUtil.calculateDiff(
object : DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size
override fun getNewListSize() = value.size
override fun areItemsTheSame(
oldItemPosition: Int,
newItemPosition: Int,
) = oldItems[oldItemPosition] == value[newItemPosition]
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int,
) = false
},
true
).dispatchUpdatesTo(this)
if (oldScrollPos == 0) {
launchAndShowError {
delay(50L)
views.rvMessages.smoothScrollToPosition(0)
}
}
}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MyViewHolder(parent)
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(items.elementAtOrNull(position))
}
}
}