package org.sugr.gearshift.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.text.Html;
import android.text.TextUtils;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.PathInterpolator;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CursorAdapter;
import android.widget.FilterQueryProvider;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.sugr.gearshift.G;
import org.sugr.gearshift.G.FilterBy;
import org.sugr.gearshift.G.SortBy;
import org.sugr.gearshift.G.SortOrder;
import org.sugr.gearshift.GearShiftApplication;
import org.sugr.gearshift.R;
import org.sugr.gearshift.core.Torrent;
import org.sugr.gearshift.core.TransmissionProfile;
import org.sugr.gearshift.core.TransmissionSession;
import org.sugr.gearshift.datasource.DataSource;
import org.sugr.gearshift.service.DataService;
import org.sugr.gearshift.service.DataServiceManager;
import org.sugr.gearshift.service.DataServiceManagerInterface;
import org.sugr.gearshift.ui.loader.TorrentTrafficLoader;
import org.sugr.gearshift.ui.settings.SettingsActivity;
import org.sugr.gearshift.ui.util.LocationDialogHelper;
import org.sugr.gearshift.ui.util.LocationDialogHelperInterface;
import org.sugr.gearshift.ui.util.QueueManagementDialogHelperInterface;
import org.sugr.gearshift.ui.util.UpdateCheckDialog;
import java.util.HashMap;
import java.util.Map;
/**
* A list fragment representing a list of Torrents. This fragment
* also supports tablet devices by allowing list items to be given an
* 'activated' state upon selection. This helps indicate which item is
* currently being viewed in a {@link TorrentDetailFragment}.
* <p>
* Activities containing this fragment MUST implement the {@link Callbacks}
* interface.
*/
public class TorrentListFragment extends ListFragment implements TorrentListNotificationInterface {
/**
* The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets.
*/
private static final String STATE_FIND_SHOWN = "find_shown";
private static final String STATE_FIND_QUERY = "find_query";
/**
* The fragment's current callback object, which is notified of list item
* clicks.
*/
private Callbacks callbacks = dummyCallbacks;
/**
* The current activated item position. Only used on tablets.
*/
private int activatedPosition = ListView.INVALID_POSITION;
private ActionMode actionMode;
private int listChoiceMode = ListView.CHOICE_MODE_NONE;
private TorrentCursorAdapter torrentAdapter;
private boolean scrollToTop = false;
private boolean findVisible = false;
private boolean initialLoading = false;
private String findQuery = "";
private SharedPreferences sharedPrefs;
private Menu menu;
private SparseBooleanArray checkAnimations = new SparseBooleanArray();
private BroadcastReceiver sessionReceiver;
/**
* A callback interface that all activities containing this fragment must
* implement. This mechanism allows activities to be notified of item
* selections.
*/
public interface Callbacks {
/**
* Callback for when an item has been selected.
*/
void onItemSelected(int position);
}
/**
* A dummy implementation of the {@link Callbacks} interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
private static Callbacks dummyCallbacks = position -> { };
private MultiChoiceModeListener listChoiceListener = new MultiChoiceModeListener() {
private SparseArray<String> selectedTorrentIds;
private boolean hasQueued = false;
@Override
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
final DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
if (manager == null || selectedTorrentIds == null)
return false;
final String[] hashStrings = new String[selectedTorrentIds.size()];
for (int i = 0; i < selectedTorrentIds.size(); ++i) {
hashStrings[i] = selectedTorrentIds.valueAt(i);
}
AlertDialog.Builder builder;
G.TorrentAction action;
switch (item.getItemId()) {
case R.id.select_all: {
ListView v = getListView();
for (int i = 0; i < torrentAdapter.getCount(); i++) {
if (!v.isItemChecked(i)) {
v.setItemChecked(i, true);
}
}
return true;
}
case R.id.remove: {
ViewGroup v = (ViewGroup) getActivity().getLayoutInflater().inflate(R.layout.remove_torrent_dialog, null);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
((CheckBox) v.findViewById(R.id.remove_data)).setChecked(prefs.getBoolean(G.PREF_DELETE_DATA, false));
builder = new AlertDialog.Builder(getActivity())
.setCancelable(false)
.setNegativeButton(android.R.string.no, null);
builder.setPositiveButton(android.R.string.yes,
(dialog, id) -> {
boolean removeData =
((CheckBox) ((AlertDialog) dialog).findViewById(R.id.remove_data))
.isChecked();
manager.removeTorrent(hashStrings, removeData);
((TransmissionSessionInterface) getActivity()).setRefreshing(true,
DataService.Requests.REMOVE_TORRENT);
mode.finish();
})
.setView(v)
.show();
return true;
}
case R.id.resume:
action = hasQueued ? G.TorrentAction.START_NOW : G.TorrentAction.START;
break;
case R.id.pause:
action = G.TorrentAction.STOP;
break;
case R.id.move:
return showMoveDialog(hashStrings);
case R.id.verify:
action = G.TorrentAction.VERIFY;
break;
case R.id.reannounce:
action = G.TorrentAction.REANNOUNCE;
break;
case R.id.queue:
return showQueueManagementDialog(hashStrings);
default:
return true;
}
manager.setTorrentAction(hashStrings, action);
((TransmissionSessionInterface) getActivity()).setRefreshing(true,
DataService.Requests.SET_TORRENT_ACTION);
mode.finish();
return true;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
if (inflater != null)
inflater.inflate(R.menu.torrent_list_multiselect, menu);
selectedTorrentIds = new SparseArray<>();
actionMode = mode;
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
G.logD("Destroying context menu");
actionMode = null;
selectedTorrentIds = null;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public void onItemCheckedStateChanged(ActionMode mode,
int position, long id, boolean checked) {
Cursor cursor = (Cursor) torrentAdapter.getItem(position);
String hash = Torrent.getHashString(cursor);
if (checked)
selectedTorrentIds.append(position, hash);
else
selectedTorrentIds.delete(position);
boolean hasPaused = false;
boolean hasRunning = false;
hasQueued = false;
for (int i = 0; i < selectedTorrentIds.size(); ++i) {
cursor = (Cursor) torrentAdapter.getItem(selectedTorrentIds.keyAt(i));
int status = Torrent.getStatus(cursor);
if (status == Torrent.Status.STOPPED) {
hasPaused = true;
} else if (Torrent.isActive(status)) {
hasRunning = true;
} else {
hasQueued = true;
}
}
Menu menu = mode.getMenu();
MenuItem item = menu.findItem(R.id.resume);
if (item != null)
item.setVisible(hasPaused || hasQueued).setEnabled(hasPaused || hasQueued);
item = menu.findItem(R.id.pause);
if (item != null)
item.setVisible(hasRunning).setEnabled(hasRunning);
ListView list = getListView();
int firstVisible = list.getFirstVisiblePosition();
int virtual = position - firstVisible;
View child = list.getChildAt(virtual);
if (child != null) {
toggleListItemChecked(checked, position, child.findViewById(R.id.type_checked),
child.findViewById(R.id.type_directory), child.findViewById(R.id.progress));
}
}
};
/* The callback will get garbage collected if its a mere anon class */
private SharedPreferences.OnSharedPreferenceChangeListener settingsChangeListener =
new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if (key.equals(G.PREF_BASE_SORT_ORDER)) {
torrentAdapter.getFilter().filter("");
} else if (key.equals(G.PREF_PROFILES)) {
if (getActivity() != null) {
((TransmissionSessionInterface) getActivity()).setRefreshing(true,
DataService.Requests.GET_TORRENTS);
}
} else if (key.equals(G.PREF_CURRENT_PROFILE)) {
if (prefs.getString(key, null) == null && getActivity() != null) {
setEmptyMessage(R.string.no_profiles_empty_list);
}
} else if (key.equals(G.PREF_SHOW_STATUS) && getView() != null) {
toggleStatusBar();
} else if (key.startsWith(G.PREF_SORT_PREFIX)) {
if (getActivity() != null
&& ((TransmissionSessionInterface) getActivity()).getSession() != null) {
int visibleCount = setupSortMenu();
menu.findItem(R.id.sort).setVisible(visibleCount > 0);
}
}
}
};
private Handler findHandler = new Handler();
private Runnable findRunnable = () -> {
if (getActivity() == null) {
return;
}
G.logD("Search query " + findQuery);
setListFilter(findQuery);
};
private LoaderManager.LoaderCallbacks<TorrentTrafficLoader.TorrentTrafficOutputData> torrentTrafficLoaderCallbacks
= new LoaderManager.LoaderCallbacks<TorrentTrafficLoader.TorrentTrafficOutputData>() {
@Override public Loader<TorrentTrafficLoader.TorrentTrafficOutputData> onCreateLoader(int id, Bundle bundle) {
if (id == G.TORRENT_LIST_TRAFFIC_LOADER_ID) {
TransmissionProfileInterface context = (TransmissionProfileInterface) getActivity();
if (context == null) {
return null;
}
return new TorrentTrafficLoader(getActivity(), context.getProfile().getId(),
sharedPrefs.getBoolean(G.PREF_SHOW_STATUS, true), false, false);
}
return null;
}
@Override public void onLoadFinished(Loader<TorrentTrafficLoader.TorrentTrafficOutputData> loader,
TorrentTrafficLoader.TorrentTrafficOutputData data) {
TransmissionSessionInterface context = (TransmissionSessionInterface) getActivity();
if (context == null) {
return;
}
TransmissionSession session = context.getSession();
Toolbar status = (Toolbar) getView().findViewById(R.id.status_bar);
TextView statusBarText = (TextView) status.findViewById(R.id.status_bar_text);
if (!sharedPrefs.getBoolean(G.PREF_SHOW_STATUS, true)) {
return;
}
if (status.getVisibility() == View.GONE) {
toggleStatusBar(status);
}
String limitDown = "";
String limitUp = "";
long free = 0;
if (session != null) {
free = session.getDownloadDirFreeSpace();
if (session.isDownloadSpeedLimitEnabled() || session.isAltSpeedLimitEnabled()) {
limitDown = String.format(
getString(R.string.status_bar_limit_format),
G.readableFileSize((
session.isAltSpeedLimitEnabled()
? session.getAltDownloadSpeedLimit()
: session.getDownloadSpeedLimit()) * 1024) + "/s"
);
}
if (session.isUploadSpeedLimitEnabled() || session.isAltSpeedLimitEnabled()) {
limitUp = String.format(
getString(R.string.status_bar_limit_format),
G.readableFileSize((
session.isAltSpeedLimitEnabled()
? session.getAltUploadSpeedLimit()
: session.getUploadSpeedLimit()) * 1024) + "/s"
);
}
}
statusBarText.setText(Html.fromHtml(String.format(
getString(R.string.status_bar_format),
G.readableFileSize(data.downloadSpeed),
limitDown,
G.readableFileSize(data.uploadSpeed),
limitUp,
free == 0 ? getString(R.string.unknown) : G.readableFileSize(free)
)));
}
@Override
public void onLoaderReset(Loader<TorrentTrafficLoader.TorrentTrafficOutputData> loader) {
}
};
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public TorrentListFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sessionReceiver = new SessionReceiver();
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
getActivity().setProgressBarIndeterminateVisibility(true);
initialLoading = true;
if (savedInstanceState == null) {
sharedPrefs.registerOnSharedPreferenceChangeListener(settingsChangeListener);
}
if (!GearShiftApplication.isStartupInitialized() && sharedPrefs.getBoolean(G.PREF_AUTO_UPDATE_CHECK, true)) {
((GearShiftApplication) getActivity().getApplication()).checkForUpdates(new GearShiftApplication.OnUpdateCheck() {
@Override public void onNewRelease(String title, String description, String url, String downloadUrl) {
new UpdateCheckDialog(getActivity(),
G.trimTrailingWhitespace(Html.fromHtml(String.format(getString(R.string.update_available), title))),
url, downloadUrl).show();
}
@Override public void onCurrentRelease() { }
@Override public void onUpdateCheckError(Exception e) { }
});
}
GearShiftApplication.setStartupInitialized(true);
}
@Override
public void onViewCreated(View view, final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Restore the previously serialized activated item position.
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(STATE_FIND_SHOWN)) {
findVisible = savedInstanceState.getBoolean(STATE_FIND_SHOWN);
if (savedInstanceState.containsKey(STATE_FIND_QUERY)) {
findQuery = savedInstanceState.getString(STATE_FIND_QUERY);
}
}
}
if (!findVisible) {
Editor e = sharedPrefs.edit();
e.putString(G.PREF_LIST_SEARCH, "");
e.apply();
}
torrentAdapter = new TorrentCursorAdapter(getActivity(), savedInstanceState);
setListAdapter(torrentAdapter);
TextView status = (TextView) view.findViewById(R.id.status_bar_text);
/* Enable the marquee animation */
status.setSelected(true);
final SwipeRefreshLayout swipeRefresh = (SwipeRefreshLayout) view.findViewById(R.id.swipe_container);
swipeRefresh.setColorSchemeResources(R.color.main_red, R.color.main_gray,
R.color.main_black, R.color.main_red);
swipeRefresh.setOnRefreshListener(() -> {
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
if (manager != null) {
manager.update();
((TransmissionSessionInterface) getActivity()).setRefreshing(true,
DataService.Requests.GET_TORRENTS);
}
});
setHasOptionsMenu(true);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final ListView list = getListView();
list.setChoiceMode(listChoiceMode);
list.setOnItemLongClickListener((parent, view, position, id) -> {
if (!((TorrentListActivity) getActivity()).isDetailPanelVisible()) {
list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
setActivatedPosition(position);
return true;
}
return false;
});
list.setMultiChoiceModeListener(listChoiceListener);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Activities containing this fragment must implement its callbacks.
if (!(activity instanceof Callbacks)) {
throw new IllegalStateException("Activity must implement fragment's callbacks.");
}
callbacks = (Callbacks) activity;
}
@Override
public void onDetach() {
super.onDetach();
// Reset the active callbacks interface to the dummy implementation.
callbacks =dummyCallbacks;
}
@Override public void onResume() {
super.onResume();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
sessionReceiver, new IntentFilter(G.INTENT_SESSION_INVALIDATED));
}
@Override public void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(sessionReceiver);
}
@Override public void onDestroyView() {
torrentAdapter.clearResources();
super.onDestroyView();
}
@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
super.onListItemClick(listView, view, position, id);
if (actionMode == null)
listView.setChoiceMode(listChoiceMode);
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
callbacks.onItemSelected(position);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_FIND_SHOWN, findVisible);
outState.putString(STATE_FIND_QUERY, findQuery);
torrentAdapter.onSaveInstanceState(outState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_torrent_list, container, false);
}
@Override public void onCreateOptionsMenu(final Menu menu, MenuInflater inflater) {
if (getActivity() == null) {
return;
}
super.onCreateOptionsMenu(menu, inflater);
this.menu = menu;
inflater.inflate(R.menu.torrent_list_fragment, menu);
MenuItem item = menu.findItem(R.id.find);
SearchView findView = (SearchView) MenuItemCompat.getActionView(item);
findView.setQueryHint(getActivity().getString(R.string.filter));
findView.setIconifiedByDefault(true);
findView.setIconified(true);
if (findVisible) {
setFindVisibility();
}
findView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
if (!newText.equals(findQuery)) {
findQuery = newText;
findHandler.removeCallbacks(findRunnable);
findHandler.postDelayed(findRunnable, 500);
sharedPrefs.edit().putString(G.PREF_LIST_SEARCH, findQuery).apply();
}
return true;
}
});
MenuItemCompat.setOnActionExpandListener(item, new MenuItemCompat.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
findVisible = true;
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
findQuery = "";
findVisible = false;
setListFilter((String) null);
return true;
}
});
if (((TransmissionSessionInterface) getActivity()).getSession() != null) {
int visibleCount = setupSortMenu();
item = menu.findItem(R.id.sort);
item.setVisible(visibleCount > 0);
menu.findItem(R.id.find).setVisible(true);
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.sort_name:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.NAME);
return true;
case R.id.sort_size:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.SIZE);
return true;
case R.id.sort_status:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.STATUS);
return true;
case R.id.sort_activity:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.ACTIVITY);
return true;
case R.id.sort_age:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.AGE);
return true;
case R.id.sort_progress:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.PROGRESS);
return true;
case R.id.sort_ratio:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.RATIO);
return true;
case R.id.sort_location:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.LOCATION);
return true;
case R.id.sort_peers:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.PEERS);
return true;
case R.id.sort_download_speed:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.RATE_DOWNLOAD);
return true;
case R.id.sort_upload_speed:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.RATE_UPLOAD);
return true;
case R.id.sort_queue:
if (item.isChecked()) {
return false;
}
item.setChecked(true);
setListFilter(SortBy.QUEUE);
return true;
case R.id.sort_order:
setListFilter(item.isChecked() ? SortOrder.ASCENDING : SortOrder.DESCENDING);
item.setChecked(!item.isChecked());
return true;
default:
return false;
}
}
@Override
public void notifyTorrentListChanged(Cursor cursor, int error, boolean added, boolean removed,
boolean statusChanged, boolean metadataNeeded,
boolean connected) {
if (error == -1) {
return;
}
if (error > 0) {
if (error != DataService.Errors.DUPLICATE_TORRENT
&& error != DataService.Errors.INVALID_TORRENT
&& actionMode != null) {
actionMode.finish();
actionMode = null;
}
} else if (cursor != null) {
if (connected) {
getActivity().getSupportLoaderManager().restartLoader(G.TORRENT_LIST_TRAFFIC_LOADER_ID,
null, torrentTrafficLoaderCallbacks);
}
if (removed || added || (statusChanged && (
sharedPrefs.getString(G.PREF_BASE_SORT, "").equals(SortBy.STATUS.name())
|| sharedPrefs.getString(G.PREF_LIST_SORT_BY, "").equals(SortBy.STATUS.name())
))) {
final Map<Long, Integer> idTopMap = new HashMap<>();
final ListView listview = getListView();
int firstVisible = listview.getFirstVisiblePosition();
for (int i = 0; i < listview.getChildCount(); ++i) {
View child = listview.getChildAt(i);
int position = firstVisible + i;
long id = torrentAdapter.getItemId(position);
idTopMap.put(id, child.getTop());
}
torrentAdapter.changeCursor(cursor);
final ViewTreeObserver observer = listview.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
public boolean onPreDraw() {
observer.removeOnPreDrawListener(this);
boolean firstAnimation = true;
int firstVisible = listview.getFirstVisiblePosition();
for (int i = 0; i < listview.getChildCount(); ++i) {
final View child = listview.getChildAt(i);
int position = firstVisible + i;
long id = torrentAdapter.getItemId(position);
Integer startTop = idTopMap.get(id);
int top = child.getTop();
if (startTop == null) {
int childHeight = child.getHeight() + listview.getDividerHeight();
startTop = top + (i > 0 ? childHeight : -childHeight);
}
int delta = startTop - top;
if (delta != 0) {
child.animate().setDuration(250);
ObjectAnimator anim = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, delta, 0);
anim.setDuration(250);
anim.start();
if (firstAnimation) {
anim.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
}
});
}
firstAnimation = false;
}
}
return true;
}
});
} else {
torrentAdapter.changeCursor(cursor);
}
}
}
public void setEmptyMessage(int stringId) {
if (stringId == -1) {
getView().findViewById(R.id.swipe_container).setVisibility(View.VISIBLE);
getView().findViewById(R.id.empty_message).setVisibility(View.GONE);
getView().findViewById(R.id.empty_button).setVisibility(View.GONE);
} else {
getView().findViewById(R.id.swipe_container).setVisibility(View.GONE);
TextView message = (TextView) getView().findViewById(R.id.empty_message);
message.setVisibility(View.VISIBLE);
message.setText(Html.fromHtml(getString(stringId)));
Button button = (Button) getView().findViewById(R.id.empty_button);
if (stringId == R.string.no_profiles_empty_list) {
button.setText(R.string.add_profile_option);
button.setOnClickListener(v -> {
Intent intent = new Intent(getActivity(), SettingsActivity.class);
intent.putExtra(G.ARG_NEW_PROFILE, true);
startActivity(intent);
getActivity().overridePendingTransition(
R.anim.slide_in_top, android.R.anim.fade_out);
});
button.setVisibility(View.VISIBLE);
} else {
button.setVisibility(View.GONE);
}
}
}
/**
* Turns on activate-on-click mode. When this mode is on, list items will be
* given the 'activated' state when touched.
*/
public void setActivateOnItemClick(boolean activateOnItemClick) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
listChoiceMode = activateOnItemClick
? ListView.CHOICE_MODE_SINGLE
: ListView.CHOICE_MODE_NONE;
getListView().setChoiceMode(listChoiceMode);
}
public void setListFilter(String query) {
torrentAdapter.applyFilter(query, G.PREF_LIST_SEARCH, false);
scrollToTop = true;
}
public void setListFilter(FilterBy e) {
torrentAdapter.applyFilter(e.name(), G.PREF_LIST_FILTER, true);
scrollToTop = true;
}
public void setListFilter(SortBy e) {
torrentAdapter.applyFilter(e.name(), G.PREF_LIST_SORT_BY, true);
scrollToTop = true;
}
public void setListFilter(SortOrder e) {
torrentAdapter.applyFilter(e.name(), G.PREF_LIST_SORT_ORDER, true);
scrollToTop = true;
}
public void setListDirectoryFilter(String e) {
torrentAdapter.applyFilter(e, G.PREF_LIST_DIRECTORY, true);
scrollToTop = true;
}
public void setListTrackerFilter(String e) {
torrentAdapter.applyFilter(e, G.PREF_LIST_TRACKER, true);
scrollToTop = true;
}
public Cursor getCursor() {
return torrentAdapter.getCursor();
}
private void setActivatedPosition(int position) {
if (position == ListView.INVALID_POSITION) {
getListView().setItemChecked(activatedPosition, false);
} else {
getListView().setItemChecked(position, true);
}
activatedPosition = position;
}
private boolean showMoveDialog(final String[] hashStrings) {
final DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
if (manager == null) {
return true;
}
final TransmissionSession session = ((TransmissionSessionInterface) getActivity()).getSession();
if (session == null) {
return true;
}
final LocationDialogHelper helper =
((LocationDialogHelperInterface) getActivity()).getLocationDialogHelper();
AlertDialog dialog = helper.showDialog(R.layout.torrent_location_dialog,
R.string.set_location, null, (dialog1, which) -> {
LocationDialogHelper.Location location = helper.getLocation();
manager.setTorrentLocation(hashStrings, location.directory, location.moveData);
((TransmissionSessionInterface) getActivity()).setRefreshing(true,
DataService.Requests.SET_TORRENT_LOCATION);
if (actionMode != null) {
actionMode.finish();
}
}
);
TransmissionProfile profile = ((TransmissionProfileInterface) getActivity()).getProfile();
((CheckBox) dialog.findViewById(R.id.move)).setChecked(
profile != null && profile.getMoveData());
return true;
}
private boolean showQueueManagementDialog(final String[] hashStrings) {
((QueueManagementDialogHelperInterface) getActivity()).getQueueManagementDialogHelper()
.showDialog(hashStrings);
return true;
}
private void setFindVisibility() {
MenuItem item = menu.findItem(R.id.find);
if (actionMode != null) {
actionMode.finish();
}
item.setVisible(true);
MenuItemCompat.expandActionView(item);
final SearchView findView = (SearchView) MenuItemCompat.getActionView(item);
if (!findQuery.equals("")) {
findView.post(() -> findView.setQuery(findQuery, true));
}
}
private void toggleStatusBar() {
View status = getView().findViewById(R.id.status_bar);
toggleStatusBar(status);
}
private void toggleStatusBar(View status) {
if (sharedPrefs.getBoolean(G.PREF_SHOW_STATUS, true)) {
status.setVisibility(View.VISIBLE);
status.setTranslationY(100);
status.animate().alpha(1f).translationY(0).setStartDelay(500);
} else {
status.setVisibility(View.GONE);
status.setAlpha(0.3f);
}
}
private void toggleListItemChecked(boolean checked, final int position, final View typeChecked,
final View typeIndicator, final View progress) {
if (checked && ((TorrentListActivity) getActivity()).isDetailPanelVisible()) {
checked = false;
}
TimeInterpolator interpolator;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (checked) {
interpolator = new PathInterpolator(0f, 0f, 0.2f, 1f);
} else {
interpolator = new PathInterpolator(0.4f, 0f, 1f, 1f);
}
} else {
if (checked) {
interpolator = new DecelerateInterpolator();
} else {
interpolator = new AccelerateInterpolator();
}
}
int duration = getActivity().getResources().getInteger(android.R.integer.config_shortAnimTime);
typeChecked.animate().cancel();
checkAnimations.put(position, true);
if (checked) {
typeChecked.setScaleX(0f);
typeChecked.setScaleY(0f);
typeChecked.setVisibility(View.VISIBLE);
typeChecked.animate().scaleX(1f).scaleY(1f).setInterpolator(
interpolator
).setDuration(duration).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
checkAnimations.put(position, false);
typeIndicator.setVisibility(View.GONE);
progress.setVisibility(View.GONE);
}
});
} else {
typeIndicator.setVisibility(View.VISIBLE);
progress.setVisibility(View.VISIBLE);
typeChecked.animate().scaleX(0.3f).scaleY(0.3f).setInterpolator(
interpolator
).setDuration(duration).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
checkAnimations.put(position, false);
typeChecked.setVisibility(View.GONE);
}
});
}
}
private int setupSortMenu() {
int visibleOptions = 0;
SortBy selectedSort = SortBy.STATUS;
if (sharedPrefs.contains(G.PREF_LIST_SORT_BY)) {
try {
selectedSort = SortBy.valueOf(
sharedPrefs.getString(G.PREF_LIST_SORT_BY, "")
);
} catch (Exception ignored) { }
}
for (SortBy sort : SortBy.values()) {
boolean visible;
MenuItem item;
switch (sort) {
case NAME:
visible = sharedPrefs.getBoolean(G.PREF_SORT_NAME, true);
item = menu.findItem(R.id.sort_name);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case SIZE:
visible = sharedPrefs.getBoolean(G.PREF_SORT_SIZE, true);
item = menu.findItem(R.id.sort_size);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case STATUS:
visible = sharedPrefs.getBoolean(G.PREF_SORT_STATUS, true);
item = menu.findItem(R.id.sort_status);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case ACTIVITY:
visible = sharedPrefs.getBoolean(G.PREF_SORT_ACTIVITY, true);
item = menu.findItem(R.id.sort_activity);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case AGE:
visible = sharedPrefs.getBoolean(G.PREF_SORT_AGE, true);
item = menu.findItem(R.id.sort_age);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case PROGRESS:
visible = sharedPrefs.getBoolean(G.PREF_SORT_PROGRESS, true);
item = menu.findItem(R.id.sort_progress);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case RATIO:
visible = sharedPrefs.getBoolean(G.PREF_SORT_RATIO, true);
item = menu.findItem(R.id.sort_ratio);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case LOCATION:
visible = sharedPrefs.getBoolean(G.PREF_SORT_LOCATION, true);
item = menu.findItem(R.id.sort_location);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case PEERS:
visible = sharedPrefs.getBoolean(G.PREF_SORT_PEERS, true);
item = menu.findItem(R.id.sort_peers);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case RATE_DOWNLOAD:
visible = sharedPrefs.getBoolean(G.PREF_SORT_RATE_DOWNLOAD, true);
item = menu.findItem(R.id.sort_download_speed);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case RATE_UPLOAD:
visible = sharedPrefs.getBoolean(G.PREF_SORT_RATE_UPLOAD, true);
item = menu.findItem(R.id.sort_upload_speed);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
case QUEUE:
visible = sharedPrefs.getBoolean(G.PREF_SORT_QUEUE, true);
item = menu.findItem(R.id.sort_queue);
item.setVisible(visible);
if (sort == selectedSort) {
item.setChecked(true);
}
if (visible) {
++visibleOptions;
}
break;
}
}
MenuItem order = menu.findItem(R.id.sort_order);
if (visibleOptions > 0) {
order.setVisible(true);
SortOrder selectedOrder = SortOrder.DESCENDING;
if (sharedPrefs.contains(G.PREF_LIST_SORT_ORDER)) {
try {
selectedOrder = SortOrder.valueOf(
sharedPrefs.getString(G.PREF_LIST_SORT_ORDER, "")
);
} catch (Exception ignored) { }
}
order.setChecked(selectedOrder == SortOrder.DESCENDING);
} else {
order.setVisible(false);
}
return visibleOptions;
}
private class TorrentCursorAdapter extends CursorAdapter {
private SparseBooleanArray addedTorrents = new SparseBooleanArray();
private DataSource readDataSource;
private boolean resourcesCleared = false;
private final Object lock = new Object();
private static final String STATE_ADDED_TORRENTS = "adapter_added_torrents";
public TorrentCursorAdapter(Context context, Bundle state) {
super(context, null, 0);
resourcesCleared = false;
readDataSource = new DataSource(context);
if (state != null) {
onRestoreInstanceState(state);
}
setFilterQueryProvider(new FilterQueryProvider() {
@Override public Cursor runQuery(CharSequence charSequence) {
synchronized (lock) {
if (resourcesCleared) {
return null;
}
TransmissionProfileInterface context =
(TransmissionProfileInterface) getActivity();
if (context == null || context.getProfile() == null) {
return null;
}
readDataSource.open();
return readDataSource.getTorrentCursor(context.getProfile().getId(),
PreferenceManager.getDefaultSharedPreferences(getActivity()));
}
}
});
}
public void clearResources() {
synchronized (lock) {
resourcesCleared = true;
readDataSource.close();
}
}
public void onSaveInstanceState(Bundle outState) {
int[] ids = new int[addedTorrents.size()];
for (int i = 0; i < addedTorrents.size(); ++i) {
int id = addedTorrents.keyAt(i);
ids[i] = id;
}
outState.putIntArray(STATE_ADDED_TORRENTS, ids);
}
public void onRestoreInstanceState(Bundle state) {
if (state.containsKey(STATE_ADDED_TORRENTS)) {
int[] ids = state.getIntArray(STATE_ADDED_TORRENTS);
if (ids != null) {
for (int id : ids) {
addedTorrents.append(id, true);
}
}
}
}
private void applyFilter(String value, String pref, boolean animate) {
if (actionMode != null) {
actionMode.finish();
}
if (pref != null) {
Editor e = sharedPrefs.edit();
e.putString(pref, value);
e.apply();
G.requestBackup(getActivity());
}
getFilter().filter("");
if (animate) {
addedTorrents = new SparseBooleanArray();
}
}
@Override public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
LayoutInflater vi = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return vi.inflate(R.layout.torrent_list_item, viewGroup, false);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override public void bindView(final View view, Context context, Cursor cursor) {
final int id = Torrent.getId(cursor);
TextView name = (TextView) view.findViewById(R.id.name);
TextView traffic = (TextView) view.findViewById(R.id.traffic);
ProgressBar progress = (ProgressBar) view.findViewById(R.id.progress);
TextView status = (TextView) view.findViewById(R.id.status);
TextView errorText = (TextView) view.findViewById(R.id.error_text);
View typeDirectory = view.findViewById(R.id.type_directory);
View typeChecked = view.findViewById(R.id.type_checked);
String search = sharedPrefs.getString(G.PREF_LIST_SEARCH, null);
if (TextUtils.isEmpty(search)) {
name.setText(Torrent.getName(cursor));
} else {
name.setText(G.trimTrailingWhitespace(Html.fromHtml(Torrent.getName(cursor))));
}
float metadata = Torrent.getMetadataPercentDone(cursor);
float percent = Torrent.getPercentDone(cursor);
if (metadata < 1) {
progress.setSecondaryProgress((int) (metadata * 100));
progress.setProgress(0);
} else if (percent < 1) {
progress.setSecondaryProgress((int) (percent * 100));
progress.setProgress(0);
} else {
progress.setSecondaryProgress(100);
int mode = Torrent.getSeedRatioMode(cursor);
float limit = Torrent.getSeedRatioLimit(cursor);
float current = Torrent.getUploadRatio(cursor);
if (mode == Torrent.SeedRatioMode.NO_LIMIT) {
limit = 0;
} else if (mode == Torrent.SeedRatioMode.GLOBAL_LIMIT) {
TransmissionSession session = ((TransmissionSessionInterface) getActivity()).getSession();
if (session != null) {
if (session.isSeedRatioLimitEnabled()) {
limit = session.getSeedRatioLimit();
} else {
limit = 0;
}
}
}
if (limit <= 0) {
progress.setProgress(100);
} else {
if (current >= limit) {
progress.setProgress(100);
} else {
progress.setProgress((int) (current / limit * 100));
}
}
}
final int position = cursor.getPosition();
progress.setOnClickListener(v -> {
if (actionMode == null) {
if (!((TorrentListActivity) getActivity()).isDetailPanelVisible()) {
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
setActivatedPosition(position);
} else {
onListItemClick(getListView(), view, position, id);
}
} else {
SparseBooleanArray checkedItems = getListView().getCheckedItemPositions();
getListView().setItemChecked(position, !checkedItems.get(position));
}
});
traffic.setText(Html.fromHtml(Torrent.getTrafficText(cursor)));
status.setText(Html.fromHtml(Torrent.getStatusText(cursor)));
int torrentStatus = Torrent.getStatus(cursor);
boolean enabled = Torrent.isActive(torrentStatus);
name.setEnabled(enabled);
traffic.setEnabled(enabled);
status.setEnabled(enabled);
errorText.setEnabled(enabled);
progress.setAlpha(enabled ? 1f : 0.5f);
typeDirectory.setAlpha(enabled ? 0.6f : 0.3f);
if (Torrent.getError(cursor) == Torrent.Error.OK) {
errorText.setVisibility(View.GONE);
} else {
errorText.setVisibility(View.VISIBLE);
errorText.setText(Torrent.getErrorString(cursor));
}
SparseBooleanArray checkedItems = getListView().getCheckedItemPositions();
if (checkedItems != null && checkedItems.get(position)) {
if (!checkAnimations.get(position) && !((TorrentListActivity) getActivity()).isDetailPanelVisible()) {
typeDirectory.setVisibility(View.GONE);
progress.setVisibility(View.GONE);
typeChecked.setVisibility(View.VISIBLE);
typeChecked.setScaleX(1f);
typeChecked.setScaleY(1f);
}
} else {
if (!checkAnimations.get(position)) {
typeDirectory.setVisibility(View.VISIBLE);
progress.setVisibility(View.VISIBLE);
typeChecked.setVisibility(View.GONE);
}
if ("inode/directory".equals(Torrent.getMimeType(cursor))) {
typeDirectory.setVisibility(View.VISIBLE);
} else {
typeDirectory.setVisibility(View.GONE);
}
}
if (!addedTorrents.get(id, false)) {
view.setTranslationY(100);
view.setAlpha((float) 0.3);
view.setRotationX(10);
view.animate().setDuration(300).translationY(0).alpha(1).rotationX(0).start();
addedTorrents.append(id, true);
}
}
@Override public Cursor swapCursor(Cursor newCursor) {
Cursor oldCursor = super.swapCursor(newCursor);
if (!isAdded()) {
return oldCursor;
}
if (newCursor.getCount() == 0) {
if (sharedPrefs.getString(G.PREF_LIST_SEARCH, "").equals("")
&& sharedPrefs.getString(G.PREF_LIST_DIRECTORY, "").equals("")
&& sharedPrefs.getString(G.PREF_LIST_TRACKER, "").equals("")
&& sharedPrefs.getString(G.PREF_LIST_FILTER, FilterBy.ALL.name()).equals(FilterBy.ALL.name())) {
setEmptyMessage(R.string.no_torrents_empty_list);
} else {
setEmptyMessage(R.string.no_filtered_torrents_empty_list);
}
} else {
setEmptyMessage(-1);
}
if (scrollToTop) {
scrollToTop = false;
if (TorrentListFragment.this.getView() != null) {
getListView().setSelectionAfterHeaderView();
}
}
if (initialLoading) {
initialLoading = false;
final int position = getListView().getCheckedItemPosition();
if (position != ListView.INVALID_POSITION) {
new Handler().postDelayed(() -> callbacks.onItemSelected(position), 500);
}
}
return oldCursor;
}
}
private class SessionReceiver extends BroadcastReceiver {
@Override public void onReceive(Context context, Intent intent) {
if (getActivity() == null || menu == null) {
return;
}
if (intent.getBooleanExtra(G.ARG_SESSION_VALID, false)) {
menu.findItem(R.id.sort).setVisible(true);
menu.findItem(R.id.find).setVisible(true);
} else {
menu.findItem(R.id.sort).setVisible(false);
menu.findItem(R.id.find).setVisible(false);
}
}
}
}