package jp.juggler.subwaytooter import android.os.SystemClock import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.entity.* import jp.juggler.util.* import java.lang.StringBuilder class ColumnTask_Gap( columnArg : Column, private val gap : TimelineItem, private val isHead : Boolean ) : ColumnTask(columnArg, ColumnTaskType.GAP) { companion object { internal val log = LogCategory("CT_Gap") private val reIToken = """"i":"[^"]+"""".toRegex() private fun String.removeIToken() = reIToken.replace(this, """"i":"**"""") } private var max_id : EntityId? = (gap as? TootGap)?.max_id private var since_id : EntityId? = (gap as? TootGap)?.since_id override fun doInBackground() : TootApiResult? { ctStarted.set(true) val client = TootApiClient(context, callback = object : TootApiCallback { override val isApiCancelled : Boolean get() = isCancelled || column.is_dispose.get() override fun publishApiProgress(s : String) { runOnMainLooper { if(isCancelled) return@runOnMainLooper column.task_progress = s column.fireShowContent(reason = "gap progress", changeList = ArrayList()) } } }) client.account = access_info try { return column.type.gap(this, client) } catch(ex : Throwable) { return TootApiResult(ex.withCaption("gap loading failed.")) } finally { try { column.updateRelation(client, list_tmp, column.who_account, parser) } catch(ex : Throwable) { log.trace(ex) } ctClosed.set(true) runOnMainLooperDelayed(333L) { if(! isCancelled) column.fireShowColumnStatus() } } } override fun onPostExecute(result : TootApiResult?) { if(column.is_dispose.get()) return if(isCancelled || result == null) { return } try { column.lastTask = null column.bRefreshLoading = false val error = result.error if(error != null) { column.mRefreshLoadingError = error column.fireShowContent(reason = "gap error", changeList = ArrayList()) return } val list_tmp = this.list_tmp if(list_tmp == null) { column.fireShowContent(reason = "gap list_tmp is null", changeList = ArrayList()) return } val list_new = when(column.type) { // 検索カラムはIDによる重複排除が不可能 ColumnType.SEARCH -> list_tmp // 他のカラムは重複排除してから追加 else -> column.duplicate_map.filterDuplicate(list_tmp) } // 0個でもギャップを消すために以下の処理を続ける val changeList = ArrayList() column.replaceConversationSummary(changeList, list_new, column.list_data) val added = list_new.size // may 0 val position = column.list_data.indexOf(gap) if(position == - 1) { log.d("gap not found..") column.fireShowContent(reason = "gap not found", changeList = ArrayList()) return } val iv = if(isHead) { Pref.ipGapHeadScrollPosition } else { Pref.ipGapTailScrollPosition }.invoke(pref) val scrollHead = iv == Pref.GSP_HEAD if(scrollHead) { // ギャップを頭から読んだ場合、スクロール位置の調整は不要 column.list_data.removeAt(position) column.list_data.addAll(position, list_new) changeList.add(AdapterChange(AdapterChangeType.RangeRemove, position)) if(added > 0) { changeList.add( AdapterChange( AdapterChangeType.RangeInsert, position, added ) ) } column.fireShowContent(reason = "gap updated", changeList = changeList) } else { // ギャップを下から読んだ場合、ギャップの次の要素が画面内で同じ位置になるようスクロール位置を調整する必要がある // idx番目の要素がListViewのtopから何ピクセル下にあるか var restore_idx = position + 1 var restore_y = 0 val holder = column.viewHolder if(holder != null) { try { restore_y = holder.getListItemOffset(restore_idx) } catch(ex : IndexOutOfBoundsException) { restore_idx = position try { restore_y = holder.getListItemOffset(restore_idx) } catch(ex2 : IndexOutOfBoundsException) { restore_idx = - 1 } } } column.list_data.removeAt(position) column.list_data.addAll(position, list_new) changeList.add(AdapterChange(AdapterChangeType.RangeRemove, position)) if(added > 0) { changeList.add( AdapterChange( AdapterChangeType.RangeInsert, position, added ) ) } column.fireShowContent(reason = "gap updated", changeList = changeList) when { // ViewHolderがない holder == null -> { val scroll_save = column.scroll_save if(scroll_save != null) { scroll_save.adapterIndex += added - 1 } } // ギャップが画面内にあるなら restore_idx >= 0 -> holder.setListItemTop(restore_idx + added - 1, restore_y) // ギャップが画面内にない場合、何もしない else -> { } } } column.updateMisskeyCapture() } finally { column.fireShowColumnStatus() } } private fun allRangeChecked(logCaption : String) : Boolean { val tmpMaxId = max_id val tmpMinId = since_id if(tmpMaxId != null && tmpMinId != null && tmpMinId >= tmpMaxId) { log.d("$logCaption: allRangeChecked. $tmpMinId >= $tmpMaxId") return true } return false } // max_id を指定してギャップの上から読む private fun readGapHeadMisskey( logCaption : String, client : TootApiClient, path_base : String, paramsCreator : (EntityId?) -> JsonObject, arrayFinder : (JsonObject) -> JsonArray? = { null }, listParser : (TootParser, JsonArray) -> List, adder : (List) -> Unit ) : TootApiResult? { list_tmp = ArrayList() val time_start = SystemClock.elapsedRealtime() var result : TootApiResult? = null var bAddGap = false val olderLimit = since_id while(true) { if(isCancelled) { log.d("$logCaption: cancelled.") break } if(result != null && SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) { log.d("$logCaption: timeout.") bAddGap = true break } if(allRangeChecked(logCaption)) break val params = paramsCreator(max_id) log.d("$logCaption: $path_base ${params.toString().removeIToken()}") val r2 = client.request( path_base, params.toPostRequestBuilder() ) val jsonObject = r2?.jsonObject if(jsonObject != null) r2.data = arrayFinder(jsonObject) val jsonArray = r2?.jsonArray if(jsonArray == null) { log.d("$logCaption: error or cancelled. make gap.") // 成功データがない場合だけ、今回のエラーを返すようにする if(result == null) result = r2 bAddGap = true break } // 成功した場合はそれを返したい result = r2 var src : List = listParser(parser, jsonArray) if(olderLimit != null) src = src.filter { it.isInjected() || it.getOrderId() > olderLimit } if(src.none { ! it.isInjected() }) { // 直前の取得でカラのデータが帰ってきたら終了 log.d("$logCaption: empty.") break } // 隙間の最新のステータスIDは取得データ末尾のステータスIDである max_id = column.parseRange(result, src).first adder(src) } val sortAllowed = true if(sortAllowed) list_tmp?.sortByDescending { it.getOrderId() } if(bAddGap) addOne(list_tmp, TootGap.mayNull(max_id, since_id)) return result } // since_idを指定してギャップの下から読む private fun readGapTailMisskey( logCaption : String, client : TootApiClient, path_base : String, paramsCreator : (EntityId?) -> JsonObject, arrayFinder : (JsonObject) -> JsonArray? = { null }, listParser : (TootParser, JsonArray) -> List, adder : (List) -> Unit ) : TootApiResult? { list_tmp = ArrayList() val time_start = SystemClock.elapsedRealtime() var result : TootApiResult? = null var bAddGap = false val newerLimit = max_id while(true) { if(isCancelled) { log.d("$logCaption: cancelled.") break } if(result != null && SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) { log.d("$logCaption: timeout.") bAddGap = true break } if(allRangeChecked(logCaption)) break val params = paramsCreator(since_id) log.d("$logCaption: $path_base ${params.toString().removeIToken()}") val r2 = client.request( path_base, params.toPostRequestBuilder() ) val jsonObject = r2?.jsonObject if(jsonObject != null) r2.data = arrayFinder(jsonObject) val jsonArray = r2?.jsonArray if(jsonArray == null) { log.d("$logCaption: error or cancelled. make gap.") // 成功データがない場合だけ、今回のエラーを返すようにする if(result == null) result = r2 bAddGap = true break } // 成功した場合はそれを返したい result = r2 var src : List = listParser(parser, jsonArray) if(newerLimit != null) src = src.filter { it.isInjected() || it.getOrderId() < newerLimit } if(src.none { ! it.isInjected() }) { // 直前の取得でカラのデータが帰ってきたら終了 log.d("$logCaption: empty.") break } // 隙間の最新のステータスIDは取得データ末尾のステータスIDである since_id = column.parseRange(result, src).second adder(src) } val sortAllowed = true if(sortAllowed) list_tmp?.sortByDescending { it.getOrderId() } if(bAddGap) addOne(list_tmp, TootGap.mayNull(max_id, since_id), head = true) return result } // max_id を指定してギャップの上から読む private fun readGapHeadMastodon( logCaption : String, client : TootApiClient, path_base : String, filterByIdRange : Boolean, listParser : (TootParser, JsonArray) -> List, adder : (List) -> Unit, ) : TootApiResult? { list_tmp = ArrayList() val delimiter = if(- 1 != path_base.indexOf('?')) '&' else '?' val requester : (EntityId?) -> TootApiResult? = { val path = StringBuilder().apply { append(path_base) val list = ArrayList() if(it != null) list.add("max_id=$it") if(since_id != null) list.add("since_id=$since_id") list.forEachIndexed { index, s -> append(if(index == 0) delimiter else '&') append(s) } }.toString() log.d("readGapHeadMastodon $path") client.request(path) } val time_start = SystemClock.elapsedRealtime() var result : TootApiResult? = null var bAddGap = false val olderLimit = if(filterByIdRange) since_id else null while(true) { if(isCancelled) { log.d("$logCaption: cancelled.") break } if(result != null && SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) { log.d("$logCaption: timeout.") // タイムアウト bAddGap = true break } if(max_id == null) { showToast(context, false, "gap-getConversationSummaryList: missing max_id") log.d("$logCaption: missing max_id") break } if(allRangeChecked(logCaption)) break val r2 = requester(max_id) val jsonArray = r2?.jsonArray if(jsonArray == null) { log.d("$logCaption: error or cancelled. make gap.") // 成功データがない場合だけ、今回のエラーを返すようにする if(result == null) result = r2 bAddGap = true break } // 成功した場合はそれを返したい result = r2 var src : List = listParser(parser, jsonArray) if(olderLimit != null) src = src.filter { it.getOrderId() > olderLimit } if(src.isEmpty()) { // 直前の取得でカラのデータが帰ってきたら終了 log.d("$logCaption: empty.") break } // 隙間の最新のステータスIDは取得データ末尾のステータスIDである max_id = column.parseRange(result, src).first adder(src) } val sortAllowed = false if(sortAllowed) list_tmp?.sortByDescending { it.getOrderId() } if(bAddGap) addOne(list_tmp, TootGap.mayNull(max_id, since_id)) return result } // since_idを指定してギャップの下から読む private fun readGapTailMastodon( logCaption : String, client : TootApiClient, path_base : String, filterByIdRange : Boolean, listParser : (TootParser, JsonArray) -> List, adder : (List) -> Unit ) : TootApiResult? { list_tmp = ArrayList() val delimiter = if(- 1 != path_base.indexOf('?')) '&' else '?' val requester : (EntityId?) -> TootApiResult? = { val path = StringBuilder().apply { append(path_base) val list = ArrayList() if(it != null) list.add("min_id=$it") if(max_id != null) list.add("max_id=$max_id") list.forEachIndexed { index, s -> append(if(index == 0) delimiter else '&') append(s) } }.toString() log.d("$logCaption: $path") client.request(path) } val time_start = SystemClock.elapsedRealtime() var result : TootApiResult? = null var bAddGap = false val newerLimit = if(filterByIdRange) max_id else null while(true) { if(isCancelled) { log.d("$logCaption: cancelled.") break } if(result != null && SystemClock.elapsedRealtime() - time_start > Column.LOOP_TIMEOUT) { log.d("$logCaption: timeout.") bAddGap = true break } if(allRangeChecked(logCaption)) break val r2 = requester(since_id) val jsonArray = r2?.jsonArray if(jsonArray == null) { log.d("$logCaption: error or cancelled. make gap.") // 成功データがない場合だけ、今回のエラーを返すようにする if(result == null) result = r2 bAddGap = true break } // 成功した場合はそれを返したい result = r2 var src : List = listParser(parser, jsonArray) if(newerLimit != null) src = src.filter { it.getOrderId() < newerLimit } if(src.isEmpty()) { // 直前の取得でカラのデータが帰ってきたら終了 log.d("$logCaption: empty.") break } // 隙間の最新のステータスIDは取得データ末尾のステータスIDである since_id = column.parseRange(result, src).second adder(src) } val sortAllowed = false if(sortAllowed) list_tmp?.sortByDescending { it.getOrderId() } if(bAddGap) addOne(list_tmp, TootGap.mayNull(max_id, since_id), head = true) return result } ////////////////////////////////////////////////////////////////////// internal fun getAccountList( client : TootApiClient, path_base : String, mastodonFilterByIdRange : Boolean, misskeyParams : JsonObject? = null, arrayFinder : (jsonObject : JsonObject) -> JsonArray? = { null }, listParser : (parser : TootParser, jsonArray : JsonArray) -> List = { parser, jsonArray -> parser.accountList(jsonArray) } ) : TootApiResult? { if( column.pagingType != ColumnPagingType.Default ) { return TootApiResult("can't support gap") } val adder : (List) -> Unit = { addAll(list_tmp, it, head = ! isHead) } return if(access_info.isMisskey) { val logCaption = "getAccountList.Misskey" val params = misskeyParams ?: column.makeMisskeyBaseParameter(parser) if(isHead) { readGapHeadMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeyUntil(it) }, arrayFinder = arrayFinder, listParser = listParser, adder = adder, ) } else { readGapTailMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeySince(it) }, arrayFinder = arrayFinder, listParser = listParser, adder = adder ) } } else { val logCaption = "getAccountList.Mastodon" if(isHead) { readGapHeadMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } else { readGapTailMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } } } internal fun getReportList( client : TootApiClient, path_base : String, mastodonFilterByIdRange : Boolean, listParser : (parser : TootParser, jsonArray : JsonArray) -> ArrayList = { _, jsonArray -> parseList(::TootReport, jsonArray) } ) : TootApiResult? { val adder : (List) -> Unit = { addAll(list_tmp, it, head = ! isHead) } return if(access_info.isMisskey) { val logCaption = "getReportList.Misskey" val params = column.makeMisskeyBaseParameter(parser) if(isHead) { readGapHeadMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeyUntil(it) }, listParser = listParser, adder = adder ) } else { readGapTailMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeySince(it) }, listParser = listParser, adder = adder ) } } else { val logCaption = "getReportList.Mastodon" if(isHead) { readGapHeadMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } else { readGapTailMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } } } internal fun getNotificationList( client : TootApiClient, fromAcct : String? = null, mastodonFilterByIdRange : Boolean, ) : TootApiResult? { val path_base : String = column.makeNotificationUrl(client, fromAcct) val listParser : (parser : TootParser, jsonArray : JsonArray) -> List = defaultNotificationListParser val adder : (List) -> Unit = { addWithFilterNotification(list_tmp, it, head = ! isHead) } return if(isMisskey) { val logCaption = "getNotificationList.Misskey" val params = column.makeMisskeyBaseParameter(parser) .addMisskeyNotificationFilter(column) if(isHead) { readGapHeadMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeyUntil(it) }, listParser = listParser, adder = adder ) } else { readGapTailMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeySince(it) }, listParser = listParser, adder = adder ) } } else { val logCaption = "getNotificationList.Mastodon" if(isHead) { readGapHeadMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } else { readGapTailMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } }.also { list_tmp?.mapNotNull { it as? TootNotification }.notEmpty()?.let { PollingWorker.injectData(context, access_info, it) } } } internal fun getStatusList( client : TootApiClient, path_base : String?, mastodonFilterByIdRange : Boolean, misskeyParams : JsonObject? = null, listParser : (parser : TootParser, jsonArray : JsonArray) -> List = defaultStatusListParser ) : TootApiResult? { path_base ?: return null // cancelled. val adder : (List) -> Unit = { addWithFilterStatus(list_tmp, it, head = ! isHead) } return if(access_info.isMisskey) { val logCaption = "getStatusList.Misskey" val params = misskeyParams ?: column.makeMisskeyTimelineParameter(parser) if(isHead) { readGapHeadMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeyUntil(it) }, listParser = listParser, adder = adder ) } else { readGapTailMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeySince(it) }, listParser = listParser, adder = adder ) } } else { val logCaption = "getStatusList.Mastodon" if(isHead) { readGapHeadMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } else { readGapTailMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } } } internal fun getConversationSummaryList( client : TootApiClient, path_base : String, mastodonFilterByIdRange : Boolean, misskeyParams : JsonObject? = null, listParser : (TootParser, JsonArray) -> ArrayList = { parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) } ) : TootApiResult? { val adder : (List) -> Unit = { addWithFilterConversationSummary(list_tmp, it, head = ! isHead) } return if(access_info.isMisskey) { val logCaption = "getConversationSummaryList.Misskey" val params = misskeyParams ?: column.makeMisskeyTimelineParameter(parser) if(isHead) { readGapHeadMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeyUntil(it) }, listParser = listParser, adder = adder ) } else { readGapTailMisskey( logCaption, client, path_base, paramsCreator = { params.putMisskeySince(it) }, listParser = listParser, adder = adder ) } } else { val logCaption = "getConversationSummaryList.Mastodon" if(isHead) { readGapHeadMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } else { readGapTailMastodon( logCaption, client, path_base, filterByIdRange = mastodonFilterByIdRange, listParser = listParser, adder = adder ) } } } fun getSearchGap(client : TootApiClient) : TootApiResult? { if(gap !is TootSearchGap) return null if(isMisskey) { val countStatuses : (TimelineItem, EntityId?) -> EntityId? = { it, minId -> if(it is TootStatus && (minId == null || it.id < minId)) it.id else minId } val (_, counter) = when(gap.type) { TootSearchGap.SearchType.Status -> Pair("statuses", countStatuses) //TootSearchGap.SearchType.Hashtag -> Pair("hashtags", countTag) //TootSearchGap.SearchType.Account -> Pair("accounts", countAccount) else -> return TootApiResult("paging for ${gap.type} is not yet supported") } var minId : EntityId? = null for(it in column.list_data) minId = counter(it, minId) minId ?: return TootApiResult("can't detect paging parameter.") val result = client.request( "/api/notes/search", access_info.putMisskeyApiToken().apply { put("query", column.search_query) put("untilId", minId.toString()) } .toPostRequestBuilder() ) val jsonArray = result?.jsonArray if(jsonArray != null) { val src = parser.statusList(jsonArray) list_tmp = addWithFilterStatus(list_tmp, src) if(src.isNotEmpty()) { addOne(list_tmp, TootSearchGap(TootSearchGap.SearchType.Status)) } } return result } else { var offset = 0 val countAccounts : (TimelineItem) -> Unit = { if(it is TootAccountRef) ++ offset } val countTags : (TimelineItem) -> Unit = { if(it is TootTag) ++ offset } val countStatuses : (TimelineItem) -> Unit = { if(it is TootStatus) ++ offset } val (type, counter) = when(gap.type) { TootSearchGap.SearchType.Account -> Pair("accounts", countAccounts) TootSearchGap.SearchType.Hashtag -> Pair("hashtags", countTags) TootSearchGap.SearchType.Status -> Pair("statuses", countStatuses) } column.list_data.forEach { counter(it) } // https://mastodon2.juggler.jp/api/v2/search?q=gargron&type=accounts&offset=5 var query = "q=${column.search_query.encodePercent()}&type=$type&offset=$offset" if(column.search_resolve) query += "&resolve=1" val (apiResult, searchResult) = client.requestMastodonSearch(parser, query) if(searchResult != null) { list_tmp = ArrayList() addAll(list_tmp, searchResult.hashtags) addAll(list_tmp, searchResult.accounts) addAll(list_tmp, searchResult.statuses) if(list_tmp?.isNotEmpty() == true) { addOne(list_tmp, TootSearchGap(gap.type)) } } return apiResult } } }