package org.sugr.gearshift.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.CardView;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import org.sugr.gearshift.G;
import org.sugr.gearshift.R;
import org.sugr.gearshift.core.Torrent;
import org.sugr.gearshift.datasource.DataSource;
import org.sugr.gearshift.datasource.TorrentDetails;
import org.sugr.gearshift.service.DataService;
import org.sugr.gearshift.service.DataServiceManager;
import org.sugr.gearshift.service.DataServiceManagerInterface;
import org.sugr.gearshift.ui.util.ExpandAnimation;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.sugr.gearshift.core.Torrent.SetterFields;
/**
* A fragment representing a single Torrent detail screen.
* This fragment is either contained in a {@link org.sugr.gearshift.ui.TorrentListActivity}
* in two-pane mode (on tablets) or a {@link org.sugr.gearshift.ui.TorrentDetailActivity}
* on handsets.
*/
public class TorrentDetailPageFragment extends Fragment {
private String torrentHash;
private TorrentDetails details;
private static List<String> priorityNames;
private static List<String> priorityValues;
private static List<String> seedRatioModeValues;
private static final String STATE_EXPANDED = "expanded_states";
private static final String STATE_SCROLL_POSITION = "scroll_position_state";
private static final String STATE_TORRENT_HASH = "torrent_hash";
private static class Expanders {
public static final int TOTAL_EXPANDERS = 4;
public static final int OVERVIEW = 0;
public static final int FILES = 1;
public static final int LIMITS = 2;
public static final int TRACKERS = 3;
}
private class Views {
public LinearLayout overviewContent;
public LinearLayout filesContent;
public LinearLayout limitsContent;
public LinearLayout trackersContent;
public LinearLayout trackersContainer;
public TextView name;
public TextView statusText;
public TextView have;
public TextView downloaded;
public TextView uploaded;
public TextView runningTime;
public TextView remainingTime;
public TextView lastActivity;
public TextView size;
public TextView hash;
public TextView privacy;
public TextView origin;
public TextView comment;
public TextView state;
public TextView added;
public TextView queue;
public TextView error;
public TextView location;
public CheckBox globalLimits;
public Spinner torrentPriority;
public EditText queuePosition;
public CheckBox downloadLimited;
public EditText downloadLimit;
public CheckBox uploadLimited;
public EditText uploadLimit;
public Spinner seedRatioMode;
public EditText seedRatioLimit;
public EditText peerLimit;
}
private Views views;
private boolean[] expandedStates = new boolean[Expanders.TOTAL_EXPANDERS];
private Map<String, Long> debouncers = new HashMap<>();
private View.OnTouchListener expanderTouchListener = new View.OnTouchListener() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override public boolean onTouch(View v, MotionEvent event) {
boolean handleRaise = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_CANCEL:
handleRaise = true;
break;
case MotionEvent.ACTION_UP:
View image;
final View content;
int index;
switch(v.getId()) {
case R.id.torrent_detail_overview_expander:
image = v.findViewById(R.id.torrent_detail_overview_expander_image);
content = views.overviewContent;
index = Expanders.OVERVIEW;
break;
case R.id.torrent_detail_files_expander:
image = v.findViewById(R.id.torrent_detail_files_expander_image);
content = views.filesContent;
index = Expanders.FILES;
break;
case R.id.torrent_detail_limits_expander:
image = v.findViewById(R.id.torrent_detail_limits_expander_image);
content = views.limitsContent;
index = Expanders.LIMITS;
break;
case R.id.torrent_detail_trackers_expander:
image = v.findViewById(R.id.torrent_detail_trackers_expander_image);
content = views.trackersContent;
index = Expanders.TRACKERS;
break;
default:
return false;
}
final boolean expand = content.getVisibility() == View.GONE;
image.animate().cancel();
if (expand) {
new ExpandAnimation(content).expand();
image.setRotation(0);
expandedStates[index] = true;
updateFields(getView());
} else {
new ExpandAnimation(content).collapse();
image.setRotation(180);
expandedStates[index] = false;
}
image.animate().rotationBy(180);
handleRaise = true;
break;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && handleRaise) {
View parent = v;
while (parent.getParent() != null) {
parent = (View) parent.getParent();
if (parent.getClass() == CardView.class) {
parent.animate().translationZ(event.getAction() == MotionEvent.ACTION_DOWN
? getResources().getDimension(R.dimen.card_raised_translation) : 0);
break;
}
}
}
return false;
}
};
private FilesAdapter filesAdapter;
private TrackersAdapter trackersAdapter;
private TrackersDataSetObserver trackersObserver;
private ActionMode fileActionMode;
private Set<View> selectedFiles = new HashSet<>();
private Map<String, Integer> fieldModifiers = new HashMap<>();
private ActionMode.Callback actionModeFiles = new ActionMode.Callback() {
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
fileActionMode= null;
for (View v : selectedFiles) {
v.setActivated(false);
}
selectedFiles.clear();
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.torrent_detail_file_multiselect, menu);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
String key;
Integer priority = null;
switch (item.getItemId()) {
case R.id.select_all:
List<View> files = filesAdapter.getViews();
for (View v : files) {
if (v != null) {
if (!v.isActivated()) {
v.setActivated(true);
selectedFiles.add(v);
}
}
}
invalidateFileActionMenu(mode.getMenu());
return true;
case R.id.priority_low:
key = SetterFields.FILES_LOW;
priority = Torrent.Priority.LOW;
break;
case R.id.priority_normal:
key = SetterFields.FILES_NORMAL;
priority = Torrent.Priority.NORMAL;
break;
case R.id.priority_high:
key = SetterFields.FILES_HIGH;
priority = Torrent.Priority.HIGH;
break;
case R.id.check_selected:
key = SetterFields.FILES_UNWANTED;
break;
case R.id.uncheck_selected:
key = SetterFields.FILES_WANTED;
break;
default:
return false;
}
List<View> allViews = filesAdapter.getViews();
ArrayList<Integer> indexes = new ArrayList<>();
for (View v : selectedFiles) {
TorrentFile file = filesAdapter.getItem(allViews.indexOf(v));
if (priority == null) {
if ((file.wanted
&& key.equals(
SetterFields.FILES_UNWANTED))
|| (!file.wanted
&& key.equals(
SetterFields.FILES_WANTED))
) {
indexes.add(file.index);
}
} else {
if (file.priority != priority) {
indexes.add(file.index);
file.changed = true;
file.priority = priority;
}
}
}
if (indexes.size() > 0) {
setTorrentProperty(key, indexes);
filesAdapter.notifyDataSetChanged();
if (priority == null) {
mode.finish();
((TransmissionSessionInterface) getActivity()).setRefreshing(true,
DataService.Requests.GET_TORRENTS);
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
if (manager != null) {
manager.update();
}
}
}
return true;
}
};
private ActionMode trackerActionMode;
private Set<View> selectedTrackers = new HashSet<>();
private ActionMode.Callback actionModeTrackers = new ActionMode.Callback() {
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
trackerActionMode = null;
for (View v : selectedTrackers) {
v.setActivated(false);
}
selectedTrackers.clear();
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.torrent_detail_tracker_multiselect, menu);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
String key;
switch (item.getItemId()) {
case R.id.select_all:
List<View> trackers = trackersAdapter.getViews();
for (View v : trackers) {
if (v != null) {
View info = v.findViewById(R.id.torrent_detail_trackers_row_info);
if (info != null && !info.isActivated()) {
info.setActivated(true);
selectedTrackers.add(info);
}
}
}
return true;
case R.id.remove:
key = SetterFields.TRACKER_REMOVE;
break;
default:
return false;
}
ArrayList<Integer> ids = new ArrayList<>();
for (View v : selectedTrackers) {
View parent = (View) v.getParent();
Tracker tracker = trackersAdapter.getItem(
trackersAdapter.getViews().indexOf(parent));
ids.add(tracker.id);
trackersAdapter.remove(tracker);
}
if (SetterFields.TRACKER_REMOVE.equals(key)) {
trackersObserver.setFrozen(true);
}
if (ids.size() > 0) {
setTorrentProperty(key, ids);
mode.finish();
((TransmissionSessionInterface) getActivity()).setRefreshing(true,
DataService.Requests.SET_TORRENT);
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
if (manager != null) {
manager.update();
}
}
return true;
}
};
private Runnable loseFocusRunnable =
() -> getView().findViewById(R.id.torrent_detail_page_container).requestFocus();
private class UpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String hash = intent.getStringExtra(G.ARG_TORRENT_HASH_STRING);
if (hash != null && hash.equals(torrentHash) && getActivity() != null) {
TorrentDetailFragment fragment
= (TorrentDetailFragment) getActivity().getSupportFragmentManager().findFragmentByTag(G.DETAIL_FRAGMENT_TAG);
if (fragment != null) {
new TorrentDetailTask().execute(torrentHash);
}
}
}
}
private UpdateReceiver updateReceiver;
private class PageUnselectedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (fileActionMode != null) {
fileActionMode.finish();
}
if (trackerActionMode != null) {
trackerActionMode.finish();
}
}
}
private PageUnselectedReceiver pageUnselectedReceiver;
private class ServiceReceiver extends BroadcastReceiver {
@Override public void onReceive(Context unused, Intent intent) {
TransmissionProfileInterface context = (TransmissionProfileInterface) getActivity();
if (context == null) {
return;
}
String profileId = intent.getStringExtra(G.ARG_PROFILE_ID);
if (profileId == null || !profileId.equals(context.getProfile().getId())) {
return;
}
String type = intent.getStringExtra(G.ARG_REQUEST_TYPE);
int error = intent.getIntExtra(G.ARG_ERROR, 0);
switch (type) {
case DataService.Requests.SET_TORRENT:
String field = intent.getStringExtra(G.ARG_TORRENT_FIELD);
synchronized (this) {
if (error == 0 && field != null) {
if (!isFieldEnabled(field)) {
fieldModifiers.put(field, fieldModifiers.get(field) - 1);
}
} else {
fieldModifiers.clear();
}
}
if (SetterFields.TRACKER_REMOVE.equals(field)) {
trackersObserver.setFrozen(false);
}
break;
}
}
}
protected ServiceReceiver serviceReceiver;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public TorrentDetailPageFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (priorityNames == null)
priorityNames = Arrays.asList(getResources().getStringArray(R.array.torrent_priority));
if (priorityValues == null)
priorityValues = Arrays.asList(getResources().getStringArray(R.array.torrent_priority_values));
if (seedRatioModeValues == null)
seedRatioModeValues = Arrays.asList(getResources().getStringArray(R.array.torrent_seed_ratio_mode_values));
expandedStates[Expanders.OVERVIEW] = true;
updateReceiver = new UpdateReceiver();
pageUnselectedReceiver = new PageUnselectedReceiver();
serviceReceiver = new ServiceReceiver();
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_TORRENT_HASH)) {
torrentHash = savedInstanceState.getString(STATE_TORRENT_HASH);
} else if (getArguments().containsKey(G.ARG_PAGE_POSITION)) {
int position = getArguments().getInt(G.ARG_PAGE_POSITION);
TorrentDetailFragment fragment
= (TorrentDetailFragment) getActivity().getSupportFragmentManager().findFragmentByTag(G.DETAIL_FRAGMENT_TAG);
if (fragment != null) {
torrentHash = fragment.getTorrentHashString(position);
}
}
if (torrentHash != null) {
new TorrentDetailTask().execute(torrentHash);
}
}
@Override public void onDestroy() {
if (details != null) {
details.torrentCursor.close();
details.filesCursor.close();
details.trackersCursor.close();
details = null;
}
super.onDestroy();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
final Bundle savedInstanceState) {
final View root = inflater.inflate(R.layout.fragment_torrent_detail_page, container, false);
root.findViewById(R.id.torrent_detail_overview_expander).setOnTouchListener(expanderTouchListener);
root.findViewById(R.id.torrent_detail_files_expander).setOnTouchListener(expanderTouchListener);
root.findViewById(R.id.torrent_detail_limits_expander).setOnTouchListener(expanderTouchListener);
root.findViewById(R.id.torrent_detail_trackers_expander).setOnTouchListener(expanderTouchListener);
views = new Views();
views.overviewContent = (LinearLayout) root.findViewById(R.id.torrent_detail_overview_content);
views.filesContent = (LinearLayout) root.findViewById(R.id.torrent_detail_files_content);
views.limitsContent = (LinearLayout) root.findViewById(R.id.torrent_detail_limits_content);
views.trackersContent = (LinearLayout) root.findViewById(R.id.torrent_detail_trackers_content);
views.trackersContainer = (LinearLayout) root.findViewById(R.id.torrent_detail_trackers_list);
views.name = (TextView) root.findViewById(R.id.torrent_detail_title);
views.statusText = (TextView) root.findViewById(R.id.torrent_detail_subtitle);
views.have = (TextView) root.findViewById(R.id.torrent_have);
views.downloaded = (TextView) root.findViewById(R.id.torrent_downloaded);
views.uploaded = (TextView) root.findViewById(R.id.torrent_uploaded);
views.runningTime = (TextView) root.findViewById(R.id.torrent_running_time);
views.remainingTime = (TextView) root.findViewById(R.id.torrent_remaining_time);
views.lastActivity = (TextView) root.findViewById(R.id.torrent_last_activity);
views.size = (TextView) root.findViewById(R.id.torrent_size);
views.hash = (TextView) root.findViewById(R.id.torrent_hash);
views.privacy = (TextView) root.findViewById(R.id.torrent_privacy);
views.origin = (TextView) root.findViewById(R.id.torrent_origin);
views.comment = (TextView) root.findViewById(R.id.torrent_comment);
views.state = (TextView) root.findViewById(R.id.torrent_state);
views.added = (TextView) root.findViewById(R.id.torrent_added);
views.queue = (TextView) root.findViewById(R.id.torrent_queue);
views.error = (TextView) root.findViewById(R.id.torrent_error);
views.location = (TextView) root.findViewById(R.id.torrent_location);
views.globalLimits = (CheckBox) root.findViewById(R.id.torrent_global_limits);
views.torrentPriority = (Spinner) root.findViewById(R.id.torrent_priority);
views.queuePosition = (EditText) root.findViewById(R.id.torrent_queue_position);
views.downloadLimited = (CheckBox) root.findViewById(R.id.torrent_limit_download_check);
views.downloadLimit = (EditText) root.findViewById(R.id.torrent_limit_download);
views.uploadLimited = (CheckBox) root.findViewById(R.id.torrent_limit_upload_check);
views.uploadLimit = (EditText) root.findViewById(R.id.torrent_limit_upload);
views.seedRatioMode = (Spinner) root.findViewById(R.id.torrent_seed_ratio_mode);
views.seedRatioLimit = (EditText) root.findViewById(R.id.torrent_seed_ratio_limit);
views.peerLimit = (EditText) root.findViewById(R.id.torrent_peer_limit);
if (savedInstanceState != null) {
new Handler().post(() -> {
if (savedInstanceState.containsKey(STATE_EXPANDED)) {
expandedStates = savedInstanceState.getBooleanArray(STATE_EXPANDED);
views.overviewContent.setVisibility(expandedStates[Expanders.OVERVIEW] ? View.VISIBLE : View.GONE);
views.filesContent.setVisibility(expandedStates[Expanders.FILES] ? View.VISIBLE : View.GONE);
views.limitsContent.setVisibility(expandedStates[Expanders.LIMITS] ? View.VISIBLE : View.GONE);
views.trackersContent.setVisibility(expandedStates[Expanders.TRACKERS] ? View.VISIBLE : View.GONE);
updateFields(root);
}
if (savedInstanceState.containsKey(STATE_SCROLL_POSITION)) {
final int position = savedInstanceState.getInt(STATE_SCROLL_POSITION);
final ScrollView scroll = (ScrollView) root.findViewById(R.id.detail_scroll);
scroll.scrollTo(0, position);
}
});
}
filesAdapter = new FilesAdapter();
FilesDataSetObserver filesObserver = new FilesDataSetObserver();
filesAdapter.registerDataSetObserver(filesObserver);
trackersAdapter = new TrackersAdapter();
trackersObserver = new TrackersDataSetObserver();
trackersAdapter.registerDataSetObserver(trackersObserver);
updateFields(root);
views.globalLimits.setOnCheckedChangeListener(
(buttonView, isChecked) -> setTorrentProperty(SetterFields.SESSION_LIMITS, isChecked));
views.torrentPriority.setOnItemSelectedListener(
new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
String val=priorityValues.get(pos);
int priority=Torrent.Priority.NORMAL;
if (val.equals("low")) {
priority=Torrent.Priority.LOW;
} else if (val.equals("high")) {
priority=Torrent.Priority.HIGH;
}
setTorrentProperty(SetterFields.TORRENT_PRIORITY, priority);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
views.queuePosition.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
int position;
try {
position=Integer.parseInt(v.getText().toString().trim());
} catch (NumberFormatException e) {
return false;
}
setTorrentProperty(SetterFields.QUEUE_POSITION, position);
}
new Handler().post(loseFocusRunnable);
return false;
});
views.downloadLimited.setOnCheckedChangeListener((buttonView, isChecked) -> {
views.downloadLimit.setEnabled(isChecked);
setTorrentProperty(SetterFields.DOWNLOAD_LIMITED, isChecked);
});
views.downloadLimit.setOnEditorActionListener((v, actionId, event) -> {
long limit;
try {
limit=Long.parseLong(v.getText().toString().trim());
} catch (NumberFormatException e) {
return false;
}
setTorrentProperty(SetterFields.DOWNLOAD_LIMIT, limit);
new Handler().post(loseFocusRunnable);
return false;
});
views.uploadLimited.setOnCheckedChangeListener((buttonView, isChecked) -> {
views.uploadLimit.setEnabled(isChecked);
setTorrentProperty(SetterFields.UPLOAD_LIMITED, isChecked);
});
views.uploadLimit.setOnEditorActionListener((v, actionId, event) -> {
long limit;
try {
limit=Long.parseLong(v.getText().toString().trim());
} catch (NumberFormatException e) {
return false;
}
setTorrentProperty(SetterFields.UPLOAD_LIMIT, limit);
new Handler().post(loseFocusRunnable);
return false;
});
views.seedRatioMode.setOnItemSelectedListener(
new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
String val = seedRatioModeValues.get(pos);
int mode = Torrent.SeedRatioMode.GLOBAL_LIMIT;
if (val.equals("user")) {
mode=Torrent.SeedRatioMode.TORRENT_LIMIT;
} else if (val.equals("infinite")) {
mode=Torrent.SeedRatioMode.NO_LIMIT;
}
views.seedRatioLimit.setEnabled(val.equals("user"));
setTorrentProperty(SetterFields.SEED_RATIO_MODE, mode);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
views.seedRatioLimit.setOnEditorActionListener((v, actionId, event) -> {
float limit;
try {
limit=Float.parseFloat(v.getText().toString().trim());
} catch (NumberFormatException e) {
return false;
}
setTorrentProperty(SetterFields.SEED_RATIO_LIMIT, limit);
new Handler().post(loseFocusRunnable);
return false;
});
views.peerLimit.setOnEditorActionListener((v, actionId, event) -> {
int limit;
try {
limit=Integer.parseInt(v.getText().toString().trim());
} catch (NumberFormatException e) {
return false;
}
setTorrentProperty(SetterFields.PEER_LIMIT, limit);
new Handler().post(loseFocusRunnable);
return false;
});
Button addTracker = (Button) root.findViewById(R.id.torrent_detail_add_tracker);
addTracker.setOnClickListener(v -> {
final ArrayList<String> urls = new ArrayList<>();
LayoutInflater inflater1 = getActivity().getLayoutInflater();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.tracker_add)
.setCancelable(false)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
EditText url = (EditText) ((AlertDialog) dialog).findViewById(R.id.tracker_announce_url);
urls.add(url.getText().toString());
setTorrentProperty(SetterFields.TRACKER_ADD, urls);
((TransmissionSessionInterface) getActivity()).setRefreshing(true,
DataService.Requests.GET_TORRENTS);
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
if (manager != null) {
manager.update();
}
}
}).setView(inflater1.inflate(R.layout.replace_tracker_dialog, null));
AlertDialog dialog = builder.create();
dialog.show();
});
root.setOnTouchListener((v, event) -> {
if (views.queuePosition.hasFocus()) {
views.queuePosition.clearFocus();
} else if (views.downloadLimit.hasFocus()) {
views.downloadLimit.clearFocus();
} else if (views.uploadLimit.hasFocus()) {
views.uploadLimit.clearFocus();
} else if (views.seedRatioLimit.hasFocus()) {
views.seedRatioLimit.clearFocus();
} else if (views.peerLimit.hasFocus()) {
views.peerLimit.clearFocus();
}
return false;
});
return root;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBooleanArray(STATE_EXPANDED, expandedStates);
ScrollView scroll = (ScrollView) getView().findViewById(R.id.detail_scroll);
if (scroll != null) {
outState.putInt(STATE_SCROLL_POSITION, scroll.getScrollY());
}
outState.putString(STATE_TORRENT_HASH, torrentHash);
}
@Override
public void onResume() {
super.onResume();
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getActivity());
broadcastManager.registerReceiver(updateReceiver, new IntentFilter(G.INTENT_TORRENT_UPDATE));
broadcastManager.registerReceiver(pageUnselectedReceiver, new IntentFilter(G.INTENT_PAGE_UNSELECTED));
broadcastManager.registerReceiver(serviceReceiver, new IntentFilter(G.INTENT_SERVICE_ACTION_COMPLETE));
}
@Override
public void onPause() {
super.onPause();
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getActivity());
broadcastManager.unregisterReceiver(updateReceiver);
broadcastManager.unregisterReceiver(pageUnselectedReceiver);
broadcastManager.unregisterReceiver(serviceReceiver);
}
private void setTorrentProperty(String key, int value) {
if (debounce(key)) {
return;
}
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
setFieldModifier(key);
if (manager != null) {
manager.setTorrent(new String[] { torrentHash }, key, value);
}
}
private void setTorrentProperty(String key, long value) {
if (debounce(key)) {
return;
}
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
setFieldModifier(key);
if (manager != null) {
manager.setTorrent(new String[] { torrentHash }, key, value);
}
}
private void setTorrentProperty(String key, boolean value) {
if (debounce(key)) {
return;
}
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
setFieldModifier(key);
if (manager != null) {
manager.setTorrent(new String[] { torrentHash }, key, value);
}
}
private void setTorrentProperty(String key, float value) {
if (debounce(key)) {
return;
}
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
setFieldModifier(key);
if (manager != null) {
manager.setTorrent(new String[] { torrentHash }, key, value);
}
}
private void setTorrentProperty(String key, ArrayList<?> value) {
if (debounce(key)) {
return;
}
DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
setFieldModifier(key);
if (manager != null) {
manager.setTorrent(new String[] { torrentHash }, key, value);
}
}
private void setFieldModifier(String key) {
synchronized (this) {
fieldModifiers.put(key, fieldModifiers.containsKey(key)
? fieldModifiers.get(key) + 1 : 1);
}
}
private boolean isFieldEnabled(String key) {
synchronized (this) {
return !fieldModifiers.containsKey(key) || fieldModifiers.get(key) < 1;
}
}
private boolean debounce(String key) {
Long last = debouncers.get(key);
long now = new Date().getTime();
if (last != null && now < last + 300) {
return true;
}
debouncers.put(key, now);
return false;
}
private void updateFields(View root) {
if (root == null || details == null || details.torrentCursor.isClosed()
|| details.torrentCursor.getCount() == 0) return;
String name = Torrent.getName(details.torrentCursor);
Spanned statusText = Html.fromHtml(Torrent.getStatusText(details.torrentCursor));
int status = Torrent.getStatus(details.torrentCursor);
float uploadRatio = Torrent.getUploadRatio(details.torrentCursor);
float seedRatioLimit = Torrent.getUploadRatio(details.torrentCursor);
int queuePosition = Torrent.getQueuePosition(details.torrentCursor);
if (!name.equals(views.name.getText())) {
views.name.setText(name);
}
if (!statusText.equals(views.statusText.getText())) {
views.statusText.setText(statusText);
}
/* Overview start */
Cursor cursor = details.torrentCursor;
if (views.overviewContent.getVisibility() != View.GONE) {
long now = new Timestamp(new Date().getTime()).getTime() / 1000;
float metadataPercent = Torrent.getMetadataPercentDone(cursor);
long haveValid = Torrent.getHaveValid(cursor);
long sizeWhenDone = Torrent.getSizeWhenDone(cursor);
long leftUntilDone = Torrent.getLeftUntilDone(cursor);
long downloadedEver = Torrent.getDownloadedEver(cursor);
long uploadedEver = Torrent.getUploadedEver(cursor);
long startDate = Torrent.getStartDate(cursor);
long activityDate = Torrent.getActivityDate(cursor);
long addedDate = Torrent.getAddedDate(cursor);
long lastActive = now - activityDate;
long eta = Torrent.getEta(cursor);
long totalSize = Torrent.getTotalSize(cursor);
long pieceSize = Torrent.getPieceSize(cursor);
int pieceCount = Torrent.getPieceCount(cursor);
String hash = Torrent.getHashString(cursor);
boolean isPrivate = Torrent.isPrivate(cursor);
long dateCreated = Torrent.getDateCreated(cursor);
String creator = Torrent.getCreator(cursor);
String comment = Torrent.getComment(cursor);
int error = Torrent.getError(cursor);
String errorString = Torrent.getErrorString(cursor);
String downloadDir = Torrent.getDownloadDir(cursor);
if (metadataPercent == 1) {
String have = String.format(
getString(R.string.torrent_have_format),
G.readableFileSize(haveValid > 0
? haveValid : sizeWhenDone - leftUntilDone),
G.readableFileSize(sizeWhenDone),
G.readablePercent(100 * (
sizeWhenDone > 0
? (float) (sizeWhenDone - leftUntilDone) / sizeWhenDone
: 1
))
);
if (!have.equals(views.have.getText())) {
views.have.setText(have);
}
String downloaded = downloadedEver == 0
? getString(R.string.unknown)
: G.readableFileSize(downloadedEver);
if (!downloaded.equals(views.downloaded.getText())) {
views.downloaded.setText(downloaded);
}
String uploaded = G.readableFileSize(uploadedEver);
if (!uploaded.equals(views.uploaded.getText())) {
views.uploaded.setText(uploaded);
}
String runningTime = status == Torrent.Status.STOPPED
? getString(R.string.status_finished)
: startDate > 0
? G.readableRemainingTime(now - startDate, getActivity())
: getString(R.string.unknown);
views.runningTime.setText(runningTime);
String remainingTime = eta < 0
? getString(R.string.unknown)
: G.readableRemainingTime(eta, getActivity());
if (!remainingTime.equals(views.remainingTime.getText())) {
views.remainingTime.setText(remainingTime);
}
String lastActivity = lastActive < 0 || activityDate <= 0
? getString(R.string.unknown)
: lastActive < 5
? getString(R.string.torrent_active_now)
: String.format(
getString(R.string.torrent_added_format),
G.readableRemainingTime(lastActive, getActivity()));
views.lastActivity.setText(lastActivity);
String size = String.format(
getString(R.string.torrent_size_format),
G.readableFileSize(totalSize),
pieceCount,
G.readableFileSize(pieceSize)
);
if (!size.equals(views.size.getText())) {
views.size.setText(size);
}
if (!hash.equals(views.hash.getText())) {
views.hash.setText(hash);
}
views.privacy.setText(isPrivate ? R.string.torrent_private : R.string.torrent_public);
Date creationDate = new Date(dateCreated * 1000);
String origin = TextUtils.isEmpty(creator)
? String.format(
getString(R.string.torrent_origin_format),
creationDate.toString()
)
: String.format(
getString(R.string.torrent_origin_creator_format),
creator,
creationDate.toString()
);
if (!origin.equals(views.origin.getText())) {
views.origin.setText(origin);
}
if (!comment.equals(views.comment.getText())) {
views.comment.setText(comment);
}
} else {
String have = getString(R.string.none);
if (!have.equals(views.have.getText())) {
views.have.setText(have);
}
}
int state = R.string.none;
switch(status) {
case Torrent.Status.STOPPED:
state = uploadRatio < seedRatioLimit
? R.string.status_paused
: R.string.status_finished;
break;
case Torrent.Status.CHECK_WAITING:
state = R.string.status_check_waiting;
break;
case Torrent.Status.CHECKING:
state = R.string.status_checking;
break;
case Torrent.Status.DOWNLOAD_WAITING:
state = R.string.status_download_waiting;
break;
case Torrent.Status.DOWNLOADING:
state = metadataPercent < 0
? R.string.status_downloading_metadata
: R.string.status_downloading;
break;
case Torrent.Status.SEED_WAITING:
state = R.string.status_seed_waiting;
break;
case Torrent.Status.SEEDING:
state = R.string.status_seeding;
break;
}
String stateString = getString(state);
if (!stateString.equals(views.state.getText())) {
views.state.setText(stateString);
}
if (addedDate > 0) {
views.added.setText(
String.format(
getString(R.string.torrent_added_format),
G.readableRemainingTime(now - addedDate, getActivity())
)
);
} else {
views.added.setText(R.string.unknown);
}
String queue = Integer.toString(queuePosition);
if (!queue.equals(views.queue.getText())) {
views.queue.setText(queue);
}
if (error == Torrent.Error.OK) {
if (views.error.isEnabled()) {
views.error.setText(R.string.no_tracker_errors);
views.error.setEnabled(false);
}
} else {
if (!views.error.isEnabled()) {
views.error.setText(errorString);
views.error.setEnabled(true);
}
}
if (!downloadDir.equals(views.location.getText())) {
views.location.setText(downloadDir);
}
}
/* Files start */
if (views.filesContent.getVisibility() != View.GONE
&& details.filesCursor.getCount() > 0) {
filesAdapter.setNotifyOnChange(false);
if (filesAdapter.getCount() == 0) {
ArrayList<TorrentFile> torrentFiles = new ArrayList<>();
details.filesCursor.moveToFirst();
if (!TextUtils.isEmpty(Torrent.File.getName(details.filesCursor))) {
int index = 0;
while (!details.filesCursor.isAfterLast()) {
TorrentFile file = new TorrentFile(index, details.filesCursor);
torrentFiles.add(file);
details.filesCursor.moveToNext();
++index;
}
Collections.sort(torrentFiles, new TorrentFileComparator());
String directory = "";
ArrayList<Integer> directories = new ArrayList<>();
for (int i = 0; i < torrentFiles.size(); i++) {
TorrentFile file = torrentFiles.get(i);
if (!directory.equals(file.directory)) {
directory = file.directory;
directories.add(i);
}
}
int offset = 0;
for (Integer i : directories) {
TorrentFile file = torrentFiles.get(i + offset);
torrentFiles.add(i + offset, new TorrentFile(file.directory));
offset++;
}
filesAdapter.addAll(torrentFiles);
}
}
filesAdapter.notifyDataSetChanged();
}
/* Limits start */
if (views.limitsContent.getVisibility() != View.GONE) {
boolean sessionLimitsHonored = Torrent.areSessionLimitsHonored(cursor);
int torrentPriority = Torrent.getTorrentPriority(cursor);
boolean downloadLimited = Torrent.isDownloadLimited(cursor);
long downloadLimit = Torrent.getDownloadLimit(cursor);
boolean uploadLimited = Torrent.isUploadLimited(cursor);
long uploadLimit = Torrent.getUploadLimit(cursor);
int seedRatioMode = Torrent.getSeedRatioMode(cursor);
int peerLimit = Torrent.getPeerLimit(cursor);
if (isFieldEnabled(SetterFields.SESSION_LIMITS)
&& sessionLimitsHonored != views.globalLimits.isChecked()) {
views.globalLimits.setChecked(sessionLimitsHonored);
}
String priority = "normal";
switch (torrentPriority) {
case Torrent.Priority.LOW:
priority = "low";
break;
case Torrent.Priority.HIGH:
priority = "high";
break;
}
if (isFieldEnabled(SetterFields.TORRENT_PRIORITY)
&& views.torrentPriority.getSelectedItemPosition() != priorityValues.indexOf(priority)) {
views.torrentPriority.setSelection(priorityValues.indexOf(priority));
}
String queue = Integer.toString(queuePosition);
if (isFieldEnabled(SetterFields.QUEUE_POSITION)
&& !views.queuePosition.isFocused()
&& !queue.equals(views.queuePosition.getText().toString())) {
views.queuePosition.setText(queue);
}
if (isFieldEnabled(SetterFields.DOWNLOAD_LIMITED)
&& downloadLimited != views.downloadLimited.isChecked()) {
views.downloadLimited.setChecked(downloadLimited);
}
String download = Long.toString(downloadLimit);
if (isFieldEnabled(SetterFields.DOWNLOAD_LIMIT)
&& !views.downloadLimit.isFocused()
&& !download.equals(views.downloadLimit.getText().toString())) {
views.downloadLimit.setText(download);
}
if (views.downloadLimit.isEnabled() != views.downloadLimited.isChecked()) {
views.downloadLimit.setEnabled(views.downloadLimited.isChecked());
}
if (isFieldEnabled(SetterFields.UPLOAD_LIMITED)
&& views.uploadLimited.isChecked() != uploadLimited) {
views.uploadLimited.setChecked(uploadLimited);
}
String upload = Long.toString(uploadLimit);
if (isFieldEnabled(SetterFields.UPLOAD_LIMIT)
&& !views.uploadLimit.isFocused()
&& !upload.equals(views.uploadLimit.getText().toString())) {
views.uploadLimit.setText(upload);
}
if (views.uploadLimit.isEnabled() != views.uploadLimited.isChecked()) {
views.uploadLimit.setEnabled(views.uploadLimited.isChecked());
}
String mode = "global";
switch (seedRatioMode) {
case Torrent.SeedRatioMode.TORRENT_LIMIT:
mode = "user";
break;
case Torrent.SeedRatioMode.NO_LIMIT:
mode = "infinite";
break;
}
if (isFieldEnabled(SetterFields.SEED_RATIO_MODE)
&& views.seedRatioMode.getSelectedItemPosition() != seedRatioModeValues.indexOf(mode)) {
views.seedRatioMode.setSelection(seedRatioModeValues.indexOf(mode));
}
String ratio = G.readablePercent(seedRatioLimit);
if (isFieldEnabled(SetterFields.SEED_RATIO_LIMIT)
&& !views.seedRatioLimit.isFocused()
&& !ratio.equals(views.seedRatioLimit.getText().toString())) {
views.seedRatioLimit.setText(ratio);
}
String peers = Integer.toString(peerLimit);
if (isFieldEnabled(SetterFields.PEER_LIMIT)
&& !views.peerLimit.isFocused()
&& !peers.equals(views.peerLimit.getText().toString())) {
views.peerLimit.setText(peers);
}
}
/* Trackers start */
if (views.trackersContent.getVisibility() != View.GONE
&& details.trackersCursor.getCount() > 0) {
trackersAdapter.setNotifyOnChange(false);
if (trackersAdapter.getCount() != details.trackersCursor.getCount()) {
trackersAdapter.clear();
int index = 0;
ArrayList<Tracker> trackers = new ArrayList<>();
details.trackersCursor.moveToFirst();
while (!details.trackersCursor.isAfterLast()) {
Tracker tracker = new Tracker(index, details.trackersCursor);
trackers.add(tracker);
details.trackersCursor.moveToNext();
++index;
}
Collections.sort(trackers, new TrackerComparator());
trackersAdapter.addAll(trackers);
}
trackersAdapter.notifyDataSetChanged();
}
}
private void invalidateFileActionMenu(Menu menu) {
boolean hasChecked = false, hasUnchecked = false;
MenuItem checked = menu.findItem(R.id.check_selected);
MenuItem unchecked = menu.findItem(R.id.uncheck_selected);
List<View> allViews = filesAdapter.getViews();
for (View v : selectedFiles) {
TorrentFile file = filesAdapter.getItem(allViews.indexOf(v));
if (file.wanted) {
hasChecked = true;
checked.setVisible(true);
} else {
hasUnchecked = true;
unchecked.setVisible(true);
}
if (hasChecked && hasUnchecked) {
break;
}
}
if (!hasChecked) {
checked.setVisible(false);
}
if (!hasUnchecked) {
unchecked.setVisible(false);
}
}
private class TorrentFile {
public int index = -1;
public String directory;
public String name;
public long bytesCompleted;
public long length;
public int priority;
public boolean wanted;
boolean changed = false;
public TorrentFile(int index, Cursor cursor) {
this.index = index;
setInfo(cursor);
}
public TorrentFile(String directory) {
this.directory = directory;
}
public void setInfo(Cursor cursor) {
String path = Torrent.File.getName(cursor);
File f = new File(path);
directory = f.getParent();
name = f.getName();
if (directory == null) {
directory = "";
}
bytesCompleted = Torrent.File.getBytesCompleted(cursor);
length = Torrent.File.getLength(cursor);
priority = Torrent.File.getPriority(cursor);
wanted = Torrent.File.isWanted(cursor);
}
}
private class TorrentFileComparator implements Comparator<TorrentFile> {
@Override
public int compare(TorrentFile lhs, TorrentFile rhs) {
int path = lhs.directory.compareTo(rhs.directory);
if (path != 0) {
return path;
}
return lhs.name.compareToIgnoreCase(rhs.name);
}
}
private class FilesAdapter extends ArrayAdapter<TorrentFile> {
private static final int fieldId = R.id.torrent_detail_files_row;
private List<View> views = new ArrayList<>();
public FilesAdapter() {
super(getActivity(), R.layout.torrent_detail_files_row);
}
public List<View> getViews() {
return views;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View rowView = convertView;
final TorrentFile file = getItem(position);
boolean initial = false;
if (rowView == null) {
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (file.name == null) {
rowView = vi.inflate(R.layout.torrent_detail_files_directory_row, null);
} else {
rowView = vi.inflate(R.layout.torrent_detail_files_row, null);
}
initial = true;
}
if (file.name == null) {
TextView row = (TextView) rowView.findViewById(fieldId);
if (TextUtils.isEmpty(file.directory)) {
row.setVisibility(View.GONE);
} else {
row.setText(file.directory);
row.setOnLongClickListener(v -> {
if (fileActionMode != null) {
return false;
}
List<View> files = filesAdapter.getViews();
for (int i = position + 1; i < files.size(); ++i) {
View view = files.get(i);
TorrentFile child = getItem(i);
if (!file.directory.equals(child.directory)) {
break;
}
if (view != null && !view.isActivated()) {
view.setActivated(true);
selectedFiles.add(view);
}
}
fileActionMode = getActivity().findViewById(R.id.toolbar).startActionMode(actionModeFiles);
invalidateFileActionMenu(fileActionMode.getMenu());
return true;
});
row.setOnClickListener(v -> {
if (fileActionMode == null) {
return;
}
List<View> files = filesAdapter.getViews();
int activated = 0;
int deactivated = 0;
for (int i = position + 1; i < files.size(); ++i) {
View view = files.get(i);
TorrentFile child = getItem(i);
if (!file.directory.equals(child.directory)) {
break;
}
if (view != null) {
if (view.isActivated()) {
++activated;
} else {
++deactivated;
}
}
}
boolean activate = activated < deactivated;
for (int i = position + 1; i < files.size(); ++i) {
View view = files.get(i);
TorrentFile child = getItem(i);
if (!file.directory.equals(child.directory)) {
break;
}
if (view != null) {
view.setActivated(activate);
if (activate) {
selectedFiles.add(view);
} else {
selectedFiles.remove(view);
}
}
}
if (selectedFiles.size() == 0) {
fileActionMode.finish();
} else {
invalidateFileActionMenu(fileActionMode.getMenu());
}
});
}
} else {
final View container = rowView;
CheckBox row = (CheckBox) rowView.findViewById(fieldId);
if (initial) {
row.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (fileActionMode != null) {
buttonView.setChecked(!isChecked);
if (container.isActivated()) {
container.setActivated(false);
selectedFiles.remove(container);
if (selectedFiles.size() == 0) {
fileActionMode.finish();
} else {
invalidateFileActionMenu(fileActionMode.getMenu());
}
} else {
container.setActivated(true);
selectedFiles.add(container);
invalidateFileActionMenu(fileActionMode.getMenu());
}
return;
}
if (file.wanted != isChecked) {
if (isChecked) {
setTorrentProperty(SetterFields.FILES_WANTED, file.index);
} else {
setTorrentProperty(SetterFields.FILES_UNWANTED, file.index);
}
}
});
row.setOnLongClickListener(v -> {
if (fileActionMode != null) {
return false;
}
container.setActivated(true);
selectedFiles.add(container);
fileActionMode = getActivity().findViewById(R.id.toolbar).startActionMode(actionModeFiles);
invalidateFileActionMenu(fileActionMode.getMenu());
return true;
});
}
String priority;
switch(file.priority) {
case Torrent.Priority.LOW:
priority = priorityNames.get(0);
break;
case Torrent.Priority.HIGH:
priority = priorityNames.get(2);
break;
default:
priority = priorityNames.get(1);
break;
}
row.setText(Html.fromHtml(String.format(
getString(R.string.torrent_detail_file_format),
file.name,
G.readableFileSize(file.bytesCompleted),
G.readableFileSize(file.length),
priority
)));
row.setChecked(file.wanted);
if (initial) {
while (views.size() <= position)
views.add(null);
views.set(position, rowView);
}
}
return rowView;
}
}
private class FilesDataSetObserver extends DataSetObserver {
public FilesDataSetObserver() { }
@Override public void onChanged() {
int position = details.filesCursor.getPosition();
for (int i = 0; i < filesAdapter.getCount(); i++) {
TorrentFile file = filesAdapter.getItem(i);
View v = null;
boolean hasChild = false;
if (file.index != -1 && !details.filesCursor.moveToPosition(file.index)) {
continue;
}
if (i < views.filesContent.getChildCount()) {
v = views.filesContent.getChildAt(i);
hasChild = true;
}
if (!hasChild || (file.index != -1 && fileChanged(file, details.filesCursor))) {
v = filesAdapter.getView(i, v, null);
if (!hasChild && v != null) {
views.filesContent.addView(v, i);
}
}
}
details.filesCursor.moveToPosition(position);
}
@Override public void onInvalidated() {
views.filesContent.removeAllViews();
}
private boolean fileChanged(TorrentFile file, Cursor cursor) {
boolean changed = false;
if (file.changed
|| file.wanted != Torrent.File.isWanted(cursor)
|| file.bytesCompleted != Torrent.File.getBytesCompleted(cursor)
|| file.priority != Torrent.File.getPriority(cursor)) {
file.setInfo(cursor);
file.changed = false;
changed = true;
}
return changed;
}
}
private class Tracker {
public int index = -1;
public String host;
public int id;
public String announce;
public String scrape;
public int tier;
public int seederCount;
public int leecherCount;
public boolean hasAnnounced;
public long lastAnnounceTime;
public boolean hasLastAnnounceSucceeded;
public int lastAnnouncePeerCount;
public String lastAnnounceResult;
public boolean hasScraped;
public long lastScrapeTime;
public boolean hasLastScrapeSucceeded;
public String lastScrapeResult;
public Tracker(int index, Cursor cursor) {
this.index = index;
setInfo(cursor);
}
public void setInfo(Cursor cursor) {
this.announce = Torrent.Tracker.getAnnounce(cursor);
try {
URI uri = new URI(this.announce);
this.host = uri.getHost();
} catch (URISyntaxException e) {
this.host = getString(R.string.tracker_unknown_host);
}
this.id = Torrent.Tracker.getId(cursor);
this.scrape = Torrent.Tracker.getScrape(cursor);
this.tier = Torrent.Tracker.getTier(cursor);
this.seederCount = Torrent.Tracker.getSeederCount(cursor);
this.leecherCount = Torrent.Tracker.getLeecherCount(cursor);
this.hasAnnounced = Torrent.Tracker.hasAnnounced(cursor);
this.lastAnnounceTime = Torrent.Tracker.getLastAnnounceTime(cursor);
this.hasLastAnnounceSucceeded = Torrent.Tracker.hasLastAnnounceSucceeded(cursor);
this.lastAnnouncePeerCount = Torrent.Tracker.getLastAnnouncePeerCount(cursor);
this.lastAnnounceResult = Torrent.Tracker.getLastAnnounceResult(cursor);
this.hasScraped = Torrent.Tracker.hasScraped(cursor);
this.lastScrapeTime = Torrent.Tracker.getLastScrapeTime(cursor);
this.hasLastScrapeSucceeded = Torrent.Tracker.hasLastScrapeSucceeded(cursor);
this.lastScrapeResult = Torrent.Tracker.getLastScrapeResult(cursor);
}
public void setIndex(int index) {
this.index = index;
}
}
private class TrackerComparator implements Comparator<Tracker> {
@Override
public int compare(Tracker lhs, Tracker rhs) {
int tier = lhs.tier - rhs.tier;
if (tier != 0) {
return tier;
}
return lhs.host.compareToIgnoreCase(rhs.host);
}
}
private class TrackersAdapter extends ArrayAdapter<Tracker> {
private List<View> views = new ArrayList<>();
private ViewGroup visibleButtons;
private List<ViewGroup> buttonsToHide = new ArrayList<>();
private AnimatorSet buttonsAnimator;
private boolean animatorStopped = false;
public TrackersAdapter() {
super(getActivity(), R.layout.torrent_detail_trackers_row);
}
public List<View> getViews() {
return views;
}
@Override
public void remove(Tracker tracker) {
int index = tracker.index;
super.remove(tracker);
for (int i = 0; i < getCount(); ++i) {
Tracker t = getItem(i);
if (t.index > index) {
t.setIndex(t.index - 1);
}
}
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View rowView = convertView;
final Tracker tracker = getItem(position);
boolean initial = false;
if (rowView == null) {
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
rowView = vi.inflate(R.layout.torrent_detail_trackers_row, null);
initial = true;
}
rowView.setTag("announce".hashCode(), tracker.announce);
TextView url = (TextView) rowView.findViewById(R.id.tracker_url);
TextView tier = (TextView) rowView.findViewById(R.id.tracker_tier);
TextView seeders = (TextView) rowView.findViewById(R.id.tracker_seeders);
TextView leechers = (TextView) rowView.findViewById(R.id.tracker_leechers);
TextView announce = (TextView) rowView.findViewById(R.id.tracker_announce);
TextView scrape = (TextView) rowView.findViewById(R.id.tracker_scrape);
url.setText(tracker.host);
tier.setText(String.format(getString(R.string.tracker_tier),
tracker.tier));
seeders.setText(String.format(getString(R.string.tracker_seeders),
tracker.seederCount));
leechers.setText(String.format(getString(R.string.tracker_leechers),
tracker.leecherCount));
long now = (new Date().getTime() / 1000);
if (tracker.hasAnnounced) {
String time = G.readableRemainingTime(now - tracker.lastAnnounceTime,
getActivity());
if (tracker.hasLastAnnounceSucceeded) {
announce.setText(String.format(
getString(R.string.tracker_announce_success),
time, getResources().getQuantityString(R.plurals.tracker_peers,
tracker.lastAnnouncePeerCount,
tracker.lastAnnouncePeerCount)));
} else {
announce.setText(String.format(
getString(R.string.tracker_announce_error),
TextUtils.isEmpty(tracker.lastAnnounceResult)
? ""
: (tracker.lastAnnounceResult + " - "),
time
));
}
} else {
announce.setText(R.string.tracker_announce_never);
}
if (tracker.hasScraped) {
String time = G.readableRemainingTime(now - tracker.lastScrapeTime,
getActivity());
if (tracker.hasLastScrapeSucceeded) {
scrape.setText(String.format(
getString(R.string.tracker_scrape_success),
time
));
} else {
scrape.setText(String.format(
getString(R.string.tracker_scrape_error),
TextUtils.isEmpty(tracker.lastScrapeResult)
? ""
: (tracker.lastScrapeResult + " - "),
time
));
}
} else {
scrape.setText(R.string.tracker_scrape_never);
}
if (initial) {
while (views.size() <= position)
views.add(null);
views.set(position, rowView);
View row = rowView.findViewById(R.id.torrent_detail_trackers_row_info);
final ViewGroup buttons = (ViewGroup) rowView.findViewById(R.id.torrent_detail_tracker_buttons);
row.setOnLongClickListener(v -> {
if (trackerActionMode != null) {
return false;
}
v.setActivated(true);
selectedTrackers.add(v);
trackerActionMode = getActivity().findViewById(R.id.toolbar).startActionMode(actionModeTrackers);
stopAnimator();
animateTrackerLayout(null, visibleButtons);
return true;
});
row.setOnClickListener(v -> {
if (trackerActionMode != null) {
if (v.isActivated()) {
selectedTrackers.remove(v);
v.setActivated(false);
if (selectedTrackers.size() == 0) {
trackerActionMode.finish();
}
} else {
selectedTrackers.add(v);
v.setActivated(true);
}
return;
}
stopAnimator();
if (buttons == visibleButtons) {
animateTrackerLayout(null, visibleButtons);
} else {
animateTrackerLayout(buttons, visibleButtons);
}
});
final TransmissionSessionInterface context = (TransmissionSessionInterface) getActivity();
final DataServiceManager manager =
((DataServiceManagerInterface) getActivity()).getDataServiceManager();
buttons.findViewById(R.id.torrent_detail_tracker_remove).setOnClickListener(v -> {
trackersAdapter.remove(tracker);
trackersObserver.setFrozen(true);
ArrayList<Integer> ids = new ArrayList<>();
ids.add(tracker.id);
setTorrentProperty(SetterFields.TRACKER_REMOVE, ids);
context.setRefreshing(true, DataService.Requests.SET_TORRENT);
if (manager != null) {
manager.update();
}
stopAnimator();
});
buttons.findViewById(R.id.torrent_detail_tracker_replace).setOnClickListener(v -> {
final ArrayList<String> tuple = new ArrayList<>();
tuple.add(Integer.toString(tracker.id));
LayoutInflater inflater = getActivity().getLayoutInflater();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.tracker_replace)
.setCancelable(false)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes,
(dialog, id) -> {
EditText url1 = (EditText) ((AlertDialog) dialog).findViewById(R.id.tracker_announce_url);
tuple.add(url1.getText().toString());
setTorrentProperty(SetterFields.TRACKER_REPLACE, tuple);
context.setRefreshing(true, DataService.Requests.GET_TORRENTS);
if (manager != null) {
manager.update();
}
}).setView(inflater.inflate(R.layout.replace_tracker_dialog, null));
AlertDialog dialog = builder.create();
dialog.show();
stopAnimator();
animateTrackerLayout(null, visibleButtons);
});
buttons.findViewById(R.id.torrent_detail_tracker_copy).setOnClickListener(v -> {
String url1 = tracker.announce;
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.tracker_announce_url), url1);
clipboard.setPrimaryClip(clip);
Toast.makeText(getActivity(),
R.string.tracker_url_copy, Toast.LENGTH_SHORT).show();
stopAnimator();
animateTrackerLayout(null, visibleButtons);
});
}
return rowView;
}
private void animateTrackerLayout(final ViewGroup toShow, ViewGroup... toHide) {
if (toShow == null && toHide.length == 0 && buttonsToHide.size() == 0) {
return;
}
final Map<View, int[]> coordinates = new HashMap<>();
for (View child : views) {
coordinates.put(child, new int[]{child.getTop(), child.getBottom()});
}
View content = TorrentDetailPageFragment.this.views.trackersContent;
final ViewTreeObserver observer =
content.getViewTreeObserver();
final View addButton = content.findViewById(R.id.torrent_detail_add_tracker);
coordinates.put(addButton,
new int[]{addButton.getTop(), addButton.getBottom()});
if (toShow != null) {
toShow.setVisibility(View.VISIBLE);
visibleButtons = toShow;
buttonsToHide.remove(toShow);
}
if (toHide.length != 0) {
for (ViewGroup b : toHide) {
if (b != null) {
buttonsToHide.add(b);
}
}
}
if (toShow == null) {
for (ViewGroup b : buttonsToHide) {
b.setVisibility(View.GONE);
if (b == visibleButtons) {
visibleButtons = null;
}
}
}
if (toShow != null) {
for (int i = 0; i < toShow.getChildCount(); ++i) {
View button = toShow.getChildAt(i);
button.setClickable(false);
}
}
for (ViewGroup b : buttonsToHide) {
for (int i = 0; i < b.getChildCount(); ++i) {
View button = b.getChildAt(i);
button.setClickable(false);
}
}
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override public boolean onPreDraw() {
observer.removeOnPreDrawListener(this);
List<Animator> animations = new ArrayList<>();
for (View child : views) {
int[] oldCoordinates = coordinates.get(child);
animations.add(getCoordinateAnimator(child, oldCoordinates));
ViewGroup buttons = (ViewGroup) child.findViewById(R.id.torrent_detail_tracker_buttons);
if (buttons == toShow) {
animations.add(ObjectAnimator.ofFloat(buttons, View.ALPHA, 0.3f, 1));
animations.add(ObjectAnimator.ofFloat(buttons, View.TRANSLATION_Y, -50f, 0f));
} else if (toShow == null && buttonsToHide.contains(buttons)) {
buttons.setVisibility(View.VISIBLE);
animations.add(ObjectAnimator.ofFloat(buttons, View.ALPHA, 1, 0));
animations.add(ObjectAnimator.ofFloat(buttons, View.TRANSLATION_Y, 0f, -50f));
}
}
animations.add(getCoordinateAnimator(addButton, coordinates.get(addButton)));
AnimatorSet set = new AnimatorSet();
set.playTogether(animations);
set.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (toShow != null) {
for (int i = 0; i < toShow.getChildCount(); ++i) {
View button = toShow.getChildAt(i);
button.setClickable(true);
}
if (animatorStopped) {
animatorStopped = false;
} else if (buttonsToHide.size() > 0) {
animateTrackerLayout(null);
}
} else if (buttonsToHide.size() > 0) {
for (ViewGroup b : buttonsToHide) {
for (int i = 0; i < b.getChildCount(); ++i) {
View button = b.getChildAt(i);
button.setClickable(true);
}
b.setAlpha(1f);
b.setVisibility(View.GONE);
}
buttonsToHide.clear();
animatorStopped = false;
}
buttonsAnimator = null;
}
});
set.start();
buttonsAnimator = set;
return true;
}
});
}
private Animator getCoordinateAnimator(View view, int[] oldCoordinates) {
PropertyValuesHolder translationTop =
PropertyValuesHolder.ofInt("top", oldCoordinates[0], view.getTop());
PropertyValuesHolder translationBottom =
PropertyValuesHolder.ofInt("bottom", oldCoordinates[1], view.getBottom());
return ObjectAnimator.ofPropertyValuesHolder(view, translationTop, translationBottom);
}
private void stopAnimator() {
if (buttonsAnimator != null) {
animatorStopped = true;
buttonsAnimator.end();
}
}
}
private class TrackersDataSetObserver extends DataSetObserver {
private boolean isFrozen = false;
public TrackersDataSetObserver() { }
@Override public void onChanged() {
if (isFrozen) {
return;
}
int position = details.trackersCursor.getPosition();
for (int i = 0; i < trackersAdapter.getCount(); i++) {
Tracker tracker = trackersAdapter.getItem(i);
View v = null;
boolean hasChild = false;
if (!details.trackersCursor.moveToPosition(tracker.index)) {
continue;
}
if (i < views.trackersContainer.getChildCount()) {
v = views.trackersContainer.getChildAt(i);
hasChild = true;
}
if (!hasChild || (tracker.index != -1 && trackerChanged(tracker, details.trackersCursor))) {
v = trackersAdapter.getView(i, v, null);
if (!hasChild) {
views.trackersContainer.addView(v, i);
}
}
}
List<View> trackerViews = trackersAdapter.getViews();
Set<String> trackerIndices = new HashSet<>();
for (int i = 0; i < trackersAdapter.getCount(); ++i) {
Tracker tracker = trackersAdapter.getItem(i);
trackerIndices.add(tracker.announce);
}
Iterator<View> iter = trackerViews.iterator();
while (iter.hasNext()) {
View v = iter.next();
if (!trackerIndices.contains(v.getTag("announce".hashCode()))) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
animateRemoveView(v);
} else {
views.trackersContainer.removeView(v);
}
iter.remove();
}
}
details.trackersCursor.moveToPosition(position);
}
@Override public void onInvalidated() {
views.trackersContainer.removeAllViews();
}
public void setFrozen(boolean isFrozen) {
this.isFrozen = isFrozen;
}
private boolean trackerChanged(Tracker tracker, Cursor cursor) {
boolean changed = false;
if (!tracker.announce.equals(Torrent.Tracker.getAnnounce(cursor))
|| tracker.lastAnnounceTime != Torrent.Tracker.getLastAnnounceTime(cursor)
|| tracker.lastScrapeTime != Torrent.Tracker.getLastScrapeTime(cursor)
|| tracker.leecherCount != Torrent.Tracker.getLeecherCount(cursor)
|| tracker.seederCount != Torrent.Tracker.getSeederCount(cursor)) {
tracker.setInfo(cursor);
changed = true;
}
return changed;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void animateRemoveView(final View v) {
v.animate().setDuration(250).alpha(0).translationY(-50).withEndAction(
() -> views.trackersContainer.removeView(v));
}
}
private class TorrentDetailTask extends AsyncTask<String, Void, TorrentDetails> {
@Override protected TorrentDetails doInBackground(String... hashStrings) {
if (!isCancelled() && getActivity() != null) {
TransmissionProfileInterface context = (TransmissionProfileInterface) getActivity();
if (context == null || context.getProfile() == null) {
return null;
}
DataSource readSource = new DataSource(getActivity());
readSource.open();
try {
TorrentDetails details = readSource.getTorrentDetails(
context.getProfile().getId(), hashStrings[0]);
/* fill the windows */
details.torrentCursor.getCount();
details.filesCursor.getCount();
details.trackersCursor.getCount();
details.torrentCursor.moveToFirst();
return details;
} finally {
readSource.close();
}
}
return null;
}
@Override protected void onPostExecute(TorrentDetails details) {
if (isResumed()) {
if (TorrentDetailPageFragment.this.details != null) {
TorrentDetailPageFragment.this.details.torrentCursor.close();
TorrentDetailPageFragment.this.details.filesCursor.close();
TorrentDetailPageFragment.this.details.trackersCursor.close();
TorrentDetailPageFragment.this.details = null;
}
TorrentDetailPageFragment.this.details = details;
long now = new Date().getTime();
for (Long time : debouncers.values()) {
if (time != null && now < time + 300) {
return;
}
}
updateFields(getView());
}
}
}
}