Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetStatusesTask.kt

265 lines
12 KiB
Kotlin
Raw Normal View History

2016-09-09 05:58:26 +02:00
package org.mariotaku.twidere.task.twitter
2016-12-04 04:58:03 +01:00
import android.accounts.AccountManager
2016-09-09 05:58:26 +02:00
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import edu.tsinghua.hotmobi.HotMobiLogger
import edu.tsinghua.hotmobi.model.RefreshEvent
import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.math.NumberUtils
import org.mariotaku.abstask.library.TaskStarter
2016-12-24 15:05:15 +01:00
import org.mariotaku.kpreferences.get
2017-03-05 09:08:09 +01:00
import org.mariotaku.library.objectcursor.ObjectCursor
2016-09-09 05:58:26 +02:00
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.ResponseList
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.sqliteqb.library.Columns
import org.mariotaku.sqliteqb.library.Expression
2017-02-04 15:47:50 +01:00
import org.mariotaku.twidere.R
2016-09-09 05:58:26 +02:00
import org.mariotaku.twidere.TwidereConstants.LOGTAG
2017-03-03 05:20:52 +01:00
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_NOTIFY_CHANGE
2016-09-09 05:58:26 +02:00
import org.mariotaku.twidere.constant.loadItemLimitKey
2016-12-08 16:45:07 +01:00
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
2016-12-04 04:58:03 +01:00
import org.mariotaku.twidere.model.AccountDetails
2017-03-05 09:08:09 +01:00
import org.mariotaku.twidere.model.ParcelableStatus
2016-09-09 05:58:26 +02:00
import org.mariotaku.twidere.model.RefreshTaskParam
import org.mariotaku.twidere.model.UserKey
2017-02-09 09:00:12 +01:00
import org.mariotaku.twidere.model.event.GetStatusesTaskEvent
2016-12-04 04:58:03 +01:00
import org.mariotaku.twidere.model.util.AccountUtils
2016-09-09 05:58:26 +02:00
import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.provider.TwidereDataStore.AccountSupportColumns
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.BaseAbstractTask
2016-09-09 05:58:26 +02:00
import org.mariotaku.twidere.task.CacheUsersStatusesTask
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.content.ContentResolverUtils
import java.util.*
/**
* Created by mariotaku on 16/1/2.
*/
2016-12-15 01:13:09 +01:00
abstract class GetStatusesTask(
context: Context
) : BaseAbstractTask<RefreshTaskParam, List<TwitterWrapper.StatusListResponse>, (Boolean) -> Unit>(context) {
2016-09-09 05:58:26 +02:00
2017-03-02 03:42:40 +01:00
private val profileImageSize = context.getString(R.string.profile_image_size)
2016-09-09 05:58:26 +02:00
@Throws(MicroBlogException::class)
abstract fun getStatuses(twitter: MicroBlog, paging: Paging): ResponseList<Status>
protected abstract val contentUri: Uri
protected abstract val timelineType: String
protected abstract val errorInfoKey: String
override fun doLongOperation(param: RefreshTaskParam): List<TwitterWrapper.StatusListResponse> {
2017-02-02 11:18:34 +01:00
if (!initialized || param.shouldAbort) return emptyList()
2016-09-09 05:58:26 +02:00
val accountKeys = param.accountKeys
val maxIds = param.maxIds
val sinceIds = param.sinceIds
val maxSortIds = param.maxSortIds
val sinceSortIds = param.sinceSortIds
val result = ArrayList<TwitterWrapper.StatusListResponse>()
val loadItemLimit = preferences[loadItemLimitKey]
for (i in 0 until accountKeys.size) {
val accountKey = accountKeys[i]
2016-12-04 04:58:03 +01:00
val details = AccountUtils.getAccountDetails(AccountManager.get(context),
2016-12-15 06:11:32 +01:00
accountKey, true) ?: continue
2016-12-08 16:45:07 +01:00
val microBlog = details.newMicroBlogInstance(context = context, cls = MicroBlog::class.java)
2016-09-09 05:58:26 +02:00
try {
val paging = Paging()
paging.count(loadItemLimit)
val maxId: String?
val sinceId: String?
var maxSortId: Long = -1
var sinceSortId: Long = -1
if (maxIds != null && maxIds[i] != null) {
maxId = maxIds[i]
paging.maxId(maxId)
if (maxSortIds != null) {
maxSortId = maxSortIds[i]
}
} else {
maxSortId = -1
maxId = null
}
if (sinceIds != null && sinceIds[i] != null) {
sinceId = sinceIds[i]
val sinceIdLong = NumberUtils.toLong(sinceId, -1)
//TODO handle non-twitter case
if (sinceIdLong != -1L) {
paging.sinceId((sinceIdLong - 1).toString())
} else {
paging.sinceId(sinceId)
}
if (sinceSortIds != null) {
sinceSortId = sinceSortIds[i]
}
if (maxIds == null) {
paging.setLatestResults(true)
}
} else {
sinceId = null
}
2016-12-04 04:58:03 +01:00
val statuses = getStatuses(microBlog, paging)
2017-02-04 15:47:50 +01:00
val storeResult = storeStatus(accountKey, details, statuses, sinceId, maxId, sinceSortId,
2016-09-09 05:58:26 +02:00
maxSortId, loadItemLimit, false)
// TODO cache related data and preload
val cacheTask = CacheUsersStatusesTask(context)
2017-02-04 15:47:50 +01:00
val response = TwitterWrapper.StatusListResponse(accountKey, statuses)
cacheTask.params = response
2016-09-09 05:58:26 +02:00
TaskStarter.execute(cacheTask)
errorInfoStore.remove(errorInfoKey, accountKey.id)
2017-02-04 15:47:50 +01:00
result.add(response)
if (storeResult != 0) {
throw GetTimelineException(storeResult)
}
2016-09-09 05:58:26 +02:00
} catch (e: MicroBlogException) {
2017-01-26 16:15:05 +01:00
DebugLog.w(LOGTAG, tr = e)
2016-09-09 05:58:26 +02:00
if (e.isCausedByNetworkIssue) {
2016-12-18 03:04:02 +01:00
errorInfoStore[errorInfoKey, accountKey.id] = ErrorInfoStore.CODE_NETWORK_ERROR
} else if (e.statusCode == 401) {
// Unauthorized
2016-09-09 05:58:26 +02:00
}
result.add(TwitterWrapper.StatusListResponse(accountKey, e))
2017-02-04 15:47:50 +01:00
} catch (e: GetTimelineException) {
result.add(TwitterWrapper.StatusListResponse(accountKey, e))
2016-09-09 05:58:26 +02:00
}
}
return result
}
2017-02-02 11:18:34 +01:00
override fun afterExecute(handler: ((Boolean) -> Unit)?, result: List<TwitterWrapper.StatusListResponse>) {
if (!initialized) return
context.contentResolver.notifyChange(contentUri, null)
2017-02-04 15:47:50 +01:00
val exception = AsyncTwitterWrapper.getException(result)
bus.post(GetStatusesTaskEvent(contentUri, false, exception))
2017-02-02 11:18:34 +01:00
handler?.invoke(true)
}
override fun beforeExecute() {
if (!initialized) return
bus.post(GetStatusesTaskEvent(contentUri, true, null))
}
2016-12-04 04:58:03 +01:00
private fun storeStatus(accountKey: UserKey, details: AccountDetails,
2017-03-02 03:42:40 +01:00
statuses: List<Status>,
sinceId: String?, maxId: String?,
sinceSortId: Long, maxSortId: Long,
loadItemLimit: Int, notify: Boolean): Int {
2016-09-09 05:58:26 +02:00
val uri = contentUri
2017-03-03 05:20:52 +01:00
val writeUri = UriUtils.appendQueryParameters(uri, QUERY_PARAM_NOTIFY_CHANGE, notify)
2016-09-09 05:58:26 +02:00
val resolver = context.contentResolver
val noItemsBefore = DataStoreUtils.getStatusCount(context, uri, accountKey) <= 0
val values = arrayOfNulls<ContentValues>(statuses.size)
val statusIds = arrayOfNulls<String>(statuses.size)
var minIdx = -1
var minPositionKey: Long = -1
var hasIntersection = false
if (!statuses.isEmpty()) {
val firstSortId = statuses.first().sortId
val lastSortId = statuses.last().sortId
// Get id diff of first and last item
val sortDiff = firstSortId - lastSortId
2017-03-05 09:08:09 +01:00
val creator = ObjectCursor.valuesCreatorFrom(ParcelableStatus::class.java)
2016-09-09 05:58:26 +02:00
for (i in 0 until statuses.size) {
val item = statuses[i]
2017-03-02 03:42:40 +01:00
val status = ParcelableStatusUtils.fromStatus(item, accountKey, false, profileImageSize)
2017-01-20 15:08:42 +01:00
ParcelableStatusUtils.updateExtraInformation(status, details)
2016-09-09 05:58:26 +02:00
status.position_key = getPositionKey(status.timestamp, status.sort_id, lastSortId,
sortDiff, i, statuses.size)
status.inserted_date = System.currentTimeMillis()
mediaPreloader.preloadStatus(status)
2017-03-05 09:08:09 +01:00
values[i] = creator.create(status)
2016-09-09 05:58:26 +02:00
if (minIdx == -1 || item < statuses[minIdx]) {
minIdx = i
minPositionKey = status.position_key
}
if (sinceId != null && item.sortId <= sinceSortId) {
hasIntersection = true
}
statusIds[i] = item.id
}
}
// Delete all rows conflicting before new data inserted.
val accountWhere = Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY)
val statusWhere = Expression.inArgs(Columns.Column(Statuses.STATUS_ID),
statusIds.size)
val deleteWhere = Expression.and(accountWhere, statusWhere).sql
val deleteWhereArgs = arrayOf(accountKey.toString(), *statusIds)
var olderCount = -1
if (minPositionKey > 0) {
2016-12-24 15:05:15 +01:00
olderCount = DataStoreUtils.getStatusesCount(context, preferences, uri, null,
minPositionKey, Statuses.POSITION_KEY, false, arrayOf(accountKey))
2016-09-09 05:58:26 +02:00
}
val rowsDeleted = resolver.delete(writeUri, deleteWhere, deleteWhereArgs)
// BEGIN HotMobi
val event = RefreshEvent.create(context, statusIds, timelineType)
HotMobiLogger.getInstance(context).log(accountKey, event)
// END HotMobi
// Insert a gap.
val deletedOldGap = rowsDeleted > 0 && ArrayUtils.contains(statusIds, maxId)
val noRowsDeleted = rowsDeleted == 0
// Why loadItemLimit / 2? because it will not acting strange in most cases
val insertGap = minIdx != -1 && olderCount > 0 && (noRowsDeleted || deletedOldGap)
&& !noItemsBefore && !hasIntersection && statuses.size > loadItemLimit / 2
if (insertGap) {
values[minIdx]!!.put(Statuses.IS_GAP, true)
}
// Insert previously fetched items.
ContentResolverUtils.bulkInsert(resolver, writeUri, values)
// Remove gap flag
if (maxId != null && sinceId == null) {
2017-02-04 15:47:50 +01:00
if (statuses.isNotEmpty()) {
// Only remove when actual result returned, otherwise it seems that gap is too old to load
val noGapValues = ContentValues()
noGapValues.put(Statuses.IS_GAP, false)
val noGapWhere = Expression.and(Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.STATUS_ID)).sql
val noGapWhereArgs = arrayOf(accountKey.toString(), maxId)
resolver.update(writeUri, noGapValues, noGapWhere, noGapWhereArgs)
} else {
return ERROR_LOAD_GAP
}
}
return 0
}
class GetTimelineException(val code: Int) : Exception() {
fun getToastMessage(context: Context): String {
when (code) {
ERROR_LOAD_GAP -> return context.getString(R.string.message_toast_unable_to_load_more_statuses)
}
return context.getString(R.string.error_unknown_error)
2016-09-09 05:58:26 +02:00
}
}
companion object {
2017-02-04 15:47:50 +01:00
const val ERROR_LOAD_GAP = 1
2016-09-09 05:58:26 +02:00
fun getPositionKey(timestamp: Long, sortId: Long, lastSortId: Long, sortDiff: Long,
2017-03-02 03:42:40 +01:00
position: Int, count: Int): Long {
2016-09-09 05:58:26 +02:00
if (sortDiff == 0L) return timestamp
val extraValue: Int
if (sortDiff > 0) {
// descent sorted by time
extraValue = count - 1 - position
} else {
// ascent sorted by time
extraValue = position
}
return timestamp + (sortId - lastSortId) * (499 - count) / sortDiff + extraValue.toLong()
}
}
}