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

438 lines
15 KiB
Kotlin
Raw Normal View History

package jp.juggler.subwaytooter
import android.content.Context
import android.content.SharedPreferences
2019-08-23 01:49:20 +02:00
import android.util.SparseArray
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
2020-12-21 03:13:03 +01:00
import jp.juggler.subwaytooter.streaming.*
import jp.juggler.subwaytooter.table.*
2018-12-01 00:02:18 +01:00
import jp.juggler.subwaytooter.util.BucketList
import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.util.*
import okhttp3.Handshake
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
class Column(
2020-12-21 03:13:03 +01:00
val app_state: AppState,
val context: Context,
val access_info: SavedAccount,
typeId: Int,
val column_id: String
2018-01-12 10:01:25 +01:00
) {
2020-12-06 21:19:02 +01:00
companion object {
internal val log = LogCategory("Column")
internal const val LOOP_TIMEOUT = 10000L
internal const val LOOP_READ_ENOUGH = 30 // フィルタ後のデータ数がコレ以上ならループを諦めます
internal const val RELATIONSHIP_LOAD_STEP = 40
internal const val ACCT_DB_STEP = 100
internal const val MISSKEY_HASHTAG_LIMIT = 30
2021-05-17 21:33:28 +02:00
internal const val HASHTAG_ELLIPSIZE = 26
2020-12-06 21:19:02 +01:00
val typeMap: SparseArray<ColumnType> = SparseArray()
internal var showOpenSticker = false
internal const val QUICK_FILTER_ALL = 0
internal const val QUICK_FILTER_MENTION = 1
internal const val QUICK_FILTER_FAVOURITE = 2
internal const val QUICK_FILTER_BOOST = 3
internal const val QUICK_FILTER_FOLLOW = 4
internal const val QUICK_FILTER_REACTION = 5
internal const val QUICK_FILTER_VOTE = 6
internal const val QUICK_FILTER_POST = 7
fun loadAccount(context: Context, src: JsonObject): SavedAccount {
2021-05-17 16:13:04 +02:00
val account_db_id = src.long(ColumnEncoder.KEY_ACCOUNT_ROW_ID) ?: -1L
2020-12-06 21:19:02 +01:00
return if (account_db_id >= 0) {
SavedAccount.loadAccount(context, account_db_id)
?: throw RuntimeException("missing account")
} else {
SavedAccount.na
}
}
2020-12-21 03:13:03 +01:00
// private val channelIdSeed = AtomicInteger(0)
2020-12-06 21:19:02 +01:00
// より古いデータの取得に使う
internal val reMaxId = """[&?]max_id=([^&?;\s]+)""".asciiPattern()
// より新しいデータの取得に使う
2021-05-17 16:13:04 +02:00
val reMinId = """[&?](min_id|since_id)=([^&?;\s]+)""".asciiPattern()
2020-12-06 21:19:02 +01:00
val COLUMN_REGEX_FILTER_DEFAULT: (CharSequence?) -> Boolean = { false }
2021-05-17 15:03:23 +02:00
var defaultColorHeaderBg = 0
var defaultColorHeaderName = 0
var defaultColorHeaderPageNumber = 0
2020-12-06 21:19:02 +01:00
var defaultColorContentBg = 0
2021-05-17 15:03:23 +02:00
var defaultColorContentAcct = 0
var defaultColorContentText = 0
2020-12-06 21:19:02 +01:00
fun reloadDefaultColor(activity: AppCompatActivity, pref: SharedPreferences) {
defaultColorHeaderBg = Pref.ipCcdHeaderBg(pref).notZero()
?: activity.attrColor(R.attr.color_column_header)
2020-12-06 21:19:02 +01:00
defaultColorHeaderName = Pref.ipCcdHeaderFg(pref).notZero()
?: activity.attrColor(R.attr.colorColumnHeaderName)
2020-12-06 21:19:02 +01:00
defaultColorHeaderPageNumber = Pref.ipCcdHeaderFg(pref).notZero()
?: activity.attrColor(R.attr.colorColumnHeaderPageNumber)
2020-12-06 21:19:02 +01:00
defaultColorContentBg = Pref.ipCcdContentBg(pref)
// may zero
defaultColorContentAcct = Pref.ipCcdContentAcct(pref).notZero()
?: activity.attrColor(R.attr.colorTimeSmall)
2020-12-06 21:19:02 +01:00
defaultColorContentText = Pref.ipCcdContentText(pref).notZero()
?: activity.attrColor(R.attr.colorContentText)
2020-12-06 21:19:02 +01:00
}
2020-12-21 03:13:03 +01:00
private val internalIdSeed = AtomicInteger(0)
2020-12-06 21:19:02 +01:00
}
2020-12-21 03:13:03 +01:00
// カラムオブジェクトの識別に使うID。
val internalId = internalIdSeed.incrementAndGet()
2020-12-06 21:19:02 +01:00
val type = ColumnType.parse(typeId)
internal var dont_close: Boolean = false
internal var with_attachment: Boolean = false
internal var with_highlight: Boolean = false
internal var dont_show_boost: Boolean = false
internal var dont_show_reply: Boolean = false
internal var dont_show_normal_toot: Boolean = false
internal var dont_show_non_public_toot: Boolean = false
internal var dont_show_favourite: Boolean = false // 通知カラムのみ
internal var dont_show_follow: Boolean = false // 通知カラムのみ
internal var dont_show_reaction: Boolean = false // 通知カラムのみ
internal var dont_show_vote: Boolean = false // 通知カラムのみ
internal var quick_filter = QUICK_FILTER_ALL
2020-12-21 03:13:03 +01:00
@Volatile
2020-12-06 21:19:02 +01:00
internal var dont_streaming: Boolean = false
2020-12-21 03:13:03 +01:00
2020-12-06 21:19:02 +01:00
internal var dont_auto_refresh: Boolean = false
internal var hide_media_default: Boolean = false
internal var system_notification_not_related: Boolean = false
internal var instance_local: Boolean = false
internal var enable_speech: Boolean = false
internal var use_old_api = false
internal var regex_text: String = ""
internal var header_bg_color: Int = 0
internal var header_fg_color: Int = 0
internal var column_bg_color: Int = 0
internal var acct_color: Int = 0
internal var content_color: Int = 0
internal var column_bg_image: String = ""
internal var column_bg_image_alpha = 1f
internal var profile_tab = ProfileTab.Status
internal var status_id: EntityId? = null
// プロフカラムではアカウントのID。リストカラムではリストのID
internal var profile_id: EntityId? = null
internal var search_query: String = ""
internal var search_resolve: Boolean = false
internal var remote_only: Boolean = false
internal var instance_uri: String = ""
internal var hashtag: String = ""
internal var hashtag_any: String = ""
internal var hashtag_all: String = ""
internal var hashtag_none: String = ""
internal var hashtag_acct: String = ""
internal var language_filter: JsonObject? = null
// 告知のリスト
internal var announcements: MutableList<TootAnnouncement>? = null
// 表示中の告知
internal var announcementId: EntityId? = null
// 告知を閉じた時刻, 0なら閉じていない
internal var announcementHideTime = 0L
// 告知データを更新したタイミング
internal var announcementUpdated = 0L
// プロフカラムでのアカウント情報
@Volatile
internal var who_account: TootAccountRef? = null
// プロフカラムでのfeatured tag 情報(Mastodon3.3.0)
@Volatile
internal var who_featured_tags: List<TootTag>? = null
// リストカラムでのリスト情報
@Volatile
internal var list_info: TootList? = null
// アンテナカラムでのリスト情報
@Volatile
internal var antenna_info: MisskeyAntenna? = null
// 「インスタンス情報」カラムに表示するインスタンス情報
// (SavedAccount中のインスタンス情報とは異なるので注意)
internal var instance_information: TootInstance? = null
internal var handshake: Handshake? = null
internal var scroll_save: ScrollPosition? = null
2021-05-17 16:13:04 +02:00
var last_viewing_item_id: EntityId? = null
2020-12-06 21:19:02 +01:00
internal val is_dispose = AtomicBoolean()
2020-12-21 03:13:03 +01:00
@Volatile
2020-12-06 21:19:02 +01:00
internal var bFirstInitialized = false
var filter_reload_required: Boolean = false
//////////////////////////////////////////////////////////////////////////////////////
// カラムを閉じた後のnotifyDataSetChangedのタイミングで、add/removeされる順序が期待通りにならないので
// 参照を1つだけ持つのではなく、リストを保持して先頭の要素を使うことにする
2021-05-17 16:13:04 +02:00
val _holder_list = LinkedList<ColumnViewHolder>()
2020-12-06 21:19:02 +01:00
internal // 複数のリスナがある場合、最も新しいものを返す
val viewHolder: ColumnViewHolder?
get() {
if (is_dispose.get()) return null
return if (_holder_list.isEmpty()) null else _holder_list.first
}
//////////////////////////////////////////////////////////////////////////////////////
internal var lastTask: ColumnTask? = null
2020-12-21 03:13:03 +01:00
@Volatile
2020-12-06 21:19:02 +01:00
internal var bInitialLoading: Boolean = false
2020-12-21 03:13:03 +01:00
@Volatile
2020-12-06 21:19:02 +01:00
internal var bRefreshLoading: Boolean = false
internal var mInitialLoadingError: String = ""
internal var mRefreshLoadingError: String = ""
internal var mRefreshLoadingErrorTime: Long = 0L
internal var mRefreshLoadingErrorPopupState: Int = 0
internal var task_progress: String? = null
internal val list_data = BucketList<TimelineItem>()
internal val duplicate_map = DuplicateMap()
2021-05-17 21:33:28 +02:00
2020-12-06 21:19:02 +01:00
@Volatile
2021-05-17 15:03:23 +02:00
var column_regex_filter = COLUMN_REGEX_FILTER_DEFAULT
2020-12-06 21:19:02 +01:00
@Volatile
2021-05-17 15:03:23 +02:00
var keywordFilterTrees: FilterTrees? = null
2020-12-06 21:19:02 +01:00
@Volatile
2021-05-17 15:03:23 +02:00
var favMuteSet: HashSet<Acct>? = null
2020-12-06 21:19:02 +01:00
@Volatile
2021-05-17 15:03:23 +02:00
var highlight_trie: WordTrieTree? = null
2020-12-06 21:19:02 +01:00
// タイムライン中のデータの始端と終端
// misskeyは
internal var idRecent: EntityId? = null
internal var idOld: EntityId? = null
internal var offsetNext: Int = 0
internal var pagingType: ColumnPagingType = ColumnPagingType.Default
var bRefreshingTop: Boolean = false
// ListViewの表示更新が追いつかないとスクロール位置が崩れるので
// 一定時間より短期間にはデータ更新しないようにする
2021-05-17 16:13:04 +02:00
val last_show_stream_data = AtomicLong(0L)
val stream_data_queue = ConcurrentLinkedQueue<TimelineItem>()
2020-12-06 21:19:02 +01:00
@Volatile
2021-05-17 16:13:04 +02:00
var bPutGap: Boolean = false
2020-12-06 21:19:02 +01:00
2021-05-17 15:03:23 +02:00
var cacheHeaderDesc: String? = null
2020-12-06 21:19:02 +01:00
// DMカラム更新時に新APIの利用に成功したなら真
internal var useConversationSummaries = false
2020-12-06 21:19:02 +01:00
// DMカラムのストリーミングイベントで新形式のイベントを利用できたなら真
internal var useConversationSummaryStreaming = false
2020-12-06 21:19:02 +01:00
////////////////////////////////////////////////////////////////
2020-12-06 21:19:02 +01:00
2020-12-22 05:27:39 +01:00
private fun runOnMainLooperForStreamingEvent(proc: () -> Unit) {
runOnMainLooper {
2020-12-22 05:27:39 +01:00
if (!canHandleStreamingMessage())
return@runOnMainLooper
proc()
}
}
2020-12-06 21:19:02 +01:00
val streamCallback = object : StreamCallback {
2020-12-06 21:19:02 +01:00
override fun onStreamStatusChanged(status: StreamStatus) {
log.d(
"onStreamStatusChanged status=${status}, bFirstInitialized=$bFirstInitialized, bInitialLoading=$bInitialLoading, column=${access_info.acct}/${
getColumnName(
true
)
}"
)
2020-12-06 21:19:02 +01:00
if (status == StreamStatus.Subscribed) {
updateMisskeyCapture()
2020-12-21 03:13:03 +01:00
}
2020-12-06 21:19:02 +01:00
runOnMainLooperForStreamingEvent {
if (is_dispose.get()) return@runOnMainLooperForStreamingEvent
fireShowColumnStatus()
2020-12-06 21:19:02 +01:00
}
}
2020-12-22 05:27:39 +01:00
override fun onTimelineItem(item: TimelineItem, channelId: String?, stream: JsonArray?) {
if (StreamManager.traceDelivery) log.v("${access_info.acct} onTimelineItem")
2020-12-22 05:27:39 +01:00
if (!canHandleStreamingMessage()) return
2020-12-06 21:19:02 +01:00
when (item) {
is TootConversationSummary -> {
if (type != ColumnType.DIRECT_MESSAGES) return
if (isFiltered(item.last_status)) return
if (use_old_api) {
useConversationSummaryStreaming = false
return
} else {
useConversationSummaryStreaming = true
}
}
2020-12-06 21:19:02 +01:00
is TootNotification -> {
if (!isNotificationColumn) return
if (isFiltered(item)) return
}
2020-12-06 21:19:02 +01:00
is TootStatus -> {
if (isNotificationColumn) return
2020-12-06 21:19:02 +01:00
// マストドン2.6.0形式のDMカラム用イベントを利用したならば、その直後に発生する普通の投稿イベントを無視する
if (useConversationSummaryStreaming) return
2020-12-06 21:19:02 +01:00
// マストドンはLTLに外部ユーザの投稿を表示しない
if (type == ColumnType.LOCAL && isMastodon && item.account.isRemote) return
2020-12-21 03:13:03 +01:00
if (isFiltered(item)) return
}
2020-12-21 03:13:03 +01:00
}
stream_data_queue.add(item)
2021-05-17 16:13:04 +02:00
app_state.handler.post(procMergeStreamingMessage)
}
2020-12-21 03:13:03 +01:00
override fun onEmojiReaction(item: TootNotification) {
runOnMainLooperForStreamingEvent {
2021-05-17 15:03:23 +02:00
this@Column.updateEmojiReaction(item.status)
}
}
override fun onNoteUpdated(ev: MisskeyNoteUpdate, channelId: String?) {
runOnMainLooperForStreamingEvent {
2021-05-17 15:03:23 +02:00
this@Column.onMisskeyNoteUpdated(ev)
2020-12-21 03:13:03 +01:00
}
}
2020-12-06 21:19:02 +01:00
override fun onAnnouncementUpdate(item: TootAnnouncement) {
runOnMainLooperForStreamingEvent {
2021-05-17 15:03:23 +02:00
this@Column.onAnnouncementUpdate(item)
2020-12-06 21:19:02 +01:00
}
}
2020-12-21 21:16:33 +01:00
override fun onAnnouncementDelete(id: EntityId) {
runOnMainLooperForStreamingEvent {
2021-05-17 15:03:23 +02:00
this@Column.onAnnouncementDelete(id)
}
}
2020-12-21 21:16:33 +01:00
override fun onAnnouncementReaction(reaction: TootReaction) {
runOnMainLooperForStreamingEvent {
2021-05-17 15:03:23 +02:00
this@Column.onAnnouncementReaction(reaction)
}
}
}
2021-05-17 16:13:04 +02:00
val procMergeStreamingMessage = Runnable {
this@Column.mergeStreamingMessage()
}
//////////////////////////////////////////////////////////////////////////////////////
2021-05-17 21:33:28 +02:00
// create from column spec
internal constructor(
app_state: AppState,
access_info: SavedAccount,
type: Int,
vararg params: Any
) : this(
2021-05-17 21:33:28 +02:00
app_state = app_state,
context = app_state.context,
access_info = access_info,
typeId = type,
column_id = ColumnEncoder.generateColumnId()
) {
2021-05-17 21:33:28 +02:00
ColumnSpec.decode( this, params)
}
internal constructor(app_state: AppState, src: JsonObject)
: this(
app_state,
app_state.context,
loadAccount(app_state.context, src),
2021-05-17 16:13:04 +02:00
src.optInt(ColumnEncoder.KEY_TYPE),
ColumnEncoder.decodeColumnId(src)
) {
2021-05-17 21:33:28 +02:00
ColumnEncoder.decode(this, src)
2020-12-06 21:19:02 +01:00
}
override fun hashCode(): Int = internalId
override fun equals(other: Any?): Boolean = this === other
2020-12-06 21:19:02 +01:00
internal fun dispose() {
is_dispose.set(true)
2020-12-21 03:13:03 +01:00
app_state.streamManager.updateStreamingColumns()
2020-12-06 21:19:02 +01:00
for (vh in _holder_list) {
try {
vh.listView.adapter = null
} catch (ignored: Throwable) {
}
}
}
init {
2021-05-17 16:13:04 +02:00
ColumnEncoder.registerColumnId(column_id, this)
2020-12-06 21:19:02 +01:00
}
}