From 13dac670d271a71a38a6f3de5a96cfa420dd3aa4 Mon Sep 17 00:00:00 2001 From: Mariotaku Lee Date: Fri, 10 Mar 2017 16:37:35 +0800 Subject: [PATCH] improved activities gap load improved load more items --- .../org/mariotaku/twidere/model/UserKey.java | 11 +- .../twidere/util/DebugModeUtils.java | 4 +- .../twidere/util/stetho/RawStreamDumper.kt | 80 ------ .../twidere/util/stetho/UserStreamDumper.kt | 118 ++++++++ .../library/twitter/TwitterUserStream.java | 3 - .../library/twitter/UserStreamCallback.java | 256 +++++++++++------- .../twitter/model/TwitterStreamObject.java | 73 ++++- .../mariotaku/ktextension/NumberExtensions.kt | 4 +- .../activity/WebLinkHandlerActivity.kt | 6 +- .../twidere/adapter/DummyItemAdapter.kt | 40 +-- .../adapter/ParcelableActivitiesAdapter.kt | 193 ++++++------- .../adapter/ParcelableStatusesAdapter.kt | 98 ++++--- .../adapter/iface/IActivitiesAdapter.kt | 6 +- .../twidere/adapter/iface/IStatusesAdapter.kt | 19 +- .../twidere/fragment/AbsActivitiesFragment.kt | 57 ++-- .../twidere/fragment/AbsStatusesFragment.kt | 66 +++-- .../fragment/CursorActivitiesFragment.kt | 6 +- .../fragment/ParcelableStatusesFragment.kt | 16 +- .../twidere/fragment/StatusFragment.kt | 98 ++++--- .../fragment/UserMediaTimelineFragment.kt | 9 +- .../twidere/model/BaseRefreshTaskParam.kt | 1 + .../twidere/model/RefreshTaskParam.kt | 2 + .../twidere/model/SimpleRefreshTaskParam.kt | 3 + .../twidere/service/StreamingService.kt | 125 ++------- .../twidere/task/twitter/GetActivitiesTask.kt | 18 +- .../org/mariotaku/twidere/util/MenuUtils.kt | 10 +- 26 files changed, 727 insertions(+), 595 deletions(-) delete mode 100644 twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/RawStreamDumper.kt create mode 100644 twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/UserStreamDumper.kt diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/UserKey.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/UserKey.java index 2d9330740..3719e8547 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/UserKey.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/UserKey.java @@ -41,7 +41,8 @@ import java.util.List; @ParcelablePlease public class UserKey implements Comparable, Parcelable { - public static final UserKey SELF_REFERENCE = new UserKey("#self#", "#self#"); + public static final UserKey SELF = new UserKey("#self#", "#self#"); + public static final UserKey INVALID = new UserKey("#invalid#", "#invalid#"); public static final Creator CREATOR = new Creator() { @Override @@ -75,8 +76,12 @@ public class UserKey implements Comparable, Parcelable { } - public boolean isSelfReference() { - return equals(SELF_REFERENCE); + public boolean isSelf() { + return equals(SELF); + } + + public boolean isValid() { + return !equals(INVALID); } @NonNull diff --git a/twidere/src/debug/java/org/mariotaku/twidere/util/DebugModeUtils.java b/twidere/src/debug/java/org/mariotaku/twidere/util/DebugModeUtils.java index 40a430af1..5c1510840 100644 --- a/twidere/src/debug/java/org/mariotaku/twidere/util/DebugModeUtils.java +++ b/twidere/src/debug/java/org/mariotaku/twidere/util/DebugModeUtils.java @@ -31,7 +31,7 @@ import com.squareup.leakcanary.RefWatcher; import org.mariotaku.twidere.BuildConfig; import org.mariotaku.twidere.util.net.NoIntercept; import org.mariotaku.twidere.util.stetho.AccountsDumper; -import org.mariotaku.twidere.util.stetho.RawStreamDumper; +import org.mariotaku.twidere.util.stetho.UserStreamDumper; import java.io.IOException; @@ -70,7 +70,7 @@ public class DebugModeUtils { public Iterable get() { return new Stetho.DefaultDumperPluginsBuilder(application) .provide(new AccountsDumper(application)) - .provide(new RawStreamDumper(application)) + .provide(new UserStreamDumper(application)) .finish(); } }) diff --git a/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/RawStreamDumper.kt b/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/RawStreamDumper.kt deleted file mode 100644 index 26e493e43..000000000 --- a/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/RawStreamDumper.kt +++ /dev/null @@ -1,80 +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.util.stetho - -import android.accounts.AccountManager -import android.content.Context -import com.facebook.stetho.dumpapp.DumpException -import com.facebook.stetho.dumpapp.DumperContext -import com.facebook.stetho.dumpapp.DumperPlugin -import org.apache.commons.cli.GnuParser -import org.apache.commons.cli.Options -import org.apache.commons.cli.ParseException -import org.mariotaku.microblog.library.MicroBlogException -import org.mariotaku.microblog.library.twitter.TwitterUserStream -import org.mariotaku.restfu.callback.RawCallback -import org.mariotaku.restfu.http.HttpResponse -import org.mariotaku.twidere.extension.model.getCredentials -import org.mariotaku.twidere.extension.model.newMicroBlogInstance -import org.mariotaku.twidere.model.UserKey -import org.mariotaku.twidere.model.util.AccountUtils - -/** - * Created by mariotaku on 2017/3/9. - */ -class RawStreamDumper(val context: Context) : DumperPlugin { - - override fun dump(dumpContext: DumperContext) { - val parser = GnuParser() - val options = Options() - options.addRequiredOption("a", "account", true, "Account key") - val cmdLine = try { - parser.parse(options, dumpContext.argsAsList.toTypedArray()) - } catch (e: ParseException) { - throw DumpException(e.message) - } - - val accountKey = UserKey.valueOf(cmdLine.getOptionValue("account")) - val am = AccountManager.get(context) - val account = AccountUtils.findByAccountKey(am, accountKey) ?: return - val credentials = account.getCredentials(am) - val userStream = credentials.newMicroBlogInstance(context, account.type, - cls = TwitterUserStream::class.java) - userStream.getUserStreamRaw(object : RawCallback { - override fun result(result: HttpResponse) { - dumpContext.stdout.println("Response: ${result.status}") - dumpContext.stdout.println("Headers:") - result.headers.toList().forEach { - dumpContext.stdout.println("${it.first}: ${it.second}") - } - dumpContext.stdout.println() - result.body.writeTo(dumpContext.stdout) - } - - override fun error(exception: MicroBlogException) { - exception.printStackTrace(dumpContext.stderr) - } - - }) - } - - override fun getName() = "raw_stream" - -} \ No newline at end of file diff --git a/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/UserStreamDumper.kt b/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/UserStreamDumper.kt new file mode 100644 index 000000000..839cc7f6a --- /dev/null +++ b/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/UserStreamDumper.kt @@ -0,0 +1,118 @@ +/* + * 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.util.stetho + +import android.accounts.AccountManager +import android.content.Context +import com.facebook.stetho.dumpapp.DumpException +import com.facebook.stetho.dumpapp.DumperContext +import com.facebook.stetho.dumpapp.DumperPlugin +import org.apache.commons.cli.GnuParser +import org.apache.commons.cli.Options +import org.apache.commons.cli.ParseException +import org.mariotaku.microblog.library.twitter.TwitterUserStream +import org.mariotaku.microblog.library.twitter.UserStreamCallback +import org.mariotaku.microblog.library.twitter.model.* +import org.mariotaku.twidere.extension.model.getCredentials +import org.mariotaku.twidere.extension.model.newMicroBlogInstance +import org.mariotaku.twidere.model.UserKey +import org.mariotaku.twidere.model.util.AccountUtils + +/** + * Created by mariotaku on 2017/3/9. + */ +class UserStreamDumper(val context: Context) : DumperPlugin { + + override fun dump(dumpContext: DumperContext) { + val parser = GnuParser() + val options = Options() + options.addRequiredOption("a", "account", true, "Account key") + val cmdLine = try { + parser.parse(options, dumpContext.argsAsList.toTypedArray()) + } catch (e: ParseException) { + throw DumpException(e.message) + } + + val accountKey = UserKey.valueOf(cmdLine.getOptionValue("account")) + val am = AccountManager.get(context) + val account = AccountUtils.findByAccountKey(am, accountKey) ?: return + val credentials = account.getCredentials(am) + val userStream = credentials.newMicroBlogInstance(context, account.type, + cls = TwitterUserStream::class.java) + dumpContext.stdout.println("Beginning user stream...") + dumpContext.stdout.flush() + val callback = object : UserStreamCallback() { + override fun onException(ex: Throwable): Boolean { + ex.printStackTrace(dumpContext.stderr) + dumpContext.stderr.flush() + return true + } + + override fun onStatus(status: Status): Boolean { + dumpContext.stdout.println("Status: @${status.user.screenName}: ${status.text.trim('\n')}") + dumpContext.stdout.flush() + return true + } + + override fun onDirectMessage(directMessage: DirectMessage): Boolean { + dumpContext.stdout.println("Message: @${directMessage.senderScreenName}: ${directMessage.text.trim('\n')}") + dumpContext.stdout.flush() + return true + } + + override fun onStatusDeleted(event: DeletionEvent): Boolean { + dumpContext.stdout.println("Status deleted: ${event.id}") + dumpContext.stdout.flush() + return true + } + + override fun onDirectMessageDeleted(event: DeletionEvent): Boolean { + dumpContext.stdout.println("Message deleted: ${event.id}") + dumpContext.stdout.flush() + return true + } + + override fun onFriendList(friendIds: Array): Boolean { + dumpContext.stdout.println("Friends list: ${friendIds.size} in total") + dumpContext.stdout.flush() + return true + } + + override fun onFavorite(source: User, target: User, targetStatus: Status): Boolean { + dumpContext.stdout.println("Favorited: @${source.screenName} -> ${targetStatus.text.trim('\n')}") + dumpContext.stdout.flush() + return true + } + + override fun onUnhandledEvent(obj: TwitterStreamObject, json: String) { + dumpContext.stdout.println("Unhandled: ${obj.determine()} = $json") + dumpContext.stdout.flush() + } + } + try { + userStream.getUserStream(callback) + } catch (e: Exception) { + e.printStackTrace(dumpContext.stderr) + } + } + + override fun getName() = "user_stream" + +} \ No newline at end of file diff --git a/twidere/src/main/java/org/mariotaku/microblog/library/twitter/TwitterUserStream.java b/twidere/src/main/java/org/mariotaku/microblog/library/twitter/TwitterUserStream.java index 6169da4fb..f49b39203 100644 --- a/twidere/src/main/java/org/mariotaku/microblog/library/twitter/TwitterUserStream.java +++ b/twidere/src/main/java/org/mariotaku/microblog/library/twitter/TwitterUserStream.java @@ -31,7 +31,4 @@ public interface TwitterUserStream { @GET("/user.json") void getUserStream(UserStreamCallback callback); - @GET("/user.json") - void getUserStreamRaw(RawCallback callback); - } diff --git a/twidere/src/main/java/org/mariotaku/microblog/library/twitter/UserStreamCallback.java b/twidere/src/main/java/org/mariotaku/microblog/library/twitter/UserStreamCallback.java index baf91441f..d9c597e6c 100644 --- a/twidere/src/main/java/org/mariotaku/microblog/library/twitter/UserStreamCallback.java +++ b/twidere/src/main/java/org/mariotaku/microblog/library/twitter/UserStreamCallback.java @@ -25,7 +25,6 @@ import android.util.Log; import com.bluelinelabs.logansquare.LoganSquare; -import org.mariotaku.commons.logansquare.LoganSquareMapperFinder; import org.mariotaku.microblog.library.MicroBlogException; import org.mariotaku.microblog.library.twitter.model.DeletionEvent; import org.mariotaku.microblog.library.twitter.model.DirectMessage; @@ -46,6 +45,7 @@ import java.io.InputStreamReader; /** * Created by mariotaku on 15/5/26. */ +@SuppressWarnings({"WeakerAccess"}) public abstract class UserStreamCallback implements RawCallback { private boolean connected; @@ -53,7 +53,7 @@ public abstract class UserStreamCallback implements RawCallback, or: Int): Int { - if (range.isEmpty()) return or +fun Int.coerceInOr(range: ClosedRange, def: Int): Int { + if (range.isEmpty()) return def return coerceIn(range) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/WebLinkHandlerActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/WebLinkHandlerActivity.kt index dd5fe8b9a..cebcde45c 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/WebLinkHandlerActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/WebLinkHandlerActivity.kt @@ -149,21 +149,21 @@ class WebLinkHandlerActivity : Activity() { val builder = Uri.Builder() builder.scheme(SCHEME_TWIDERE) builder.authority(AUTHORITY_USER_FRIENDS) - builder.appendQueryParameter(QUERY_PARAM_USER_KEY, UserKey.SELF_REFERENCE.toString()) + builder.appendQueryParameter(QUERY_PARAM_USER_KEY, UserKey.SELF.toString()) return Pair(Intent(Intent.ACTION_VIEW, builder.build()), true) } "followers" -> { val builder = Uri.Builder() builder.scheme(SCHEME_TWIDERE) builder.authority(AUTHORITY_USER_FOLLOWERS) - builder.appendQueryParameter(QUERY_PARAM_USER_KEY, UserKey.SELF_REFERENCE.toString()) + builder.appendQueryParameter(QUERY_PARAM_USER_KEY, UserKey.SELF.toString()) return Pair(Intent(Intent.ACTION_VIEW, builder.build()), true) } "favorites" -> { val builder = Uri.Builder() builder.scheme(SCHEME_TWIDERE) builder.authority(AUTHORITY_USER_FAVORITES) - builder.appendQueryParameter(QUERY_PARAM_USER_KEY, UserKey.SELF_REFERENCE.toString()) + builder.appendQueryParameter(QUERY_PARAM_USER_KEY, UserKey.SELF.toString()) return Pair(Intent(Intent.ACTION_VIEW, builder.build()), true) } else -> { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/DummyItemAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/DummyItemAdapter.kt index 11bf25698..89d3ad3b4 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/DummyItemAdapter.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/DummyItemAdapter.kt @@ -5,7 +5,6 @@ import android.support.v4.text.BidiFormatter import android.support.v7.widget.RecyclerView import com.bumptech.glide.RequestManager import org.mariotaku.kpreferences.get -import org.mariotaku.twidere.R import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter import org.mariotaku.twidere.adapter.iface.IStatusesAdapter import org.mariotaku.twidere.adapter.iface.IUserListsAdapter @@ -13,7 +12,10 @@ import org.mariotaku.twidere.adapter.iface.IUsersAdapter import org.mariotaku.twidere.constant.* import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.util.getActivityStatus -import org.mariotaku.twidere.util.* +import org.mariotaku.twidere.util.AsyncTwitterWrapper +import org.mariotaku.twidere.util.SharedPreferencesWrapper +import org.mariotaku.twidere.util.TwidereLinkify +import org.mariotaku.twidere.util.UserColorNameManager import org.mariotaku.twidere.util.dagger.GeneralComponentHelper import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder import javax.inject.Inject @@ -72,42 +74,28 @@ class DummyItemAdapter( return 0 } - override fun getStatus(position: Int): ParcelableStatus? { + override fun getStatus(position: Int, raw: Boolean): ParcelableStatus { if (adapter is ParcelableStatusesAdapter) { - return adapter.getStatus(position) + return adapter.getStatus(position, raw) } else if (adapter is VariousItemsAdapter) { return adapter.getItem(position) as ParcelableStatus } else if (adapter is ParcelableActivitiesAdapter) { - return adapter.getActivity(position)?.getActivityStatus() + return adapter.getActivity(position)?.getActivityStatus()!! } - return null + throw IndexOutOfBoundsException() } - override val statusCount: Int - get() = 0 + override fun getStatusCount(raw: Boolean) = 0 - override val rawStatusCount: Int - get() = 0 + override fun getStatusId(position: Int, raw: Boolean) = "" - override fun getStatusId(position: Int): String? { - return null - } + override fun getStatusTimestamp(position: Int, raw: Boolean) = -1L - override fun getStatusTimestamp(position: Int): Long { - return -1 - } + override fun getStatusPositionKey(position: Int, raw: Boolean) = -1L - override fun getStatusPositionKey(position: Int): Long { - return -1 - } + override fun getAccountKey(position: Int, raw: Boolean) = UserKey.INVALID - override fun getAccountKey(position: Int): UserKey? { - return null - } - - override fun findStatusById(accountKey: UserKey, statusId: String): ParcelableStatus? { - return null - } + override fun findStatusById(accountKey: UserKey, statusId: String) = null override fun isCardActionsShown(position: Int): Boolean { if (position == RecyclerView.NO_POSITION) return showCardActions diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableActivitiesAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableActivitiesAdapter.kt index e5d8487be..94da6c0b2 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableActivitiesAdapter.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableActivitiesAdapter.kt @@ -19,6 +19,7 @@ package org.mariotaku.twidere.adapter +import android.annotation.SuppressLint import android.content.Context import android.support.v4.widget.Space import android.support.v7.widget.RecyclerView @@ -36,6 +37,7 @@ import org.mariotaku.microblog.library.twitter.model.Activity import org.mariotaku.twidere.R import org.mariotaku.twidere.adapter.iface.IActivitiesAdapter import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter +import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter import org.mariotaku.twidere.annotation.Referral import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_NEW_DOCUMENT_API @@ -58,7 +60,10 @@ import java.util.* class ParcelableActivitiesAdapter( context: Context, requestManager: RequestManager -) : LoadMoreSupportAdapter(context, requestManager), IActivitiesAdapter> { +) : LoadMoreSupportAdapter(context, requestManager), + IActivitiesAdapter>, IItemCountsAdapter { + + override val itemCounts: ItemCounts = ItemCounts(2) private val inflater = LayoutInflater.from(context) private val twidereLinkify = TwidereLinkify(OnLinkClickHandler(context, null, preferences)) @@ -80,6 +85,20 @@ class ParcelableActivitiesAdapter( notifyDataSetChanged() } + override var loadMoreIndicatorPosition: Long + get() = super.loadMoreIndicatorPosition + set(value) { + super.loadMoreIndicatorPosition = value + updateItemCount() + } + + override var loadMoreSupportedPosition: Long + get() = super.loadMoreSupportedPosition + set(value) { + super.loadMoreSupportedPosition = value + updateItemCount() + } + init { eventListener = EventListener(this) statusAdapterDelegate.updateOptions() @@ -87,7 +106,7 @@ class ParcelableActivitiesAdapter( override fun isGapItem(position: Int): Boolean { val dataPosition = position - activityStartIndex - val activityCount = activityCount + val activityCount = getActivityCount(false) if (dataPosition < 0 || dataPosition >= activityCount) return false // Don't show gap if it's last item if (dataPosition == activityCount - 1) { @@ -104,7 +123,6 @@ class ParcelableActivitiesAdapter( override fun getItemId(position: Int): Long { val dataPosition = position - activityStartIndex - if (dataPosition < 0 || dataPosition >= activityCount) return RecyclerView.NO_ID if (data is ObjectCursor) { val cursor = (data as ObjectCursor).cursor if (!cursor.moveToPosition(dataPosition)) return -1 @@ -116,47 +134,37 @@ class ParcelableActivitiesAdapter( return ParcelableActivity.calculateHashCode(accountKey, timestamp, maxPosition, minPosition).toLong() } - return data!![dataPosition].hashCode().toLong() + return getActivity(position, false).hashCode().toLong() } - fun getActivityAction(adapterPosition: Int): String? { + fun getTimestamp(adapterPosition: Int, raw: Boolean = false): Long { val dataPosition = adapterPosition - activityStartIndex - if (dataPosition < 0 || dataPosition >= activityCount) return null - if (data is ObjectCursor) { - val cursor = (data as ObjectCursor).cursor - if (!cursor.safeMoveToPosition(dataPosition)) return null - val indices = (data as ObjectCursor).indices as ParcelableActivityCursorIndices - return cursor.getString(indices.action) - } - return data!![dataPosition].action - } - - fun getTimestamp(adapterPosition: Int): Long { - val dataPosition = adapterPosition - activityStartIndex - if (dataPosition < 0 || dataPosition >= activityCount) return RecyclerView.NO_ID + if (dataPosition < 0 || dataPosition >= getActivityCount(raw)) return RecyclerView.NO_ID if (data is ObjectCursor) { val cursor = (data as ObjectCursor).cursor if (!cursor.safeMoveToPosition(dataPosition)) return -1 val indices = (data as ObjectCursor).indices as ParcelableActivityCursorIndices return cursor.getLong(indices.timestamp) } - return data!![dataPosition].timestamp + return getActivity(adapterPosition, raw).timestamp } - override fun getActivity(position: Int): ParcelableActivity? { + override fun getActivity(position: Int, raw: Boolean): ParcelableActivity { val dataPosition = position - activityStartIndex - if (dataPosition < 0 || dataPosition >= activityCount) return null + if (dataPosition < 0 || dataPosition >= data!!.size) { + val validRange = rangeOfSize(activityStartIndex, getActivityCount(raw)) + throw IndexOutOfBoundsException("index: $position, valid range is $validRange") + } return data!![dataPosition] } - override val activityCount: Int - get() { - if (data == null) return 0 - return data!!.size - } + override fun getActivityCount(raw: Boolean): Int { + if (data == null) return 0 + return data!!.size + } private fun bindTitleSummaryViewHolder(holder: ActivityTitleSummaryViewHolder, position: Int) { - holder.displayActivity(getActivity(position)!!) + holder.displayActivity(getActivity(position)) } fun getData(): List? { @@ -169,6 +177,7 @@ class ParcelableActivitiesAdapter( } this.data = data gapLoadingIds.clear() + updateItemCount() notifyDataSetChanged() } @@ -225,7 +234,7 @@ class ParcelableActivitiesAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder.itemViewType) { ITEM_VIEW_TYPE_STATUS -> { - val status = getActivity(position)?.getActivityStatus() ?: return + val status = getActivity(position).getActivityStatus() ?: return val statusViewHolder = holder as IStatusViewHolder statusViewHolder.displayStatus(status = status, displayInReplyTo = true) } @@ -233,10 +242,10 @@ class ParcelableActivitiesAdapter( bindTitleSummaryViewHolder(holder as ActivityTitleSummaryViewHolder, position) } ITEM_VIEW_TYPE_STUB -> { - (holder as StubViewHolder).displayActivity(getActivity(position)!!) + (holder as StubViewHolder).displayActivity(getActivity(position)) } ITEM_VIEW_TYPE_GAP -> { - val activity = getActivity(position)!! + val activity = getActivity(position) val loading = gapLoadingIds.any { it.accountKey == activity.account_key && it.id == activity.id } (holder as GapViewHolder).display(loading) } @@ -244,51 +253,54 @@ class ParcelableActivitiesAdapter( } override fun getItemViewType(position: Int): Int { - if (position == 0 && ILoadMoreSupportAdapter.START in loadMoreIndicatorPosition) { - return ITEM_VIEW_TYPE_LOAD_INDICATOR - } else if (position == activityCount) { - return ITEM_VIEW_TYPE_LOAD_INDICATOR - } else if (isGapItem(position)) { - return ITEM_VIEW_TYPE_GAP - } - val action = getActivityAction(position) ?: return ITEM_VIEW_TYPE_EMPTY - val activity = getActivity(position) ?: return ITEM_VIEW_TYPE_EMPTY - when (action) { - Activity.Action.MENTION -> { - if (ArrayUtils.isEmpty(activity.target_object_statuses)) { - return ITEM_VIEW_TYPE_STUB + when (getItemCountIndex(position)) { + ITEM_INDEX_ACTIVITY -> { + if (isGapItem(position)) { + return ITEM_VIEW_TYPE_GAP } - return ITEM_VIEW_TYPE_STATUS - } - Activity.Action.REPLY -> { - if (ArrayUtils.isEmpty(activity.target_statuses)) { - return ITEM_VIEW_TYPE_STUB - } - return ITEM_VIEW_TYPE_STATUS - } - Activity.Action.QUOTE -> { - if (ArrayUtils.isEmpty(activity.target_statuses)) { - return ITEM_VIEW_TYPE_STUB - } - return ITEM_VIEW_TYPE_STATUS - } - Activity.Action.FOLLOW, Activity.Action.FAVORITE, Activity.Action.RETWEET, - Activity.Action.FAVORITED_RETWEET, Activity.Action.RETWEETED_RETWEET, - Activity.Action.RETWEETED_MENTION, Activity.Action.FAVORITED_MENTION, - Activity.Action.LIST_CREATED, Activity.Action.LIST_MEMBER_ADDED, - Activity.Action.MEDIA_TAGGED, Activity.Action.RETWEETED_MEDIA_TAGGED, - Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> { - if (mentionsOnly) return ITEM_VIEW_TYPE_EMPTY - filteredUserIds?.let { - ParcelableActivityUtils.initAfterFilteredSourceIds(activity, it, followingOnly) - if (activity.after_filtered_source_ids.isEmpty()) { - return ITEM_VIEW_TYPE_EMPTY + val activity = getActivity(position) + when (activity.action) { + Activity.Action.MENTION -> { + if (ArrayUtils.isEmpty(activity.target_object_statuses)) { + return ITEM_VIEW_TYPE_STUB + } + return ITEM_VIEW_TYPE_STATUS + } + Activity.Action.REPLY -> { + if (ArrayUtils.isEmpty(activity.target_statuses)) { + return ITEM_VIEW_TYPE_STUB + } + return ITEM_VIEW_TYPE_STATUS + } + Activity.Action.QUOTE -> { + if (ArrayUtils.isEmpty(activity.target_statuses)) { + return ITEM_VIEW_TYPE_STUB + } + return ITEM_VIEW_TYPE_STATUS + } + Activity.Action.FOLLOW, Activity.Action.FAVORITE, Activity.Action.RETWEET, + Activity.Action.FAVORITED_RETWEET, Activity.Action.RETWEETED_RETWEET, + Activity.Action.RETWEETED_MENTION, Activity.Action.FAVORITED_MENTION, + Activity.Action.LIST_CREATED, Activity.Action.LIST_MEMBER_ADDED, + Activity.Action.MEDIA_TAGGED, Activity.Action.RETWEETED_MEDIA_TAGGED, + Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> { + if (mentionsOnly) return ITEM_VIEW_TYPE_EMPTY + filteredUserIds?.let { + ParcelableActivityUtils.initAfterFilteredSourceIds(activity, it, followingOnly) + if (activity.after_filtered_source_ids.isEmpty()) { + return ITEM_VIEW_TYPE_EMPTY + } + } + return ITEM_VIEW_TYPE_TITLE_SUMMARY } } - return ITEM_VIEW_TYPE_TITLE_SUMMARY + return ITEM_VIEW_TYPE_STUB + } + ITEM_INDEX_LOAD_MORE_INDICATOR -> { + return ITEM_VIEW_TYPE_LOAD_INDICATOR } } - return ITEM_VIEW_TYPE_STUB + throw UnsupportedOperationException() } override fun addGapLoadingId(id: ObjectId) { @@ -300,16 +312,7 @@ class ParcelableActivitiesAdapter( } override fun getItemCount(): Int { - val position = loadMoreIndicatorPosition - var count = 0 - if (position and ILoadMoreSupportAdapter.START != 0L) { - count += 1 - } - count += activityCount - if (position and ILoadMoreSupportAdapter.END != 0L) { - count += 1 - } - return count + return itemCounts.itemCount } fun setListener(listener: ActivityAdapterListener) { @@ -330,23 +333,16 @@ class ParcelableActivitiesAdapter( } - fun isActivity(position: Int): Boolean { - return position < activityCount + fun isActivity(position: Int, raw: Boolean = false): Boolean { + return position < getActivityCount(raw) } val activityStartIndex: Int - get() { - val position = loadMoreIndicatorPosition - var start = 0 - if (position and ILoadMoreSupportAdapter.START != 0L) { - start += 1 - } - return start - } + get() = getItemStartPosition(ITEM_INDEX_ACTIVITY) - fun findPositionBySortTimestamp(timestamp: Long): Int { + fun findPositionBySortTimestamp(timestamp: Long, raw: Boolean = false): Int { if (timestamp <= 0) return RecyclerView.NO_POSITION - val range = rangeOfSize(activityStartIndex, activityCount) + val range = rangeOfSize(activityStartIndex, getActivityCount(raw)) if (range.isEmpty()) return RecyclerView.NO_POSITION if (timestamp < getTimestamp(range.last)) { return range.last @@ -354,6 +350,11 @@ class ParcelableActivitiesAdapter( return range.indexOfFirst { timestamp >= getTimestamp(it) } } + private fun updateItemCount() { + itemCounts[0] = getActivityCount(false) + itemCounts[1] = if (ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition) 1 else 0 + } + interface ActivityAdapterListener { fun onGapClick(holder: GapViewHolder, position: Int) @@ -383,6 +384,7 @@ class ParcelableActivitiesAdapter( text2.setSingleLine(false) } + @SuppressLint("SetTextI18n") fun displayActivity(activity: ParcelableActivity) { text1.text = text1.resources.getString(R.string.unsupported_activity_action_title, activity.action) @@ -397,7 +399,7 @@ class ParcelableActivitiesAdapter( override fun onGapClick(holder: GapViewHolder, position: Int) { val adapter = adapterRef.get() ?: return - val activity = adapter.getActivity(position) ?: return + val activity = adapter.getActivity(position) adapter.addGapLoadingId(ObjectId(activity.account_key, activity.id)) adapter.activityAdapterListener?.onGapClick(holder, position) } @@ -418,7 +420,7 @@ class ParcelableActivitiesAdapter( override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) { val adapter = adapterRef.get() ?: return - val status = adapter.getActivity(position)?.getActivityStatus() ?: return + val status = adapter.getActivity(position).getActivityStatus() ?: return IntentUtils.openUserProfile(adapter.context, status.account_key, status.user_key, status.user_screen_name, adapter.preferences.getBoolean(KEY_NEW_DOCUMENT_API), Referral.TIMELINE_STATUS, null) @@ -459,5 +461,8 @@ class ParcelableActivitiesAdapter( const val ITEM_VIEW_TYPE_STATUS = 4 const val ITEM_VIEW_TYPE_EMPTY = 5 + const val ITEM_INDEX_ACTIVITY = 0 + const val ITEM_INDEX_LOAD_MORE_INDICATOR = 1 + } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt index d55cfa87c..628abefbc 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt @@ -143,7 +143,7 @@ abstract class ParcelableStatusesAdapter( override fun isGapItem(position: Int): Boolean { val dataPosition = position - statusStartIndex - val statusCount = statusCount + val statusCount = getStatusCount(false) if (dataPosition < 0 || dataPosition >= statusCount) return false // Don't show gap if it's last item if (dataPosition == statusCount - 1) return false @@ -153,18 +153,17 @@ abstract class ParcelableStatusesAdapter( val indices = (data as ObjectCursor).indices as ParcelableStatusCursorIndices return cursor.getShort(indices.is_gap).toInt() == 1 } - return getStatus(position)!!.is_gap + return getStatus(position).is_gap } - override fun getStatus(position: Int): ParcelableStatus? { - return getStatus(position, getItemCountIndex(position)) + override fun getStatus(position: Int, raw: Boolean): ParcelableStatus { + return getStatus(position, getItemCountIndex(position, raw), raw) } - override val statusCount: Int - get() = displayDataCount - - override val rawStatusCount: Int - get() = data?.size ?: 0 + override fun getStatusCount(raw: Boolean): Int { + if (raw) return data?.size ?: 0 + return displayDataCount + } override fun setData(data: List?): Boolean { var changed = true @@ -186,7 +185,7 @@ abstract class ParcelableStatusesAdapter( } } displayDataCount = data.size - filteredCount - changed = data != data + changed = this.data != data } this.data = data gapLoadingIds.clear() @@ -218,13 +217,12 @@ abstract class ParcelableStatusesAdapter( } } - override fun getStatusId(position: Int): String? { - val def: String? = null + override fun getStatusId(position: Int, raw: Boolean): String { return getFieldValue(position, { cursor, indices -> return@getFieldValue cursor.getString(indices.id) }, { status -> return@getFieldValue status.id - }, def) + }, "") } fun getStatusSortId(position: Int): Long { @@ -235,7 +233,7 @@ abstract class ParcelableStatusesAdapter( }, -1L) } - override fun getStatusTimestamp(position: Int): Long { + override fun getStatusTimestamp(position: Int, raw: Boolean): Long { return getFieldValue(position, { cursor, indices -> return@getFieldValue cursor.getLong(indices.timestamp) }, { status -> @@ -243,7 +241,7 @@ abstract class ParcelableStatusesAdapter( }, -1L) } - override fun getStatusPositionKey(position: Int): Long { + override fun getStatusPositionKey(position: Int, raw: Boolean): Long { return getFieldValue(position, { cursor, indices -> val positionKey = cursor.getLong(indices.position_key) if (positionKey > 0) return@getFieldValue positionKey @@ -255,13 +253,13 @@ abstract class ParcelableStatusesAdapter( }, -1L) } - override fun getAccountKey(position: Int): UserKey? { + override fun getAccountKey(position: Int, raw: Boolean): UserKey { val def: UserKey? = null return getFieldValue(position, { cursor, indices -> return@getFieldValue UserKey.valueOf(cursor.getString(indices.account_key)) }, { status -> return@getFieldValue status.account_key - }, def) + }, def, raw)!! } override fun isCardActionsShown(position: Int): Boolean { @@ -307,12 +305,12 @@ abstract class ParcelableStatusesAdapter( when (holder.itemViewType) { VIEW_TYPE_STATUS -> { val countIdx = getItemCountIndex(position) - val status = getStatus(position, countIdx)!! + val status = getStatus(position, countIdx) (holder as IStatusViewHolder).displayStatus(status, displayInReplyTo = isShowInReplyTo, displayPinned = countIdx == ITEM_INDEX_PINNED_STATUS) } ITEM_VIEW_TYPE_GAP -> { - val status = getStatus(position)!! + val status = getStatus(position) val loading = gapLoadingIds.any { it.accountKey == status.account_key && it.id == status.id } (holder as GapViewHolder).display(loading) } @@ -351,8 +349,8 @@ abstract class ParcelableStatusesAdapter( gapLoadingIds.remove(id) } - fun isStatus(position: Int): Boolean { - return position < statusCount + fun isStatus(position: Int, raw: Boolean = false): Boolean { + return position < getStatusCount(raw) } override fun getItemCount(): Int { @@ -360,19 +358,19 @@ abstract class ParcelableStatusesAdapter( } override fun findStatusById(accountKey: UserKey, statusId: String): ParcelableStatus? { - for (i in 0 until statusCount) { - if (accountKey == getAccountKey(i) && statusId == getStatusId(i)) { - return getStatus(i) + for (i in 0 until getStatusCount(true)) { + if (accountKey == getAccountKey(i, true) && statusId == getStatusId(i, true)) { + return getStatus(i, true) } } return null } - fun findPositionByPositionKey(positionKey: Long): Int { + fun findPositionByPositionKey(positionKey: Long, raw: Boolean = false): Int { // Assume statuses are descend sorted by id, so break at first status with id // lesser equals than read position if (positionKey <= 0) return RecyclerView.NO_POSITION - val range = rangeOfSize(statusStartIndex, statusCount) + val range = rangeOfSize(statusStartIndex, getStatusCount(raw)) if (range.isEmpty()) return RecyclerView.NO_POSITION if (positionKey < getStatusPositionKey(range.last)) { return range.last @@ -380,11 +378,11 @@ abstract class ParcelableStatusesAdapter( return range.indexOfFirst { positionKey >= getStatusPositionKey(it) } } - fun findPositionBySortId(sortId: Long): Int { + fun findPositionBySortId(sortId: Long, raw: Boolean = false): Int { // Assume statuses are descend sorted by id, so break at first status with id // lesser equals than read position if (sortId <= 0) return RecyclerView.NO_POSITION - val range = rangeOfSize(statusStartIndex, statusCount) + val range = rangeOfSize(statusStartIndex, getStatusCount(raw)) if (range.isEmpty()) return RecyclerView.NO_POSITION if (sortId < getStatusSortId(range.last)) { return range.last @@ -392,48 +390,62 @@ abstract class ParcelableStatusesAdapter( return range.indexOfFirst { sortId >= getStatusSortId(it) } } - private inline fun getFieldValue( - position: Int, + private fun getItemCountIndex(position: Int, raw: Boolean): Int { + if (!raw) return itemCounts.getItemCountIndex(position) + var sum = 0 + for (i in 0 until itemCounts.size) { + sum += when (i) { + ITEM_INDEX_STATUS -> data!!.size + else -> itemCounts[i] + } + if (position < sum) { + return i + } + } + return -1 + } + + private inline fun getFieldValue(position: Int, readCursorValueAction: (cursor: Cursor, indices: ParcelableStatusCursorIndices) -> T, readStatusValueAction: (status: ParcelableStatus) -> T, - defValue: T - ): T { + defValue: T, raw: Boolean = false): T { if (data is ObjectCursor) { - val dataPosition = position - getItemStartPosition(ITEM_INDEX_STATUS) - if (dataPosition < 0 || dataPosition >= rawStatusCount) return defValue + val dataPosition = position - statusStartIndex + if (dataPosition < 0 || dataPosition >= getStatusCount(true)) return defValue val cursor = (data as ObjectCursor).cursor if (!cursor.safeMoveToPosition(dataPosition)) return defValue val indices = (data as ObjectCursor).indices as ParcelableStatusCursorIndices return readCursorValueAction(cursor, indices) } - return readStatusValueAction(getStatus(position)!!) + return readStatusValueAction(getStatus(position)) } - private fun getStatus(position: Int, countIndex: Int): ParcelableStatus? { + private fun getStatus(position: Int, countIndex: Int, 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 - getItemStartPosition(ITEM_INDEX_STATUS) + val dataPosition = position - statusStartIndex val positions = displayPositions - if (positions != null) { + if (positions != null && !raw) { return data[positions[dataPosition]] } else { return data[dataPosition] } } } - return null + val validStart = getItemStartPosition(ITEM_INDEX_PINNED_STATUS) + val validEnd = getItemStartPosition(ITEM_INDEX_STATUS) + getStatusCount(raw) - 1 + throw IndexOutOfBoundsException("index: $position, valid range is $validStart..$validEnd") } private fun updateItemCount() { - val position = loadMoreIndicatorPosition - itemCounts[ITEM_INDEX_LOAD_START_INDICATOR] = if (position and ILoadMoreSupportAdapter.START != 0L) 1 else 0 + itemCounts[ITEM_INDEX_LOAD_START_INDICATOR] = if (ILoadMoreSupportAdapter.START in loadMoreIndicatorPosition) 1 else 0 itemCounts[ITEM_INDEX_PINNED_STATUS] = pinnedStatuses?.size ?: 0 - itemCounts[ITEM_INDEX_STATUS] = statusCount - itemCounts[ITEM_INDEX_LOAD_END_INDICATOR] = if (position and ILoadMoreSupportAdapter.END != 0L) 1 else 0 + itemCounts[ITEM_INDEX_STATUS] = getStatusCount(false) + itemCounts[ITEM_INDEX_LOAD_END_INDICATOR] = if (ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition) 1 else 0 } companion object { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/iface/IActivitiesAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/iface/IActivitiesAdapter.kt index cb50ef40e..1dfc1159a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/iface/IActivitiesAdapter.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/iface/IActivitiesAdapter.kt @@ -27,8 +27,6 @@ import org.mariotaku.twidere.view.holder.ActivityTitleSummaryViewHolder */ interface IActivitiesAdapter : IContentAdapter, IGapSupportedAdapter { - val activityCount: Int - val mediaPreviewStyle: Int val mediaPreviewEnabled: Boolean @@ -41,7 +39,9 @@ interface IActivitiesAdapter : IContentAdapter, IGapSupportedAdapter { val lightFont: Boolean - fun getActivity(position: Int): ParcelableActivity? + fun getActivityCount(raw: Boolean = false): Int + + fun getActivity(position: Int, raw: Boolean = false): ParcelableActivity fun setData(data: Data?) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/iface/IStatusesAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/iface/IStatusesAdapter.kt index 0adda9284..0dfbd558a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/iface/IStatusesAdapter.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/iface/IStatusesAdapter.kt @@ -19,10 +19,6 @@ interface IStatusesAdapter : IContentAdapter, IGapSupportedAdapter { @PreviewStyle val mediaPreviewStyle: Int - val statusCount: Int - - val rawStatusCount: Int - val twidereLinkify: TwidereLinkify val mediaPreviewEnabled: Boolean @@ -43,15 +39,20 @@ interface IStatusesAdapter : IContentAdapter, IGapSupportedAdapter { fun setData(data: Data?): Boolean - fun getStatus(position: Int): ParcelableStatus? + /** + * @param raw Count hidden (filtered) item if `true ` + */ + fun getStatusCount(raw: Boolean = false): Int - fun getStatusId(position: Int): String? + fun getStatus(position: Int, raw: Boolean = false): ParcelableStatus - fun getStatusTimestamp(position: Int): Long + fun getStatusId(position: Int, raw: Boolean = false): String - fun getStatusPositionKey(position: Int): Long + fun getStatusTimestamp(position: Int, raw: Boolean = false): Long - fun getAccountKey(position: Int): UserKey? + fun getStatusPositionKey(position: Int, raw: Boolean = false): Long + + fun getAccountKey(position: Int, raw: Boolean = false): UserKey fun findStatusById(accountKey: UserKey, statusId: String): ParcelableStatus? diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsActivitiesFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsActivitiesFragment.kt index 6154ded69..32c11b0fd 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsActivitiesFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsActivitiesFragment.kt @@ -20,6 +20,7 @@ package org.mariotaku.twidere.fragment import android.accounts.AccountManager +import android.content.ContentValues import android.content.Context import android.content.Intent import android.graphics.Rect @@ -40,6 +41,7 @@ import org.mariotaku.kpreferences.get import org.mariotaku.ktextension.coerceInOr import org.mariotaku.ktextension.isNullOrEmpty import org.mariotaku.ktextension.rangeOfSize +import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.twidere.R import org.mariotaku.twidere.adapter.ParcelableActivitiesAdapter import org.mariotaku.twidere.adapter.ParcelableActivitiesAdapter.Companion.ITEM_VIEW_TYPE_GAP @@ -64,6 +66,7 @@ import org.mariotaku.twidere.model.event.StatusListChangedEvent import org.mariotaku.twidere.model.util.AccountUtils import org.mariotaku.twidere.model.util.ParcelableActivityUtils import org.mariotaku.twidere.model.util.getActivityStatus +import org.mariotaku.twidere.provider.TwidereDataStore.Activities import org.mariotaku.twidere.util.* import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback import org.mariotaku.twidere.util.glide.PauseRecyclerViewOnScrollListener @@ -134,7 +137,7 @@ abstract class AbsActivitiesFragment protected constructor() : position = recyclerView.getChildLayoutPosition(focusedChild) } if (position != RecyclerView.NO_POSITION) { - val activity = adapter.getActivity(position) ?: return false + val activity = adapter.getActivity(position) if (keyCode == KeyEvent.KEYCODE_ENTER) { openActivity(activity) return true @@ -239,7 +242,8 @@ abstract class AbsActivitiesFragment protected constructor() : val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() wasAtTop = firstVisibleItemPosition == 0 - val activityRange = rangeOfSize(adapter.activityStartIndex, Math.max(0, adapter.activityCount)) + // Get display range of activities + val activityRange = rangeOfSize(adapter.activityStartIndex, adapter.getActivityCount(raw = false)) val lastReadPosition = if (loadMore || readFromBottom) { lastVisibleItemPosition } else { @@ -247,7 +251,7 @@ abstract class AbsActivitiesFragment protected constructor() : }.coerceInOr(activityRange, -1) lastReadId = adapter.getTimestamp(lastReadPosition) lastReadViewTop = layoutManager.findViewByPosition(lastReadPosition)?.top ?: 0 - loadMore = activityRange.endInclusive >= 0 && lastVisibleItemPosition >= activityRange.endInclusive + loadMore = activityRange.endInclusive in 0..lastVisibleItemPosition } else if (rememberPosition && readPositionTag != null) { lastReadId = readStateManager.getPosition(readPositionTag) lastReadViewTop = 0 @@ -304,17 +308,19 @@ abstract class AbsActivitiesFragment protected constructor() : } override fun onGapClick(holder: GapViewHolder, position: Int) { - val activity = adapter.getActivity(position) ?: return + val activity = adapter.getActivity(position) DebugLog.v(msg = "Load activity gap $activity") val accountIds = arrayOf(activity.account_key) val maxIds = arrayOf(activity.min_position) val maxSortIds = longArrayOf(activity.min_sort_position) getActivities(BaseRefreshTaskParam(accountKeys = accountIds, maxIds = maxIds, - sinceIds = null, maxSortIds = maxSortIds, sinceSortIds = null)) + sinceIds = null, maxSortIds = maxSortIds, sinceSortIds = null).also { + it.extraId = activity._id + }) } override fun onMediaClick(holder: IStatusViewHolder, view: View, media: ParcelableMedia, position: Int) { - val status = adapter.getActivity(position)?.getActivityStatus() ?: return + val status = adapter.getActivity(position).getActivityStatus() ?: return IntentUtils.openMedia(activity, status, media, preferences[newDocumentApiKey], preferences[displaySensitiveContentsKey], null) @@ -355,7 +361,7 @@ abstract class AbsActivitiesFragment protected constructor() : } override fun onActivityClick(holder: ActivityTitleSummaryViewHolder, position: Int) { - val activity = adapter.getActivity(position) ?: return + val activity = adapter.getActivity(position) val list = ArrayList() if (activity.target_object_statuses?.isNotEmpty() ?: false) { list.addAll(activity.target_object_statuses) @@ -387,7 +393,7 @@ abstract class AbsActivitiesFragment protected constructor() : } private fun getActivityStatus(position: Int): ParcelableStatus? { - return adapter.getActivity(position)?.getActivityStatus() + return adapter.getActivity(position).getActivityStatus() } override fun onStart() { @@ -458,7 +464,7 @@ abstract class AbsActivitiesFragment protected constructor() : protected fun saveReadPosition(position: Int) { if (host == null) return if (position == RecyclerView.NO_POSITION) return - val item = adapter.getActivity(position) ?: return + val item = adapter.getActivity(position) var positionUpdated = false readPositionTag?.let { for (accountKey in accountKeys) { @@ -502,22 +508,33 @@ abstract class AbsActivitiesFragment protected constructor() : if (!userVisibleHint) return false val contextMenuInfo = item.menuInfo as ExtendedRecyclerView.ContextMenuInfo val position = contextMenuInfo.position - when (adapter.getItemViewType(position)) { ITEM_VIEW_TYPE_STATUS -> { val status = getActivityStatus(position) ?: return false - if (item.itemId == R.id.share) { - val shareIntent = Utils.createStatusShareIntent(activity, status) - val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status)) - startActivity(chooser) + when (item.itemId) { + R.id.share -> { + val shareIntent = Utils.createStatusShareIntent(activity, status) + val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status)) + startActivity(chooser) - val am = AccountManager.get(context) - val accountType = AccountUtils.findByAccountKey(am, status.account_key)?.getAccountType(am) - Analyzer.log(Share.status(accountType, status)) - return true + val am = AccountManager.get(context) + val accountType = AccountUtils.findByAccountKey(am, status.account_key)?.getAccountType(am) + Analyzer.log(Share.status(accountType, status)) + return true + } + R.id.make_gap -> { + if (this !is CursorActivitiesFragment) return true + val resolver = context.contentResolver + val values = ContentValues() + values.put(Activities.IS_GAP, 1) + val where = Expression.equalsArgs(Activities._ID).sql + val whereArgs = arrayOf(adapter.getActivity(position)._id.toString()) + resolver.update(contentUri, values, where, whereArgs) + return true + } + else -> MenuUtils.handleStatusClick(activity, this, fragmentManager, + userColorNameManager, twitterWrapper, status, item) } - return MenuUtils.handleStatusClick(activity, this, fragmentManager, - userColorNameManager, twitterWrapper, status, item) } } return false 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 d56082a35..21ef1c8b6 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsStatusesFragment.kt @@ -21,6 +21,7 @@ package org.mariotaku.twidere.fragment import android.accounts.AccountManager import android.app.Activity +import android.content.ContentValues import android.content.Context import android.content.Intent import android.graphics.Rect @@ -39,6 +40,7 @@ import edu.tsinghua.hotmobi.model.MediaEvent import kotlinx.android.synthetic.main.fragment_content_recyclerview.* import org.mariotaku.kpreferences.get import org.mariotaku.ktextension.* +import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.twidere.R import org.mariotaku.twidere.activity.AccountSelectorActivity import org.mariotaku.twidere.adapter.ParcelableStatusesAdapter @@ -56,6 +58,8 @@ import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.analyzer.Share import org.mariotaku.twidere.model.event.StatusListChangedEvent import org.mariotaku.twidere.model.util.AccountUtils +import org.mariotaku.twidere.provider.TwidereDataStore +import org.mariotaku.twidere.provider.TwidereDataStore.* import org.mariotaku.twidere.util.* import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback import org.mariotaku.twidere.util.glide.PauseRecyclerViewOnScrollListener @@ -182,7 +186,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment= 0 && lastVisibleItemPosition >= statusRange.endInclusive + loadMore = statusRange.endInclusive in 0..lastVisibleItemPosition } else if (rememberPosition && readPositionTag != null) { lastReadId = readStateManager.getPosition(readPositionTag) lastReadViewTop = 0 @@ -351,8 +356,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment 0) status.position_key else status.timestamp readPositionTagWithArguments?.let { accountKeys.map { accountKey -> Utils.getReadPositionTagWithAccount(it, accountKey) } @@ -508,26 +512,38 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment { + val shareIntent = Utils.createStatusShareIntent(activity, status) + val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status)) + startActivity(chooser) - val am = AccountManager.get(context) - val accountType = AccountUtils.findByAccountKey(am, status.account_key)?.getAccountType(am) - Analyzer.log(Share.status(accountType, status)) - return true + val am = AccountManager.get(context) + val accountType = AccountUtils.findByAccountKey(am, status.account_key)?.getAccountType(am) + Analyzer.log(Share.status(accountType, status)) + return true + } + R.id.make_gap -> { + if (this !is CursorStatusesFragment) return true + 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(contentUri, values, where, whereArgs) + return true + } + else -> return MenuUtils.handleStatusClick(activity, this, fragmentManager, + userColorNameManager, twitterWrapper, status, item) } - return MenuUtils.handleStatusClick(activity, this, fragmentManager, - userColorNameManager, twitterWrapper, status, item) } class DefaultOnLikedListener( diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CursorActivitiesFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CursorActivitiesFragment.kt index 81805c97a..0fc0c5053 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CursorActivitiesFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CursorActivitiesFragment.kt @@ -269,10 +269,10 @@ abstract class CursorActivitiesFragment : AbsActivitiesFragment() { if (result == null) return val lm = layoutManager val rangeStart = Math.max(adapter.activityStartIndex, lm.findFirstVisibleItemPosition()) - val rangeEnd = Math.min(lm.findLastVisibleItemPosition(), adapter.activityStartIndex + adapter.activityCount - 1) + val rangeEnd = Math.min(lm.findLastVisibleItemPosition(), adapter.activityStartIndex + adapter.getActivityCount(false) - 1) loop@ for (i in rangeStart..rangeEnd) { - val activity = adapter.getActivity(i) - if (result.account_key == activity!!.account_key && result.id == activity.status_id) { + val activity = adapter.getActivity(i, false) + if (result.account_key == activity.account_key && result.id == activity.status_id) { if (result.id != activity.status_id) { continue@loop } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/ParcelableStatusesFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/ParcelableStatusesFragment.kt index 725fc9575..f536e7d89 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/ParcelableStatusesFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/ParcelableStatusesFragment.kt @@ -152,9 +152,11 @@ abstract class ParcelableStatusesFragment : AbsStatusesFragment() { super.onLoadMoreContents(position) if (position == 0L) return // Load the last item - val idx = adapter.rawStatusCount - 1 - if (idx < 0) return - val status = adapter.getData()?.get(idx) ?: return + val startIdx = adapter.statusStartIndex + if (startIdx < 0) return + val statusCount = adapter.getStatusCount(true) + if (statusCount <= 0) return + val status = adapter.getStatus(startIdx + statusCount - 1, true) val accountKeys = arrayOf(status.account_key) val maxIds = arrayOf(status.id) val param = StatusesRefreshTaskParam(accountKeys, maxIds, null, page + pageDelta) @@ -166,9 +168,9 @@ abstract class ParcelableStatusesFragment : AbsStatusesFragment() { if (status == null) return val lm = layoutManager val rangeStart = Math.max(adapter.statusStartIndex, lm.findFirstVisibleItemPosition()) - val rangeEnd = Math.min(lm.findLastVisibleItemPosition(), adapter.statusStartIndex + adapter.statusCount - 1) + val rangeEnd = Math.min(lm.findLastVisibleItemPosition(), adapter.statusStartIndex + adapter.getStatusCount(false) - 1) for (i in rangeStart..rangeEnd) { - val item = adapter.getStatus(i) + val item = adapter.getStatus(i, false) if (status == item) { item.favorite_count = status.favorite_count item.retweet_count = status.retweet_count @@ -183,8 +185,8 @@ abstract class ParcelableStatusesFragment : AbsStatusesFragment() { override fun triggerRefresh(): Boolean { super.triggerRefresh() val accountKeys = accountKeys - if (adapter.statusCount > 0) { - val firstStatus = adapter.getStatus(0)!! + if (adapter.getStatusCount(true) > 0) { + val firstStatus = adapter.getStatus(0, true) val sinceIds = Array(accountKeys.size) { return@Array if (firstStatus.account_key == accountKeys[it]) firstStatus.id else null } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/StatusFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/StatusFragment.kt index 97e1e6228..03bf097aa 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/StatusFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/StatusFragment.kt @@ -85,6 +85,7 @@ import org.mariotaku.twidere.adapter.ListParcelableStatusesAdapter import org.mariotaku.twidere.adapter.LoadMoreSupportAdapter import org.mariotaku.twidere.adapter.decorator.DividerItemDecoration import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter +import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition import org.mariotaku.twidere.adapter.iface.IStatusesAdapter @@ -303,7 +304,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks { @@ -520,11 +521,11 @@ class StatusFragment : BaseFragment(), LoaderCallbacks(fragment.context, Glide.with(fragment)), IStatusesAdapter> { + ) : LoadMoreSupportAdapter(fragment.context, Glide.with(fragment)), + IStatusesAdapter>, IItemCountsAdapter { private val inflater: LayoutInflater override val twidereLinkify: TwidereLinkify @@ -1496,7 +1498,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks { - return data?.get(position - getIndexStart(ITEM_IDX_CONVERSATION)) + return data!![position - getIndexStart(ITEM_IDX_CONVERSATION)] } ITEM_IDX_REPLY -> { - if (replyStart < 0) return null - return data?.get(position - getIndexStart(ITEM_IDX_CONVERSATION) - - getTypeCount(ITEM_IDX_CONVERSATION) - getTypeCount(ITEM_IDX_STATUS) + replyStart) + return data!![position - getIndexStart(ITEM_IDX_CONVERSATION) - getTypeCount(ITEM_IDX_CONVERSATION) - getTypeCount(ITEM_IDX_STATUS) + replyStart] } ITEM_IDX_STATUS -> { - return status + return status!! } } - return null + throw IndexOutOfBoundsException("index: $position") } fun getIndexStart(index: Int): Int { @@ -1578,25 +1577,20 @@ class StatusFragment : BaseFragment(), LoaderCallbacks 0) status.timestamp else getStatusTimestamp(position) + override fun getStatusPositionKey(position: Int, raw: Boolean): Long { + val status = getStatus(position, raw) + return if (status.position_key > 0) status.timestamp else getStatusTimestamp(position, raw) } - override fun getAccountKey(position: Int): UserKey? { - val status = getStatus(position) - return status?.account_key - } + override fun getAccountKey(position: Int, raw: Boolean) = getStatus(position, raw).account_key override fun findStatusById(accountKey: UserKey, statusId: String): ParcelableStatus? { if (status != null && accountKey == status!!.account_key && TextUtils.equals(statusId, status!!.id)) { @@ -1605,13 +1599,9 @@ class StatusFragment : BaseFragment(), LoaderCallbacks { val status = getStatus(position) val detailHolder = holder as DetailStatusViewHolder - detailHolder.displayStatus(statusAccount, status, statusActivity, - translationResult) + detailHolder.displayStatus(statusAccount, status, statusActivity, translationResult) } VIEW_TYPE_LIST_STATUS -> { val status = getStatus(position) @@ -1743,7 +1732,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks { val errorHolder = holder as StatusErrorItemViewHolder @@ -1803,12 +1792,16 @@ class StatusFragment : BaseFragment(), LoaderCallbacks= typeStart && position < typeEnd) return type + if (position in typeStart until typeEnd) return type typeStart = typeEnd } throw IllegalStateException("Unknown position " + position) @@ -1819,15 +1812,18 @@ class StatusFragment : BaseFragment(), LoaderCallbacks= typeStart && position < typeEnd) return typeStart + if (position in typeStart until typeEnd) return typeStart typeStart = typeEnd } throw IllegalStateException() } override fun getItemId(position: Int): Long { - val status = getStatus(position) - if (status != null) return status.hashCode().toLong() + when (getItemCountIndex(position)) { + ITEM_IDX_CONVERSATION, ITEM_IDX_STATUS, ITEM_IDX_REPLY -> { + return getStatus(position).hashCode().toLong() + } + } return getItemType(position).toLong() } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserMediaTimelineFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserMediaTimelineFragment.kt index b6050194f..ab3c2b466 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserMediaTimelineFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserMediaTimelineFragment.kt @@ -9,7 +9,6 @@ import android.support.v7.widget.RecyclerView import android.support.v7.widget.StaggeredGridLayoutManager import android.text.TextUtils import com.bumptech.glide.Glide -import org.mariotaku.ktextension.contains import org.mariotaku.twidere.adapter.StaggeredGridParcelableStatusesAdapter import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter import org.mariotaku.twidere.constant.IntentConstants.* @@ -119,11 +118,11 @@ class UserMediaTimelineFragment : AbsContentRecyclerViewFragment): Boolean { + return true } - override fun onScrubGeo(userId: Long, upToStatusId: Long) { + override fun onScrubGeo(userId: String, upToStatusId: String): Boolean { val resolver = context.contentResolver val where = Expression.and(Expression.equalsArgs(Statuses.USER_KEY), Expression.greaterEqualsArgs(Statuses.SORT_ID)).sql - val whereArgs = arrayOf(userId.toString(), upToStatusId.toString()) + val whereArgs = arrayOf(userId, upToStatusId) val values = ContentValues() values.putNull(Statuses.LOCATION) resolver.update(Statuses.CONTENT_URI, values, where, whereArgs) + return true } - override fun onStallWarning(warn: Warning) { - + override fun onStallWarning(warn: Warning): Boolean { + return true } @Throws(IOException::class) - override fun onStatus(status: Status) { - + override fun onStatus(status: Status): Boolean { + return true } - override fun onTrackLimitationNotice(numberOfLimitedStatuses: Int) { - - } - - override fun onUnblock(source: User, unblockedUser: User) { + override fun onUnblock(source: User, unblockedUser: User): Boolean { val message = String.format("%s unblocked %s", source.screenName, unblockedUser.screenName) Log.d(LOGTAG, message) + return true } - override fun onUnfavorite(source: User, target: User, targetStatus: Status) { + override fun onUnfavorite(source: User, target: User, targetStatus: Status): Boolean { val message = String.format("%s unfavorited %s's tweet: %s", source.screenName, target.screenName, targetStatus.extendedText) Log.d(LOGTAG, message) + return true } - override fun onUserListCreation(listOwner: User, list: UserList) { - - } - - override fun onUserListDeletion(listOwner: User, list: UserList) { - - } - - override fun onUserListMemberAddition(addedMember: User, listOwner: User, list: UserList) { - - } - - override fun onUserListMemberDeletion(deletedMember: User, listOwner: User, list: UserList) { - - } - - override fun onUserListSubscription(subscriber: User, listOwner: User, list: UserList) { - - } - - override fun onUserListUnsubscription(subscriber: User, listOwner: User, list: UserList) { - - } - - override fun onUserListUpdate(listOwner: User, list: UserList) { - - } - - override fun onUserProfileUpdate(updatedUser: User) { - - } } companion object { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesTask.kt index ea914fabb..099bcf0aa 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesTask.kt @@ -168,7 +168,9 @@ abstract class GetActivitiesTask( Expression.greaterEqualsArgs(Activities.MIN_SORT_POSITION), Expression.lesserEqualsArgs(Activities.MAX_SORT_POSITION)) val whereArgs = arrayOf(details.key.toString(), deleteBound[0].toString(), deleteBound[1].toString()) - val rowsDeleted = cr.delete(writeUri, where.sql, whereArgs) + // First item after gap doesn't count + val localDeleted = if (maxId != null && sinceId == null) 1 else 0 + val rowsDeleted = cr.delete(writeUri, where.sql, whereArgs) - localDeleted // Why loadItemLimit / 2? because it will not acting strange in most cases val insertGap = !noItemsBefore && olderCount > 0 && rowsDeleted <= 0 && activities.size > loadItemLimit / 2 if (insertGap && !valuesList.isEmpty()) { @@ -182,13 +184,13 @@ abstract class GetActivitiesTask( if (maxId != null && sinceId == null) { 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) + if (params.extraId != -1L) { + val noGapValues = ContentValues() + noGapValues.put(Activities.IS_GAP, false) + val noGapWhere = Expression.equalsArgs(Activities._ID).sql + val noGapWhereArgs = arrayOf(params.extraId.toString()) + cr.update(writeUri, noGapValues, noGapWhere, noGapWhereArgs) + } } else { return GetStatusesTask.ERROR_LOAD_GAP } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/MenuUtils.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/MenuUtils.kt index 50b60ba9f..5c916a89a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/MenuUtils.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/MenuUtils.kt @@ -95,7 +95,7 @@ object MenuUtils { item.setTitle(icon) } - @JvmOverloads fun addIntentToMenu(context: Context?, menu: Menu?, queryIntent: Intent?, + fun addIntentToMenu(context: Context?, menu: Menu?, queryIntent: Intent?, groupId: Int = Menu.NONE) { if (context == null || menu == null || queryIntent == null) return val pm = context.packageManager @@ -330,14 +330,6 @@ 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) - } R.id.mute_users -> { val df = MuteStatusUsersDialogFragment() df.arguments = Bundle {