1
0
mirror of https://github.com/TwidereProject/Twidere-Android synced 2025-02-17 04:00:48 +01:00

improved gap load

This commit is contained in:
Mariotaku Lee 2017-02-04 22:47:50 +08:00
parent 66ff961f2d
commit f6aa30a66a
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
14 changed files with 165 additions and 78 deletions

View File

@ -72,7 +72,7 @@ public class ParcelableStatus implements Parcelable, Comparable<ParcelableStatus
}
};
@CursorField(value = Statuses._ID, excludeWrite = true, type = TwidereDataStore.TYPE_PRIMARY_KEY)
long _id;
public long _id;
@SuppressWarnings("NullableProblems")
@ParcelableThisPlease

View File

@ -498,6 +498,7 @@ public class AsyncTwitterWrapper extends TwitterWrapper {
return asyncTaskManager.add(task, true);
}
@Nullable
public static <T extends Response<?>> Exception getException(List<T> responses) {
for (T response : responses) {
if (response.hasException()) return response.getException();

View File

@ -237,7 +237,7 @@ class ParcelableActivitiesAdapter(
}
ITEM_VIEW_TYPE_GAP -> {
val activity = getActivity(position)!!
val loading = gapLoadingIds.find { it.accountKey == activity.account_key && it.id == activity.id } != null
val loading = gapLoadingIds.any { it.accountKey == activity.account_key && it.id == activity.id }
(holder as GapViewHolder).display(loading)
}
}

View File

@ -283,7 +283,7 @@ abstract class ParcelableStatusesAdapter(
(holder as IStatusViewHolder).displayStatus(status, isShowInReplyTo)
}
ITEM_VIEW_TYPE_GAP -> {
val loading = gapLoadingIds.find { it.accountKey == status.account_key && it.id == status.id } != null
val loading = gapLoadingIds.any { it.accountKey == status.account_key && it.id == status.id }
(holder as GapViewHolder).display(loading)
}
}

View File

@ -274,7 +274,7 @@ abstract class AbsActivitiesFragment protected constructor() :
if (loader is IExtendedLoader) {
loader.fromUser = false
}
onLoadingFinished()
onContentLoaded(loader, data)
}
override fun onLoaderReset(loader: Loader<List<ParcelableActivity>>) {
@ -434,7 +434,7 @@ abstract class AbsActivitiesFragment protected constructor() :
protected abstract fun onCreateActivitiesLoader(context: Context, args: Bundle,
fromUser: Boolean): Loader<List<ParcelableActivity>>
protected abstract fun onLoadingFinished()
protected abstract fun onContentLoaded(loader: Loader<List<ParcelableActivity>>, data: List<ParcelableActivity>?)
protected fun saveReadPosition(position: Int) {
if (host == null) return

View File

@ -28,6 +28,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.v4.content.Loader
import android.widget.Toast
import com.squareup.otto.Subscribe
import org.mariotaku.ktextension.addOnAccountsUpdatedListenerSafe
import org.mariotaku.ktextension.removeOnAccountsUpdatedListenerSafe
@ -44,6 +45,7 @@ import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.message.*
import org.mariotaku.twidere.provider.TwidereDataStore.Activities
import org.mariotaku.twidere.provider.TwidereDataStore.Filters
import org.mariotaku.twidere.task.twitter.GetStatusesTask
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.DataStoreUtils.getTableNameByUri
import org.mariotaku.twidere.util.ErrorInfoStore
@ -55,32 +57,26 @@ import org.mariotaku.twidere.util.Utils
*/
abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
override fun onLoadingFinished() {
val accountKeys = accountKeys
if (adapter.itemCount > 0) {
showContent()
} else if (accountKeys.isNotEmpty()) {
val errorInfo = ErrorInfoStore.getErrorInfo(context,
errorInfoStore[errorInfoKey, accountKeys[0]])
if (errorInfo != null) {
showEmpty(errorInfo.icon, errorInfo.message)
} else {
showEmpty(R.drawable.ic_info_refresh, getString(R.string.swipe_down_to_refresh))
}
} else {
showError(R.drawable.ic_info_accounts, getString(R.string.message_toast_no_account_selected))
}
}
protected abstract val errorInfoKey: String
private var contentObserver: ContentObserver? = null
private val accountListener: OnAccountsUpdateListener = OnAccountsUpdateListener { accounts ->
reloadActivities()
}
override val accountKeys: Array<UserKey>
get() = Utils.getAccountKeys(context, arguments) ?: DataStoreUtils.getActivatedAccountKeys(context)
protected abstract val errorInfoKey: String
private val sortOrder: String
get() = Activities.DEFAULT_SORT_ORDER
abstract val contentUri: Uri
override fun onContentLoaded(loader: Loader<List<ParcelableActivity>>, data: List<ParcelableActivity>?) {
showContentOrError()
}
override fun onCreateActivitiesLoader(context: Context,
args: Bundle,
fromUser: Boolean): Loader<List<ParcelableActivity>> {
@ -113,9 +109,6 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
return CursorActivitiesBusCallback()
}
override val accountKeys: Array<UserKey>
get() = Utils.getAccountKeys(context, arguments) ?: DataStoreUtils.getActivatedAccountKeys(context)
override fun onStart() {
super.onStart()
if (contentObserver == null) {
@ -140,17 +133,6 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
super.onStop()
}
protected fun reloadActivities() {
if (activity == null || isDetached) return
val args = Bundle()
val fragmentArgs = arguments
if (fragmentArgs != null) {
args.putAll(fragmentArgs)
args.putBoolean(EXTRA_FROM_USER, true)
}
loaderManager.restartLoader(0, args, this)
}
override fun hasMoreData(data: List<ParcelableActivity>?): Boolean {
return data?.size != 0
}
@ -249,8 +231,34 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
protected abstract fun updateRefreshState()
private val sortOrder: String
get() = Activities.DEFAULT_SORT_ORDER
protected fun reloadActivities() {
if (activity == null || isDetached) return
val args = Bundle()
val fragmentArgs = arguments
if (fragmentArgs != null) {
args.putAll(fragmentArgs)
args.putBoolean(EXTRA_FROM_USER, true)
}
loaderManager.restartLoader(0, args, this)
}
private fun showContentOrError() {
val accountKeys = accountKeys
if (adapter.itemCount > 0) {
showContent()
} else if (accountKeys.isNotEmpty()) {
val errorInfo = ErrorInfoStore.getErrorInfo(context,
errorInfoStore[errorInfoKey, accountKeys[0]])
if (errorInfo != null) {
showEmpty(errorInfo.icon, errorInfo.message)
} else {
showEmpty(R.drawable.ic_info_refresh, getString(R.string.swipe_down_to_refresh))
}
} else {
showError(R.drawable.ic_info_accounts, getString(R.string.message_toast_no_account_selected))
}
}
private fun updateFavoritedStatus(status: ParcelableStatus) {
@ -296,7 +304,11 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() {
if (!event.running) {
setLoadMoreIndicatorPosition(ILoadMoreSupportAdapter.NONE)
refreshEnabled = true
onLoadingFinished()
showContentOrError()
if (event.exception is GetStatusesTask.GetTimelineException && userVisibleHint) {
Toast.makeText(context, event.exception.getToastMessage(context), Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -27,6 +27,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.v4.content.Loader
import android.widget.Toast
import com.squareup.otto.Subscribe
import kotlinx.android.synthetic.main.fragment_content_recyclerview.*
import org.mariotaku.ktextension.addOnAccountsUpdatedListenerSafe
@ -44,6 +45,7 @@ import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.message.*
import org.mariotaku.twidere.provider.TwidereDataStore.Filters
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.twitter.GetStatusesTask
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.ErrorInfoStore
import org.mariotaku.twidere.util.Utils
@ -263,6 +265,10 @@ abstract class CursorStatusesFragment : AbsStatusesFragment() {
setLoadMoreIndicatorPosition(ILoadMoreSupportAdapter.NONE)
refreshEnabled = true
showContentOrError()
if (event.exception is GetStatusesTask.GetTimelineException && userVisibleHint) {
Toast.makeText(context, event.exception.getToastMessage(context), Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -27,6 +27,7 @@ import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableActivityUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Activities
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.TwitterWrapper.TwitterListResponse
import org.mariotaku.twidere.util.content.ContentResolverUtils
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
import java.util.*
@ -37,7 +38,7 @@ import javax.inject.Inject
*/
abstract class GetActivitiesTask(
protected val context: Context
) : AbstractTask<RefreshTaskParam, Unit, (Boolean) -> Unit>() {
) : AbstractTask<RefreshTaskParam, List<TwitterListResponse<Activity>>, (Boolean) -> Unit>() {
private var initialized: Boolean = false
@Inject
lateinit var preferences: KPreferences
@ -61,13 +62,14 @@ abstract class GetActivitiesTask(
initialized = true
}
override fun doLongOperation(param: RefreshTaskParam) {
if (!initialized || param.shouldAbort) return
override fun doLongOperation(param: RefreshTaskParam): List<TwitterListResponse<Activity>> {
if (!initialized || param.shouldAbort) return emptyList()
val accountIds = param.accountKeys
val maxIds = param.maxIds
val maxSortIds = param.maxSortIds
val sinceIds = param.sinceIds
val cr = context.contentResolver
val result = ArrayList<TwitterListResponse<Activity>>()
val loadItemLimit = preferences[loadItemLimitKey]
var saveReadPosition = false
for (i in accountIds.indices) {
@ -102,12 +104,15 @@ abstract class GetActivitiesTask(
// We should delete old activities has intersection with new items
try {
val activities = getActivities(microBlog, credentials, paging)
storeActivities(cr, loadItemLimit, credentials, noItemsBefore, activities, sinceId,
val storeResult = storeActivities(cr, loadItemLimit, credentials, noItemsBefore, activities, sinceId,
maxId, false)
if (saveReadPosition) {
saveReadPosition(accountKey, credentials, microBlog)
}
errorInfoStore.remove(errorInfoKey, accountKey)
if (storeResult != 0) {
throw GetStatusesTask.GetTimelineException(storeResult)
}
} catch (e: MicroBlogException) {
DebugLog.w(LOGTAG, tr = e)
if (e.errorCode == 220) {
@ -115,21 +120,24 @@ abstract class GetActivitiesTask(
} else if (e.isCausedByNetworkIssue) {
errorInfoStore[errorInfoKey, accountKey] = ErrorInfoStore.CODE_NETWORK_ERROR
}
} catch (e: GetStatusesTask.GetTimelineException) {
result.add(TwitterListResponse(accountKey, e))
}
}
return result
}
override fun afterExecute(handler: ((Boolean) -> Unit)?, result: Unit) {
override fun afterExecute(handler: ((Boolean) -> Unit)?, result: List<TwitterListResponse<Activity>>) {
if (!initialized) return
context.contentResolver.notifyChange(contentUri, null)
bus.post(GetActivitiesTaskEvent(contentUri, false, null))
val exception = AsyncTwitterWrapper.getException(result)
bus.post(GetActivitiesTaskEvent(contentUri, false, exception))
handler?.invoke(true)
}
private fun storeActivities(cr: ContentResolver, loadItemLimit: Int, details: AccountDetails,
noItemsBefore: Boolean, activities: ResponseList<Activity>,
sinceId: String?, maxId: String?, notify: Boolean) {
sinceId: String?, maxId: String?, notify: Boolean): Int {
val deleteBound = LongArray(2) { -1 }
val valuesList = ArrayList<ContentValues>()
var minIdx = -1
@ -185,17 +193,25 @@ abstract class GetActivitiesTask(
valuesList[valuesList.size - 1].put(Activities.IS_GAP, true)
}
}
// Insert previously fetched items.
ContentResolverUtils.bulkInsert(cr, writeUri, valuesList)
// Remove gap flag
if (maxId != null && sinceId == null) {
val noGapValues = ContentValues()
noGapValues.put(Activities.IS_GAP, false)
val noGapWhere = Expression.and(Expression.equalsArgs(Activities.ACCOUNT_KEY),
Expression.equalsArgs(Activities.MIN_REQUEST_POSITION),
Expression.equalsArgs(Activities.MAX_REQUEST_POSITION)).sql
val noGapWhereArgs = arrayOf(details.key.toString(), maxId, maxId)
cr.update(writeUri, noGapValues, noGapWhere, noGapWhereArgs)
if (activities.isNotEmpty()) {
// Only remove when actual result returned, otherwise it seems that gap is too old to load
val noGapValues = ContentValues()
noGapValues.put(Activities.IS_GAP, false)
val noGapWhere = Expression.and(Expression.equalsArgs(Activities.ACCOUNT_KEY),
Expression.equalsArgs(Activities.MIN_REQUEST_POSITION),
Expression.equalsArgs(Activities.MAX_REQUEST_POSITION)).sql
val noGapWhereArgs = arrayOf(details.key.toString(), maxId, maxId)
cr.update(writeUri, noGapValues, noGapWhere, noGapWhereArgs)
} else {
return GetStatusesTask.ERROR_LOAD_GAP
}
}
return 0
}
@UiThread

View File

@ -19,6 +19,7 @@ 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
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.LOGTAG
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_NOTIFY
import org.mariotaku.twidere.constant.loadItemLimitKey
@ -123,13 +124,18 @@ abstract class GetStatusesTask(
sinceId = null
}
val statuses = getStatuses(microBlog, paging)
storeStatus(accountKey, details, statuses, sinceId, maxId, sinceSortId,
val storeResult = storeStatus(accountKey, details, statuses, sinceId, maxId, sinceSortId,
maxSortId, loadItemLimit, false)
// TODO cache related data and preload
val cacheTask = CacheUsersStatusesTask(context)
cacheTask.params = TwitterWrapper.StatusListResponse(accountKey, statuses)
val response = TwitterWrapper.StatusListResponse(accountKey, statuses)
cacheTask.params = response
TaskStarter.execute(cacheTask)
errorInfoStore.remove(errorInfoKey, accountKey.id)
result.add(response)
if (storeResult != 0) {
throw GetTimelineException(storeResult)
}
} catch (e: MicroBlogException) {
DebugLog.w(LOGTAG, tr = e)
if (e.isCausedByNetworkIssue) {
@ -138,6 +144,8 @@ abstract class GetStatusesTask(
// Unauthorized
}
result.add(TwitterWrapper.StatusListResponse(accountKey, e))
} catch (e: GetTimelineException) {
result.add(TwitterWrapper.StatusListResponse(accountKey, e))
}
}
return result
@ -146,7 +154,8 @@ abstract class GetStatusesTask(
override fun afterExecute(handler: ((Boolean) -> Unit)?, result: List<TwitterWrapper.StatusListResponse>) {
if (!initialized) return
context.contentResolver.notifyChange(contentUri, null)
bus.post(GetStatusesTaskEvent(contentUri, false, AsyncTwitterWrapper.getException(result)))
val exception = AsyncTwitterWrapper.getException(result)
bus.post(GetStatusesTaskEvent(contentUri, false, exception))
handler?.invoke(true)
}
@ -159,7 +168,7 @@ abstract class GetStatusesTask(
statuses: List<Status>,
sinceId: String?, maxId: String?,
sinceSortId: Long, maxSortId: Long,
loadItemLimit: Int, notify: Boolean) {
loadItemLimit: Int, notify: Boolean): Int {
val uri = contentUri
val writeUri = UriUtils.appendQueryParameters(uri, QUERY_PARAM_NOTIFY, notify)
val resolver = context.contentResolver
@ -227,17 +236,34 @@ abstract class GetStatusesTask(
// Remove gap flag
if (maxId != null && sinceId == null) {
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)
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)
}
}
companion object {
const val ERROR_LOAD_GAP = 1
fun getPositionKey(timestamp: Long, sortId: Long, lastSortId: Long, sortDiff: Long,
position: Int, count: Int): Long {
if (sortDiff == 0L) return timestamp

View File

@ -22,6 +22,7 @@ package org.mariotaku.twidere.util
import android.accounts.AccountManager
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.support.annotation.DrawableRes
@ -39,6 +40,7 @@ import android.view.MenuItem
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.setItemChecked
import org.mariotaku.ktextension.setMenuItemIcon
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.Constants
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.*
@ -57,6 +59,7 @@ import org.mariotaku.twidere.menu.SupportStatusShareProvider
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.util.menu.TwidereMenuInfo
/**
@ -322,6 +325,14 @@ object MenuUtils {
ClipboardUtils.setText(context, uri.toString())
Utils.showOkMessage(context, R.string.message_toast_link_copied_to_clipboard, false)
}
R.id.make_gap -> {
val resolver = context.contentResolver
val values = ContentValues()
values.put(Statuses.IS_GAP, 1)
val where = Expression.equalsArgs(Statuses._ID).sql
val whereArgs = arrayOf(status._id.toString())
resolver.update(Statuses.CONTENT_URI, values, where, whereArgs)
}
else -> {
if (item.intent != null) {
try {

View File

@ -22,11 +22,8 @@ package org.mariotaku.twidere.view.holder
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.View.OnClickListener
import android.widget.ProgressBar
import android.widget.TextView
import kotlinx.android.synthetic.main.card_item_gap.view.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter
/**
@ -37,13 +34,11 @@ class GapViewHolder(
itemView: View
) : RecyclerView.ViewHolder(itemView), OnClickListener {
private val gapText: TextView
private val gapProgress: ProgressBar
private val gapText = itemView.gapText
private val gapProgress = itemView.gapProgress
init {
itemView.setOnClickListener(this)
gapText = itemView.gapText
gapProgress = itemView.gapProgress
}
override fun onClick(v: View) {
@ -52,8 +47,14 @@ class GapViewHolder(
}
fun display(showProgress: Boolean) {
gapText.visibility = if (showProgress) View.GONE else View.VISIBLE
gapProgress.visibility = if (showProgress) View.VISIBLE else View.GONE
if (showProgress) {
gapText.visibility = View.INVISIBLE
gapProgress.visibility = View.VISIBLE
gapProgress.spin()
} else {
gapText.visibility = View.VISIBLE
gapProgress.visibility = View.INVISIBLE
}
}
companion object {

View File

@ -21,6 +21,7 @@
<FrameLayout
android:id="@+id/gapIndicator"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="@dimen/element_size_normal"
android:focusable="true">
@ -37,11 +38,15 @@
android:textColor="?android:textColorPrimary"
android:textStyle="bold"/>
<ProgressBar
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/gapProgress"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
android:padding="@dimen/element_spacing_small"
android:visibility="invisible"
app:matProg_barColor="?colorAccent"
app:matProg_barWidth="3dp"
app:matProg_progressIndeterminate="true"/>
</FrameLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@id/share"
@ -34,5 +35,12 @@
android:icon="@drawable/ic_action_delete"
android:title="@string/action_delete"
android:visible="false"/>
<item
android:id="@+id/make_gap"
android:enabled="@bool/debug"
android:title="Make gap"
android:visible="@bool/debug"
tools:ignore="HardcodedText"/>
</menu>

View File

@ -727,6 +727,7 @@
<string name="message_toast_status_saved_to_draft">Tweet saved to draft</string>
<string name="message_toast_status_unfavorited">Tweet unfavorited</string>
<string name="message_toast_status_updated">Tweet sent</string>
<string name="message_toast_unable_to_load_more_statuses">Unable to load more tweets</string>
<string name="message_toast_user_filters_removed">Removed from filters</string>
<string name="message_toast_users_filters_added">Added to filters</string>
<string name="message_toast_wrong_api_key">Incorrect API settings</string>