Manage your torrents from your Android device
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

501 lines
20 KiB

/*
* Copyright 2010-2018 Eric Kok et al.
*
* Transdroid 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.
*
* Transdroid 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 Transdroid. If not, see <http://www.gnu.org/licenses/>.
*/
package org.transdroid.core.gui;
import android.content.Context;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.ActionMenuView;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.InstanceState;
import org.androidannotations.annotations.ItemClick;
import org.androidannotations.annotations.ViewById;
import org.transdroid.R;
import org.transdroid.core.app.settings.ApplicationSettings;
import org.transdroid.core.app.settings.SystemSettings;
import org.transdroid.core.gui.lists.TorrentsAdapter;
import org.transdroid.core.gui.lists.TorrentsAdapter_;
import org.transdroid.core.gui.navigation.Label;
import org.transdroid.core.gui.navigation.NavigationFilter;
import org.transdroid.core.gui.navigation.RefreshableActivity;
import org.transdroid.core.gui.navigation.SelectionManagerMode;
import org.transdroid.core.gui.navigation.SetLabelDialog;
import org.transdroid.core.gui.navigation.SetLabelDialog.OnLabelPickedListener;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentsComparator;
import org.transdroid.daemon.TorrentsSortBy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Locale;
/**
* Fragment that shows a list of torrents that are active on the server. It supports sorting and filtering and can show connection progress and
* issues. However, actual task starting and execution and overall navigation elements are part of the containing activity, not this fragment.
*
* @author Eric Kok
*/
@EFragment(R.layout.fragment_torrents)
public class TorrentsFragment extends Fragment implements OnLabelPickedListener {
// HACK Working around #391 while hopefully we rework the UI in the future to persist the list in db or something
protected static ArrayList<Torrent> torrents = null;
// Local data
@Bean
protected ApplicationSettings applicationSettings;
@Bean
protected SystemSettings systemSettings;
@InstanceState
protected ArrayList<Torrent> lastMultiSelectedTorrents;
@InstanceState
protected ArrayList<Label> currentLabels;
@InstanceState
protected NavigationFilter currentNavigationFilter = null;
@InstanceState
protected TorrentsSortBy currentSortOrder = TorrentsSortBy.Alphanumeric;
@InstanceState
protected boolean currentSortDescending = false;
@InstanceState
protected String currentTextFilter = null;
@InstanceState
protected boolean hasAConnection = false;
@InstanceState
protected boolean isLoading = true;
@InstanceState
protected String connectionErrorMessage = null;
@InstanceState
protected Daemon daemonType;
// Views
@ViewById
protected SwipeRefreshLayout swipeRefreshLayout;
@ViewById
protected ListView torrentsList;
@ViewById
protected TextView emptyText;
@ViewById
protected TextView nosettingsText;
@ViewById
protected TextView errorText;
@ViewById
protected ProgressBar loadingProgress;
private MultiChoiceModeListener onTorrentsSelected = new MultiChoiceModeListener() {
private SelectionManagerMode selectionManagerMode;
private ActionMenuView actionsMenu;
private Toolbar actionsToolbar;
private FloatingActionsMenu addmenuButton;
@Override
public boolean onCreateActionMode(final ActionMode mode, Menu menu) {
// Show contextual action bars to start/stop/remove/etc. torrents in batch mode
if (actionsMenu == null) {
actionsMenu = ((TorrentsActivity) getActivity()).contextualMenu;
actionsToolbar = ((TorrentsActivity) getActivity()).actionsToolbar;
addmenuButton = ((TorrentsActivity) getActivity()).addmenuButton;
}
actionsToolbar.setEnabled(false);
actionsMenu.setVisibility(View.VISIBLE);
addmenuButton.setVisibility(View.GONE);
actionsMenu.setOnMenuItemClickListener(menuItem -> onActionItemClicked(mode, menuItem));
actionsMenu.getMenu().clear();
getActivity().getMenuInflater().inflate(R.menu.fragment_torrents_cab, actionsMenu.getMenu());
Context themedContext = ((AppCompatActivity) getActivity()).getSupportActionBar().getThemedContext();
selectionManagerMode = new SelectionManagerMode(themedContext, torrentsList, R.plurals.navigation_torrentsselected);
selectionManagerMode.onCreateActionMode(mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
selectionManagerMode.onPrepareActionMode(mode, menu);
// Hide/show options depending on the type of server we are connected to
if (daemonType != null) {
actionsMenu.getMenu().findItem(R.id.action_start).setVisible(Daemon.supportsStoppingStarting(daemonType));
actionsMenu.getMenu().findItem(R.id.action_stop).setVisible(Daemon.supportsStoppingStarting(daemonType));
actionsMenu.getMenu().findItem(R.id.action_setlabel).setVisible(Daemon.supportsSetLabel(daemonType));
}
// Pause autorefresh
if (getActivity() != null && getActivity() instanceof TorrentsActivity) {
((TorrentsActivity) getActivity()).stopRefresh = true;
((TorrentsActivity) getActivity()).stopAutoRefresh();
}
return true;
}
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// Get checked torrents
ArrayList<Torrent> checked = new ArrayList<>();
for (int i = 0; i < torrentsList.getCheckedItemPositions().size(); i++) {
if (torrentsList.getCheckedItemPositions().valueAt(i) && i < torrentsList.getAdapter().getCount()) {
checked.add((Torrent) torrentsList.getAdapter().getItem(torrentsList.getCheckedItemPositions().keyAt(i)));
}
}
int itemId = item.getItemId();
if (itemId == R.id.action_resume) {
for (Torrent torrent : checked) {
getTasksExecutor().resumeTorrent(torrent);
}
mode.finish();
return true;
} else if (itemId == R.id.action_pause) {
for (Torrent torrent : checked) {
getTasksExecutor().pauseTorrent(torrent);
}
mode.finish();
return true;
} else if (itemId == R.id.action_start) {
for (Torrent torrent : checked) {
getTasksExecutor().startTorrent(torrent, false);
}
mode.finish();
return true;
} else if (itemId == R.id.action_stop) {
for (Torrent torrent : checked) {
getTasksExecutor().stopTorrent(torrent);
}
mode.finish();
return true;
} else if (itemId == R.id.action_remove_default) {
for (Torrent torrent : checked) {
getTasksExecutor().removeTorrent(torrent, false);
}
mode.finish();
return true;
} else if (itemId == R.id.action_remove_withdata) {
for (Torrent torrent : checked) {
getTasksExecutor().removeTorrent(torrent, true);
}
mode.finish();
return true;
} else if (itemId == R.id.action_setlabel) {
lastMultiSelectedTorrents = checked;
if (currentLabels != null) {
SetLabelDialog.show(getActivity(), TorrentsFragment.this, currentLabels);
}
mode.finish();
return true;
} else {
return false;
}
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked);
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Resume autorefresh
if (getActivity() != null && getActivity() instanceof TorrentsActivity) {
((TorrentsActivity) getActivity()).stopRefresh = false;
((TorrentsActivity) getActivity()).startAutoRefresh();
}
selectionManagerMode.onDestroyActionMode(mode);
actionsMenu.setVisibility(View.GONE);
actionsToolbar.setEnabled(true);
addmenuButton.setVisibility(View.VISIBLE);
}
};
@AfterViews
protected void init() {
// Load the requested sort order from the user settings
this.currentSortOrder = applicationSettings.getLastUsedSortOrder();
this.currentSortDescending = applicationSettings.getLastUsedSortDescending();
// Set up the list adapter, which allows multi-select and fast scrolling
torrentsList.setAdapter(TorrentsAdapter_.getInstance_(getActivity()));
torrentsList.setMultiChoiceModeListener(onTorrentsSelected);
torrentsList.setFastScrollEnabled(true);
if (torrents != null) {
updateTorrents(torrents, currentLabels);
}
// Allow pulls on the list view to refresh the torrents
if (getActivity() != null && getActivity() instanceof RefreshableActivity) {
swipeRefreshLayout.setOnRefreshListener(() -> {
((RefreshableActivity) getActivity()).refreshScreen();
swipeRefreshLayout.setRefreshing(false); // Use our custom indicator
});
}
nosettingsText.setText(getString(R.string.navigation_nosettings, getString(R.string.app_name)));
}
/**
* Updates the list adapter to show a new list of torrent objects, replacing the old torrents completely
*
* @param newTorrents The new, updated list of torrents
*/
public void updateTorrents(ArrayList<Torrent> newTorrents, ArrayList<Label> currentLabels) {
if (this.isDetached()) {
return;
}
torrents = newTorrents;
this.currentLabels = currentLabels;
applyAllFilters();
}
/**
* Just look for a specific torrent in the currently shown list (by its unique id) and update only this
*
* @param affected The affected torrent to update
* @param wasRemoved Whether the affected torrent was indeed removed; otherwise it was updated somehow
*/
public void quickUpdateTorrent(Torrent affected, boolean wasRemoved) {
if (this.isDetached()) {
return;
}
// Remove the old torrent object first
Iterator<Torrent> iter = torrents.iterator();
while (iter.hasNext()) {
Torrent torrent = iter.next();
if (torrent.getUniqueID().equals(affected.getUniqueID())) {
iter.remove();
break;
}
}
// In case it was an update, add the updated torrent object
if (!wasRemoved) {
torrents.add(affected);
}
// Now refresh the screen
applyAllFilters();
}
/**
* Clears the currently visible list of torrents.
*
* @param clearError Also clear any error message
* @param clearFilter Also clear any selected filter
*/
public void clear(boolean clearError, boolean clearFilter) {
torrents = null;
if (clearError) {
this.connectionErrorMessage = null;
}
if (clearFilter) {
this.currentTextFilter = null;
this.currentNavigationFilter = null;
}
applyAllFilters();
}
/**
* Stores the new sort order (for future refreshes) and sorts the current visible list. If the given new sort property equals the existing
* property, the list sort order is reversed instead.
*
* @param newSortOrder The sort order that the user selected.
*/
public void sortBy(TorrentsSortBy newSortOrder) {
// Update the sort order property and direction and store this last used setting
if (this.currentSortOrder == newSortOrder) {
this.currentSortDescending = !this.currentSortDescending;
} else {
this.currentSortOrder = newSortOrder;
this.currentSortDescending = false;
}
applicationSettings.setLastUsedSortOrder(this.currentSortOrder, this.currentSortDescending);
applyAllFilters();
}
public void applyTextFilter(String newTextFilter) {
this.currentTextFilter = newTextFilter;
// Show the new filtered list
applyAllFilters();
}
/**
* Apply a filter on the current list of all torrents, showing the appropriate sublist of torrents only
*
* @param newFilter The new filter to apply to the local list of torrents
*/
public void applyNavigationFilter(NavigationFilter newFilter) {
this.currentNavigationFilter = newFilter;
applyAllFilters();
}
private void applyAllFilters() {
// No torrents? Directly update views accordingly
if (torrents == null) {
updateViewVisibility();
return;
}
// Filter the list of torrents to show according to navigation and text filters
ArrayList<Torrent> filteredTorrents = new ArrayList<>(torrents);
if (currentNavigationFilter != null) {
// Remove torrents that do not match the selected navigation filter
for (Iterator<Torrent> torrentIter = filteredTorrents.iterator(); torrentIter.hasNext(); ) {
if (!currentNavigationFilter.matches(torrentIter.next(), systemSettings.treatDormantAsInactive())) {
torrentIter.remove();
}
}
}
if (currentTextFilter != null) {
// Remove torrents that do not contain the text filter string
for (Iterator<Torrent> torrentIter = filteredTorrents.iterator(); torrentIter.hasNext(); ) {
if (!torrentIter.next().getName().toLowerCase(Locale.getDefault()).contains(currentTextFilter.toLowerCase(Locale.getDefault()))) {
torrentIter.remove();
}
}
}
// Sort the list of filtered torrents
Collections.sort(filteredTorrents, new TorrentsComparator(daemonType, this.currentSortOrder, this.currentSortDescending));
if (torrentsList != null && torrentsList.getAdapter() != null) {
((TorrentsAdapter) torrentsList.getAdapter()).update(filteredTorrents);
}
updateViewVisibility();
}
@Click
protected void emptyTextClicked() {
// Refresh the activity (that contains this fragment) when the empty view gear is clicked
if (getActivity() != null && getActivity() instanceof RefreshableActivity) {
((RefreshableActivity) getActivity()).refreshScreen();
}
}
@Click
protected void errorTextClicked() {
// Refresh the activity (that contains this fragment) when the error view gear is clicked
if (getActivity() != null && getActivity() instanceof RefreshableActivity) {
((RefreshableActivity) getActivity()).refreshScreen();
}
}
@ItemClick(R.id.torrents_list)
protected void torrentsListClicked(Torrent torrent) {
// Show the torrent details fragment
((TorrentsActivity) getActivity()).openDetails(torrent);
}
@Override
public void onLabelPicked(String newLabel) {
for (Torrent torrent : lastMultiSelectedTorrents) {
getTasksExecutor().updateLabel(torrent, newLabel);
}
}
/**
* Updates the shown screen depending on whether we have a connection (so torrents can be shown) or not (in case we need to show a message
* suggesting help). This should only ever be called on the UI thread.
*
* @param hasAConnection True if the user has servers configured and therefore has a connection that can be used
*/
public void updateConnectionStatus(boolean hasAConnection, Daemon daemonType) {
if (!isResumed()) return;
this.hasAConnection = hasAConnection;
this.daemonType = daemonType;
if (!hasAConnection) {
torrentsList.setVisibility(View.GONE);
emptyText.setVisibility(View.GONE);
loadingProgress.setVisibility(View.GONE);
errorText.setVisibility(View.GONE);
nosettingsText.setVisibility(View.VISIBLE);
swipeRefreshLayout.setEnabled(false);
clear(true, true); // Indirectly also calls updateViewVisibility()
} else {
updateViewVisibility();
}
}
/**
* Updates the shown screen depending on whether the torrents are loading. This should only ever be called on the UI thread.
*
* @param isLoading True if the list of torrents is (re)loading, false otherwise
*/
public void updateIsLoading(boolean isLoading) {
if (!isResumed()) return;
this.isLoading = isLoading;
if (isLoading) {
clear(true, false); // Indirectly also calls updateViewVisibility()
} else {
updateViewVisibility();
}
}
/**
* Updates the shown screen depending on whether a connection error occurred. This should only ever be called on the UI thread.
*
* @param connectionErrorMessage The error message from the last failed connection attempt, or null to clear the visible error text
*/
public void updateError(String connectionErrorMessage) {
if (!isResumed()) return;
this.connectionErrorMessage = connectionErrorMessage;
errorText.setText(connectionErrorMessage);
if (connectionErrorMessage != null) {
clear(false, false); // Indirectly also calls updateViewVisibility()
} else {
updateViewVisibility();
}
}
private void updateViewVisibility() {
if (!hasAConnection) {
return;
}
boolean isEmpty = torrents == null || torrentsList.getAdapter() != null && torrentsList.getAdapter().isEmpty();
boolean hasError = connectionErrorMessage != null;
nosettingsText.setVisibility(View.GONE);
errorText.setVisibility(hasError ? View.VISIBLE : View.GONE);
torrentsList.setVisibility(!hasError && !isLoading && !isEmpty ? View.VISIBLE : View.GONE);
loadingProgress.setVisibility(!hasError && isLoading ? View.VISIBLE : View.GONE);
emptyText.setVisibility(!hasError && !isLoading && isEmpty ? View.VISIBLE : View.GONE);
swipeRefreshLayout.setEnabled(true);
}
/**
* Returns the object responsible for executing torrent tasks against a connected server
*
* @return The executor for tasks on some torrent
*/
private TorrentTasksExecutor getTasksExecutor() {
// NOTE: Assumes the activity implements all the required torrent tasks
return (TorrentTasksExecutor) getActivity();
}
}