Browse Source

Merge remote-tracking branch 'remotes/origin/master' into combine_rss_feed_views

pull/526/head
Twig N 5 years ago
parent
commit
7584c77271
  1. 6
      app/build.gradle
  2. 34
      app/src/main/java/org/transdroid/core/gui/DetailsActivity.java
  3. 29
      app/src/main/java/org/transdroid/core/gui/DetailsFragment.java
  4. 4
      app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java
  5. 26
      app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java
  6. 32
      app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java
  7. 3
      app/src/main/java/org/transdroid/core/gui/lists/LocalTorrent.java
  8. 150
      app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java
  9. 14
      app/src/main/java/org/transdroid/daemon/Daemon.java
  10. 4
      app/src/main/java/org/transdroid/daemon/DaemonMethod.java
  11. 347
      app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java
  12. 23
      app/src/main/java/org/transdroid/daemon/Torrent.java
  13. 27
      app/src/main/java/org/transdroid/daemon/TorrentDetails.java
  14. 31
      app/src/main/java/org/transdroid/daemon/task/ToggleFirstLastPieceDownloadTask.java
  15. 31
      app/src/main/java/org/transdroid/daemon/task/ToggleSequentialDownloadTask.java
  16. 31
      app/src/main/res/menu/fragment_details.xml
  17. 9
      app/src/main/res/values-ru/strings.xml
  18. 5
      app/src/main/res/values/changelog.xml
  19. 10
      app/src/main/res/values/strings.xml
  20. 2
      latest-app.html

6
app/build.gradle

@ -7,9 +7,9 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 28 targetSdkVersion 29
versionCode 236 versionCode 237
versionName '2.5.16' versionName '2.5.17'
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {

34
app/src/main/java/org/transdroid/core/gui/DetailsActivity.java

@ -56,6 +56,8 @@ import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.task.DaemonTaskFailureResult; import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult; import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult; import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.ToggleSequentialDownloadTask;
import org.transdroid.daemon.task.ToggleFirstLastPieceDownloadTask;
import org.transdroid.daemon.task.ForceRecheckTask; import org.transdroid.daemon.task.ForceRecheckTask;
import org.transdroid.daemon.task.GetFileListTask; import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult; import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
@ -278,6 +280,36 @@ public class DetailsActivity extends AppCompatActivity implements TorrentTasksEx
} }
} }
@Background
@Override
public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) {
torrent.mimicSequentialDownload(sequentialState);
String onState = getString(R.string.result_togglesequential_onstate);
String offState = getString(R.string.result_togglesequential_offstate);
String stateString = sequentialState ? onState : offState;
DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential, torrent.getName(), stateString));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) {
torrent.mimicFirstLastPieceDownload(firstLastPieceState);
String onState = getString(R.string.result_togglefirstlastpiece_onstate);
String offState = getString(R.string.result_togglefirstlastpiece_offstate);
String stateString = firstLastPieceState ? onState : offState;
DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglefirstlastpiece, torrent.getName(), stateString));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background @Background
@Override @Override
public void forceRecheckTorrent(Torrent torrent) { public void forceRecheckTorrent(Torrent torrent) {
@ -330,7 +362,7 @@ public class DetailsActivity extends AppCompatActivity implements TorrentTasksEx
// Refresh the screen as well // Refresh the screen as well
refreshTorrent(); refreshTorrent();
refreshTorrentDetails(torrent); refreshTorrentDetails(torrent);
SnackbarManager.show(Snackbar.with(this).text(successMessage)); SnackbarManager.show(Snackbar.with(this).text(successMessage).duration(Snackbar.SnackbarDuration.LENGTH_SHORT));
} }
@UiThread @UiThread

29
app/src/main/java/org/transdroid/core/gui/DetailsFragment.java

@ -203,6 +203,8 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen
.updateTrackers(SimpleListItemAdapter.SimpleStringItem.wrapStringsList(newTorrentDetails.getTrackers())); .updateTrackers(SimpleListItemAdapter.SimpleStringItem.wrapStringsList(newTorrentDetails.getTrackers()));
((DetailsAdapter) detailsList.getAdapter()) ((DetailsAdapter) detailsList.getAdapter())
.updateErrors(SimpleListItemAdapter.SimpleStringItem.wrapStringsList(newTorrentDetails.getErrors())); .updateErrors(SimpleListItemAdapter.SimpleStringItem.wrapStringsList(newTorrentDetails.getErrors()));
((DetailsAdapter) detailsList.getAdapter())
.updatePieces(newTorrentDetails.getPieces());
} }
/** /**
@ -301,6 +303,12 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen
case R.id.action_stop: case R.id.action_stop:
stopTorrent(); stopTorrent();
return true; return true;
case R.id.action_toggle_sequential:
toggleSequentialDownload(menuItem);
return true;
case R.id.action_toggle_firstlastpiece:
toggleFirstLastPieceDownload(menuItem);
return true;
case R.id.action_forcerecheck: case R.id.action_forcerecheck:
setForceRecheck(); setForceRecheck();
return true; return true;
@ -359,6 +367,15 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen
detailsMenu.getMenu().findItem(R.id.action_setlabel).setVisible(setLabel); detailsMenu.getMenu().findItem(R.id.action_setlabel).setVisible(setLabel);
boolean forceRecheck = Daemon.supportsForceRecheck(torrent.getDaemon()); boolean forceRecheck = Daemon.supportsForceRecheck(torrent.getDaemon());
detailsMenu.getMenu().findItem(R.id.action_forcerecheck).setVisible(forceRecheck); detailsMenu.getMenu().findItem(R.id.action_forcerecheck).setVisible(forceRecheck);
boolean sequentialdl = Daemon.supportsSequentialDownload(torrent.getDaemon());
MenuItem seqMenuItem = detailsMenu.getMenu().findItem(R.id.action_toggle_sequential);
seqMenuItem.setVisible(sequentialdl);
seqMenuItem.setChecked(torrent.isSequentiallyDownloading());
boolean firstlastpiecedl = Daemon.supportsFirstLastPiece(torrent.getDaemon());
MenuItem flpMenuItem = detailsMenu.getMenu().findItem(R.id.action_toggle_firstlastpiece);
flpMenuItem.setVisible(firstlastpiecedl);
flpMenuItem.setChecked(torrent.isDownloadingFirstLastPieceFirst());
detailsMenu.getMenu().findItem(R.id.action_download_mode).setVisible(!torrent.isFinished() && (firstlastpiecedl || sequentialdl));
boolean setTrackers = Daemon.supportsSetTrackers(torrent.getDaemon()); boolean setTrackers = Daemon.supportsSetTrackers(torrent.getDaemon());
detailsMenu.getMenu().findItem(R.id.action_updatetrackers).setVisible(setTrackers); detailsMenu.getMenu().findItem(R.id.action_updatetrackers).setVisible(setTrackers);
boolean setLocation = Daemon.supportsSetDownloadLocation(torrent.getDaemon()); boolean setLocation = Daemon.supportsSetDownloadLocation(torrent.getDaemon());
@ -421,6 +438,18 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen
} }
} }
@OptionsItem(R.id.action_toggle_sequential)
protected void toggleSequentialDownload(MenuItem menuItem) {
if (getTasksExecutor() != null)
getTasksExecutor().toggleSequentialDownload(torrent, !menuItem.isChecked());
}
@OptionsItem(R.id.action_toggle_firstlastpiece)
protected void toggleFirstLastPieceDownload(MenuItem menuItem) {
if (getTasksExecutor() != null)
getTasksExecutor().toggleFirstLastPieceDownload(torrent, !menuItem.isChecked());
}
@OptionsItem(R.id.action_forcerecheck) @OptionsItem(R.id.action_forcerecheck)
protected void setForceRecheck() { protected void setForceRecheck() {
if (getTasksExecutor() != null) if (getTasksExecutor() != null)

4
app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java

@ -40,6 +40,10 @@ public interface TorrentTasksExecutor {
void removeTorrent(Torrent torrent, boolean withData); void removeTorrent(Torrent torrent, boolean withData);
void toggleSequentialDownload(Torrent torrent, boolean sequentialState);
void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState);
void forceRecheckTorrent(Torrent torrent); void forceRecheckTorrent(Torrent torrent);
void updateLabel(Torrent torrent, String newLabel); void updateLabel(Torrent torrent, String newLabel);

26
app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java

@ -103,6 +103,8 @@ import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult; import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult; import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult; import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.ToggleSequentialDownloadTask;
import org.transdroid.daemon.task.ToggleFirstLastPieceDownloadTask;
import org.transdroid.daemon.task.ForceRecheckTask; import org.transdroid.daemon.task.ForceRecheckTask;
import org.transdroid.daemon.task.GetFileListTask; import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult; import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
@ -1225,6 +1227,30 @@ public class TorrentsActivity extends AppCompatActivity implements TorrentTasksE
} }
} }
@Background
@Override
public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) {
torrent.mimicSequentialDownload(sequentialState);
DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background
@Override
public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) {
torrent.mimicFirstLastPieceDownload(firstLastPieceState);
DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log);
if (result instanceof DaemonTaskSuccessResult) {
onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.action_toggle_firstlastpiece));
} else {
onCommunicationError((DaemonTaskFailureResult) result, false);
}
}
@Background @Background
@Override @Override
public void forceRecheckTorrent(Torrent torrent) { public void forceRecheckTorrent(Torrent torrent) {

32
app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java

@ -21,6 +21,7 @@ import java.util.List;
import org.transdroid.R; import org.transdroid.R;
import org.transdroid.core.gui.navigation.*; import org.transdroid.core.gui.navigation.*;
import org.transdroid.core.gui.lists.PiecesMapView;
import org.transdroid.daemon.Torrent; import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile; import org.transdroid.daemon.TorrentFile;
@ -38,6 +39,9 @@ public class DetailsAdapter extends MergeAdapter {
private ViewHolderAdapter torrentDetailsViewAdapter = null; private ViewHolderAdapter torrentDetailsViewAdapter = null;
private TorrentDetailsView torrentDetailsView = null; private TorrentDetailsView torrentDetailsView = null;
private ViewHolderAdapter piecesSeparatorAdapter = null;
private ViewHolderAdapter piecesMapViewAdapter = null;
private PiecesMapView piecesMapView = null;
private ViewHolderAdapter trackersSeparatorAdapter = null; private ViewHolderAdapter trackersSeparatorAdapter = null;
private SimpleListItemAdapter trackersAdapter = null; private SimpleListItemAdapter trackersAdapter = null;
private ViewHolderAdapter errorsSeparatorAdapter = null; private ViewHolderAdapter errorsSeparatorAdapter = null;
@ -56,6 +60,18 @@ public class DetailsAdapter extends MergeAdapter {
torrentDetailsViewAdapter.setViewVisibility(View.GONE); torrentDetailsViewAdapter.setViewVisibility(View.GONE);
addAdapter(torrentDetailsViewAdapter); addAdapter(torrentDetailsViewAdapter);
// Pieces map
piecesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_pieces)));
piecesSeparatorAdapter.setViewEnabled(false);
piecesSeparatorAdapter.setViewVisibility(View.GONE);
addAdapter(piecesSeparatorAdapter);
piecesMapView = new PiecesMapView(context);
piecesMapViewAdapter = new ViewHolderAdapter(piecesMapView);
piecesMapViewAdapter.setViewEnabled(false);
piecesMapViewAdapter.setViewVisibility(View.GONE);
addAdapter(piecesMapViewAdapter);
// Tracker errors // Tracker errors
errorsSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText( errorsSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText(
context.getString(R.string.status_errors))); context.getString(R.string.status_errors)));
@ -137,6 +153,22 @@ public class DetailsAdapter extends MergeAdapter {
} }
} }
public void updatePieces(List<Integer> pieces) {
if (pieces == null || pieces.isEmpty()) {
piecesSeparatorAdapter.setViewEnabled(false);
piecesSeparatorAdapter.setViewVisibility(View.GONE);
piecesMapViewAdapter.setViewEnabled(false);
piecesMapViewAdapter.setViewVisibility(View.GONE);
} else {
piecesMapView.setPieces(pieces);
piecesMapViewAdapter.setViewEnabled(true);
piecesMapViewAdapter.setViewVisibility(View.VISIBLE);
piecesSeparatorAdapter.setViewEnabled(true);
piecesSeparatorAdapter.setViewVisibility(View.VISIBLE);
}
}
/** /**
* Clear currently visible torrent, including header and shown lists * Clear currently visible torrent, including header and shown lists
*/ */

3
app/src/main/java/org/transdroid/core/gui/lists/LocalTorrent.java

@ -82,10 +82,11 @@ public class LocalTorrent {
switch (t.getStatusCode()) { switch (t.getStatusCode()) {
case Waiting: case Waiting:
case Checking:
case Error: case Error:
// Not downloading yet // Not downloading yet
return r.getString(R.string.status_waitingtodl, FileSizeConverter.getSize(t.getTotalSize())); return r.getString(R.string.status_waitingtodl, FileSizeConverter.getSize(t.getTotalSize()));
case Checking:
return r.getString(R.string.status_checking);
case Downloading: case Downloading:
// Downloading // Downloading
return r.getString( return r.getString(

150
app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java

@ -0,0 +1,150 @@
package org.transdroid.core.gui.lists;
import org.transdroid.R;
import android.content.Context;
import android.view.View;
import android.graphics.Canvas;
import android.graphics.Paint;
import java.util.ArrayList;
import java.util.List;
import java.lang.Math;
class PiecesMapView extends View {
private final float scale = getContext().getResources().getDisplayMetrics().density;
private final int MINIMUM_HEIGHT = (int) (25 * scale);
private final int MINIMUM_PIECE_WIDTH = (int) (2 * scale);
private ArrayList<Integer> pieces = null;
private final Paint downloadingPaint = new Paint();
private final Paint donePaint = new Paint();
private final Paint partialDonePaint = new Paint();
public PiecesMapView(Context context) {
super(context);
initPaints();
}
private void initPaints() {
downloadingPaint.setColor(getResources().getColor(R.color.torrent_downloading));
donePaint.setColor(getResources().getColor(R.color.torrent_seeding));
partialDonePaint.setColor(getResources().getColor(R.color.file_low));
}
public void setPieces(List<Integer> pieces) {
this.pieces = new ArrayList<Integer>(pieces);
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int ws = MeasureSpec.getSize(widthMeasureSpec);
int hs = Math.max(getHeight(), MINIMUM_HEIGHT);
setMeasuredDimension(ws, hs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (this.pieces == null) {
return;
}
int height = getHeight();
int width = getWidth();
// downscale
ArrayList<Integer> piecesScaled;
int pieceWidth;
pieceWidth = MINIMUM_PIECE_WIDTH;
piecesScaled = new ArrayList<Integer>();
int bucketCount = (int) Math.ceil((double) width / (double) pieceWidth);
int bucketSize = (int) Math.floor((double)this.pieces.size() / (double) bucketCount);
// loop buckets
for (int i = 0; i < bucketCount; i++) {
// Get segment of pieces that fall into bucket
int start = i * bucketSize;
// If this is the last bucket, throw the remainder of the pieces array into it
int end = (i == bucketCount-1) ? this.pieces.size() : (i+1) * bucketSize;
ArrayList<Integer> bucket = new ArrayList<Integer>(this.pieces.subList(start, end));
int doneCount = 0;
int downloadingCount = 0;
// loop pieces in bucket
for(int j = 0; j < bucket.size(); j++) {
// Count downloading pieces
if (bucket.get(j) == 1) {
downloadingCount++;
}
// Count finished pieces
else if (bucket.get(j) == 2) {
doneCount++;
}
}
int state;
// If a piece is downloading show bucket as downloading
if (downloadingCount > 0) {
state = 1;
}
// If all pieces are done, show bucket as done
else if (doneCount == bucket.size()) {
state = 2;
}
// Some done pieces, show bucket as partially done
else if (doneCount > 0) {
state = 3;
}
// bucket is not downloaded
else {
state = 0;
}
piecesScaled.add(state);
}
String scaledPiecesString = "";
for (int s : piecesScaled)
{
scaledPiecesString += s;
}
// Draw downscaled peices
for (int i = 0; i < piecesScaled.size(); i++) {
int piece = piecesScaled.get(i);
if (piece == 0) {
continue;
}
Paint paint = new Paint();
switch (piece) {
case 1:
paint = downloadingPaint;
break;
case 2:
paint = donePaint;
break;
case 3:
paint = partialDonePaint;
break;
}
int x = i * pieceWidth;
canvas.drawRect(x, 0, x + pieceWidth, height, paint);
}
}
}

14
app/src/main/java/org/transdroid/daemon/Daemon.java

@ -363,12 +363,12 @@ public enum Daemon {
public static boolean supportsAddByMagnetUrl(Daemon type) { public static boolean supportsAddByMagnetUrl(Daemon type) {
return type == uTorrent || type == BitTorrent || type == Transmission || type == Synology || type == Deluge || type == DelugeRpc return type == uTorrent || type == BitTorrent || type == Transmission || type == Synology || type == Deluge || type == DelugeRpc
|| type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet || type == Aria2 || type == Deluge2Rpc || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet
|| type == tTorrent || type == Dummy; || type == Aria2 || type == tTorrent || type == Dummy;
} }
public static boolean supportsRemoveWithData(Daemon type) { public static boolean supportsRemoveWithData(Daemon type) {
return type == uTorrent || type == Vuze || type == Transmission || type == Deluge || type == DelugeRpc return type == uTorrent || type == Vuze || type == Transmission || type == Deluge || type == DelugeRpc || type == Deluge2Rpc
|| type == BitTorrent || type == Tfb4rt || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitTorrent || type == Tfb4rt || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas
|| type == BitComet || type == rTorrent || type == Aria2 || type == tTorrent || type == Dummy; || type == BitComet || type == rTorrent || type == Aria2 || type == tTorrent || type == Dummy;
} }
@ -412,6 +412,14 @@ public enum Daemon {
|| type == Transmission || type == Dummy || type == qBittorrent; || type == Transmission || type == Dummy || type == qBittorrent;
} }
public static boolean supportsSequentialDownload(Daemon type) {
return type == qBittorrent;
}
public static boolean supportsFirstLastPiece(Daemon type) {
return type == qBittorrent;
}
public static boolean supportsExtraPassword(Daemon type) { public static boolean supportsExtraPassword(Daemon type) {
return type == Deluge || type == Aria2; return type == Deluge || type == Aria2;
} }

4
app/src/main/java/org/transdroid/daemon/DaemonMethod.java

@ -44,7 +44,9 @@ public enum DaemonMethod {
SetTrackers (19), SetTrackers (19),
SetAlternativeMode (20), SetAlternativeMode (20),
GetStats (21), GetStats (21),
ForceRecheck (22); ForceRecheck (22),
ToggleSequentialDownload(23),
ToggleFirstLastPieceDownload(24);
private int code; private int code;
private static final Map<Integer,DaemonMethod> lookup = new HashMap<>(); private static final Map<Integer,DaemonMethod> lookup = new HashMap<>();

347
app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java

@ -45,7 +45,26 @@ import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentDetails; import org.transdroid.daemon.TorrentDetails;
import org.transdroid.daemon.TorrentFile; import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus; import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.task.*; import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByMagnetUrlTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
import org.transdroid.daemon.task.GetStatsTask;
import org.transdroid.daemon.task.GetStatsTaskSuccessResult;
import org.transdroid.daemon.task.GetTorrentDetailsTask;
import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetDownloadLocationTask;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.task.SetLabelTask;
import org.transdroid.daemon.task.SetTransferRatesTask;
import org.transdroid.daemon.util.HttpHelper; import org.transdroid.daemon.util.HttpHelper;
import java.io.File; import java.io.File;
@ -61,6 +80,7 @@ import java.util.Map;
/** /**
* The daemon adapter for the qBittorrent torrent client. * The daemon adapter for the qBittorrent torrent client.
*
* @author erickok * @author erickok
*/ */
public class QbittorrentAdapter implements IDaemonAdapter { public class QbittorrentAdapter implements IDaemonAdapter {
@ -70,35 +90,28 @@ public class QbittorrentAdapter implements IDaemonAdapter {
private DaemonSettings settings; private DaemonSettings settings;
private DefaultHttpClient httpclient; private DefaultHttpClient httpclient;
private int version = -1; private int version = -1;
private int apiVersion = -1;
public QbittorrentAdapter(DaemonSettings settings) { public QbittorrentAdapter(DaemonSettings settings) {
this.settings = settings; this.settings = settings;
} }
private synchronized void ensureVersion(Log log) throws DaemonException { private synchronized void ensureVersion(Log log) {
// Still need to retrieve the API and qBittorrent version numbers from the server? // Still need to retrieve the API and qBittorrent version numbers from the server?
if (version > 0 && apiVersion > 0) if (version > 0)
return; return;
// Since 4.1, API v2 is used. Since qBittorrent 3.2, API v1 is used. Otherwise we use unofficial legacy json endpoints.
try { try {
// The API version is only supported since qBittorrent 3.2, so otherwise we assume version 1
try {
String apiVerText = makeRequest(log, "/version/api");
apiVersion = Integer.parseInt(apiVerText.trim());
} catch (DaemonException | NumberFormatException e) {
apiVersion = 1;
}
log.d(LOG_NAME, "qBittorrent API version is " + apiVersion);
// The qBittorent version is only supported since 3.2; for earlier versions we parse the about dialog and parse it
String versionText = ""; String versionText = "";
if (apiVersion > 1) { try {
// Format is something like 'v3.2.0' // Try v2 API first, which returns version number in 'v4.1.9' format
versionText = makeRequest(log, "/api/v2/app/version").substring(1);
} catch (Exception e1) {
// Try v1 API, which returns version number in 'v3.2.0' format
try {
versionText = makeRequest(log, "/version/qbittorrent").substring(1); versionText = makeRequest(log, "/version/qbittorrent").substring(1);
} else { } catch (Exception e2) {
// Format is something like 'qBittorrent v2.9.7 (Web UI)' or 'qBittorrent v3.0.0-alpha5 (Web UI)' // Legacy mode; format is something like 'qBittorrent v2.9.7 (Web UI)' or 'qBittorrent v3.0.0-alpha5 (Web UI)'
String about = makeRequest(log, "/about.html"); String about = makeRequest(log, "/about.html");
String aboutStartText = "qBittorrent v"; String aboutStartText = "qBittorrent v";
String aboutEndText = " (Web UI)"; String aboutEndText = " (Web UI)";
@ -108,8 +121,20 @@ public class QbittorrentAdapter implements IDaemonAdapter {
versionText = about.substring(aboutStart + aboutStartText.length(), aboutEnd); versionText = about.substring(aboutStart + aboutStartText.length(), aboutEnd);
} }
} }
}
version = parseVersionNumber(versionText);
} catch (Exception e) {
// Unable to establish version number; assume an old version by setting it to version 1
version = 10000;
}
}
private int parseVersionNumber(String versionText) {
// String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .) // String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .)
int version = -1;
String[] parts = versionText.split("\\."); String[] parts = versionText.split("\\.");
if (parts.length > 0) { if (parts.length > 0) {
version = Integer.parseInt(parts[0]) * 100 * 100; version = Integer.parseInt(parts[0]) * 100 * 100;
@ -129,64 +154,71 @@ public class QbittorrentAdapter implements IDaemonAdapter {
} }
} }
version += Integer.parseInt(numbers); version += Integer.parseInt(numbers);
return;
} }
} }
} }
return version;
} catch (Exception e) {
// Unable to establish version number; assume an old version by setting it to version 1
version = 10000;
apiVersion = 1;
}
} }
private synchronized void ensureAuthenticated(Log log) throws DaemonException { private synchronized void ensureAuthenticated(Log log) throws DaemonException {
// API changed in 3.2.0, login is now handled by its own request, which provides you a cookie. // API changed in 3.2.0, login is now handled by its own request, which provides you a cookie.
// If we don't have that cookie, let's try and get it. // If we don't have that cookie, let's try and get it.
if (version != -1 && version < 30200) {
if (apiVersion < 2) {
return; return;
} }
// Have we already authenticated? Check if we have the cookie that we need // Have we already authenticated? Check if we have the cookie that we need
List<Cookie> cookies = httpclient.getCookieStore().getCookies(); if (isAuthenticated()) {
for (Cookie c : cookies) {
if (c.getName().equals("SID")) {
// And here it is! Okay, no need authenticate again.
return; return;
} }
final BasicNameValuePair usernameParam = new BasicNameValuePair("username", settings.getUsername());
final BasicNameValuePair passwordParam = new BasicNameValuePair("password", settings.getPassword());
if (version == -1 || version >= 40100) {
try {
makeRequest(log, "/api/v2/auth/login", usernameParam, passwordParam);
} catch (DaemonException ignored) {
}
}
if (!isAuthenticated()) {
try {
makeRequest(log, "/login", usernameParam, passwordParam);
} catch (DaemonException ignored) {
}
} }
makeRequest(log, "/login", new BasicNameValuePair("username", settings.getUsername()), if (!isAuthenticated()) {
new BasicNameValuePair("password", settings.getPassword())); throw new DaemonException(ExceptionType.AuthenticationFailure, "Server rejected our login");
// The HttpClient will automatically remember the cookie for us, no need to parse it out. }
}
// However, we would like to see if authentication was successful or not... private boolean isAuthenticated() {
cookies = httpclient.getCookieStore().getCookies(); List<Cookie> cookies = httpclient.getCookieStore().getCookies();
for (Cookie c : cookies) { for (Cookie c : cookies) {
if (c.getName().equals("SID")) { if (c.getName().equals("SID")) {
// Good. Let's get out of here. // And here it is! Okay, no need authenticate again.
return; return true;
} }
} }
return false;
// No cookie found, we didn't authenticate.
throw new DaemonException(ExceptionType.AuthenticationFailure, "Server rejected our login");
} }
@Override @Override
public DaemonTaskResult executeTask(Log log, DaemonTask task) { public DaemonTaskResult executeTask(Log log, DaemonTask task) {
try { try {
ensureVersion(log); initialise();
ensureAuthenticated(log); ensureAuthenticated(log);
ensureVersion(log);
switch (task.getMethod()) { switch (task.getMethod()) {
case Retrieve: case Retrieve:
// Request all torrents from server
String path; String path;
if (version >= 30200) { if (version >= 40100) {
path = "/api/v2/torrents/info";
} else if (version >= 30200) {
path = "/query/torrents"; path = "/query/torrents";
} else if (version >= 30000) { } else if (version >= 30000) {
path = "/json/torrents"; path = "/json/torrents";
@ -194,77 +226,145 @@ public class QbittorrentAdapter implements IDaemonAdapter {
path = "/json/events"; path = "/json/events";
} }
// Request all torrents from server
JSONArray result = new JSONArray(makeRequest(log, path)); JSONArray result = new JSONArray(makeRequest(log, path));
return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonTorrents(result), parseJsonLabels(result)); return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonTorrents(result), parseJsonLabels(result));
case GetTorrentDetails: case GetTorrentDetails:
// Request tracker and error details for a specific teacher // Request tracker and error details for a specific teacher
String mhash = task.getTargetTorrent().getUniqueID(); String mhash = task.getTargetTorrent().getUniqueID();
JSONArray messages = JSONArray messages;
new JSONArray(makeRequest(log, (version >= 30200 ? "/query/propertiesTrackers/" : "/json/propertiesTrackers/") + mhash)); JSONArray pieces;
return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(messages)); if (version >= 40100) {
messages = new JSONArray(makeRequest(log, "/api/v2/torrents/trackers", new BasicNameValuePair("hash", mhash)));
pieces = new JSONArray(makeRequest(log, "/api/v2/torrents/pieceStates", new BasicNameValuePair("hash", mhash)));
} else {
messages = new JSONArray(makeRequest(log, "/query/propertiesTrackers/" + mhash));
pieces = new JSONArray(makeRequest(log, "/query/getPieceStates/" + mhash));
}
return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(messages, pieces));
case GetFileList: case GetFileList:
// Request files listing for a specific torrent // Request files listing for a specific torrent
String fhash = task.getTargetTorrent().getUniqueID(); String fhash = task.getTargetTorrent().getUniqueID();
JSONArray files = JSONArray files;
new JSONArray(makeRequest(log, (version >= 30200 ? "/query/propertiesFiles/" : "/json/propertiesFiles/") + fhash)); if (version >= 40100) {
files = new JSONArray(makeRequest(log, "/api/v2/torrents/files", new BasicNameValuePair("hash", fhash)));
} else if (version >= 30200) {
files = new JSONArray(makeRequest(log, "/query/propertiesFiles/" + fhash));
} else {
files = new JSONArray(makeRequest(log, "/json/propertiesFiles/" + fhash));
}
return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonFiles(files)); return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonFiles(files));
case AddByFile: case AddByFile:
// Upload a local .torrent file // Upload a local .torrent file
if (version >= 40100) {
path = "/api/v2/torrents/add";
} else {
path = "/command/upload";
}
String ufile = ((AddByFileTask) task).getFile(); String ufile = ((AddByFileTask) task).getFile();
makeUploadRequest("/command/upload", ufile, log); makeUploadRequest(path, ufile, log);
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case AddByUrl: case AddByUrl:
// Request to add a torrent by URL // Request to add a torrent by URL
String url = ((AddByUrlTask) task).getUrl(); String url = ((AddByUrlTask) task).getUrl();
makeRequest(log, "/command/download", new BasicNameValuePair("urls", url)); if (version >= 40100) {
path = "/api/v2/torrents/add";
} else {
path = "/command/upload";
}
makeRequest(log, path, new BasicNameValuePair("urls", url));
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case AddByMagnetUrl: case AddByMagnetUrl:
// Request to add a magnet link by URL // Request to add a magnet link by URL
String magnet = ((AddByMagnetUrlTask) task).getUrl(); String magnet = ((AddByMagnetUrlTask) task).getUrl();
makeRequest(log, "/command/download", new BasicNameValuePair("urls", magnet)); if (version >= 40100) {
path = "/api/v2/torrents/add";
} else {
path = "/command/download";
}
makeRequest(log, path, new BasicNameValuePair("urls", magnet));
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case Remove: case Remove:
// Remove a torrent // Remove a torrent
RemoveTask removeTask = (RemoveTask) task; RemoveTask removeTask = (RemoveTask) task;
makeRequest(log, (removeTask.includingData() ? "/command/deletePerm" : "/command/delete"), if (version >= 40100) {
new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID())); if (removeTask.includingData()) {
makeRequest(log, "/api/v2/torrents/delete",
new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()),
new BasicNameValuePair("deleteFiles", "true"));
} else {
makeRequest(log, "/api/v2/torrents/delete",
new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()),
new BasicNameValuePair("deleteFiles", "false"));
}
} else {
path = (removeTask.includingData() ? "/command/deletePerm" : "/command/delete");
makeRequest(log, path, new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()));
}
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case Pause: case Pause:
// Pause a torrent // Pause a torrent
if (version >= 40100) {
makeRequest(log, "/api/v2/torrents/pause", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()));
} else {
makeRequest(log, "/command/pause", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); makeRequest(log, "/command/pause", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID()));
}
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case PauseAll: case PauseAll:
// Resume all torrents // Resume all torrents
if (version >= 40100) {
makeRequest(log, "/api/v2/torrents/pause", new BasicNameValuePair("hashes", "all"));
} else {
makeRequest(log, "/command/pauseall"); makeRequest(log, "/command/pauseall");
}
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case Resume: case Resume:
// Resume a torrent // Resume a torrent
if (version >= 40100) {
makeRequest(log, "/api/v2/torrents/resume", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()));
} else {
makeRequest(log, "/command/resume", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); makeRequest(log, "/command/resume", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID()));
}
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case ResumeAll: case ResumeAll:
// Resume all torrents // Resume all torrents
if (version >= 40100) {
path = "/api/v2/torrents/resume";
makeRequest(log, path, new BasicNameValuePair("hashes", "all"));
} else {
makeRequest(log, "/command/resumeall"); makeRequest(log, "/command/resumeall");
}
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case SetFilePriorities: case SetFilePriorities:
@ -281,21 +381,59 @@ public class QbittorrentAdapter implements IDaemonAdapter {
} }
// We have to make a separate request per file, it seems // We have to make a separate request per file, it seems
for (TorrentFile file : setPrio.getForFiles()) { for (TorrentFile file : setPrio.getForFiles()) {
makeRequest(log, "/command/setFilePrio", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID()), if (version >= 40100) {
path = "/api/v2/torrents/filePrio";
} else {
path = "/command/setFilePrio";
}
makeRequest(log, path, new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID()),
new BasicNameValuePair("id", file.getKey()), new BasicNameValuePair("priority", newPrio)); new BasicNameValuePair("id", file.getKey()), new BasicNameValuePair("priority", newPrio));
} }
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case ForceRecheck: case ForceRecheck:
// Force recheck a torrent // Force recheck a torrent
makeRequest(log, "/command/recheck", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); if (version >= 40100) {
path = "/api/v2/torrents/recheck";
} else {
path = "/command/recheck";
}
makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()));
return new DaemonTaskSuccessResult(task);
case ToggleSequentialDownload:
// Toggle sequential download mode on a torrent
if (version >= 40100) {
path = "/api/v2/torrents/toggleSequentialDownload";
} else {
path = "/command/toggleSequentialDownload";
}
makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()));
return new DaemonTaskSuccessResult(task);
case ToggleFirstLastPieceDownload:
// Set policy for downloading first and last piece first on a torrent
if (version >= 40100) {
path = "/api/v2/torrents/toggleFirstLastPiecePrio";
} else {
path = "/command/toggleFirstLastPiecePrio";
}
makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()));
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case SetLabel: case SetLabel:
SetLabelTask labelTask = (SetLabelTask) task; SetLabelTask labelTask = (SetLabelTask) task;
makeRequest(log, "/command/setCategory", if (version >= 40100) {
path = "/api/v2/torrents/setCategory";
} else {
path = "/command/setCategory";
}
makeRequest(log, path,
new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()), new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()),
new BasicNameValuePair("category", labelTask.getNewLabel())); new BasicNameValuePair("category", labelTask.getNewLabel()));
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
@ -303,7 +441,12 @@ public class QbittorrentAdapter implements IDaemonAdapter {
case SetDownloadLocation: case SetDownloadLocation:
SetDownloadLocationTask setLocationTask = (SetDownloadLocationTask) task; SetDownloadLocationTask setLocationTask = (SetDownloadLocationTask) task;
makeRequest(log, "/command/setLocation", if (version >= 40100) {
path = "/api/v2/torrents/setLocation";
} else {
path = "/command/setLocation";
}
makeRequest(log, path,
new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()), new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()),
new BasicNameValuePair("location", setLocationTask.getNewLocation())); new BasicNameValuePair("location", setLocationTask.getNewLocation()));
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
@ -311,18 +454,33 @@ public class QbittorrentAdapter implements IDaemonAdapter {
case SetTransferRates: case SetTransferRates:
// Request to set the maximum transfer rates // Request to set the maximum transfer rates
String pathDL;
String pathUL;
SetTransferRatesTask ratesTask = (SetTransferRatesTask) task; SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
String dl = (ratesTask.getDownloadRate() == null ? "NaN" : Long.toString(ratesTask.getDownloadRate() * 1024)); String dl = (ratesTask.getDownloadRate() == null ? "NaN" : Long.toString(ratesTask.getDownloadRate() * 1024));
String ul = (ratesTask.getUploadRate() == null ? "NaN" : Long.toString(ratesTask.getUploadRate() * 1024)); String ul = (ratesTask.getUploadRate() == null ? "NaN" : Long.toString(ratesTask.getUploadRate() * 1024));
makeRequest(log, "/command/setGlobalDlLimit", new BasicNameValuePair("limit", dl)); if (version >= 40100) {
makeRequest(log, "/command/setGlobalUpLimit", new BasicNameValuePair("limit", ul)); pathDL = "/api/v2/torrents/setDownloadLimit";
pathUL = "/api/v2/torrents/setUploadLimit";
} else {
pathDL = "/command/setGlobalDlLimit";
pathUL = "/command/setGlobalUpLimit";
}
makeRequest(log, pathDL, new BasicNameValuePair("limit", dl));
makeRequest(log, pathUL, new BasicNameValuePair("limit", ul));
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
case GetStats: case GetStats:
// Refresh alternative download speeds setting // Refresh alternative download speeds setting
JSONObject stats = new JSONObject(makeRequest(log, "/sync/maindata?rid=0")); if (version >= 40100) {
path = "/api/v2/sync/maindata?rid=0";
} else {
path = "/sync/maindata?rid=0";
}
JSONObject stats = new JSONObject(makeRequest(log, path));
JSONObject serverStats = stats.optJSONObject("server_state"); JSONObject serverStats = stats.optJSONObject("server_state");
boolean alternativeSpeeds = false; boolean alternativeSpeeds = false;
if (serverStats != null) { if (serverStats != null) {
@ -333,7 +491,12 @@ public class QbittorrentAdapter implements IDaemonAdapter {
case SetAlternativeMode: case SetAlternativeMode:
// Flip alternative speed mode // Flip alternative speed mode
makeRequest(log, "/command/toggleAlternativeSpeedLimits"); if (version >= 40100) {
path = "/api/v2/transfer/toggleSpeedLimitsMode";
} else {
path = "/command/toggleAlternativeSpeedLimits";
}
makeRequest(log, path);
return new DaemonTaskSuccessResult(task); return new DaemonTaskSuccessResult(task);
default: default:
@ -352,7 +515,9 @@ public class QbittorrentAdapter implements IDaemonAdapter {
try { try {
// Setup request using POST // Setup request using POST
HttpPost httppost = new HttpPost(buildWebUIUrl(path)); String url_to_request = buildWebUIUrl(path);
HttpPost httppost = new HttpPost(url_to_request);
List<NameValuePair> nvps = new ArrayList<>(); List<NameValuePair> nvps = new ArrayList<>();
Collections.addAll(nvps, params); Collections.addAll(nvps, params);
httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
@ -384,15 +549,14 @@ public class QbittorrentAdapter implements IDaemonAdapter {
private String makeWebRequest(HttpPost httppost, Log log) throws DaemonException { private String makeWebRequest(HttpPost httppost, Log log) throws DaemonException {
try { try {
// Initialise the HTTP client
if (httpclient == null) {
initialise();
}
// Execute // Execute
HttpResponse response = httpclient.execute(httppost); HttpResponse response = httpclient.execute(httppost);
// Throw exception on 403
if (response.getStatusLine().getStatusCode() == 403) {
throw new DaemonException(ExceptionType.AuthenticationFailure, "Response code 403");
}
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (entity != null) { if (entity != null) {
@ -413,21 +577,30 @@ public class QbittorrentAdapter implements IDaemonAdapter {
} catch (Exception e) { } catch (Exception e) {
log.d(LOG_NAME, "Error: " + e.toString()); log.d(LOG_NAME, "Error: " + e.toString());
if (e instanceof DaemonException) {
throw (DaemonException) e;
} else {
throw new DaemonException(ExceptionType.ConnectionError, e.toString()); throw new DaemonException(ExceptionType.ConnectionError, e.toString());
} }
}
} }
/** /**
* Instantiates an HTTP client with proper credentials that can be used for all qBittorrent requests. * Instantiates an HTTP client with proper credentials that can be used for all qBittorrent requests.
*
* @throws DaemonException On conflicting or missing settings * @throws DaemonException On conflicting or missing settings
*/ */
private void initialise() throws DaemonException { private void initialise() throws DaemonException {
if (httpclient == null) {
httpclient = HttpHelper.createStandardHttpClient(settings, true); httpclient = HttpHelper.createStandardHttpClient(settings, true);
} }
}
/** /**
* Build the URL of the web UI request from the user settings * Build the URL of the web UI request from the user settings
*
* @return The URL to request * @return The URL to request
*/ */
private String buildWebUIUrl(String path) { private String buildWebUIUrl(String path) {
@ -439,7 +612,7 @@ public class QbittorrentAdapter implements IDaemonAdapter {
return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + proxyFolder + path; return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + proxyFolder + path;
} }
private TorrentDetails parseJsonTorrentDetails(JSONArray messages) throws JSONException { private TorrentDetails parseJsonTorrentDetails(JSONArray messages, JSONArray pieceStates) throws JSONException {
ArrayList<String> trackers = new ArrayList<>(); ArrayList<String> trackers = new ArrayList<>();
ArrayList<String> errors = new ArrayList<>(); ArrayList<String> errors = new ArrayList<>();
@ -455,8 +628,15 @@ public class QbittorrentAdapter implements IDaemonAdapter {
} }
} }
ArrayList<Integer> pieces = new ArrayList<>();
if (pieceStates.length() > 0) {
for (int i = 0; i < pieceStates.length(); i++) {
pieces.add(pieceStates.getInt(i));
}
}
// Return the list // Return the list
return new TorrentDetails(trackers, errors); return new TorrentDetails(trackers, errors, pieces);
} }
@ -466,7 +646,7 @@ public class QbittorrentAdapter implements IDaemonAdapter {
Map<String, Label> labels = new HashMap<>(); Map<String, Label> labels = new HashMap<>();
for (int i = 0; i < response.length(); i++) { for (int i = 0; i < response.length(); i++) {
JSONObject tor = response.getJSONObject(i); JSONObject tor = response.getJSONObject(i);
if (apiVersion >= 2) { if (version >= 40100) {
String label = tor.optString("category"); String label = tor.optString("category");
if (label != null && label.length() > 0) { if (label != null && label.length() > 0) {
final Label labelObject = labels.get(label); final Label labelObject = labels.get(label);
@ -492,11 +672,13 @@ public class QbittorrentAdapter implements IDaemonAdapter {
long uploaded; long uploaded;
int dlspeed; int dlspeed;
int upspeed; int upspeed;
boolean dlseq = false;
boolean dlflp = false;
Date addedOn = null; Date addedOn = null;
Date completionOn = null; Date completionOn = null;
String label = null; String label = null;
if (apiVersion >= 2) { if (version >= 30200) {
leechers = new int[2]; leechers = new int[2];
leechers[0] = tor.getInt("num_leechs"); leechers[0] = tor.getInt("num_leechs");
leechers[1] = tor.getInt("num_complete") + tor.getInt("num_incomplete"); leechers[1] = tor.getInt("num_complete") + tor.getInt("num_incomplete");
@ -507,6 +689,12 @@ public class QbittorrentAdapter implements IDaemonAdapter {
ratio = tor.getDouble("ratio"); ratio = tor.getDouble("ratio");
dlspeed = tor.getInt("dlspeed"); dlspeed = tor.getInt("dlspeed");
upspeed = tor.getInt("upspeed"); upspeed = tor.getInt("upspeed");
if (tor.has("seq_dl")) {
dlseq = tor.getBoolean("seq_dl");
}
if (tor.has("f_l_piece_prio")) {
dlflp = tor.getBoolean("f_l_piece_prio");
}
if (tor.has("uploaded")) { if (tor.has("uploaded")) {
uploaded = tor.getLong("uploaded"); uploaded = tor.getLong("uploaded");
} else { } else {
@ -535,7 +723,7 @@ public class QbittorrentAdapter implements IDaemonAdapter {
eta = (long) (size - (size * progress)) / dlspeed; eta = (long) (size - (size * progress)) / dlspeed;
// Add the parsed torrent to the list // Add the parsed torrent to the list
// @formatter:off // @formatter:off
torrents.add(new Torrent( Torrent torrent = new Torrent(
(long) i, (long) i,
tor.getString("hash"), tor.getString("hash"),
tor.getString("name"), tor.getString("name"),
@ -557,7 +745,10 @@ public class QbittorrentAdapter implements IDaemonAdapter {
addedOn, addedOn,
completionOn, completionOn,
null, null,
settings.getType())); settings.getType());
torrent.mimicSequentialDownload(dlseq);
torrent.mimicFirstLastPieceDownload(dlflp);
torrents.add(torrent);
// @formatter:on // @formatter:on
} }
@ -679,7 +870,7 @@ public class QbittorrentAdapter implements IDaemonAdapter {
JSONObject file = response.getJSONObject(i); JSONObject file = response.getJSONObject(i);
long size; long size;
if (apiVersion >= 2) { if (version >= 30200) {
size = file.getLong("size"); size = file.getLong("size");
} else { } else {
size = parseSize(file.getString("size")); size = parseSize(file.getString("size"));

23
app/src/main/java/org/transdroid/daemon/Torrent.java

@ -49,6 +49,8 @@ public final class Torrent implements Parcelable, Comparable<Torrent>, Finishabl
final private float partDone; final private float partDone;
final private float available; final private float available;
private String label; private String label;
private boolean sequentialDownload;
private boolean firstLastPieceDownload;
final private Date dateAdded; final private Date dateAdded;
final private Date dateDone; final private Date dateDone;
@ -76,6 +78,8 @@ public final class Torrent implements Parcelable, Comparable<Torrent>, Finishabl
this.partDone = in.readFloat(); this.partDone = in.readFloat();
this.available = in.readFloat(); this.available = in.readFloat();
this.label = in.readString(); this.label = in.readString();
this.sequentialDownload = in.readByte() != 0;
this.firstLastPieceDownload = in.readByte() != 0;
long lDateAdded = in.readLong(); long lDateAdded = in.readLong();
this.dateAdded = (lDateAdded == -1) ? null : new Date(lDateAdded); this.dateAdded = (lDateAdded == -1) ? null : new Date(lDateAdded);
@ -109,6 +113,8 @@ public final class Torrent implements Parcelable, Comparable<Torrent>, Finishabl
this.partDone = partDone; this.partDone = partDone;
this.available = available; this.available = available;
this.label = label; this.label = label;
this.sequentialDownload = false;
this.firstLastPieceDownload = false;
this.dateAdded = dateAdded; this.dateAdded = dateAdded;
if (realDateDone != null) { if (realDateDone != null) {
@ -197,6 +203,13 @@ public final class Torrent implements Parcelable, Comparable<Torrent>, Finishabl
return label; return label;
} }
public boolean isSequentiallyDownloading() {
return sequentialDownload;
}
public boolean isDownloadingFirstLastPieceFirst() {
return firstLastPieceDownload;
}
public Date getDateAdded() { public Date getDateAdded() {
return dateAdded; return dateAdded;
} }
@ -342,6 +355,14 @@ public final class Torrent implements Parcelable, Comparable<Torrent>, Finishabl
label = newLabel; label = newLabel;
} }
public void mimicSequentialDownload(boolean sequentialDownload) {
this.sequentialDownload = sequentialDownload;
}
public void mimicFirstLastPieceDownload(boolean firstLastPieceDownload) {
this.firstLastPieceDownload = firstLastPieceDownload;
}
public void mimicCheckingStatus() { public void mimicCheckingStatus() {
statusCode = TorrentStatus.Checking; statusCode = TorrentStatus.Checking;
} }
@ -399,6 +420,8 @@ public final class Torrent implements Parcelable, Comparable<Torrent>, Finishabl
dest.writeFloat(partDone); dest.writeFloat(partDone);
dest.writeFloat(available); dest.writeFloat(available);
dest.writeString(label); dest.writeString(label);
dest.writeByte((byte) (sequentialDownload ? 1 : 0));
dest.writeByte((byte) (firstLastPieceDownload ? 1 : 0));
dest.writeLong((dateAdded == null) ? -1 : dateAdded.getTime()); dest.writeLong((dateAdded == null) ? -1 : dateAdded.getTime());
dest.writeLong((dateDone == null) ? -1 : dateDone.getTime()); dest.writeLong((dateDone == null) ? -1 : dateDone.getTime());

27
app/src/main/java/org/transdroid/daemon/TorrentDetails.java

@ -18,6 +18,7 @@
package org.transdroid.daemon; package org.transdroid.daemon;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
@ -32,15 +33,29 @@ public final class TorrentDetails implements Parcelable {
private final List<String> trackers; private final List<String> trackers;
private final List<String> errors; private final List<String> errors;
private final List<Integer> pieces;
public TorrentDetails(List<String> trackers, List<String> errors) { public TorrentDetails(List<String> trackers, List<String> errors) {
this.trackers = trackers; this.trackers = trackers;
this.errors = errors; this.errors = errors;
this.pieces = null;
}
public TorrentDetails(List<String> trackers, List<String> errors, List<Integer> pieces) {
this.trackers = trackers;
this.errors = errors;
this.pieces = pieces;
} }
private TorrentDetails(Parcel in) { private TorrentDetails(Parcel in) {
this.trackers = in.createStringArrayList(); this.trackers = in.createStringArrayList();
this.errors = in.createStringArrayList(); this.errors = in.createStringArrayList();
int[] piecesarray = in.createIntArray();
this.pieces = new ArrayList<Integer>(piecesarray.length);
for (int i : piecesarray) {
this.pieces.add(i);
}
} }
public List<String> getTrackers() { public List<String> getTrackers() {
@ -77,6 +92,10 @@ public final class TorrentDetails implements Parcelable {
return errorsText; return errorsText;
} }
public List<Integer> getPieces() {
return this.pieces;
}
public static final Parcelable.Creator<TorrentDetails> CREATOR = new Parcelable.Creator<TorrentDetails>() { public static final Parcelable.Creator<TorrentDetails> CREATOR = new Parcelable.Creator<TorrentDetails>() {
public TorrentDetails createFromParcel(Parcel in) { public TorrentDetails createFromParcel(Parcel in) {
return new TorrentDetails(in); return new TorrentDetails(in);
@ -96,6 +115,14 @@ public final class TorrentDetails implements Parcelable {
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeStringList(trackers); dest.writeStringList(trackers);
dest.writeStringList(errors); dest.writeStringList(errors);
int[] piecesarray = new int[this.pieces.size()];
for(int i = 0; i < this.pieces.size(); i++) {
if (this.pieces.get(i) != null) {
piecesarray[i] = this.pieces.get(i);
}
}
dest.writeIntArray(piecesarray);
} }
} }

31
app/src/main/java/org/transdroid/daemon/task/ToggleFirstLastPieceDownloadTask.java

@ -0,0 +1,31 @@
/*
* This file is part of Transdroid <http://www.transdroid.org>
*
* 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.daemon.task;
import org.transdroid.daemon.DaemonMethod;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Torrent;
public class ToggleFirstLastPieceDownloadTask extends DaemonTask {
protected ToggleFirstLastPieceDownloadTask(IDaemonAdapter adapter, Torrent targetTorrent) {
super(adapter, DaemonMethod.ToggleFirstLastPieceDownload, targetTorrent, null);
}
public static ToggleFirstLastPieceDownloadTask create(IDaemonAdapter adapter, Torrent targetTorrent) {
return new ToggleFirstLastPieceDownloadTask(adapter, targetTorrent);
}
}

31
app/src/main/java/org/transdroid/daemon/task/ToggleSequentialDownloadTask.java

@ -0,0 +1,31 @@
/*
* This file is part of Transdroid <http://www.transdroid.org>
*
* 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.daemon.task;
import org.transdroid.daemon.DaemonMethod;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Torrent;
public class ToggleSequentialDownloadTask extends DaemonTask {
protected ToggleSequentialDownloadTask(IDaemonAdapter adapter, Torrent targetTorrent) {
super(adapter, DaemonMethod.ToggleSequentialDownload, targetTorrent, null);
}
public static ToggleSequentialDownloadTask create(IDaemonAdapter adapter, Torrent targetTorrent) {
return new ToggleSequentialDownloadTask(adapter, targetTorrent);
}
}

31
app/src/main/res/menu/fragment_details.xml

@ -47,19 +47,36 @@
<item <item
android:id="@+id/action_start_direct" android:id="@+id/action_start_direct"
android:icon="@drawable/ic_action_start" android:icon="@drawable/ic_action_start"
android:orderInCategory="202" android:orderInCategory="203"
android:title="@string/action_start" android:title="@string/action_start"
app:showAsAction="always" /> app:showAsAction="always" />
<item <item
android:id="@+id/action_stop" android:id="@+id/action_stop"
android:icon="@drawable/ic_action_stop" android:icon="@drawable/ic_action_stop"
android:orderInCategory="203" android:orderInCategory="204"
android:title="@string/action_stop" android:title="@string/action_stop"
app:showAsAction="always" /> app:showAsAction="always" />
<item
android:id="@+id/action_download_mode"
android:icon="@drawable/ic_action_download"
android:orderInCategory="205"
android:title="@string/action_download_mode"
app:showAsAction="always" >
<menu>
<item
android:id="@+id/action_toggle_sequential"
android:title="@string/action_toggle_sequential"
android:checkable="true" />
<item
android:id="@+id/action_toggle_firstlastpiece"
android:title="@string/action_toggle_firstlastpiece"
android:checkable="true" />
</menu>
</item>
<item <item
android:id="@+id/action_remove" android:id="@+id/action_remove"
android:icon="@drawable/ic_action_remove" android:icon="@drawable/ic_action_remove"
android:orderInCategory="204" android:orderInCategory="206"
android:title="@string/action_remove" android:title="@string/action_remove"
app:showAsAction="always"> app:showAsAction="always">
<menu> <menu>
@ -74,25 +91,25 @@
<item <item
android:id="@+id/action_setlabel" android:id="@+id/action_setlabel"
android:icon="@drawable/ic_action_labels" android:icon="@drawable/ic_action_labels"
android:orderInCategory="205" android:orderInCategory="207"
android:title="@string/action_setlabel" android:title="@string/action_setlabel"
app:showAsAction="always" /> app:showAsAction="always" />
<item <item
android:id="@+id/action_forcerecheck" android:id="@+id/action_forcerecheck"
android:icon="@drawable/ic_action_force_recheck" android:icon="@drawable/ic_action_force_recheck"
android:orderInCategory="206" android:orderInCategory="208"
android:title="@string/action_forcerecheck" android:title="@string/action_forcerecheck"
app:showAsAction="always" /> app:showAsAction="always" />
<item <item
android:id="@+id/action_updatetrackers" android:id="@+id/action_updatetrackers"
android:icon="@drawable/ic_action_trackers" android:icon="@drawable/ic_action_trackers"
android:orderInCategory="207" android:orderInCategory="209"
android:title="@string/action_updatetrackers" android:title="@string/action_updatetrackers"
app:showAsAction="always" /> app:showAsAction="always" />
<item <item
android:id="@+id/action_changelocation" android:id="@+id/action_changelocation"
android:icon="@drawable/ic_action_save" android:icon="@drawable/ic_action_save"
android:orderInCategory="208" android:orderInCategory="210"
android:title="@string/action_changelocation" android:title="@string/action_changelocation"
app:showAsAction="always" /> app:showAsAction="always" />

9
app/src/main/res/values-ru/strings.xml

@ -54,6 +54,9 @@
<string name="action_remove_default">Удалить торрент-файл</string> <string name="action_remove_default">Удалить торрент-файл</string>
<string name="action_remove_withdata">Удалить вместе с данными</string> <string name="action_remove_withdata">Удалить вместе с данными</string>
<string name="action_setlabel">Установить метку</string> <string name="action_setlabel">Установить метку</string>
<string name="action_download_mode">Режим скачивания</string>
<string name="action_toggle_sequential">Скачать последовательно</string>
<string name="action_toggle_firstlastpiece">Скачать начало и конец первыми</string>
<string name="action_updatetrackers">Обновить трекеры</string> <string name="action_updatetrackers">Обновить трекеры</string>
<string name="action_changelocation">Изменить место расположения</string> <string name="action_changelocation">Изменить место расположения</string>
<string name="action_forcerecheck">Пересчитать хэш</string> <string name="action_forcerecheck">Пересчитать хэш</string>
@ -167,6 +170,12 @@
<string name="result_trackersupdated">Трекеры обновлены</string> <string name="result_trackersupdated">Трекеры обновлены</string>
<string name="result_labelset">Метка установлена в \'%1$s\'</string> <string name="result_labelset">Метка установлена в \'%1$s\'</string>
<string name="result_labelremoved">Метка удалена</string> <string name="result_labelremoved">Метка удалена</string>
<string name="result_togglesequential">%1$s скачивается %2$s</string>
<string name="result_togglesequential_offstate">последовательно</string>
<string name="result_togglesequential_onstate">обычным образом</string>
<string name="result_togglefirstlastpiece">%1$s имеет %2$s</string>
<string name="result_togglefirstlastpiece_onstate">приоритет первого и последнего куска</string>
<string name="result_togglefirstlastpiece_offstate">обычый приоритет кусков</string>
<string name="result_recheckedstarted">Проверка данных %1$s</string> <string name="result_recheckedstarted">Проверка данных %1$s</string>
<string name="result_locationset">Торрент перемещен в \'%1$s\'</string> <string name="result_locationset">Торрент перемещен в \'%1$s\'</string>
<string name="result_priotitiesset">Приоритеты файлов обновлены</string> <string name="result_priotitiesset">Приоритеты файлов обновлены</string>

5
app/src/main/res/values/changelog.xml

@ -17,6 +17,11 @@
--> -->
<resources> <resources>
<string name="system_changelog"> <string name="system_changelog">
Transdroid 2.5.17\n
- qBittorrent 4.2+ support\n
- Proper label for checking status\n
- Deluge 2 RPC magnet support\n
\n
Transdroid 2.5.16\n Transdroid 2.5.16\n
- Deluge 2 via RPC support\n - Deluge 2 via RPC support\n
- Fix Transmission with digest auth\n - Fix Transmission with digest auth\n

10
app/src/main/res/values/strings.xml

@ -56,6 +56,9 @@
<string name="action_remove_default">Remove torrent</string> <string name="action_remove_default">Remove torrent</string>
<string name="action_remove_withdata">Remove and delete data</string> <string name="action_remove_withdata">Remove and delete data</string>
<string name="action_setlabel">Set label</string> <string name="action_setlabel">Set label</string>
<string name="action_download_mode">Download mode</string>
<string name="action_toggle_sequential">Download sequentially</string>
<string name="action_toggle_firstlastpiece">Prioritize first and last piece</string>
<string name="action_updatetrackers">Update trackers</string> <string name="action_updatetrackers">Update trackers</string>
<string name="action_changelocation">Change storage location</string> <string name="action_changelocation">Change storage location</string>
<string name="action_forcerecheck">Force data recheck</string> <string name="action_forcerecheck">Force data recheck</string>
@ -130,6 +133,7 @@
<string name="status_priority_low">Low priority</string> <string name="status_priority_low">Low priority</string>
<string name="status_priority_normal">Normal priority</string> <string name="status_priority_normal">Normal priority</string>
<string name="status_priority_high">High priority</string> <string name="status_priority_high">High priority</string>
<string name="status_pieces">PIECES</string>
<string name="status_trackers">TRACKERS</string> <string name="status_trackers">TRACKERS</string>
<string name="status_errors">ERRORS</string> <string name="status_errors">ERRORS</string>
<string name="status_files">FILES</string> <string name="status_files">FILES</string>
@ -177,6 +181,12 @@
<string name="result_trackersupdated">Trackers updated</string> <string name="result_trackersupdated">Trackers updated</string>
<string name="result_labelset">Label set to \'%1$s\'</string> <string name="result_labelset">Label set to \'%1$s\'</string>
<string name="result_labelremoved">Label removed</string> <string name="result_labelremoved">Label removed</string>
<string name="result_togglesequential">%1$s is downloading %2$s</string>
<string name="result_togglesequential_offstate">normally</string>
<string name="result_togglesequential_onstate">sequentially</string>
<string name="result_togglefirstlastpiece">%1$s has %2$s</string>
<string name="result_togglefirstlastpiece_onstate">first and last piece priority</string>
<string name="result_togglefirstlastpiece_offstate">normal piece priority</string>
<string name="result_recheckedstarted">Checking %1$s data</string> <string name="result_recheckedstarted">Checking %1$s data</string>
<string name="result_locationset">Torrent moved to \'%1$s\'</string> <string name="result_locationset">Torrent moved to \'%1$s\'</string>
<string name="result_priotitiesset">File priorities updated</string> <string name="result_priotitiesset">File priorities updated</string>

2
latest-app.html

@ -1 +1 @@
235|2.5.15 237|2.5.17

Loading…
Cancel
Save