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
|
2017-02-08 17:45:20 +01:00
|
|
|
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(
|
2017-02-08 17:45:20 +01:00
|
|
|
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
|
|
|
|
|
2017-01-30 14:41:34 +01:00
|
|
|
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()
|
2017-02-02 11:18:34 +01:00
|
|
|
mediaLoader.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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|