improved activities gap load

improved load more items
This commit is contained in:
Mariotaku Lee 2017-03-10 16:37:35 +08:00
parent 4610e2a3f7
commit 13dac670d2
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
26 changed files with 727 additions and 595 deletions

View File

@ -41,7 +41,8 @@ import java.util.List;
@ParcelablePlease
public class UserKey implements Comparable<UserKey>, 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<UserKey> CREATOR = new Creator<UserKey>() {
@Override
@ -75,8 +76,12 @@ public class UserKey implements Comparable<UserKey>, Parcelable {
}
public boolean isSelfReference() {
return equals(SELF_REFERENCE);
public boolean isSelf() {
return equals(SELF);
}
public boolean isValid() {
return !equals(INVALID);
}
@NonNull

View File

@ -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<DumperPlugin> get() {
return new Stetho.DefaultDumperPluginsBuilder(application)
.provide(new AccountsDumper(application))
.provide(new RawStreamDumper(application))
.provide(new UserStreamDumper(application))
.finish();
}
})

View File

@ -1,80 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<MicroBlogException> {
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"
}

View File

@ -0,0 +1,118 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String>): 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"
}

View File

@ -31,7 +31,4 @@ public interface TwitterUserStream {
@GET("/user.json")
void getUserStream(UserStreamCallback callback);
@GET("/user.json")
void getUserStreamRaw(RawCallback<MicroBlogException> callback);
}

View File

@ -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<MicroBlogException> {
private boolean connected;
@ -53,7 +53,7 @@ public abstract class UserStreamCallback implements RawCallback<MicroBlogExcepti
private boolean disconnected;
@Override
public final void result(final HttpResponse response) throws MicroBlogException, IOException {
public final void result(@NonNull final HttpResponse response) throws MicroBlogException, IOException {
if (!response.isSuccessful()) {
final MicroBlogException cause = new MicroBlogException();
cause.setHttpResponse(response);
@ -69,77 +69,8 @@ public abstract class UserStreamCallback implements RawCallback<MicroBlogExcepti
}
if (TextUtils.isEmpty(line)) continue;
final TwitterStreamObject object = LoganSquare.parse(line, TwitterStreamObject.class);
switch (object.determine()) {
case Type.SENDER: {
break;
}
case Type.STATUS: {
onStatus(LoganSquareMapperFinder.mapperFor(Status.class).parse(line));
break;
}
case Type.DIRECT_MESSAGE: {
onDirectMessage(object.getDirectMessage());
break;
}
case Type.DELETE: {
final TwitterStreamObject.Delete delete = object.getDelete();
if (delete.getStatus() != null) {
onStatusDeleted(delete.getStatus());
} else if (delete.getDirectMessage() != null) {
onDirectMessageDeleted(delete.getDirectMessage());
}
break;
}
case Type.LIMIT:
break;
case Type.STALL_WARNING:
break;
case Type.SCRUB_GEO:
break;
case Type.FRIENDS:
break;
case Type.FAVORITE: {
StatusFavoriteEvent event = LoganSquareMapperFinder.mapperFor(StatusFavoriteEvent.class).parse(line);
onFavorite(event.getSource(), event.getTarget(), event.getTargetObject());
break;
}
case Type.UNFAVORITE: {
StatusFavoriteEvent event = LoganSquareMapperFinder.mapperFor(StatusFavoriteEvent.class).parse(line);
onUnfavorite(event.getSource(), event.getTarget(), event.getTargetObject());
break;
}
case Type.FOLLOW:
break;
case Type.UNFOLLOW:
break;
case Type.USER_LIST_MEMBER_ADDED:
break;
case Type.USER_LIST_MEMBER_DELETED:
break;
case Type.USER_LIST_SUBSCRIBED:
break;
case Type.USER_LIST_UNSUBSCRIBED:
break;
case Type.USER_LIST_CREATED:
break;
case Type.USER_LIST_UPDATED:
break;
case Type.USER_LIST_DESTROYED:
break;
case Type.USER_UPDATE:
break;
case Type.USER_DELETE:
break;
case Type.USER_SUSPEND:
break;
case Type.BLOCK:
break;
case Type.UNBLOCK:
break;
case Type.DISCONNECTION:
break;
case Type.UNKNOWN:
break;
if (!handleEvent(object, line)) {
onUnhandledEvent(object, line);
}
}
} catch (IOException e) {
@ -147,13 +78,89 @@ public abstract class UserStreamCallback implements RawCallback<MicroBlogExcepti
} finally {
Log.d("Twidere.Stream", "Cleaning up...");
reader.close();
response.close();
}
}
private boolean handleEvent(final TwitterStreamObject object, final String json) throws IOException {
switch (object.determine()) {
case Type.SENDER: {
break;
}
case Type.STATUS: {
return onStatus(LoganSquare.parse(json, Status.class));
}
case Type.DIRECT_MESSAGE: {
return onDirectMessage(object.getDirectMessage());
}
case Type.DELETE: {
final TwitterStreamObject.Delete delete = object.getDelete();
if (delete.getStatus() != null) {
return onStatusDeleted(delete.getStatus());
} else if (delete.getDirectMessage() != null) {
return onDirectMessageDeleted(delete.getDirectMessage());
}
break;
}
case Type.LIMIT: {
return onTrackLimitationNotice(object.getLimit().getTrack());
}
case Type.STALL_WARNING:
break;
case Type.SCRUB_GEO: {
TwitterStreamObject.ScrubGeo scrubGeo = object.getScrubGeo();
return onScrubGeo(scrubGeo.getUserId(), scrubGeo.getUpToStatusId());
}
case Type.FRIENDS: {
return onFriendList(object.getFriends());
}
case Type.FAVORITE: {
StatusFavoriteEvent event = LoganSquare.parse(json, StatusFavoriteEvent.class);
return onFavorite(event.getSource(), event.getTarget(), event.getTargetObject());
}
case Type.UNFAVORITE: {
StatusFavoriteEvent event = LoganSquare.parse(json, StatusFavoriteEvent.class);
return onUnfavorite(event.getSource(), event.getTarget(), event.getTargetObject());
}
case Type.FOLLOW:
break;
case Type.UNFOLLOW:
break;
case Type.USER_LIST_MEMBER_ADDED:
break;
case Type.USER_LIST_MEMBER_DELETED:
break;
case Type.USER_LIST_SUBSCRIBED:
break;
case Type.USER_LIST_UNSUBSCRIBED:
break;
case Type.USER_LIST_CREATED:
break;
case Type.USER_LIST_UPDATED:
break;
case Type.USER_LIST_DESTROYED:
break;
case Type.USER_UPDATE:
break;
case Type.USER_DELETE:
break;
case Type.USER_SUSPEND:
break;
case Type.BLOCK:
break;
case Type.UNBLOCK:
break;
case Type.DISCONNECTION:
TwitterStreamObject.Disconnect disconnect = object.getDisconnect();
return onDisconnect(disconnect.getCode(), disconnect.getReason());
case Type.UNKNOWN:
break;
}
return false;
}
@Override
public final void error(final MicroBlogException cause) {
public final void error(@NonNull final MicroBlogException cause) {
onException(cause);
}
@ -161,49 +168,102 @@ public abstract class UserStreamCallback implements RawCallback<MicroBlogExcepti
disconnected = true;
}
public abstract void onConnected();
protected boolean onConnected() {
return false;
}
public abstract void onStatus(Status status) throws IOException;
protected boolean onDisconnect(int code, String reason) {
return false;
}
public abstract void onDirectMessage(@NonNull DirectMessage directMessage) throws IOException;
protected boolean onStatus(@NonNull Status status) {
return false;
}
public abstract void onBlock(User source, User blockedUser);
protected boolean onDirectMessage(@NonNull DirectMessage directMessage) {
return false;
}
public abstract void onDirectMessageDeleted(DeletionEvent event);
protected boolean onBlock(User source, User blockedUser) {
return false;
}
public abstract void onStatusDeleted(DeletionEvent event);
protected boolean onDirectMessageDeleted(@NonNull DeletionEvent event) {
return false;
}
public abstract void onException(Throwable ex);
protected boolean onStatusDeleted(@NonNull DeletionEvent event) {
return false;
}
public abstract void onFavorite(User source, User target, Status targetStatus);
protected boolean onException(@NonNull Throwable ex) {
return false;
}
public abstract void onFollow(User source, User followedUser);
protected boolean onFavorite(@NonNull User source, @NonNull User target, @NonNull Status targetStatus) {
return false;
}
public abstract void onFriendList(long[] friendIds);
protected boolean onFollow(User source, User followedUser) {
return false;
}
public abstract void onScrubGeo(long userId, long upToStatusId);
protected boolean onFriendList(@NonNull String[] friendIds) {
return false;
}
public abstract void onStallWarning(Warning warn);
protected boolean onScrubGeo(String userId, String upToStatusId) {
return false;
}
public abstract void onTrackLimitationNotice(int numberOfLimitedStatuses);
protected boolean onStallWarning(Warning warn) {
return false;
}
public abstract void onUnblock(User source, User unblockedUser);
protected boolean onTrackLimitationNotice(int numberOfLimitedStatuses) {
return false;
}
public abstract void onUnfavorite(User source, User target, Status targetStatus);
protected boolean onUnblock(User source, User unblockedUser) {
return false;
}
public abstract void onUserListCreation(User listOwner, UserList list);
protected boolean onUnfavorite(User source, User target, Status targetStatus) {
return false;
}
public abstract void onUserListDeletion(User listOwner, UserList list);
protected boolean onUserListCreation(User listOwner, UserList list) {
return false;
}
public abstract void onUserListMemberAddition(User addedMember, User listOwner, UserList list);
protected boolean onUserListDeletion(User listOwner, UserList list) {
return false;
}
public abstract void onUserListMemberDeletion(User deletedMember, User listOwner, UserList list);
protected boolean onUserListMemberAddition(User addedMember, User listOwner, UserList list) {
return false;
}
public abstract void onUserListSubscription(User subscriber, User listOwner, UserList list);
protected boolean onUserListMemberDeletion(User deletedMember, User listOwner, UserList list) {
return false;
}
public abstract void onUserListUnsubscription(User subscriber, User listOwner, UserList list);
protected boolean onUserListSubscription(User subscriber, User listOwner, UserList list) {
return false;
}
public abstract void onUserListUpdate(User listOwner, UserList list);
protected boolean onUserListUnsubscription(User subscriber, User listOwner, UserList list) {
return false;
}
public abstract void onUserProfileUpdate(User updatedUser);
protected boolean onUserListUpdate(User listOwner, UserList list) {
return false;
}
protected boolean onUserProfileUpdate(User updatedUser) {
return false;
}
protected void onUnhandledEvent(@NonNull final TwitterStreamObject obj, @NonNull final String json) throws IOException {
}
}

View File

@ -23,15 +23,15 @@ public class TwitterStreamObject {
@JsonField(name = "delete")
Delete delete;
@JsonField(name = "disconnect")
EmptyObject disconnect;
Disconnect disconnect;
@JsonField(name = "limit")
EmptyObject limit;
Limit limit;
@JsonField(name = "warning")
EmptyObject warning;
@JsonField(name = "scrub_geo")
EmptyObject scrubGeo;
ScrubGeo scrubGeo;
@JsonField(name = "friends")
EmptyObject friends;
String[] friends;
@Type
public String determine() {
@ -103,6 +103,22 @@ public class TwitterStreamObject {
return delete;
}
public ScrubGeo getScrubGeo() {
return scrubGeo;
}
public Limit getLimit() {
return limit;
}
public Disconnect getDisconnect() {
return disconnect;
}
public String[] getFriends() {
return friends;
}
@StringDef({Type.SENDER, Type.STATUS, Type.DIRECT_MESSAGE, Type.DELETE, Type.LIMIT,
Type.STALL_WARNING, Type.SCRUB_GEO, Type.FRIENDS, Type.FAVORITE, Type.UNFAVORITE,
Type.FOLLOW, Type.UNFOLLOW, Type.USER_LIST_MEMBER_ADDED, Type.USER_LIST_MEMBER_DELETED,
@ -158,4 +174,53 @@ public class TwitterStreamObject {
return directMessage;
}
}
@JsonObject
public static class ScrubGeo {
@JsonField(name = "user_id")
String userId;
@JsonField(name = "up_to_status_id")
String upToStatusId;
public String getUserId() {
return userId;
}
public String getUpToStatusId() {
return upToStatusId;
}
}
@JsonObject
public static class Limit {
@JsonField(name = "track")
int track;
public int getTrack() {
return track;
}
}
@JsonObject
public static class Disconnect {
@JsonField(name = "code")
int code;
@JsonField(name = "stream_name")
String streamName;
@JsonField(name = "reason")
String reason;
public int getCode() {
return code;
}
public String getStreamName() {
return streamName;
}
public String getReason() {
return reason;
}
}
}

View File

@ -28,8 +28,8 @@ fun String?.toDouble(def: Double): Double {
}
}
fun Int.coerceInOr(range: ClosedRange<Int>, or: Int): Int {
if (range.isEmpty()) return or
fun Int.coerceInOr(range: ClosedRange<Int>, def: Int): Int {
if (range.isEmpty()) return def
return coerceIn(range)
}

View File

@ -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 -> {

View File

@ -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

View File

@ -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<RecyclerView.ViewHolder>(context, requestManager), IActivitiesAdapter<List<ParcelableActivity>> {
) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(context, requestManager),
IActivitiesAdapter<List<ParcelableActivity>>, 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<ParcelableActivity>? {
@ -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
}
}

View File

@ -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<ParcelableStatus>?): 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 <T> 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 <T> 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 {

View File

@ -27,8 +27,6 @@ import org.mariotaku.twidere.view.holder.ActivityTitleSummaryViewHolder
*/
interface IActivitiesAdapter<in Data> : IContentAdapter, IGapSupportedAdapter {
val activityCount: Int
val mediaPreviewStyle: Int
val mediaPreviewEnabled: Boolean
@ -41,7 +39,9 @@ interface IActivitiesAdapter<in Data> : 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?)

View File

@ -19,10 +19,6 @@ interface IStatusesAdapter<in Data> : IContentAdapter, IGapSupportedAdapter {
@PreviewStyle
val mediaPreviewStyle: Int
val statusCount: Int
val rawStatusCount: Int
val twidereLinkify: TwidereLinkify
val mediaPreviewEnabled: Boolean
@ -43,15 +39,20 @@ interface IStatusesAdapter<in Data> : 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?

View File

@ -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<Parcelable>()
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

View File

@ -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<Parcelab
position = recyclerView.getChildLayoutPosition(focusedChild)
}
if (position != -1) {
val status = adapter.getStatus(position) ?: return false
val status = adapter.getStatus(position)
if (keyCode == KeyEvent.KEYCODE_ENTER) {
IntentUtils.openStatus(activity, status, null)
return true
@ -283,7 +287,8 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
wasAtTop = firstVisibleItemPosition == 0
val statusRange = rangeOfSize(adapter.statusStartIndex, adapter.statusCount)
// Get display range of statuses
val statusRange = rangeOfSize(adapter.statusStartIndex, adapter.getStatusCount(raw = false))
val lastReadPosition = if (loadMore || readFromBottom) {
lastVisibleItemPosition
} else {
@ -295,7 +300,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
adapter.getStatusPositionKey(lastReadPosition)
}
lastReadViewTop = layoutManager.findViewByPosition(lastReadPosition)?.top ?: 0
loadMore = statusRange.endInclusive >= 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<Parcelab
override fun onGapClick(holder: GapViewHolder, position: Int) {
val adapter = this.adapter
val status = adapter.getStatus(position) ?: return
val status = adapter.getStatus(position)
DebugLog.v(msg = "Load activity gap $status")
adapter.addGapLoadingId(ObjectId(status.account_key, status.id))
val accountIds = arrayOf(status.account_key)
@ -364,7 +368,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia,
statusPosition: Int) {
val status = adapter.getStatus(statusPosition) ?: return
val status = adapter.getStatus(statusPosition)
IntentUtils.openMedia(activity, status, current, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])
// BEGIN HotMobi
@ -376,7 +380,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia,
statusPosition: Int) {
val status = adapter.getStatus(statusPosition) ?: return
val status = adapter.getStatus(statusPosition)
val quotedMedia = status.quoted_media ?: return
IntentUtils.openMedia(activity, status.account_key, status.is_possibly_sensitive, status,
current, quotedMedia, preferences[newDocumentApiKey], preferences[displaySensitiveContentsKey])
@ -388,12 +392,12 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
}
override fun onItemActionClick(holder: RecyclerView.ViewHolder, id: Int, position: Int) {
val status = adapter.getStatus(position) ?: return
val status = adapter.getStatus(position)
handleActionClick(holder as StatusViewHolder, status, id)
}
override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean {
val status = adapter.getStatus(position) ?: return false
val status = adapter.getStatus(position)
return handleActionLongClick(this, status, adapter.getItemId(position), id)
}
@ -428,12 +432,12 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
}
override fun onStatusClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position) ?: return
val status = adapter.getStatus(position)
IntentUtils.openStatus(activity, status, null)
}
override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position) ?: return
val status = adapter.getStatus(position)
val quotedId = status.quoted_id ?: return
IntentUtils.openStatus(activity, status.account_key, quotedId)
}
@ -450,7 +454,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
}
override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position)!!
val status = adapter.getStatus(position)
val intent = IntentUtils.userProfile(status.account_key, status.user_key,
status.user_screen_name, Referral.TIMELINE_STATUS,
status.extras.user_statusnet_profile_url)
@ -482,7 +486,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
protected fun saveReadPosition(position: Int) {
if (host == null) return
if (position == RecyclerView.NO_POSITION) return
val status = adapter.getStatus(position) ?: return
val status = adapter.getStatus(position)
val positionKey = if (status.position_key > 0) status.position_key else status.timestamp
readPositionTagWithArguments?.let {
accountKeys.map { accountKey -> Utils.getReadPositionTagWithAccount(it, accountKey) }
@ -508,26 +512,38 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
val contextMenuInfo = menuInfo as ExtendedRecyclerView.ContextMenuInfo?
val status = adapter.getStatus(contextMenuInfo!!.position)
inflater.inflate(R.menu.action_status, menu)
MenuUtils.setupForStatus(context, preferences, menu, status!!, twitterWrapper,
MenuUtils.setupForStatus(context, preferences, menu, status, twitterWrapper,
userColorNameManager)
}
override fun onContextItemSelected(item: MenuItem): Boolean {
if (!userVisibleHint) return false
val contextMenuInfo = item.menuInfo as ExtendedRecyclerView.ContextMenuInfo
val status = adapter.getStatus(contextMenuInfo.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)
val status = adapter.getStatus(contextMenuInfo.position)
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 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(

View File

@ -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
}

View File

@ -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<String?>(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
}

View File

@ -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<SingleResponse<Parcelable
}
override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
val status = adapter.getStatus(statusPosition) ?: return
val status = adapter.getStatus(statusPosition)
IntentUtils.openMedia(activity, status, current, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])
@ -317,23 +318,23 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
}
override fun onItemActionClick(holder: ViewHolder, id: Int, position: Int) {
val status = adapter.getStatus(position) ?: return
val status = adapter.getStatus(position)
handleActionClick(holder as StatusViewHolder, status, id)
}
override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean {
val status = adapter.getStatus(position) ?: return false
val status = adapter.getStatus(position)
return AbsStatusesFragment.handleActionLongClick(this, status, adapter.getItemId(position), id)
}
override fun onStatusClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position) ?: return
val status = adapter.getStatus(position)
IntentUtils.openStatus(activity, status)
}
override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position) ?: return
val status = adapter.getStatus(position)
val quotedId = status.quoted_id ?: return
IntentUtils.openStatus(activity, status.account_key, quotedId)
}
@ -349,7 +350,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
}
override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position)!!
val status = adapter.getStatus(position)
IntentUtils.openUserProfile(activity, status.account_key, status.user_key,
status.user_screen_name, preferences[newDocumentApiKey], Referral.TIMELINE_STATUS,
null)
@ -378,7 +379,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
return false
}
if (position == -1) return false
val status = adapter.getStatus(position) ?: return false
val status = adapter.getStatus(position)
val action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState) ?: return false
when (action) {
ACTION_STATUS_REPLY -> {
@ -520,11 +521,11 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
if (position and ILoadMoreSupportAdapter.START != 0L) {
val start = adapter.getIndexStart(StatusAdapter.ITEM_IDX_CONVERSATION)
val status = adapter.getStatus(start)
if (status == null || status.in_reply_to_status_id == null) return
if (status.in_reply_to_status_id == null) return
loadConversation(status, null, status.id)
} else if (position and ILoadMoreSupportAdapter.END != 0L) {
val start = adapter.getIndexStart(StatusAdapter.ITEM_IDX_CONVERSATION)
val status = adapter.getStatus(start + adapter.statusCount - 1) ?: return
val status = adapter.getStatus(start + adapter.getStatusCount(true) - 1)
loadConversation(status, status.id, null)
}
adapter.loadMoreIndicatorPosition = position
@ -663,7 +664,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
if (!userVisibleHint) return
val contextMenuInfo = menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: return
val status = adapter.getStatus(contextMenuInfo.position) ?: return
val status = adapter.getStatus(contextMenuInfo.position)
val inflater = MenuInflater(context)
inflater.inflate(R.menu.action_status, menu)
MenuUtils.setupForStatus(context, preferences, menu, status, twitterWrapper,
@ -673,7 +674,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
override fun onContextItemSelected(item: MenuItem): Boolean {
if (!userVisibleHint) return false
val contextMenuInfo = item.menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: return false
val status = adapter.getStatus(contextMenuInfo.position) ?: return false
val status = adapter.getStatus(contextMenuInfo.position)
if (item.itemId == R.id.share) {
val shareIntent = Utils.createStatusShareIntent(activity, status)
val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status))
@ -1084,7 +1085,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
}
override fun onClick(v: View) {
val status = adapter.getStatus(layoutPosition) ?: return
val status = adapter.getStatus(layoutPosition)
val fragment = adapter.fragment
val preferences = fragment.preferences
when (v) {
@ -1129,7 +1130,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
val layoutPosition = layoutPosition
if (layoutPosition < 0) return false
val fragment = adapter.fragment
val status = adapter.getStatus(layoutPosition) ?: return false
val status = adapter.getStatus(layoutPosition)
val twitter = fragment.twitterWrapper
val manager = fragment.userColorNameManager
val activity = fragment.activity
@ -1488,7 +1489,8 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
class StatusAdapter(
val fragment: StatusFragment
) : LoadMoreSupportAdapter<ViewHolder>(fragment.context, Glide.with(fragment)), IStatusesAdapter<List<ParcelableStatus>> {
) : LoadMoreSupportAdapter<ViewHolder>(fragment.context, Glide.with(fragment)),
IStatusesAdapter<List<ParcelableStatus>>, IItemCountsAdapter {
private val inflater: LayoutInflater
override val twidereLinkify: TwidereLinkify
@ -1496,7 +1498,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
private var recyclerView: RecyclerView? = null
private var statusViewHolder: DetailStatusViewHolder? = null
private val itemCounts = ItemCounts(ITEM_TYPES_SUM)
override val itemCounts = ItemCounts(ITEM_TYPES_SUM)
private val cardBackgroundColor: Int
override val nameFirst = preferences[nameFirstKey]
@ -1555,22 +1557,19 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
twidereLinkify = TwidereLinkify(listener)
}
override fun getStatus(position: Int): ParcelableStatus? {
val itemType = getItemType(position)
when (itemType) {
override fun getStatus(position: Int, raw: Boolean): ParcelableStatus {
when (getItemCountIndex(position, raw)) {
ITEM_IDX_CONVERSATION -> {
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<SingleResponse<Parcelable
return itemCounts.getItemStartPosition(index)
}
override fun getStatusId(position: Int): String? {
val status = getStatus(position)
return status?.id
override fun getStatusId(position: Int, raw: Boolean): String {
return getStatus(position, raw).id
}
override fun getStatusTimestamp(position: Int): Long {
val status = getStatus(position)
return status?.timestamp ?: -1
override fun getStatusTimestamp(position: Int, raw: Boolean): Long {
return getStatus(position, raw).timestamp
}
override fun getStatusPositionKey(position: Int): Long {
val status = getStatus(position) ?: return -1
return if (status.position_key > 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<SingleResponse<Parcelable
return data?.firstOrNull { accountKey == it.account_key && TextUtils.equals(it.id, statusId) }
}
override val statusCount: Int
get() = rawStatusCount
override val rawStatusCount: Int
get() {
return getTypeCount(ITEM_IDX_CONVERSATION) + getTypeCount(ITEM_IDX_STATUS) + getTypeCount(ITEM_IDX_REPLY)
}
override fun getStatusCount(raw: Boolean): Int {
return getTypeCount(ITEM_IDX_CONVERSATION) + getTypeCount(ITEM_IDX_STATUS) + getTypeCount(ITEM_IDX_REPLY)
}
override fun isCardActionsShown(position: Int): Boolean {
if (position == RecyclerView.NO_POSITION) return showCardActions
@ -1732,8 +1722,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
VIEW_TYPE_DETAIL_STATUS -> {
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<SingleResponse<Parcelable
// We only display that indicator for first conversation item
val itemType = getItemType(position)
val displayInReplyTo = itemType == ITEM_IDX_CONVERSATION && position - getItemTypeStart(position) == 0
statusHolder.displayStatus(status = status!!, displayInReplyTo = displayInReplyTo)
statusHolder.displayStatus(status = status, displayInReplyTo = displayInReplyTo)
}
VIEW_TYPE_REPLY_ERROR -> {
val errorHolder = holder as StatusErrorItemViewHolder
@ -1803,12 +1792,16 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
throw IllegalStateException()
}
private fun getItemCountIndex(position: Int, raw: Boolean): Int {
return itemCounts.getItemCountIndex(position)
}
fun getItemType(position: Int): Int {
var typeStart = 0
for (type in 0..ITEM_TYPES_SUM - 1) {
val typeCount = getTypeCount(type)
val typeEnd = typeStart + typeCount
if (position >= 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<SingleResponse<Parcelable
for (type in 0..ITEM_TYPES_SUM - 1) {
val typeCount = getTypeCount(type)
val typeEnd = typeStart + typeCount
if (position >= 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()
}

View File

@ -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<StaggeredGridPa
}
override fun onLoadMoreContents(position: Long) {
// Only supports load from end, skip START flag
if (ILoadMoreSupportAdapter.START in position) return
// Only supports load from end
if (ILoadMoreSupportAdapter.END != position) return
super.onLoadMoreContents(position)
if (position == 0L) return
val maxId = adapter.getStatusId(adapter.statusCount - 1)
// Get last raw status
val maxId = adapter.getStatusId(adapter.statusStartIndex + adapter.getStatusCount(true) - 1)
getStatuses(maxId, null)
}

View File

@ -11,6 +11,7 @@ open class BaseRefreshTaskParam(
override val maxSortIds: LongArray? = null,
override val sinceSortIds: LongArray? = null
) : RefreshTaskParam {
override var extraId: Long = -1L
override var isLoadingMore: Boolean = false
override var shouldAbort: Boolean = false

View File

@ -25,6 +25,8 @@ interface RefreshTaskParam {
val hasCursors: Boolean
get() = cursors != null
val extraId: Long
val isLoadingMore: Boolean
val shouldAbort: Boolean

View File

@ -22,6 +22,9 @@ abstract class SimpleRefreshTaskParam : RefreshTaskParam {
override val maxSortIds: LongArray?
get() = null
override val extraId: Long
get() = -1
override val isLoadingMore: Boolean
get() = false

View File

@ -14,10 +14,12 @@ import android.support.v4.util.SimpleArrayMap
import android.util.Log
import org.mariotaku.ktextension.addOnAccountsUpdatedListenerSafe
import org.mariotaku.ktextension.removeOnAccountsUpdatedListenerSafe
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.TwitterUserStream
import org.mariotaku.microblog.library.twitter.UserStreamCallback
import org.mariotaku.microblog.library.twitter.model.*
import org.mariotaku.microblog.library.twitter.model.DeletionEvent
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.microblog.library.twitter.model.User
import org.mariotaku.microblog.library.twitter.model.Warning
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.R
@ -33,9 +35,7 @@ import org.mariotaku.twidere.provider.TwidereDataStore.*
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.DebugLog
import org.mariotaku.twidere.util.TwidereArrayUtils
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.charset.Charset
class StreamingService : Service() {
@ -153,152 +153,83 @@ class StreamingService : Service() {
private var statusStreamStarted: Boolean = false
private val mentionsStreamStarted: Boolean = false
override fun onConnected() {
override fun onConnected() = true
}
override fun onBlock(source: User, blockedUser: User) {
override fun onBlock(source: User, blockedUser: User): Boolean {
val message = String.format("%s blocked %s", source.screenName, blockedUser.screenName)
Log.d(LOGTAG, message)
return true
}
override fun onDirectMessageDeleted(event: DeletionEvent) {
override fun onDirectMessageDeleted(event: DeletionEvent): Boolean {
val where = Expression.equalsArgs(Messages.MESSAGE_ID).sql
val whereArgs = arrayOf(event.id)
context.contentResolver.delete(Messages.CONTENT_URI, where, whereArgs)
return true
}
override fun onStatusDeleted(event: DeletionEvent) {
override fun onStatusDeleted(event: DeletionEvent): Boolean {
val statusId = event.id
context.contentResolver.delete(Statuses.CONTENT_URI, Expression.equalsArgs(Statuses.STATUS_ID).sql,
arrayOf(statusId))
context.contentResolver.delete(Activities.AboutMe.CONTENT_URI, Expression.equalsArgs(Activities.STATUS_ID).sql,
arrayOf(statusId))
return true
}
@Throws(IOException::class)
override fun onDirectMessage(directMessage: DirectMessage) {
if (directMessage.id == null) return
}
override fun onException(ex: Throwable) {
if (ex is MicroBlogException) {
Log.w(LOGTAG, String.format("Error %d", ex.statusCode), ex)
val response = ex.httpResponse
if (response != null) {
try {
val body = response.body
if (body != null) {
val os = ByteArrayOutputStream()
body.writeTo(os)
val charsetName: String
val contentType = body.contentType()
if (contentType != null) {
val charset = contentType.charset
if (charset != null) {
charsetName = charset.name()
} else {
charsetName = Charset.defaultCharset().name()
}
} else {
charsetName = Charset.defaultCharset().name()
}
Log.w(LOGTAG, os.toString(charsetName))
}
} catch (e: IOException) {
Log.w(LOGTAG, e)
}
}
} else {
Log.w(LOGTAG, ex)
}
}
override fun onFavorite(source: User, target: User, targetStatus: Status) {
override fun onFavorite(source: User, target: User, targetStatus: Status): Boolean {
val message = String.format("%s favorited %s's tweet: %s", source.screenName,
target.screenName, targetStatus.extendedText)
Log.d(LOGTAG, message)
return true
}
override fun onFollow(source: User, followedUser: User) {
override fun onFollow(source: User, followedUser: User): Boolean {
val message = String
.format("%s followed %s", source.screenName, followedUser.screenName)
Log.d(LOGTAG, message)
return true
}
override fun onFriendList(friendIds: LongArray) {
override fun onFriendList(friendIds: Array<String>): 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 {

View File

@ -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
}

View File

@ -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 {