From af9dadb142bcb95d3d63797ea07ecbd64fdc33e6 Mon Sep 17 00:00:00 2001 From: Mariotaku Lee Date: Fri, 13 Oct 2017 19:33:14 +0800 Subject: [PATCH] started to use paged list --- build.gradle | 81 ++++---- .../twidere/model/ParcelableStatus.java | 2 +- twidere/build.gradle | 3 +- .../twidere/activity/HomeActivity.kt | 4 +- .../adapter/ParcelableStatusesAdapter.kt | 191 +++++------------- .../twidere/data/ObjectCursorLiveData.kt | 79 -------- .../CursorObjectLivePagedListProvider.kt} | 30 +-- .../source/CursorObjectTiledDataSource.kt | 66 ++++++ .../twidere/data/status/StatusesLiveData.kt | 31 --- .../extension/ContentResolverExtensions.kt | 5 +- .../extension/model/FiltersDataExtensions.kt | 4 +- .../twidere/fragment/AbsStatusesFragment.kt | 30 +-- .../fragment/CursorStatusesFragment.kt | 2 +- .../statuses/MediaStatusesSearchFragment.kt | 2 +- .../statuses/UserMediaTimelineFragment.kt | 2 +- .../fragment/timeline/AbsTimelineFragment.kt | 146 +++++++++++-- .../fragment/timeline/HomeTimelineFragment.kt | 2 +- .../task/cache/CacheTimelineResultTask.kt | 2 +- 18 files changed, 322 insertions(+), 360 deletions(-) delete mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/data/ObjectCursorLiveData.kt rename twidere/src/main/kotlin/org/mariotaku/twidere/data/{ReloadableLiveData.kt => source/CursorObjectLivePagedListProvider.kt} (53%) create mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/data/source/CursorObjectTiledDataSource.kt delete mode 100644 twidere/src/main/kotlin/org/mariotaku/twidere/data/status/StatusesLiveData.kt diff --git a/build.gradle b/build.gradle index 83d16b949..c75182daf 100644 --- a/build.gradle +++ b/build.gradle @@ -41,46 +41,47 @@ subprojects { PlayServices : '3.1.1', ] libVersions = [ - Kotlin : "${kotlinVersion}", - SupportLib : '26.1.0', - SupportTest : '1.0.0', - MariotakuCommons : '0.9.20', - RestFu : '0.9.60', - ObjectCursor : '0.9.21', - PlayServices : '11.4.0', - MapsUtils : '0.5', - DropboxCoreSdk : '3.0.3', - GoogleDriveApi : 'v3-rev81-1.22.0', - Exoplayer : 'r2.2.0', - Toro : '2.1.0', - LoganSquare : '1.3.7', - IABv3 : '1.0.38', - Mime4J : '0.7.2', - OkHttp : '3.8.1', - Stetho : '1.5.0', - OSMDroid : '5.6.5', - LeakCanary : '1.5.1', - TwitterText : '1.14.7', - MediaViewerLibrary : '0.9.23', - MultiValueSwitch : '0.9.8', - PickNCrop : '0.9.27', - AndroidGIFDrawable : '1.2.6', - KPreferences : '0.9.7', - Kovenant : '3.3.0', - ParcelablePlease : '1.0.2', - Chameleon : '0.9.22', - UniqR : '0.9.4', - SQLiteQB : '0.9.15', - Glide : '3.7.0', - GlideOkHttp3 : '1.4.0', - GlideTransformations : '2.0.2', - AndroidImageCropper : '2.4.6', - ExportablePreferences : '0.9.7', - ACRA : '4.9.2', - AbstractTask : '0.9.5', - Dagger : '2.11', - StethoBeanShellREPL : '0.3', - SupportLifecycleExtensions: '1.0.0-beta2', + Kotlin : "${kotlinVersion}", + SupportLib : '26.1.0', + SupportTest : '1.0.0', + MariotakuCommons : '0.9.20', + RestFu : '0.9.60', + ObjectCursor : '0.9.21', + PlayServices : '11.4.0', + MapsUtils : '0.5', + DropboxCoreSdk : '3.0.3', + GoogleDriveApi : 'v3-rev81-1.22.0', + Exoplayer : 'r2.2.0', + Toro : '2.1.0', + LoganSquare : '1.3.7', + IABv3 : '1.0.38', + Mime4J : '0.7.2', + OkHttp : '3.8.1', + Stetho : '1.5.0', + OSMDroid : '5.6.5', + LeakCanary : '1.5.1', + TwitterText : '1.14.7', + MediaViewerLibrary : '0.9.23', + MultiValueSwitch : '0.9.8', + PickNCrop : '0.9.27', + AndroidGIFDrawable : '1.2.6', + KPreferences : '0.9.7', + Kovenant : '3.3.0', + ParcelablePlease : '1.0.2', + Chameleon : '0.9.22', + UniqR : '0.9.4', + SQLiteQB : '0.9.15', + Glide : '3.7.0', + GlideOkHttp3 : '1.4.0', + GlideTransformations : '2.0.2', + AndroidImageCropper : '2.4.6', + ExportablePreferences : '0.9.7', + ACRA : '4.9.2', + AbstractTask : '0.9.5', + Dagger : '2.11', + StethoBeanShellREPL : '0.3', + ArchLifecycleExtensions: '1.0.0-beta2', + ArchPaging : '1.0.0-alpha3', ] } diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableStatus.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableStatus.java index 66b47a008..e72085f5b 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableStatus.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ParcelableStatus.java @@ -72,7 +72,7 @@ public class ParcelableStatus implements Parcelable, Comparable(context, requestManager), IStatusesAdapter, IItemCountsAdapter { - protected val inflater: LayoutInflater = LayoutInflater.from(context) - override val twidereLinkify: TwidereLinkify + @PreviewStyle override val mediaPreviewStyle: Int = preferences[mediaPreviewStyleKey] override val nameFirst: Boolean = preferences[nameFirstKey] @@ -74,11 +75,6 @@ class ParcelableStatusesAdapter( override val lightFont: Boolean = preferences[lightFontKey] override val mediaPreviewEnabled: Boolean = Utils.isMediaPreviewEnabled(context, preferences) override val sensitiveContentEnabled: Boolean = preferences.getBoolean(KEY_DISPLAY_SENSITIVE_CONTENTS, false) - private val showCardActions: Boolean = !preferences[hideCardActionsKey] - - private val gapLoadingIds: MutableSet = HashSet() - - override var statusClickListener: IStatusViewHolder.StatusClickListener? = null override val gapClickListener: IGapSupportedAdapter.GapClickListener? get() = statusClickListener @@ -90,6 +86,10 @@ class ParcelableStatusesAdapter( notifyDataSetChanged() } + override var statusClickListener: IStatusViewHolder.StatusClickListener? = null + + override val itemCounts = ItemCounts(5) + var isShowInReplyTo: Boolean = false set(value) { if (field == value) return @@ -111,47 +111,35 @@ class ParcelableStatusesAdapter( notifyDataSetChanged() } - private var displayPositions: IntArray? = null - private var displayDataCount: Int = 0 - private var showingActionCardId = RecyclerView.NO_ID - private val showingFullTextStates = SparseBooleanArray() - private val reuseStatus = ParcelableStatus() - private var infoCache: Array? = null + private val inflater: LayoutInflater = LayoutInflater.from(context) - var data: List? = null - set(data) { - when (data) { - null -> { - displayPositions = null - displayDataCount = 0 - } - is ObjectCursor -> { - displayPositions = null - displayDataCount = data.size - } - else -> { - var filteredCount = 0 - displayPositions = IntArray(data.size).apply { - data.forEachIndexed { i, item -> - if (!item.is_gap && item.is_filtered) { - filteredCount++ - } else { - this[i - filteredCount] = i - } - } - } - displayDataCount = data.size - filteredCount - } - } - field = data - this.infoCache = if (data != null) arrayOfNulls(data.size) else null + private val showCardActions: Boolean = !preferences[hideCardActionsKey] + + private val gapLoadingIds: MutableSet = HashSet() + private var showingActionCardId = RecyclerView.NO_ID + + private val showingFullTextStates = SparseBooleanArray() + + private var pagedStatusesHelper = PagedListAdapterHelper(this, object : DiffCallback() { + override fun areContentsTheSame(oldItem: ParcelableStatus, newItem: ParcelableStatus): Boolean { + return oldItem == newItem + } + + override fun areItemsTheSame(oldItem: ParcelableStatus, newItem: ParcelableStatus): Boolean { + return oldItem._id == newItem._id || oldItem === newItem + } + + }) + + var statuses: PagedList? + get() = pagedStatusesHelper.currentList + set(value) { + pagedStatusesHelper.setList(value) gapLoadingIds.clear() updateItemCount() notifyDataSetChanged() } - override val itemCounts = ItemCounts(5) - val statusStartIndex: Int get() = getItemStartPosition(ITEM_INDEX_STATUS) @@ -178,20 +166,16 @@ class ParcelableStatusesAdapter( } override fun isGapItem(position: Int): Boolean { - return getFieldValue(position, { info -> - return@getFieldValue info.gap - }, { status -> - return@getFieldValue status.is_gap - }, false) + return getStatus(position).is_gap } override fun getStatus(position: Int, raw: Boolean): ParcelableStatus { - return getStatusInternal(position, getItemCountIndex(position, raw), raw, reuse = false) + return getStatusInternal(position, getItemCountIndex(position, raw), raw) } override fun getStatusCount(raw: Boolean): Int { - if (raw) return data?.size ?: 0 - return displayDataCount + if (raw) return statuses?.size ?: 0 + return pagedStatusesHelper.itemCount } override fun getItemId(position: Int): Long { @@ -201,57 +185,29 @@ class ParcelableStatusesAdapter( val status = pinnedStatuses!![position - getItemStartPosition(ITEM_INDEX_PINNED_STATUS)] return status.hashCode().toLong() } - ITEM_INDEX_STATUS -> getFieldValue(position, { (_, accountKey, id) -> - return@getFieldValue ParcelableStatus.calculateHashCode(accountKey, id) - }, { status -> - return@getFieldValue status.hashCode() - }, -1).toLong() + ITEM_INDEX_STATUS -> return getStatus(position, false).hashCode().toLong() else -> position.toLong() } } override fun getStatusId(position: Int, raw: Boolean): String { - return getFieldValue(position, { info -> - return@getFieldValue info.id - }, { status -> - return@getFieldValue status.id - }, "") + return getStatus(position, raw).id } fun getStatusSortId(position: Int, raw: Boolean): Long { - return getFieldValue(position, { info -> - return@getFieldValue info.sortId - }, { status -> - return@getFieldValue status.sort_id - }, -1L, raw) + return getStatus(position, raw).sort_id } override fun getStatusTimestamp(position: Int, raw: Boolean): Long { - return getFieldValue(position, { info -> - return@getFieldValue info.timestamp - }, { status -> - return@getFieldValue status.timestamp - }, -1L) + return getStatus(position, raw).timestamp } override fun getStatusPositionKey(position: Int, raw: Boolean): Long { - return getFieldValue(position, { info -> - if (info.positionKey > 0) return@getFieldValue info.positionKey - return@getFieldValue info.timestamp - }, { status -> - val positionKey = status.position_key - if (positionKey > 0) return@getFieldValue positionKey - return@getFieldValue status.timestamp - }, -1L) + return getStatus(position, raw).position_key } override fun getAccountKey(position: Int, raw: Boolean): UserKey { - val def: UserKey? = null - return getFieldValue(position, { info -> - return@getFieldValue info.accountKey - }, { status -> - return@getFieldValue status.account_key - }, def, raw)!! + return getStatus(position, raw).account_key } override fun isCardActionsShown(position: Int): Boolean { @@ -313,7 +269,7 @@ class ParcelableStatusesAdapter( when (holder.itemViewType) { VIEW_TYPE_STATUS -> { val countIndex: Int = getItemCountIndex(position) - val status = getStatusInternal(position, countIndex = countIndex, reuse = true) + val status = getStatusInternal(position, countIndex = countIndex) (holder as IStatusViewHolder).display(status, displayInReplyTo = isShowInReplyTo, displayPinned = countIndex == ITEM_INDEX_PINNED_STATUS) } @@ -321,7 +277,7 @@ class ParcelableStatusesAdapter( (holder as TimelineFilterHeaderViewHolder).display(timelineFilter!!) } ITEM_VIEW_TYPE_GAP -> { - val status = getStatusInternal(position, reuse = true) + val status = getStatusInternal(position) val loading = gapLoadingIds.any { it.accountKey == status.account_key && it.id == status.id } (holder as GapViewHolder).display(loading) } @@ -378,11 +334,8 @@ class ParcelableStatusesAdapter( } fun getRowId(adapterPosition: Int, raw: Boolean = false): Long { - return getFieldValue(adapterPosition, readInfoValueAction = { - it._id - }, readStatusValueAction = { status -> - status.hashCode().toLong() - }, defValue = -1L, raw = raw) + val status = getStatus(adapterPosition, raw) + return if (status._id < 0) status.hashCode().toLong() else status._id } fun findPositionByPositionKey(positionKey: Long, raw: Boolean = false): Int { @@ -414,7 +367,7 @@ class ParcelableStatusesAdapter( var sum = 0 for (i in 0 until itemCounts.size) { sum += when (i) { - ITEM_INDEX_STATUS -> data?.size ?: 0 + ITEM_INDEX_STATUS -> statuses?.size ?: 0 else -> itemCounts[i] } if (position < sum) { @@ -424,57 +377,15 @@ class ParcelableStatusesAdapter( return -1 } - private inline fun getFieldValue(position: Int, - readInfoValueAction: (StatusInfo) -> T, - readStatusValueAction: (status: ParcelableStatus) -> T, - defValue: T, raw: Boolean = false): T { - val data = this.data - if (data is ObjectCursor) { - val dataPosition = position - statusStartIndex - if (dataPosition < 0 || dataPosition >= getStatusCount(true)) { - throw CursorIndexOutOfBoundsException("index: $position, valid range is $0..${getStatusCount(true)}") - } - val info = infoCache?.get(dataPosition) ?: run { - val cursor = data.cursor - if (!cursor.safeMoveToPosition(dataPosition)) return defValue - val indices = data.indices - val _id = cursor.safeGetLong(indices[Statuses._ID]) - val accountKey = UserKey.valueOf(cursor.safeGetString(indices[Statuses.ACCOUNT_KEY])) - val id = cursor.safeGetString(indices[Statuses.ID]) - val timestamp = cursor.safeGetLong(indices[Statuses.TIMESTAMP]) - val sortId = cursor.safeGetLong(indices[Statuses.SORT_ID]) - val positionKey = cursor.safeGetLong(indices[Statuses.POSITION_KEY]) - val gap = cursor.getInt(indices[Statuses.IS_GAP]) == 1 - val newInfo = StatusInfo(_id, accountKey, id, timestamp, sortId, positionKey, gap) - infoCache?.set(dataPosition, newInfo) - return@run newInfo - } - return readInfoValueAction(info) - } - return readStatusValueAction(getStatus(position, raw)) - } - private fun getStatusInternal(position: Int, countIndex: Int = getItemCountIndex(position), - raw: Boolean = false, reuse: Boolean): ParcelableStatus { + raw: Boolean = false): ParcelableStatus { when (countIndex) { ITEM_INDEX_PINNED_STATUS -> { return pinnedStatuses!![position - getItemStartPosition(ITEM_INDEX_PINNED_STATUS)] } ITEM_INDEX_STATUS -> { - val data = this.data!! val dataPosition = position - statusStartIndex - val positions = displayPositions - val listPosition = if (positions != null && !raw) { - positions[dataPosition] - } else { - dataPosition - } - if (reuse && data is ObjectCursor) { - reuseStatus.is_filtered = false - return data.setInto(listPosition, reuseStatus) - } else { - return data[listPosition] - } + return pagedStatusesHelper.getItem(dataPosition)!! } } val validStart = getItemStartPosition(ITEM_INDEX_PINNED_STATUS) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/data/ObjectCursorLiveData.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/data/ObjectCursorLiveData.kt deleted file mode 100644 index 1c9b84d08..000000000 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/data/ObjectCursorLiveData.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Copyright (C) 2012-2017 Mariotaku Lee - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.mariotaku.twidere.data - -import android.content.ContentResolver -import android.database.ContentObserver -import android.net.Uri -import android.os.Handler -import android.os.Looper -import nl.komponents.kovenant.task -import nl.komponents.kovenant.ui.successUi -import org.mariotaku.ktextension.weak -import org.mariotaku.library.objectcursor.ObjectCursor -import org.mariotaku.twidere.extension.queryReference - -class ObjectCursorLiveData( - val resolver: ContentResolver, - val uri: Uri, - val projection: Array? = null, - val selection: String? = null, - val selectionArgs: Array? = null, - val orderBy: String? = null, - val cls: Class -) : ReloadableLiveData?>() { - - private val reloadObserver = object : ContentObserver(MainThreadHandler) { - override fun onChange(selfChange: Boolean) { - if (hasActiveObservers()) { - loadData() - } - } - } - - override fun onLoadData(callback: (List?) -> Unit) { - val weakThis = weak() - task { - val (c) = resolver.queryReference(uri, projection, selection, selectionArgs) ?: - throw NullPointerException() - c.registerContentObserver(reloadObserver) - val i = ObjectCursor.indicesFrom(c, cls) - return@task ObjectCursor(c, i) - }.successUi { data -> - val ld = weakThis.get() - val oldValue = ld?.value - if (oldValue is ObjectCursor<*>) { - oldValue.close() - } - callback(data) - } - } - - override fun onInactive() { - val oldValue = this.value - if (oldValue is ObjectCursor<*>) { - oldValue.cursor.unregisterContentObserver(reloadObserver) - oldValue.close() - } - value = null - } - - object MainThreadHandler : Handler(Looper.getMainLooper()) -} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/data/ReloadableLiveData.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/data/source/CursorObjectLivePagedListProvider.kt similarity index 53% rename from twidere/src/main/kotlin/org/mariotaku/twidere/data/ReloadableLiveData.kt rename to twidere/src/main/kotlin/org/mariotaku/twidere/data/source/CursorObjectLivePagedListProvider.kt index 55cef8d83..1d8c1f11d 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/data/ReloadableLiveData.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/data/source/CursorObjectLivePagedListProvider.kt @@ -17,21 +17,27 @@ * along with this program. If not, see . */ -package org.mariotaku.twidere.data +package org.mariotaku.twidere.data.source -import android.arch.lifecycle.LiveData +import android.arch.paging.LivePagedListProvider +import android.content.ContentResolver +import android.net.Uri +/** + * Created by mariotaku on 2017/10/13. + */ -abstract class ReloadableLiveData : LiveData() { +class CursorObjectLivePagedListProvider( + private val resolver: ContentResolver, + val uri: Uri, + val projection: Array? = null, + val selection: String? = null, + val selectionArgs: Array? = null, + val sortOrder: String? = null, + val cls: Class +) : LivePagedListProvider() { - fun loadData() { - onLoadData(this::setValue) - } - - override fun onActive() { - loadData() - } - - protected abstract fun onLoadData(callback: (T) -> Unit) + override fun createDataSource() = CursorObjectTiledDataSource(resolver, uri, projection, + selection, selectionArgs, sortOrder, cls) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/data/source/CursorObjectTiledDataSource.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/data/source/CursorObjectTiledDataSource.kt new file mode 100644 index 000000000..bea3838e2 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/data/source/CursorObjectTiledDataSource.kt @@ -0,0 +1,66 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.mariotaku.twidere.data.source + +import android.arch.paging.TiledDataSource +import android.content.ContentResolver +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import org.mariotaku.twidere.extension.queryAll +import org.mariotaku.twidere.extension.queryCount + +/** + * Created by mariotaku on 2017/10/13. + */ + +class CursorObjectTiledDataSource( + private val resolver: ContentResolver, + val uri: Uri, + val projection: Array? = null, + val selection: String? = null, + val selectionArgs: Array? = null, + val sortOrder: String? = null, + val cls: Class +) : TiledDataSource() { + + init { + val observer = object : ContentObserver(MainHandler) { + override fun onChange(selfChange: Boolean) { + invalidate() + } + } + resolver.registerContentObserver(uri, false, observer) + removeInvalidatedCallback { + resolver.unregisterContentObserver(observer) + } + } + + override fun countItems() = resolver.queryCount(uri, selection, selectionArgs) + + override fun loadRange(startPosition: Int, count: Int): List { + return resolver.queryAll(uri, projection, selection, selectionArgs, sortOrder, + "$startPosition,$count", cls) + } + + private object MainHandler : Handler(Looper.getMainLooper()) + +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/data/status/StatusesLiveData.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/data/status/StatusesLiveData.kt deleted file mode 100644 index 6ae18370d..000000000 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/data/status/StatusesLiveData.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Copyright (C) 2012-2017 Mariotaku Lee - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.mariotaku.twidere.data.status - -import org.mariotaku.twidere.data.ReloadableLiveData -import org.mariotaku.twidere.model.ParcelableStatus - - -class StatusesLiveData : ReloadableLiveData>() { - override fun onLoadData(callback: (List) -> Unit) { - - } - -} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/ContentResolverExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/ContentResolverExtensions.kt index e8ca5bc63..c396abf28 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/ContentResolverExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/ContentResolverExtensions.kt @@ -71,8 +71,9 @@ fun ContentResolver.queryOne(uri: Uri, projection: Array?, selection } fun ContentResolver.queryAll(uri: Uri, projection: Array?, selection: String?, - selectionArgs: Array?, sortOrder: String? = null, cls: Class): List { - return queryReference(uri, projection, selection, selectionArgs, sortOrder)?.use { (cur) -> + selectionArgs: Array?, sortOrder: String? = null, limit: String? = null, + cls: Class): List { + return queryReference(uri, projection, selection, selectionArgs, sortOrder, limit)?.use { (cur) -> return@use cur.map(ObjectCursor.indicesFrom(cur, cls)) } ?: emptyList() } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/FiltersDataExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/FiltersDataExtensions.kt index 5273b213f..7a4e75c11 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/FiltersDataExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/FiltersDataExtensions.kt @@ -25,12 +25,12 @@ fun FiltersData.read(cr: ContentResolver, loadSubscription: Boolean = false) { fun readBaseItems(uri: Uri): List? { val where = if (loadSubscription) null else Expression.lesserThan(Filters.SOURCE, 0).sql return cr.queryAll(uri, Filters.COLUMNS, where, null, null, - FiltersData.BaseItem::class.java) + cls = FiltersData.BaseItem::class.java) } this.users = run { val where = if (loadSubscription) null else Expression.lesserThan(Filters.Users.SOURCE, 0).sql return@run cr.queryAll(Filters.Users.CONTENT_URI, Filters.Users.COLUMNS, where, null, - null, FiltersData.UserItem::class.java) + null, cls = FiltersData.UserItem::class.java) } this.keywords = readBaseItems(Filters.Keywords.CONTENT_URI) this.sources = readBaseItems(Filters.Sources.CONTENT_URI) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt index 51a143e42..27df099ca 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt @@ -43,7 +43,6 @@ import org.mariotaku.twidere.R import org.mariotaku.twidere.activity.AccountSelectorActivity import org.mariotaku.twidere.activity.ComposeActivity import org.mariotaku.twidere.adapter.ParcelableStatusesAdapter -import org.mariotaku.twidere.adapter.decorator.ExtendedDividerItemDecoration import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter import org.mariotaku.twidere.annotation.ReadPositionTag import org.mariotaku.twidere.constant.* @@ -52,6 +51,7 @@ import org.mariotaku.twidere.constant.KeyboardShortcutConstants.* import org.mariotaku.twidere.extension.model.getAccountType import org.mariotaku.twidere.fragment.status.FavoriteConfirmDialogFragment import org.mariotaku.twidere.fragment.status.RetweetQuoteDialogFragment +import org.mariotaku.twidere.fragment.timeline.AbsTimelineFragment import org.mariotaku.twidere.graphic.like.LikeAnimationDrawable import org.mariotaku.twidere.loader.iface.IExtendedLoader import org.mariotaku.twidere.model.ObjectId @@ -100,9 +100,9 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment protected var adapterData: List? - get() = adapter.data + get() = adapter.statuses set(data) { - adapter.data = data +// adapter.statuses = data } @ReadPositionTag @@ -400,27 +400,9 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment - val itemViewType = adapter.getItemViewType(position) - var nextItemIsStatus = false - if (position < adapter.itemCount - 1) { - nextItemIsStatus = adapter.getItemViewType(position + 1) == ParcelableStatusesAdapter.VIEW_TYPE_STATUS - } - if (nextItemIsStatus && itemViewType == ParcelableStatusesAdapter.VIEW_TYPE_STATUS) { - rect.left = decorPaddingLeft - } else { - rect.left = 0 - } - true - } - } - itemDecoration.setDecorationEndOffset(1) - return itemDecoration + override fun onCreateItemDecoration(context: Context, recyclerView: RecyclerView, + layoutManager: LinearLayoutManager): RecyclerView.ItemDecoration? { + return AbsTimelineFragment.createStatusesListItemDecoration(context, recyclerView, adapter) } protected fun saveReadPosition() { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CursorStatusesFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CursorStatusesFragment.kt index 3fa9e2efe..a75393fbf 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CursorStatusesFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CursorStatusesFragment.kt @@ -149,7 +149,7 @@ abstract class CursorStatusesFragment : AbsStatusesFragment() { } override fun onLoaderReset(loader: Loader?>) { - adapter.data = null + adapter.statuses = null } override fun onLoadMoreContents(@IndicatorPosition position: Long) { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/statuses/MediaStatusesSearchFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/statuses/MediaStatusesSearchFragment.kt index b4bcdf6a3..e49954e28 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/statuses/MediaStatusesSearchFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/statuses/MediaStatusesSearchFragment.kt @@ -41,7 +41,7 @@ class MediaStatusesSearchFragment : ParcelableStatusesFragment() { val tabPosition = args.getInt(EXTRA_TAB_POSITION, -1) val makeGap = args.getBoolean(EXTRA_MAKE_GAP, true) val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false) - return MediaStatusesSearchLoader(activity, accountKey, query, adapter.data, fromUser, makeGap, + return MediaStatusesSearchLoader(activity, accountKey, query, adapter.statuses, fromUser, makeGap, loadingMore) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/statuses/UserMediaTimelineFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/statuses/UserMediaTimelineFragment.kt index d7eb994e6..a444081d6 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/statuses/UserMediaTimelineFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/statuses/UserMediaTimelineFragment.kt @@ -43,7 +43,7 @@ class UserMediaTimelineFragment : ParcelableStatusesFragment() { val userKey = args.getParcelable(EXTRA_USER_KEY) val screenName = args.getString(EXTRA_SCREEN_NAME) val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false) - return MediaTimelineLoader(context, accountKey, userKey, screenName, adapter.data, + return MediaTimelineLoader(context, accountKey, userKey, screenName, adapter.statuses, fromUser, loadingMore) } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/AbsTimelineFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/AbsTimelineFragment.kt index d17891dff..112a5d46f 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/AbsTimelineFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/AbsTimelineFragment.kt @@ -21,29 +21,39 @@ package org.mariotaku.twidere.fragment.timeline import android.arch.lifecycle.LiveData import android.arch.lifecycle.Observer +import android.arch.paging.PagedList import android.content.Context import android.net.Uri import android.os.Bundle import android.support.v7.widget.FixedLinearLayoutManager import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView.LayoutManager import android.support.v7.widget.StaggeredGridLayoutManager +import android.widget.Toast import com.bumptech.glide.RequestManager +import com.squareup.otto.Subscribe import kotlinx.android.synthetic.main.fragment_content_listview.* import org.mariotaku.twidere.R import org.mariotaku.twidere.adapter.ParcelableStatusesAdapter +import org.mariotaku.twidere.adapter.decorator.ExtendedDividerItemDecoration +import org.mariotaku.twidere.adapter.iface.IContentAdapter import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter import org.mariotaku.twidere.annotation.FilterScope import org.mariotaku.twidere.annotation.TimelineStyle -import org.mariotaku.twidere.data.ObjectCursorLiveData +import org.mariotaku.twidere.data.source.CursorObjectLivePagedListProvider import org.mariotaku.twidere.fragment.AbsContentRecyclerViewFragment import org.mariotaku.twidere.fragment.CursorStatusesFragment -import org.mariotaku.twidere.model.ExceptionResponseList +import org.mariotaku.twidere.model.ObjectId import org.mariotaku.twidere.model.ParcelableStatus import org.mariotaku.twidere.model.UserKey +import org.mariotaku.twidere.model.event.GetStatusesTaskEvent +import org.mariotaku.twidere.model.pagination.Pagination import org.mariotaku.twidere.model.pagination.SinceMaxPagination import org.mariotaku.twidere.model.refresh.BaseRefreshTaskParam import org.mariotaku.twidere.model.refresh.RefreshTaskParam +import org.mariotaku.twidere.provider.TwidereDataStore.Statuses +import org.mariotaku.twidere.task.statuses.GetStatusesTask import org.mariotaku.twidere.util.DataStoreUtils import org.mariotaku.twidere.util.Utils @@ -60,7 +70,7 @@ abstract class AbsTimelineFragment : protected open val timelineStyle: Int get() = TimelineStyle.PLAIN - protected open val isLive: Boolean + protected open val isStandalone: Boolean get() = tabId <= 0 @FilterScope @@ -71,16 +81,18 @@ abstract class AbsTimelineFragment : */ protected abstract val contentUri: Uri - protected lateinit var statuses: LiveData?> + protected lateinit var statuses: LiveData?> private set protected val accountKeys: Array - get() = Utils.getAccountKeys(context, arguments) ?: if (isLive) { + get() = Utils.getAccountKeys(context, arguments) ?: if (isStandalone) { emptyArray() } else { DataStoreUtils.getActivatedAccountKeys(context) } + private val busEventHandler = BusEventHandler() + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) statuses = createLiveData() @@ -89,6 +101,16 @@ abstract class AbsTimelineFragment : showProgress() } + override fun onStart() { + super.onStart() + bus.register(busEventHandler) + } + + override fun onStop() { + bus.unregister(busEventHandler) + super.onStop() + } + override fun onCreateLayoutManager(context: Context): LayoutManager = when (timelineStyle) { TimelineStyle.STAGGERED -> StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) else -> FixedLinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) @@ -98,15 +120,29 @@ abstract class AbsTimelineFragment : return ParcelableStatusesAdapter(context, requestManager, timelineStyle) } + override fun onCreateItemDecoration(context: Context, recyclerView: RecyclerView, + layoutManager: LayoutManager): RecyclerView.ItemDecoration? { + return when (timelineStyle) { + TimelineStyle.PLAIN -> { + createStatusesListItemDecoration(context, recyclerView, adapter) + } + else -> { + super.onCreateItemDecoration(context, recyclerView, layoutManager) + } + } + } + override fun triggerRefresh(): Boolean { - val param = onCreateRefreshParam(REFRESH_POSITION_START) + val param = onCreateRefreshParam(REFRESH_POSITION_START) ?: return false return getStatuses(param) } override fun onLoadMoreContents(position: Long) { if (position != ILoadMoreSupportAdapter.END) return - val param = onCreateRefreshParam(REFRESH_POSITION_END) - getStatuses(param) + val param = onCreateRefreshParam(REFRESH_POSITION_END) ?: return + if (getStatuses(param)) { + adapter.loadMoreIndicatorPosition = position + } } override fun scrollToPositionWithOffset(position: Int, offset: Int) { @@ -121,12 +157,12 @@ abstract class AbsTimelineFragment : } } - protected open fun onDataLoaded(data: List?) { - adapter.data = data + protected open fun onDataLoaded(data: PagedList?) { + adapter.statuses = data when { - data is ExceptionResponseList -> { - showEmpty(R.drawable.ic_info_error_generic, data.exception.toString()) - } +// data is ExceptionResponseList -> { +// showEmpty(R.drawable.ic_info_error_generic, data.exception.toString()) +// } data == null || data.isEmpty() -> { showEmpty(R.drawable.ic_info_refresh, getString(R.string.swipe_down_to_refresh)) } @@ -138,32 +174,72 @@ abstract class AbsTimelineFragment : protected abstract fun getStatuses(param: RefreshParam): Boolean - protected abstract fun onCreateRefreshParam(position: Int): RefreshParam + protected abstract fun onCreateRefreshParam(position: Int): RefreshParam? - protected open fun onCreateRealTimeLiveData(): LiveData?> { + protected open fun onCreateStandaloneLiveData(): LiveData?> { throw UnsupportedOperationException() } - private fun createLiveData(): LiveData?> { - if (isLive) return onCreateRealTimeLiveData() - return ObjectCursorLiveData(context.contentResolver, contentUri, - CursorStatusesFragment.statusColumnsLite, cls = ParcelableStatus::class.java) + private fun createLiveData(): LiveData?> { + if (isStandalone) return onCreateStandaloneLiveData() + val provider = CursorObjectLivePagedListProvider(context.contentResolver, contentUri, + CursorStatusesFragment.statusColumnsLite, sortOrder = Statuses.DEFAULT_SORT_ORDER, + cls = ParcelableStatus::class.java) + return provider.create(null, 20) + } + + private inner class BusEventHandler { + + @Subscribe + fun notifyGetStatusesTaskChanged(event: GetStatusesTaskEvent) { + if (event.uri != contentUri) return + refreshing = event.running + if (!event.running) { + setLoadMoreIndicatorPosition(ILoadMoreSupportAdapter.NONE) + refreshEnabled = true + // TODO: showContentOrError() + + val exception = event.exception + if (exception is GetStatusesTask.GetTimelineException && userVisibleHint) { + Toast.makeText(context, exception.getToastMessage(context), Toast.LENGTH_SHORT).show() + } + } + } + } companion object { const val REFRESH_POSITION_START = -1 const val REFRESH_POSITION_END = -2 - fun getBaseRefreshTaskParam(fragment: AbsTimelineFragment<*>, position: Int): BaseRefreshTaskParam { + fun getBaseRefreshTaskParam(fragment: AbsTimelineFragment<*>, position: Int): BaseRefreshTaskParam? { when (position) { REFRESH_POSITION_START -> { return getRefreshBaseRefreshTaskParam(fragment) } REFRESH_POSITION_END -> { - + val adapter = fragment.adapter + // Get last raw status + val startIdx = adapter.statusStartIndex + if (startIdx < 0) return null + val statusCount = adapter.getStatusCount(true) + if (statusCount <= 0) return null + val status = adapter.getStatus(startIdx + statusCount - 1, true) + val accountKeys = arrayOf(status.account_key) + val pagination = arrayOf(SinceMaxPagination.maxId(status.id, -1)) + val param = BaseRefreshTaskParam(accountKeys, pagination) + param.isLoadingMore = true + return param + } + else -> { + val adapter = fragment.adapter + val status = adapter.getStatus(position) + adapter.addGapLoadingId(ObjectId(status.account_key, status.id)) + val accountKeys = arrayOf(status.account_key) + val pagination = arrayOf(SinceMaxPagination.maxId(status.id, status.sort_id)) + return BaseRefreshTaskParam(accountKeys, pagination) } } - TODO() } private fun getRefreshBaseRefreshTaskParam(fragment: AbsTimelineFragment<*>): BaseRefreshTaskParam { @@ -184,5 +260,31 @@ abstract class AbsTimelineFragment : return BaseRefreshTaskParam(accountKeys, null) } } + + + fun createStatusesListItemDecoration(context: Context, recyclerView: RecyclerView, + adapter: IContentAdapter): RecyclerView.ItemDecoration { + adapter as RecyclerView.Adapter<*> + val itemDecoration = ExtendedDividerItemDecoration(context, (recyclerView.layoutManager as LinearLayoutManager).orientation) + val res = context.resources + if (adapter.profileImageEnabled) { + val decorPaddingLeft = res.getDimensionPixelSize(R.dimen.element_spacing_normal) * 2 + res.getDimensionPixelSize(R.dimen.icon_size_status_profile_image) + itemDecoration.setPadding { position, rect -> + val itemViewType = adapter.getItemViewType(position) + var nextItemIsStatus = false + if (position < adapter.itemCount - 1) { + nextItemIsStatus = adapter.getItemViewType(position + 1) == ParcelableStatusesAdapter.VIEW_TYPE_STATUS + } + if (nextItemIsStatus && itemViewType == ParcelableStatusesAdapter.VIEW_TYPE_STATUS) { + rect.left = decorPaddingLeft + } else { + rect.left = 0 + } + true + } + } + itemDecoration.setDecorationEndOffset(1) + return itemDecoration + } } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/HomeTimelineFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/HomeTimelineFragment.kt index 3c8e1d334..6ec64e7df 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/HomeTimelineFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/HomeTimelineFragment.kt @@ -29,7 +29,7 @@ class HomeTimelineFragment : AbsTimelineFragment() { override val contentUri: Uri = Statuses.CONTENT_URI - override fun onCreateRefreshParam(position: Int): RefreshTaskParam { + override fun onCreateRefreshParam(position: Int): RefreshTaskParam? { return getBaseRefreshTaskParam(this, position) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/cache/CacheTimelineResultTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/cache/CacheTimelineResultTask.kt index 070bb9525..b8f3967af 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/cache/CacheTimelineResultTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/cache/CacheTimelineResultTask.kt @@ -40,7 +40,7 @@ class CacheTimelineResultTask( val localRelationships = cr.queryAll(CachedRelationships.CONTENT_URI, CachedRelationships.COLUMNS, Expression.and(Expression.equalsArgs(CachedRelationships.ACCOUNT_KEY), Expression.inArgs(CachedRelationships.USER_KEY, users.size)).sql, - selectionArgsList.toTypedArray(), null, ParcelableRelationship::class.java) + selectionArgsList.toTypedArray(), cls = ParcelableRelationship::class.java) val relationships = users.mapTo(ArraySet()) { user -> val userKey = user.key return@mapTo localRelationships.find {