fix account list loading and clean up a lot of code (#823)

* fix account list loading and clean up a lot of code

* remove ACCESS_COARSE_LOCATION for API levels 23+

* small improvements
This commit is contained in:
Konrad Pozniak 2018-08-31 21:52:09 +02:00 committed by GitHub
parent ca881af7c5
commit 28c1c90a98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 135 additions and 378 deletions

View File

@ -15,6 +15,7 @@
package com.keylesspalace.tusky.adapter; package com.keylesspalace.tusky.adapter;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@ -26,55 +27,40 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public abstract class AccountAdapter extends RecyclerView.Adapter { public abstract class AccountAdapter extends RecyclerView.Adapter {
static final int VIEW_TYPE_ACCOUNT = 0;
static final int VIEW_TYPE_FOOTER = 1;
List<Account> accountList; List<Account> accountList;
AccountActionListener accountActionListener; AccountActionListener accountActionListener;
FooterViewHolder.State footerState; private boolean bottomLoading;
private String topId;
private String bottomId;
AccountAdapter(AccountActionListener accountActionListener) { AccountAdapter(AccountActionListener accountActionListener) {
super(); this.accountList = new ArrayList<>();
accountList = new ArrayList<>();
this.accountActionListener = accountActionListener; this.accountActionListener = accountActionListener;
footerState = FooterViewHolder.State.END; bottomLoading = false;
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return accountList.size() + 1; return accountList.size() + (bottomLoading ? 1 : 0);
} }
public void update(@Nullable List<Account> newAccounts, @Nullable String fromId, @Override
@Nullable String uptoId) { public int getItemViewType(int position) {
if (newAccounts == null || newAccounts.isEmpty()) { if (position == accountList.size() && bottomLoading) {
return; return VIEW_TYPE_FOOTER;
}
bottomId = fromId;
topId = uptoId;
if (accountList.isEmpty()) {
accountList = ListUtils.removeDuplicates(newAccounts);
} else { } else {
int index = accountList.indexOf(newAccounts.get(newAccounts.size() - 1)); return VIEW_TYPE_ACCOUNT;
for (int i = 0; i < index; i++) {
accountList.remove(0);
}
int newIndex = newAccounts.indexOf(accountList.get(0));
if (newIndex == -1) {
accountList.addAll(0, newAccounts);
} else {
accountList.addAll(0, newAccounts.subList(0, newIndex));
}
} }
}
public void update(@NonNull List<Account> newAccounts) {
accountList = ListUtils.removeDuplicates(newAccounts);
notifyDataSetChanged(); notifyDataSetChanged();
} }
public void addItems(List<Account> newAccounts, @Nullable String fromId) { public void addItems(List<Account> newAccounts) {
if (fromId != null) {
bottomId = fromId;
}
int end = accountList.size(); int end = accountList.size();
Account last = accountList.get(end - 1); Account last = accountList.get(end - 1);
if (last != null && !findAccount(newAccounts, last.getId())) { if (last != null && !findAccount(newAccounts, last.getId())) {
@ -83,6 +69,19 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
} }
} }
public void setBottomLoading(boolean loading) {
boolean wasLoading = bottomLoading;
if(wasLoading == loading) {
return;
}
bottomLoading = loading;
if(loading) {
notifyItemInserted(accountList.size());
} else {
notifyItemRemoved(accountList.size());
}
}
private static boolean findAccount(List<Account> accounts, String id) { private static boolean findAccount(List<Account> accounts, String id) {
for (Account account : accounts) { for (Account account : accounts) {
if (account.getId().equals(id)) { if (account.getId().equals(id)) {
@ -110,25 +109,5 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
notifyItemInserted(position); notifyItemInserted(position);
} }
@Nullable
public Account getItem(int position) {
if (position >= 0 && position < accountList.size()) {
return accountList.get(position);
}
return null;
}
public void setFooterState(FooterViewHolder.State newFooterState) {
footerState = newFooterState;
}
@Nullable
public String getBottomId() {
return bottomId;
}
@Nullable
public String getTopId() {
return topId;
}
} }

View File

@ -31,8 +31,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
public class BlocksAdapter extends AccountAdapter { public class BlocksAdapter extends AccountAdapter {
private static final int VIEW_TYPE_BLOCKED_USER = 0;
private static final int VIEW_TYPE_FOOTER = 1;
public BlocksAdapter(AccountActionListener accountActionListener) { public BlocksAdapter(AccountActionListener accountActionListener) {
super(accountActionListener); super(accountActionListener);
@ -43,7 +41,7 @@ public class BlocksAdapter extends AccountAdapter {
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) { switch (viewType) {
default: default:
case VIEW_TYPE_BLOCKED_USER: { case VIEW_TYPE_ACCOUNT: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_blocked_user, parent, false); .inflate(R.layout.item_blocked_user, parent, false);
return new BlockedUserViewHolder(view); return new BlockedUserViewHolder(view);
@ -51,29 +49,17 @@ public class BlocksAdapter extends AccountAdapter {
case VIEW_TYPE_FOOTER: { case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false); .inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(view); return new LoadingFooterViewHolder(view);
} }
} }
} }
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (position < accountList.size()) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder; BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
}
}
@Override
public int getItemViewType(int position) {
if (position == accountList.size()) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_BLOCKED_USER;
} }
} }

View File

@ -26,8 +26,6 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
/** Both for follows and following lists. */ /** Both for follows and following lists. */
public class FollowAdapter extends AccountAdapter { public class FollowAdapter extends AccountAdapter {
private static final int VIEW_TYPE_ACCOUNT = 0;
private static final int VIEW_TYPE_FOOTER = 1;
public FollowAdapter(AccountActionListener accountActionListener) { public FollowAdapter(AccountActionListener accountActionListener) {
super(accountActionListener); super(accountActionListener);
@ -46,29 +44,18 @@ public class FollowAdapter extends AccountAdapter {
case VIEW_TYPE_FOOTER: { case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false); .inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(view); return new LoadingFooterViewHolder(view);
} }
} }
} }
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (position < accountList.size()) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
AccountViewHolder holder = (AccountViewHolder) viewHolder; AccountViewHolder holder = (AccountViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
} }
} }
@Override
public int getItemViewType(int position) {
if (position == accountList.size()) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_ACCOUNT;
}
}
} }

View File

@ -31,8 +31,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
public class FollowRequestsAdapter extends AccountAdapter { public class FollowRequestsAdapter extends AccountAdapter {
private static final int VIEW_TYPE_FOLLOW_REQUEST = 0;
private static final int VIEW_TYPE_FOOTER = 1;
public FollowRequestsAdapter(AccountActionListener accountActionListener) { public FollowRequestsAdapter(AccountActionListener accountActionListener) {
super(accountActionListener); super(accountActionListener);
@ -43,7 +41,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) { switch (viewType) {
default: default:
case VIEW_TYPE_FOLLOW_REQUEST: { case VIEW_TYPE_ACCOUNT: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_follow_request, parent, false); .inflate(R.layout.item_follow_request, parent, false);
return new FollowRequestViewHolder(view); return new FollowRequestViewHolder(view);
@ -51,29 +49,17 @@ public class FollowRequestsAdapter extends AccountAdapter {
case VIEW_TYPE_FOOTER: { case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false); .inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(view); return new LoadingFooterViewHolder(view);
} }
} }
} }
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (position < accountList.size()) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
}
}
@Override
public int getItemViewType(int position) {
if (position == accountList.size()) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_FOLLOW_REQUEST;
} }
} }

View File

@ -1,80 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter;
import android.graphics.drawable.Drawable;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.support.v7.widget.RecyclerView.LayoutParams;
import com.keylesspalace.tusky.R;
public class FooterViewHolder extends RecyclerView.ViewHolder {
public enum State {
EMPTY,
END,
LOADING
}
private View container;
private ProgressBar progressBar;
private TextView endMessage;
FooterViewHolder(View itemView) {
super(itemView);
container = itemView.findViewById(R.id.footer_container);
progressBar = itemView.findViewById(R.id.footer_progress_bar);
endMessage = itemView.findViewById(R.id.footer_end_message);
Drawable top = AppCompatResources.getDrawable(itemView.getContext(),
R.drawable.elephant_friend_empty);
endMessage.setCompoundDrawablesWithIntrinsicBounds(null, top, null, null);
}
public void setState(State state) {
switch (state) {
case LOADING: {
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
container.setLayoutParams(layoutParams);
container.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.VISIBLE);
endMessage.setVisibility(View.GONE);
break;
}
case END: {
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT);
container.setLayoutParams(layoutParams);
container.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
endMessage.setVisibility(View.GONE);
break;
}
case EMPTY: {
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
container.setLayoutParams(layoutParams);
container.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
endMessage.setVisibility(View.VISIBLE);
break;
}
}
}
}

View File

@ -0,0 +1,21 @@
/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

View File

@ -16,8 +16,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
public class MutesAdapter extends AccountAdapter { public class MutesAdapter extends AccountAdapter {
private static final int VIEW_TYPE_MUTED_USER = 0;
private static final int VIEW_TYPE_FOOTER = 1;
public MutesAdapter(AccountActionListener accountActionListener) { public MutesAdapter(AccountActionListener accountActionListener) {
super(accountActionListener); super(accountActionListener);
@ -28,7 +26,7 @@ public class MutesAdapter extends AccountAdapter {
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) { switch (viewType) {
default: default:
case VIEW_TYPE_MUTED_USER: { case VIEW_TYPE_ACCOUNT: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_muted_user, parent, false); .inflate(R.layout.item_muted_user, parent, false);
return new MutesAdapter.MutedUserViewHolder(view); return new MutesAdapter.MutedUserViewHolder(view);
@ -36,31 +34,20 @@ public class MutesAdapter extends AccountAdapter {
case VIEW_TYPE_FOOTER: { case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false); .inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(view); return new LoadingFooterViewHolder(view);
} }
} }
} }
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (position < accountList.size()) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder; MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
} }
} }
@Override
public int getItemViewType(int position) {
if (position == accountList.size()) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_MUTED_USER;
}
}
static class MutedUserViewHolder extends RecyclerView.ViewHolder { static class MutedUserViewHolder extends RecyclerView.ViewHolder {
private ImageView avatar; private ImageView avatar;

View File

@ -56,10 +56,9 @@ import java.util.List;
public class NotificationsAdapter extends RecyclerView.Adapter { public class NotificationsAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_MENTION = 0; private static final int VIEW_TYPE_MENTION = 0;
private static final int VIEW_TYPE_FOOTER = 1; private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1;
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2; private static final int VIEW_TYPE_FOLLOW = 2;
private static final int VIEW_TYPE_FOLLOW = 3; private static final int VIEW_TYPE_PLACEHOLDER = 3;
private static final int VIEW_TYPE_PLACEHOLDER = 4;
private List<NotificationViewData> notifications; private List<NotificationViewData> notifications;
private StatusActionListener statusListener; private StatusActionListener statusListener;
@ -87,11 +86,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_status, parent, false); .inflate(R.layout.item_status, parent, false);
return new StatusViewHolder(view); return new StatusViewHolder(view);
} }
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(view);
}
case VIEW_TYPE_STATUS_NOTIFICATION: { case VIEW_TYPE_STATUS_NOTIFICATION: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_status_notification, parent, false); .inflate(R.layout.item_status_notification, parent, false);
@ -172,31 +166,28 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
if (position == notifications.size()) { NotificationViewData notification = notifications.get(position);
return VIEW_TYPE_FOOTER; if (notification instanceof NotificationViewData.Concrete) {
} else { NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification);
NotificationViewData notification = notifications.get(position); switch (concrete.getType()) {
if (notification instanceof NotificationViewData.Concrete) { default:
NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification); case MENTION: {
switch (concrete.getType()) { return VIEW_TYPE_MENTION;
default: }
case MENTION: { case FAVOURITE:
return VIEW_TYPE_MENTION; case REBLOG: {
} return VIEW_TYPE_STATUS_NOTIFICATION;
case FAVOURITE: }
case REBLOG: { case FOLLOW: {
return VIEW_TYPE_STATUS_NOTIFICATION; return VIEW_TYPE_FOLLOW;
}
case FOLLOW: {
return VIEW_TYPE_FOLLOW;
}
} }
} else if (notification instanceof NotificationViewData.Placeholder) {
return VIEW_TYPE_PLACEHOLDER;
} else {
throw new AssertionError("Unknown notification type");
} }
} else if (notification instanceof NotificationViewData.Placeholder) {
return VIEW_TYPE_PLACEHOLDER;
} else {
throw new AssertionError("Unknown notification type");
} }
} }
public void update(@Nullable List<NotificationViewData> newNotifications) { public void update(@Nullable List<NotificationViewData> newNotifications) {
@ -364,8 +355,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private void setCreatedAt(@Nullable Date createdAt) { private void setCreatedAt(@Nullable Date createdAt) {
// This is the visible timestampInfo. // This is the visible timestampInfo.
String readout; String readout;
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
* as 17 meters instead of minutes. */ * as 17 meters instead of minutes. */
CharSequence readoutAloud; CharSequence readoutAloud;
if (createdAt != null) { if (createdAt != null) {
long then = createdAt.getTime(); long then = createdAt.getTime();

View File

@ -36,7 +36,6 @@ import com.keylesspalace.tusky.adapter.AccountAdapter;
import com.keylesspalace.tusky.adapter.BlocksAdapter; import com.keylesspalace.tusky.adapter.BlocksAdapter;
import com.keylesspalace.tusky.adapter.FollowAdapter; import com.keylesspalace.tusky.adapter.FollowAdapter;
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter; import com.keylesspalace.tusky.adapter.FollowRequestsAdapter;
import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.MutesAdapter; import com.keylesspalace.tusky.adapter.MutesAdapter;
import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
@ -79,10 +78,8 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
private RecyclerView recyclerView; private RecyclerView recyclerView;
private EndlessOnScrollListener scrollListener; private EndlessOnScrollListener scrollListener;
private AccountAdapter adapter; private AccountAdapter adapter;
private boolean bottomLoading; private boolean fetching = false;
private int bottomFetches; private String bottomId;
private boolean topLoading;
private int topFetches;
public static AccountListFragment newInstance(Type type) { public static AccountListFragment newInstance(Type type) {
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
@ -140,10 +137,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
} }
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
bottomLoading = false;
bottomFetches = 0;
topLoading = false;
topFetches = 0;
return rootView; return rootView;
} }
@ -155,13 +148,13 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
scrollListener = new EndlessOnScrollListener(layoutManager) { scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override @Override
public void onLoadMore(int totalItemsCount, RecyclerView view) { public void onLoadMore(int totalItemsCount, RecyclerView view) {
AccountListFragment.this.onLoadMore(view); AccountListFragment.this.onLoadMore();
} }
}; };
recyclerView.addOnScrollListener(scrollListener); recyclerView.addOnScrollListener(scrollListener);
fetchAccounts(null, null, FetchEnd.BOTTOM); fetchAccounts(null);
} }
@ -176,14 +169,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
@Override @Override
public void onMute(final boolean mute, final String id, final int position) { public void onMute(final boolean mute, final String id, final int position) {
if (api == null) {
/* If somehow an unmute button is clicked after onCreateView but before
* onActivityCreated, then this would get called with a null api object, so this eats
* that input. */
Log.d(TAG, "MastodonApi isn't initialised so this mute can't occur.");
return;
}
Callback<Relationship> callback = new Callback<Relationship>() { Callback<Relationship> callback = new Callback<Relationship>() {
@Override @Override
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) { public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
@ -237,14 +222,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
@Override @Override
public void onBlock(final boolean block, final String id, final int position) { public void onBlock(final boolean block, final String id, final int position) {
if (api == null) {
/* If somehow an unblock button is clicked after onCreateView but before
* onActivityCreated, then this would get called with a null api object, so this eats
* that input. */
Log.d(TAG, "MastodonApi isn't initialised so this block can't occur.");
return;
}
Callback<Relationship> cb = new Callback<Relationship>() { Callback<Relationship> cb = new Callback<Relationship>() {
@Override @Override
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) { public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
@ -299,13 +276,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
@Override @Override
public void onRespondToFollowRequest(final boolean accept, final String accountId, public void onRespondToFollowRequest(final boolean accept, final String accountId,
final int position) { final int position) {
if (api == null) {
/* If somehow an response button is clicked after onCreateView but before
* onActivityCreated, then this would get called with a null api object, so this eats
* that input. */
Log.d(TAG, "MastodonApi isn't initialised, so follow requests can't be responded to.");
return;
}
Callback<Relationship> callback = new Callback<Relationship>() { Callback<Relationship> callback = new Callback<Relationship>() {
@Override @Override
@ -349,44 +319,30 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
Log.e(TAG, message); Log.e(TAG, message);
} }
private enum FetchEnd { private Call<List<Account>> getFetchCallByListType(Type type, String fromId) {
TOP,
BOTTOM
}
private Call<List<Account>> getFetchCallByListType(Type type, String fromId, String uptoId) {
switch (type) { switch (type) {
default: default:
case FOLLOWS: case FOLLOWS:
return api.accountFollowing(accountId, fromId, uptoId, null); return api.accountFollowing(accountId, fromId, null, null);
case FOLLOWERS: case FOLLOWERS:
return api.accountFollowers(accountId, fromId, uptoId, null); return api.accountFollowers(accountId, fromId, null, null);
case BLOCKS: case BLOCKS:
return api.blocks(fromId, uptoId, null); return api.blocks(fromId, null, null);
case MUTES: case MUTES:
return api.mutes(fromId, uptoId, null); return api.mutes(fromId, null, null);
case FOLLOW_REQUESTS: case FOLLOW_REQUESTS:
return api.followRequests(fromId, uptoId, null); return api.followRequests(fromId, null, null);
} }
} }
private void fetchAccounts(String fromId, String uptoId, final FetchEnd fetchEnd) { private void fetchAccounts(String id) {
/* If there is a fetch already ongoing, record however many fetches are requested and if (fetching) {
* fulfill them after it's complete. */
if (fetchEnd == FetchEnd.TOP && topLoading) {
topFetches++;
return;
}
if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) {
bottomFetches++;
return; return;
} }
fetching = true;
if (fromId != null || adapter.getItemCount() <= 1) { if (id != null) {
/* When this is called by the EndlessScrollListener it cannot refresh the footer state recyclerView.post(() -> adapter.setBottomLoading(true));
* using adapter.notifyItemChanged. So its necessary to postpone doing so until a
* convenient time for the UI thread using a Runnable. */
recyclerView.post(() -> adapter.setFooterState(FooterViewHolder.State.LOADING));
} }
Callback<List<Account>> cb = new Callback<List<Account>>() { Callback<List<Account>> cb = new Callback<List<Account>>() {
@ -394,99 +350,55 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
public void onResponse(@NonNull Call<List<Account>> call, @NonNull Response<List<Account>> response) { public void onResponse(@NonNull Call<List<Account>> call, @NonNull Response<List<Account>> response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String linkHeader = response.headers().get("Link"); String linkHeader = response.headers().get("Link");
onFetchAccountsSuccess(response.body(), linkHeader, fetchEnd); onFetchAccountsSuccess(response.body(), linkHeader);
} else { } else {
onFetchAccountsFailure(new Exception(response.message()), fetchEnd); onFetchAccountsFailure(new Exception(response.message()));
} }
} }
@Override @Override
public void onFailure(@NonNull Call<List<Account>> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<List<Account>> call, @NonNull Throwable t) {
onFetchAccountsFailure((Exception) t, fetchEnd); onFetchAccountsFailure((Exception) t);
} }
}; };
Call<List<Account>> listCall = getFetchCallByListType(type, fromId, uptoId); Call<List<Account>> listCall = getFetchCallByListType(type, id);
callList.add(listCall); callList.add(listCall);
listCall.enqueue(cb); listCall.enqueue(cb);
} }
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader, private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader) {
FetchEnd fetchEnd) { adapter.setBottomLoading(false);
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader); List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
switch (fetchEnd) { HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
case TOP: { String fromId = null;
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev"); if (next != null) {
String uptoId = null; fromId = next.uri.getQueryParameter("max_id");
if (previous != null) {
uptoId = previous.uri.getQueryParameter("since_id");
}
adapter.update(accounts, null, uptoId);
break;
}
case BOTTOM: {
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
String fromId = null;
if (next != null) {
fromId = next.uri.getQueryParameter("max_id");
}
if (adapter.getItemCount() > 1) {
adapter.addItems(accounts, fromId);
} else {
/* If this is the first fetch, also save the id from the "previous" link and
* treat this operation as a refresh so the scroll position doesn't get pushed
* down to the end. */
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
String uptoId = null;
if (previous != null) {
uptoId = previous.uri.getQueryParameter("since_id");
}
adapter.update(accounts, fromId, uptoId);
}
break;
}
} }
fulfillAnyQueuedFetches(fetchEnd); if (adapter.getItemCount() > 1) {
if (accounts.size() == 0 && adapter.getItemCount() == 1) { adapter.addItems(accounts);
adapter.setFooterState(FooterViewHolder.State.EMPTY);
} else { } else {
adapter.setFooterState(FooterViewHolder.State.END); adapter.update(accounts);
} }
bottomId = fromId;
fetching = false;
adapter.setBottomLoading(false);
} }
private void onFetchAccountsFailure(Exception exception, FetchEnd fetchEnd) { private void onFetchAccountsFailure(Exception exception) {
fetching = false;
Log.e(TAG, "Fetch failure: " + exception.getMessage()); Log.e(TAG, "Fetch failure: " + exception.getMessage());
fulfillAnyQueuedFetches(fetchEnd);
} }
private void onRefresh() { private void onLoadMore() {
fetchAccounts(null, adapter.getTopId(), FetchEnd.TOP); if(bottomId == null) {
} return;
private void onLoadMore(RecyclerView recyclerView) {
AccountAdapter adapter = (AccountAdapter) recyclerView.getAdapter();
//if we do not have a bottom id, we know we do not need to load more
if (adapter.getBottomId() == null) return;
fetchAccounts(adapter.getBottomId(), null, FetchEnd.BOTTOM);
}
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
switch (fetchEnd) {
case BOTTOM: {
bottomLoading = false;
if (bottomFetches > 0) {
bottomFetches--;
onLoadMore(recyclerView);
}
break;
}
case TOP: {
topLoading = false;
if (topFetches > 0) {
topFetches--;
onRefresh();
}
break;
}
} }
fetchAccounts(bottomId);
} }
} }

View File

@ -1,24 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/footer_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="72dp">
<ProgressBar <ProgressBar
android:id="@+id/footer_progress_bar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_gravity="center"
android:indeterminate="true" /> android:indeterminate="true" />
<TextView </FrameLayout>
android:id="@+id/footer_end_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:drawablePadding="32dp"
android:text="@string/footer_empty"
android:textAlignment="center"
android:textSize="?attr/status_text_medium" />
</RelativeLayout>