chinwag-android/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java
Ivan Kupalov a5cffe0fea Add Dagger (#554)
* Add Dagger DI

* Preemptively fix tests

* Add missing licenses

* DI fixes

* ci fixes
2018-03-27 19:47:00 +02:00

492 lines
18 KiB
Java

/* 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.fragment;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.keylesspalace.tusky.AccountActivity;
import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.AccountAdapter;
import com.keylesspalace.tusky.adapter.BlocksAdapter;
import com.keylesspalace.tusky.adapter.FollowAdapter;
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter;
import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.MutesAdapter;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.HttpHeaderLink;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import java.util.List;
import javax.inject.Inject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AccountListFragment extends BaseFragment implements AccountActionListener,
Injectable {
private static final String TAG = "AccountList"; // logging tag
public AccountListFragment() {
}
public enum Type {
FOLLOWS,
FOLLOWERS,
BLOCKS,
MUTES,
FOLLOW_REQUESTS,
}
@Inject
public MastodonApi api;
private Type type;
private String accountId;
private LinearLayoutManager layoutManager;
private RecyclerView recyclerView;
private EndlessOnScrollListener scrollListener;
private AccountAdapter adapter;
private boolean bottomLoading;
private int bottomFetches;
private boolean topLoading;
private int topFetches;
public static AccountListFragment newInstance(Type type) {
Bundle arguments = new Bundle();
AccountListFragment fragment = new AccountListFragment();
arguments.putSerializable("type", type);
fragment.setArguments(arguments);
return fragment;
}
public static AccountListFragment newInstance(Type type, String accountId) {
Bundle arguments = new Bundle();
AccountListFragment fragment = new AccountListFragment();
arguments.putSerializable("type", type);
arguments.putString("accountId", accountId);
fragment.setArguments(arguments);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle arguments = getArguments();
type = (Type) arguments.getSerializable("type");
accountId = arguments.getString("accountId");
api = null;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_account_list, container, false);
Context context = getContext();
recyclerView = rootView.findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true);
layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
DividerItemDecoration divider = new DividerItemDecoration(
context, layoutManager.getOrientation());
Drawable drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable,
R.drawable.status_divider_dark);
divider.setDrawable(drawable);
recyclerView.addItemDecoration(divider);
scrollListener = null;
if (type == Type.BLOCKS) {
adapter = new BlocksAdapter(this);
} else if (type == Type.MUTES) {
adapter = new MutesAdapter(this);
} else if (type == Type.FOLLOW_REQUESTS) {
adapter = new FollowRequestsAdapter(this);
} else {
adapter = new FollowAdapter(this);
}
recyclerView.setAdapter(adapter);
bottomLoading = false;
bottomFetches = 0;
topLoading = false;
topFetches = 0;
return rootView;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Just use the basic scroll listener to load more accounts.
scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
AccountListFragment.this.onLoadMore(view);
}
};
recyclerView.addOnScrollListener(scrollListener);
fetchAccounts(null, null, FetchEnd.BOTTOM);
}
@Override
public void onViewAccount(String id) {
Intent intent = new Intent(getContext(), AccountActivity.class);
intent.putExtra("id", id);
startActivity(intent);
}
@Override
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>() {
@Override
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
if (response.isSuccessful()) {
onMuteSuccess(mute, id, position);
} else {
onMuteFailure(mute, id);
}
}
@Override
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {
onMuteFailure(mute, id);
}
};
Call<Relationship> call;
if (!mute) {
call = api.unmuteAccount(id);
} else {
call = api.muteAccount(id);
}
callList.add(call);
call.enqueue(callback);
}
private void onMuteSuccess(boolean muted, final String id, final int position) {
if (muted) {
return;
}
final MutesAdapter mutesAdapter = (MutesAdapter) adapter;
final Account unmutedUser = mutesAdapter.removeItem(position);
View.OnClickListener listener = v -> {
mutesAdapter.addItem(unmutedUser, position);
onMute(true, id, position);
};
Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo, listener)
.show();
}
private void onMuteFailure(boolean mute, String id) {
String verb;
if (mute) {
verb = "mute";
} else {
verb = "unmute";
}
Log.e(TAG, String.format("Failed to %s account id %s", verb, id));
}
@Override
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>() {
@Override
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
if (response.isSuccessful()) {
onBlockSuccess(block, id, position);
} else {
onBlockFailure(block, id);
}
}
@Override
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {
onBlockFailure(block, id);
}
};
Call<Relationship> call;
if (!block) {
call = api.unblockAccount(id);
} else {
call = api.blockAccount(id);
}
callList.add(call);
call.enqueue(cb);
}
private void onBlockSuccess(boolean blocked, final String id, final int position) {
if (blocked) {
return;
}
final BlocksAdapter blocksAdapter = (BlocksAdapter) adapter;
final Account unblockedUser = blocksAdapter.removeItem(position);
View.OnClickListener listener = v -> {
blocksAdapter.addItem(unblockedUser, position);
onBlock(true, id, position);
};
Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo, listener)
.show();
}
private void onBlockFailure(boolean block, String id) {
String verb;
if (block) {
verb = "block";
} else {
verb = "unblock";
}
Log.e(TAG, String.format("Failed to %s account id %s", verb, id));
}
@Override
public void onRespondToFollowRequest(final boolean accept, final String accountId,
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>() {
@Override
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
if (response.isSuccessful()) {
onRespondToFollowRequestSuccess(position);
} else {
onRespondToFollowRequestFailure(accept, accountId);
}
}
@Override
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) {
onRespondToFollowRequestFailure(accept, accountId);
}
};
Call<Relationship> call;
if (accept) {
call = api.authorizeFollowRequest(accountId);
} else {
call = api.rejectFollowRequest(accountId);
}
callList.add(call);
call.enqueue(callback);
}
private void onRespondToFollowRequestSuccess(int position) {
FollowRequestsAdapter followRequestsAdapter = (FollowRequestsAdapter) adapter;
followRequestsAdapter.removeItem(position);
}
private void onRespondToFollowRequestFailure(boolean accept, String accountId) {
String verb;
if (accept) {
verb = "accept";
} else {
verb = "reject";
}
String message = String.format("Failed to %s account id %s.", verb, accountId);
Log.e(TAG, message);
}
private enum FetchEnd {
TOP,
BOTTOM
}
private Call<List<Account>> getFetchCallByListType(Type type, String fromId, String uptoId) {
switch (type) {
default:
case FOLLOWS:
return api.accountFollowing(accountId, fromId, uptoId, null);
case FOLLOWERS:
return api.accountFollowers(accountId, fromId, uptoId, null);
case BLOCKS:
return api.blocks(fromId, uptoId, null);
case MUTES:
return api.mutes(fromId, uptoId, null);
case FOLLOW_REQUESTS:
return api.followRequests(fromId, uptoId, null);
}
}
private void fetchAccounts(String fromId, String uptoId, final FetchEnd fetchEnd) {
/* If there is a fetch already ongoing, record however many fetches are requested and
* fulfill them after it's complete. */
if (fetchEnd == FetchEnd.TOP && topLoading) {
topFetches++;
return;
}
if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) {
bottomFetches++;
return;
}
if (fromId != null || adapter.getItemCount() <= 1) {
/* When this is called by the EndlessScrollListener it cannot refresh the footer state
* 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>>() {
@Override
public void onResponse(@NonNull Call<List<Account>> call, @NonNull Response<List<Account>> response) {
if (response.isSuccessful()) {
String linkHeader = response.headers().get("Link");
onFetchAccountsSuccess(response.body(), linkHeader, fetchEnd);
} else {
onFetchAccountsFailure(new Exception(response.message()), fetchEnd);
}
}
@Override
public void onFailure(@NonNull Call<List<Account>> call, @NonNull Throwable t) {
onFetchAccountsFailure((Exception) t, fetchEnd);
}
};
Call<List<Account>> listCall = getFetchCallByListType(type, fromId, uptoId);
callList.add(listCall);
listCall.enqueue(cb);
}
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader,
FetchEnd fetchEnd) {
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
switch (fetchEnd) {
case TOP: {
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
String uptoId = null;
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 (accounts.size() == 0 && adapter.getItemCount() == 1) {
adapter.setFooterState(FooterViewHolder.State.EMPTY);
} else {
adapter.setFooterState(FooterViewHolder.State.END);
}
}
private void onFetchAccountsFailure(Exception exception, FetchEnd fetchEnd) {
Log.e(TAG, "Fetch failure: " + exception.getMessage());
fulfillAnyQueuedFetches(fetchEnd);
}
private void onRefresh() {
fetchAccounts(null, adapter.getTopId(), FetchEnd.TOP);
}
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;
}
}
}
}