org.transdroid.daemon.Aria2c.Aria2Adapter.java Source code

Java tutorial

Introduction

Here is the source code for org.transdroid.daemon.Aria2c.Aria2Adapter.java

Source

/*
 *   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.Aria2c;

import android.net.Uri;
import android.text.TextUtils;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.base64.android.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.transdroid.core.gui.log.Log;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentDetails;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus;
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.GetTorrentDetailsTask;
import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
import org.transdroid.daemon.task.PauseTask;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.ResumeTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetTransferRatesTask;
import org.transdroid.daemon.util.HttpHelper;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

/**
 * The daemon adapter from the Aria2 torrent client. Documentation available at http://aria2.sourceforge.net/manual/en/html/aria2c.html
 * @author erickok
 */
public class Aria2Adapter implements IDaemonAdapter {

    private static final String LOG_NAME = "Aria2 daemon";

    private DaemonSettings settings;
    private DefaultHttpClient httpclient;

    public Aria2Adapter(DaemonSettings settings) {
        this.settings = settings;
    }

    @Override
    public DaemonTaskResult executeTask(Log log, DaemonTask task) {

        try {
            JSONArray params = new JSONArray();

            switch (task.getMethod()) {
            case Retrieve:

                // Request all torrents from server
                // NOTE Since there is no aria2.tellAll (or something) we have to use batch requests
                JSONArray fields = new JSONArray().put("gid").put("status").put("totalLength")
                        .put("completedLength").put("uploadLength").put("downloadSpeed").put("uploadSpeed")
                        .put("numSeeders").put("dir").put("connections").put("errorCode").put("bittorrent")
                        .put("files");
                JSONObject active = buildRequest("aria2.tellActive", new JSONArray().put(fields));
                JSONObject waiting = buildRequest("aria2.tellWaiting",
                        new JSONArray().put(0).put(9999).put(fields));
                JSONObject stopped = buildRequest("aria2.tellStopped",
                        new JSONArray().put(0).put(9999).put(fields));
                params.put(active).put(waiting).put(stopped);

                List<Torrent> torrents = new ArrayList<Torrent>();
                JSONArray lists = makeRequestForArray(log, params.toString());
                for (int i = 0; i < lists.length(); i++) {
                    torrents.addAll(parseJsonRetrieveTorrents(lists.getJSONObject(i).getJSONArray("result")));
                }
                return new RetrieveTaskSuccessResult((RetrieveTask) task, torrents, null);

            case GetTorrentDetails:

                // Request file listing of a torrent
                params.put(task.getTargetTorrent().getUniqueID()); // gid
                params.put(new JSONArray().put("bittorrent").put("errorCode"));

                JSONObject dinfo = makeRequest(log, buildRequest("aria2.tellStatus", params).toString());
                return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task,
                        parseJsonTorrentDetails(dinfo.getJSONObject("result")));

            case GetFileList:

                // Request file listing of a torrent
                params.put(task.getTargetTorrent().getUniqueID()); // torrent_id

                JSONObject finfo = makeRequest(log, buildRequest("aria2.getFiles", params).toString());
                return new GetFileListTaskSuccessResult((GetFileListTask) task,
                        parseJsonFileListing(finfo.getJSONArray("result"), task.getTargetTorrent()));

            case AddByFile:

                // Encode the .torrent file's data
                String file = ((AddByFileTask) task).getFile();
                InputStream in = new Base64.InputStream(new FileInputStream(new File(URI.create(file))),
                        Base64.ENCODE);
                StringWriter writer = new StringWriter();
                int c;
                while ((c = in.read()) != -1) {
                    writer.write(c);
                }
                in.close();

                // Request to add a torrent by local .torrent file
                params.put(writer.toString());
                makeRequest(log, buildRequest("aria2.addTorrent", params).toString());
                return new DaemonTaskSuccessResult(task);

            case AddByUrl:

                // Request to add a torrent by URL
                String url = ((AddByUrlTask) task).getUrl();
                params.put(new JSONArray().put(url));

                makeRequest(log, buildRequest("aria2.addUri", params).toString());
                return new DaemonTaskSuccessResult(task);

            case AddByMagnetUrl:

                // Request to add a magnet link by URL
                String magnet = ((AddByMagnetUrlTask) task).getUrl();
                params.put(new JSONArray().put(magnet));

                makeRequest(log, buildRequest("aria2.addUri", params).toString());
                return new DaemonTaskSuccessResult(task);

            case Remove:

                // Remove a torrent
                RemoveTask removeTask = (RemoveTask) task;
                makeRequest(log,
                        buildRequest(removeTask.includingData() ? "aria2.removeDownloadResult" : "aria2.remove",
                                params.put(removeTask.getTargetTorrent().getUniqueID())).toString());
                return new DaemonTaskSuccessResult(task);

            case Pause:

                // Pause a torrent
                PauseTask pauseTask = (PauseTask) task;
                makeRequest(log, buildRequest("aria2.pause", params.put(pauseTask.getTargetTorrent().getUniqueID()))
                        .toString());
                return new DaemonTaskSuccessResult(task);

            case PauseAll:

                // Resume all torrents
                makeRequest(log, buildRequest("aria2.pauseAll", null).toString());
                return new DaemonTaskSuccessResult(task);

            case Resume:

                // Resume a torrent
                ResumeTask resumeTask = (ResumeTask) task;
                makeRequest(log,
                        buildRequest("aria2.unpause", params.put(resumeTask.getTargetTorrent().getUniqueID()))
                                .toString());
                return new DaemonTaskSuccessResult(task);

            case ResumeAll:

                // Resume all torrents
                makeRequest(log, buildRequest("aria2.unpauseAll", null).toString());
                return new DaemonTaskSuccessResult(task);

            case SetTransferRates:

                // Request to set the maximum transfer rates
                SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
                JSONObject options = new JSONObject();
                options.put("max-overall-download-limit",
                        (ratesTask.getDownloadRate() == null ? -1 : ratesTask.getDownloadRate()));
                options.put("max-overall-upload-limit",
                        (ratesTask.getUploadRate() == null ? -1 : ratesTask.getUploadRate()));

                makeRequest(log, buildRequest("aria2.changeGlobalOption", params.put(options)).toString());
                return new DaemonTaskSuccessResult(task);

            default:
                return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported,
                        task.getMethod() + " is not supported by " + getType()));
            }
        } catch (JSONException e) {
            return new DaemonTaskFailureResult(task,
                    new DaemonException(ExceptionType.ParsingFailed, e.toString()));
        } catch (DaemonException e) {
            return new DaemonTaskFailureResult(task, e);
        } catch (FileNotFoundException e) {
            return new DaemonTaskFailureResult(task,
                    new DaemonException(ExceptionType.FileAccessError, e.toString()));
        } catch (IOException e) {
            return new DaemonTaskFailureResult(task,
                    new DaemonException(ExceptionType.FileAccessError, e.toString()));
        }
    }

    private JSONObject buildRequest(String sendMethod, JSONArray params) throws JSONException {

        // Build request for method
        if (!TextUtils.isEmpty(settings.getExtraPassword())) {
            JSONArray signed = new JSONArray();
            // Start with the secret token as parameter and then add the normal parameters
            signed.put("token:" + settings.getExtraPassword());
            if (params != null) {
                for (int i = 0; i < params.length(); i++) {
                    signed.put(params.get(i));
                }
            }
            params = signed;
        }
        JSONObject request = new JSONObject();
        request.put("id", "transdroid");
        request.put("jsonrpc", "2.0");
        request.put("method", sendMethod);
        request.put("params", params);
        return request;

    }

    private synchronized JSONObject makeRequest(Log log, String data) throws DaemonException {
        String raw = makeRawRequest(log, data);
        try {
            return new JSONObject(raw);
        } catch (JSONException e) {
            log.d(LOG_NAME, "Error: " + e.toString());
            throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString());
        }
    }

    private synchronized JSONArray makeRequestForArray(Log log, String data) throws DaemonException {
        String raw = makeRawRequest(log, data);
        try {
            return new JSONArray(raw);
        } catch (JSONException e) {
            log.d(LOG_NAME, "Error: " + e.toString());
            throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString());
        }
    }

    private synchronized String makeRawRequest(Log log, String data) throws DaemonException {

        try {

            // Initialise the HTTP client
            if (httpclient == null) {
                httpclient = HttpHelper.createStandardHttpClient(settings,
                        !TextUtils.isEmpty(settings.getUsername()));
                httpclient.addRequestInterceptor(HttpHelper.gzipRequestInterceptor);
                httpclient.addResponseInterceptor(HttpHelper.gzipResponseInterceptor);
            }

            // Set POST URL and data
            String url = (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":"
                    + settings.getPort() + (settings.getFolder() == null ? "" : settings.getFolder()) + "/jsonrpc";
            HttpPost httppost = new HttpPost(url);
            httppost.setEntity(new StringEntity(data));
            httppost.setHeader("Content-Type", "application/json");
            httppost.setHeader("Accept", "application/json");

            // Execute
            HttpResponse response = httpclient.execute(httppost);

            HttpEntity entity = response.getEntity();
            if (entity == null) {
                throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity in response object.");
            }

            // Read JSON response
            InputStream instream = entity.getContent();
            String result = HttpHelper.convertStreamToString(instream);
            instream.close();

            log.d(LOG_NAME,
                    "Success: " + (result.length() > 300
                            ? result.substring(0, 300) + "... (" + result.length() + " chars)"
                            : result));
            return result;

        } catch (Exception e) {
            log.d(LOG_NAME, "Error: " + e.toString());
            throw new DaemonException(ExceptionType.ConnectionError, e.toString());
        }

    }

    private ArrayList<Torrent> parseJsonRetrieveTorrents(JSONArray response) throws JSONException, DaemonException {

        // Parse response
        ArrayList<Torrent> torrents = new ArrayList<Torrent>();
        for (int j = 0; j < response.length(); j++) {

            // Add the parsed torrent to the list
            JSONObject tor = response.getJSONObject(j);
            int downloadSpeed = tor.getInt("downloadSpeed");
            long totalLength = tor.getLong("totalLength");
            long completedLength = tor.getLong("completedLength");
            int numSeeders = tor.has("numSeeders") ? tor.getInt("numSeeders") : 0;
            TorrentStatus status = convertAriaState(tor.getString("status"), completedLength == totalLength);
            int errorCode = tor.optInt("errorCode", 0);
            String error = errorCode > 0 ? convertAriaError(errorCode) : null;
            String name = null;
            JSONObject bittorrent;
            if (tor.has("bittorrent")) {
                // Get name form the bittorrent info object
                bittorrent = tor.getJSONObject("bittorrent");
                if (bittorrent.has("info")) {
                    name = bittorrent.getJSONObject("info").getString("name");
                }
            } else if (tor.has("files")) {
                // Get name from the first included file we can find
                JSONArray files = tor.getJSONArray("files");
                if (files.length() > 0) {
                    name = Uri.parse(files.getJSONObject(0).getString("path")).getLastPathSegment();
                    if (name == null) {
                        name = files.getJSONObject(0).getString("path");
                    }
                }
            }
            if (name == null) {
                name = tor.getString("gid"); // Fallback name
            }
            // @formatter:off
            torrents.add(new Torrent(j, tor.getString("gid"), name, status, tor.getString("dir"), downloadSpeed,
                    tor.getInt("uploadSpeed"), tor.getInt("connections"), numSeeders, tor.getInt("connections"),
                    numSeeders, (downloadSpeed > 0 ? (int) (totalLength / downloadSpeed) : -1), completedLength,
                    tor.getLong("uploadLength"), totalLength, completedLength / (float) totalLength, // Percentage to [0..1]
                    0f, // Not available
                    null, // Not available
                    null, // Not available
                    null, // Not available
                    error, settings.getType()));
            // @formatter:on

        }
        return torrents;

    }

    private ArrayList<TorrentFile> parseJsonFileListing(JSONArray response, Torrent torrent) throws JSONException {

        // Parse response
        ArrayList<TorrentFile> files = new ArrayList<TorrentFile>();
        for (int j = 0; j < response.length(); j++) {

            JSONObject file = response.getJSONObject(j);
            // Add the parsed torrent to the list
            String rel = file.getString("path");
            if (rel.startsWith(torrent.getLocationDir())) {
                rel = rel.substring(torrent.getLocationDir().length());
            }
            // @formatter:off
            files.add(new TorrentFile(Integer.toString(file.getInt("index")), rel, rel, file.getString("path"),
                    file.getLong("length"), file.getLong("completedLength"),
                    file.getBoolean("selected") ? Priority.Normal : Priority.Off));
            // @formatter:on

        }
        return files;

    }

    private TorrentDetails parseJsonTorrentDetails(JSONObject response) throws JSONException {

        // Parse response
        List<String> trackers = new ArrayList<String>();
        List<String> errors = new ArrayList<String>();

        int error = response.optInt("errorCode", 0);
        if (error > 0) {
            errors.add(convertAriaError(error));
        }

        if (response.has("bittorrent")) {
            JSONObject bittorrent = response.getJSONObject("bittorrent");
            JSONArray announceList = bittorrent.getJSONArray("announceList");
            for (int i = 0; i < announceList.length(); i++) {
                JSONArray announceUrlList = announceList.getJSONArray(i);
                for (int j = 0; j < announceUrlList.length(); j++) {
                    trackers.add(announceUrlList.getString(j));
                }
            }
        }

        return new TorrentDetails(trackers, errors);

    }

    private TorrentStatus convertAriaState(String state, boolean isFinished) {
        // Aria2 sends a string as status code
        // (http://aria2.sourceforge.net/manual/en/html/aria2c.html#aria2.tellStatus)
        if (state.equals("active")) {
            return isFinished ? TorrentStatus.Seeding : TorrentStatus.Downloading;
        } else if (state.equals("waiting")) {
            return TorrentStatus.Queued;
        } else if (state.equals("paused") || state.equals("complete")) {
            return TorrentStatus.Paused;
        } else if (state.equals("error")) {
            return TorrentStatus.Error;
        } else if (state.equals("removed")) {
            return TorrentStatus.Checking;
        }
        return TorrentStatus.Unknown;
    }

    private String convertAriaError(int errorCode) {
        // Aria2 sends an exit code as error (http://aria2.sourceforge.net/manual/en/html/aria2c.html#id1)
        String error = "Aria error #" + Integer.toString(errorCode);
        switch (errorCode) {
        case 3:
        case 4:
            return error + ": Resource was not found";
        case 5:
            return error + ": Aborted because download speed was too slow";
        case 6:
            return error + ": Network problem occurred";
        case 8:
            return error + ": Remote server did not support resume when resume was required to complete download";
        case 9:
            return error + ": There was not enough disk space available";
        case 11:
        case 12:
            return error + ": Duplicate file or info hash download";
        case 15:
        case 16:
            return error + ": Aria2 could not create new or open or truncate existing file";
        case 17:
        case 18:
        case 19:
            return error + ": File I/O error occurred";
        case 20:
        case 27:
            return error + ": Aria2 could not parse Magnet URI or Metalink document";
        case 21:
            return error + ": FTP command failed";
        case 22:
            return error + ": HTTP response header was bad or unexpected";
        case 23:
            return error + ": Too many redirects occurred";
        case 24:
            return error + ": HTTP authorization failed";
        case 26:
            return error + ": \".torrent\" file is corrupted or missing information that aria2 needs";
        default:
            return error;
        }
    }

    @Override
    public Daemon getType() {
        return settings.getType();
    }

    @Override
    public DaemonSettings getSettings() {
        return this.settings;
    }

}