cmd = new ArrayList<>(3);
54 | StringBuilder scriptCmd = new StringBuilder(100);
55 |
56 | scriptCmd.append('\'').append(f.getAbsolutePath()).append('\'');
57 | for (String a : args) scriptCmd.append(' ').append(a);
58 |
59 | cmd.add("su");
60 | cmd.add("-c");
61 | cmd.add(scriptCmd.toString());
62 | return Utils.exec(3000, null, cmd);
63 | }
64 |
65 | public enum Script {
66 | set_so_buf, create_dir, create_file
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/de/full_description.txt:
--------------------------------------------------------------------------------
1 | Transmission ist ein leistungsstarker und dennoch extrem leichter und schneller BitTorrent-Client. Es bietet die Funktionen, die Sie von einem BitTorrent-Client erwarten: Verschlüsselung, Webschnittstelle, Peer-Exchange, Magnetlinks, DHT, µTP, UPnP und NAT-PMP-Portweiterleitung, Webseed-Unterstützung, Tracker-Bearbeitung, Geschwindigkeitsbegrenzungen für globale und pro Torrent und mehr.
Diese App ist ein Port des Transmission-Daemons für Android, der folgende Funktionen enthält:
Verwalten Sie Downloads direkt aus der Anwendung. Mehrere Watch- / Download-Verzeichnisse. Unterstützung für HTTP (S) / SOCKS-Proxy. WiFi / Ethernet-Modus zum Speichern mobiler Daten. Zugelassene WLAN-SSIDs: Wenn konfiguriert, wird der Dienst nur ausgeführt, wenn er mit den angegebenen Netzwerken verbunden ist. Behalten Sie die CPU / WLAN-Verbindung bei, um alle Downloads abzuschließen, bevor das Gerät in den Ruhezustand wechselt. Sequentieller Download ermöglicht das Abspielen von Mediendateien während des Herunterladens. Öffnen Sie Torrent-Dateien oder Torrent- / Magnet-URLs und streamen Sie die ausgewählten Dateien auf einen Media Player. Integrierter UPnP MediaServer. Laden Sie Mediendateien auf ein Telefon / Tablet / TV-Box herunter und sehen Sie sich auf einem Fernsehgerät oder einem anderen UPnP-kompatiblen Mediaplayer an, der mit demselben Netzwerk verbunden ist. M3U-Wiedergabelisten für alle Torrents / Ordner, die Audio- / Videodateien enthalten. Um die Wiedergabelisten-URL abzurufen, klicken Sie lange auf das Wiedergabesymbol. Alternative Weboberfläche - Transmission Web Control . Der Daemon läuft als Hintergrunddienst. Für die Fernkommunikation mit dem Dämon können Sie entweder die integrierte WEB-Schnittstelle oder eine beliebige Transmission-Fernbedienung verwenden.
Weitere Informationen finden Sie auf der offiziellen Website des Transmission -Projekts: transmissionbt.com .
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/PowerLock.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.net.wifi.WifiManager;
6 | import android.net.wifi.WifiManager.WifiLock;
7 | import android.os.PowerManager;
8 | import android.os.PowerManager.WakeLock;
9 |
10 | import static android.content.Context.POWER_SERVICE;
11 | import static android.content.Context.WIFI_SERVICE;
12 | import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
13 | import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
14 | import static com.ap.transmission.btc.Utils.debug;
15 |
16 | /**
17 | * @author Andrey Pavlenko
18 | */
19 | public class PowerLock {
20 | private static final String TAG = "TransmissionLock";
21 | private final WakeLock wakeLock;
22 | private final WifiLock wifiLock;
23 |
24 | private PowerLock(PowerManager.WakeLock wakeLock, WifiManager.WifiLock wifiLock) {
25 | this.wakeLock = wakeLock;
26 | this.wifiLock = wifiLock;
27 | }
28 |
29 | @SuppressLint("WakelockTimeout")
30 | public void acquire() {
31 | if (wakeLock != null) {
32 | debug(TAG, "Acquiring CPU lock");
33 | wakeLock.acquire();
34 | }
35 | if (wifiLock != null) {
36 | debug(TAG, "Acquiring WiFi lock");
37 | wifiLock.acquire();
38 | }
39 | }
40 |
41 | public void release() {
42 | if (wakeLock != null) {
43 | debug(TAG, "Releasing CPU lock");
44 | wakeLock.release();
45 | }
46 | if (wifiLock != null) {
47 | debug(TAG, "Releasing WiFi lock");
48 | wifiLock.release();
49 | }
50 | }
51 |
52 | public static PowerLock newLock(Context ctx) {
53 | PowerManager pmgr = (PowerManager) ctx.getApplicationContext().getSystemService(POWER_SERVICE);
54 | WifiManager wmgr = (WifiManager) ctx.getApplicationContext().getSystemService(WIFI_SERVICE);
55 | WakeLock wakeLock = (pmgr == null) ? null : pmgr.newWakeLock(PARTIAL_WAKE_LOCK, TAG);
56 | WifiLock wifiLock = (wmgr == null) ? null : wmgr.createWifiLock(WIFI_MODE_FULL, TAG);
57 | return ((wakeLock == null) && (wifiLock == null)) ? null : new PowerLock(wakeLock, wifiLock);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/cmake/OpenSSL.cmake:
--------------------------------------------------------------------------------
1 |
2 | include(ExternalProject)
3 | include(ProcessorCount)
4 |
5 | if (ANDROID_ABI MATCHES "armeabi")
6 | set(OPENSSL_ANDROID_ABI "android-arm")
7 | elseif (ANDROID_ABI STREQUAL "arm64-v8a")
8 | set(OPENSSL_ANDROID_ABI "android-arm64")
9 | elseif (ANDROID_ABI STREQUAL "x86_64")
10 | set(OPENSSL_ANDROID_ABI "android-x86_64")
11 | elseif (ANDROID_ABI STREQUAL "x86")
12 | set(OPENSSL_ANDROID_ABI "android-x86")
13 | else ()
14 | message(FATAL_ERROR "Unsupported ANDROID_ABI: ${ANDROID_ABI}")
15 | endif ()
16 |
17 | ProcessorCount(NCPU)
18 | find_program(MAKE_EXE NAMES make gmake nmake)
19 |
20 | set(OPENSSL_PATH "${ANDROID_TOOLCHAIN_ROOT}/bin:$ENV{PATH}")
21 | set(OPENSSL_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}")
22 | set(OPENSSL_ENV "export ANDROID_NDK_HOME='${ANDROID_NDK}' && \
23 | export PATH='${ANDROID_TOOLCHAIN_ROOT}/bin:$ENV{PATH}'")
24 | set(OPENSSL_CONFIGURE_CMD eval ${OPENSSL_ENV} && ./Configure)
25 | set(OPENSSL_BUILD_CMD eval ${OPENSSL_ENV} && ${MAKE_EXE} -j${NCPU})
26 |
27 | set(OPENSSL_CONFIGURE_OPTS --prefix=${CMAKE_INSTALL_PREFIX} no-shared no-idea no-camellia
28 | no-seed no-bf no-cast no-rc2 no-md2 no-md4 no-mdc2 no-dsa no-err no-engine
29 | no-tests no-unit-test no-external-tests no-dso no-dynamic-engine no-stdio zlib
30 | ${OPENSSL_ANDROID_ABI} -D__ANDROID_API__=${ANDROID_NATIVE_API_LEVEL})
31 |
32 | set(OPENSSL_ROOT_DIR "${OPENSSL_INSTALL_DIR}${CMAKE_INSTALL_PREFIX}")
33 | set(OPENSSL_LIB_DIR "${OPENSSL_ROOT_DIR}/lib")
34 | set(OPENSSL_LIBRARIES "${OPENSSL_LIB_DIR}/libssl.a" "${OPENSSL_LIB_DIR}/libcrypto.a")
35 | set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/include")
36 |
37 | message(STATUS "OpenSSL config: ${OPENSSL_CONFIGURE_OPTS}")
38 |
39 | ExternalProject_Add(openssl
40 | URL "https://www.openssl.org/source/openssl-1.1.1l.tar.gz"
41 | PREFIX openssl
42 | BUILD_IN_SOURCE 1
43 | CONFIGURE_COMMAND ${OPENSSL_CONFIGURE_CMD} ${OPENSSL_CONFIGURE_OPTS}
44 | BUILD_COMMAND ${OPENSSL_BUILD_CMD}
45 | INSTALL_COMMAND ${MAKE_EXE} install_sw DESTDIR=${OPENSSL_INSTALL_DIR}
46 | BUILD_BYPRODUCTS ${OPENSSL_LIBRARIES})
47 |
48 | add_dependencies(${PROJECT_NAME} openssl)
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Transmission BitTorrent Client for Android
2 | [ ](https://play.google.com/store/apps/details?id=com.ap.transmission.btc)
3 | [ ](https://apt.izzysoft.de/packages/com.ap.transmission.btc)
4 |
5 | ## About
6 | This application is a port of the Transmission daemon for Android complemented with the following features:
7 |
8 | * Manage downloads directly from the application
9 | * Multiple watch/download directories
10 | * Support for HTTP(S)/SOCKS proxy
11 | * WiFi/Ethernet mode to save on mobile data
12 | * Keep CPU/WiFi awake to complete all downloads before the device goes to sleep
13 | * Sequential download allows playing media files while downloading
14 | * Open .torrent files or torrent/magnet URLs and stream the selected files to a media player
15 | * Builtin UPnP MediaServer. Download media files to a phone/tablet/TV-box and watch on a TV or another UPnP compatible media player connected to the same network
16 | * M3U playlists for all torrents/folders containing audio/video files. To get the playlist URL - long click on the play icon
17 |
18 | ## Building
19 |
20 | * Download the latest Android NDK from https://developer.android.com/ndk/downloads/
21 | * Download the latest Android SDK or Android Studio from https://developer.android.com/studio/
22 | * Extract the downloaded archives
23 | * Export the environment variable ANDROID_NDK_ROOT pointing to the NDK directory
24 |
25 | ### Build dependencies
26 |
27 | $ git clone https://github.com/AndreyPavlenko/android-build.git
28 | $ export ANDROID_BUILD_ROOT="$PWD/android-build"
29 | $ cd "$ANDROID_BUILD_ROOT/packages/transmission"
30 | $ ./build.sh all
31 |
32 | ### Build the apks
33 | $ git clone https://github.com/AndreyPavlenko/transmissionbtc.git
34 |
35 | Open the local.properties file in a text editor and enter the valid paths to ndk.dir, sdk.dir, depends and keystore, set the keystore properties.
36 |
37 | $ cd transmissionbtc
38 | $ gradle cleanAll assembleRelease
39 |
--------------------------------------------------------------------------------
/src/main/res/layout/dir_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
24 |
25 |
26 |
39 |
40 |
49 |
50 |
57 |
--------------------------------------------------------------------------------
/src/main/res/layout/file_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
24 |
25 |
26 |
39 |
40 |
49 |
50 |
58 |
--------------------------------------------------------------------------------
/src/main/res/layout/select_file.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
14 |
15 |
21 |
22 |
32 |
33 |
39 |
40 |
46 |
47 |
53 |
54 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/main/res/layout/browse_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
18 |
19 |
35 |
36 |
51 |
52 |
62 |
63 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/http/Response.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.http;
2 |
3 | import java.io.IOException;
4 | import java.io.OutputStream;
5 |
6 | import static com.ap.transmission.btc.Utils.ASCII;
7 |
8 | /**
9 | * @author Andrey Pavlenko
10 | */
11 | @SuppressWarnings("ALL")
12 | public interface Response {
13 | void write(OutputStream out) throws IOException;
14 |
15 | static class StaticResponse implements Response {
16 | private final byte[] data;
17 |
18 | public StaticResponse(byte[] data) {this.data = data;}
19 |
20 | @Override
21 | public final void write(OutputStream out) throws IOException {
22 | out.write(data);
23 | out.close();
24 | }
25 | }
26 |
27 | static final class BadRequest {
28 | public static final Response instance = new StaticResponse(("HTTP/1.1 400 Bad Request\r\n" +
29 | "Connection: close\r\n" +
30 | "Content-Length: 0\r\n\r\n").getBytes(ASCII)
31 | );
32 | }
33 |
34 | static final class NotFound {
35 | public static final Response instance = new StaticResponse(("HTTP/1.1 404 Not Found\r\n" +
36 | "Connection: close\r\n" +
37 | "Content-Length: 0\r\n\r\n").getBytes(ASCII)
38 | );
39 | }
40 |
41 | static final class MethodNotAllowed {
42 | public static final Response instance = new StaticResponse(("HTTP/1.1 405 Method Not Allowed\r\n" +
43 | "Connection: close\r\n" +
44 | "Content-Length: 0\r\n\r\n").getBytes(ASCII)
45 | );
46 | }
47 |
48 | static final class PayloadTooLarge {
49 | public static final Response instance = new StaticResponse(("HTTP/1.1 413 Payload Too Large\r\n" +
50 | "Connection: close\r\n" +
51 | "Content-Length: 0\r\n\r\n").getBytes(ASCII)
52 | );
53 | }
54 |
55 | static final class ServerError {
56 | public static final Response instance = new StaticResponse(("HTTP/1.1 500 Internal Server Error\r\n" +
57 | "Connection: close\r\n" +
58 | "Content-Length: 0\r\n\r\n").getBytes(ASCII)
59 | );
60 | }
61 |
62 | static final class ServiceUnavailable {
63 | public static final Response instance = new StaticResponse(("HTTP/1.1 503 Service Unavailable\r\n" +
64 | "Connection: close\r\n" +
65 | "Content-Length: 0\r\n\r\n").getBytes(ASCII)
66 | );
67 | }
68 |
69 | static final class VersionNotSupported {
70 | public static final Response instance = new StaticResponse(("HTTP/1.1 505 HTTP Version Not Supported\r\n" +
71 | "Connection: close\r\n" +
72 | "Content-Length: 0\r\n\r\n").getBytes(ASCII)
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/test/java/com/ap/transmission/btc/MiscTest.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc;
2 |
3 | import com.ap.transmission.btc.views.PathItem;
4 |
5 | import org.junit.Test;
6 |
7 | import java.io.ByteArrayOutputStream;
8 | import java.io.PrintStream;
9 | import java.util.ArrayList;
10 | import java.util.Arrays;
11 | import java.util.Collections;
12 | import java.util.List;
13 | import java.util.Map;
14 |
15 | import static org.junit.Assert.assertEquals;
16 | import static org.junit.Assert.assertTrue;
17 |
18 | public class MiscTest {
19 | @Test
20 | public void testPathItem() {
21 | String[] ls = new String[]{
22 | "/a/b/c/d",
23 | "a/b/c/e",
24 | "c",
25 | "a/c",
26 | "a/f",
27 | "e",
28 | "b/c",
29 | "b/d/",
30 | };
31 | String sorted = "a/\n" +
32 | "a/b/\n" +
33 | "a/b/c/\n" +
34 | "a/b/c/d\n" +
35 | "a/b/c/e\n" +
36 | "a/c\n" +
37 | "a/f\n" +
38 | "b/\n" +
39 | "b/c\n" +
40 | "b/d\n" +
41 | "c\n" +
42 | "e\n";
43 |
44 | Map roots = PathItem.split(ls);
45 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
46 | PrintStream out = new PrintStream(baos);
47 |
48 | print(roots, out);
49 | out.flush();
50 | assertEquals(sorted, baos.toString());
51 |
52 | baos.reset();
53 | for (PathItem i : PathItem.ls(roots)) {
54 | out.print(i);
55 | out.print('\n');
56 | }
57 | out.flush();
58 | assertEquals(sorted, baos.toString());
59 | }
60 |
61 | private static void print(Map items, PrintStream out) {
62 | List l = new ArrayList<>(items.values());
63 | Collections.sort(l);
64 |
65 | for (PathItem i : l) {
66 | out.print(i);
67 | out.print('\n');
68 | print(i.getChildren(), out);
69 | }
70 | }
71 |
72 | @Test
73 | public void testNaturalOrderComparator() {
74 | NaturalOrderComparator cmp = new NaturalOrderComparator();
75 | String[] unsorted = new String[]{
76 | "a", "b", "abc", "zxx", "aaa1", "aaa10a", "aaa01", "aaa2", "aaa3", "aaa2aa", "aaa11",
77 | "aaa11382193812093", "aaa12382193812093", "aaa" + Long.MAX_VALUE
78 | };
79 | String[] sorted = new String[]{
80 | "a", "aaa1", "aaa01", "aaa2", "aaa2aa", "aaa3", "aaa10a", "aaa11", "aaa11382193812093",
81 | "aaa12382193812093", "aaa9223372036854775807", "abc", "b", "zxx"
82 | };
83 |
84 | Arrays.sort(unsorted, cmp);
85 | // for (String s : unsorted) System.out.print("\"" + s + "\", ");
86 | assertTrue(Arrays.equals(unsorted, sorted));
87 | }
88 | }
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/views/WatchItemView.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.views;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.GridLayout;
9 | import android.widget.ImageView;
10 |
11 | import com.ap.transmission.btc.Prefs;
12 | import com.ap.transmission.btc.R;
13 | import com.ap.transmission.btc.Utils;
14 |
15 | /**
16 | * @author Andrey Pavlenko
17 | */
18 | public class WatchItemView extends GridLayout {
19 | private int index = -1;
20 |
21 | public WatchItemView(Context context, AttributeSet attrs) {
22 | super(context, attrs);
23 | setColumnCount(1);
24 |
25 | LayoutInflater i = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
26 | if (i == null) throw new RuntimeException("Inflater is null");
27 | i.inflate(R.layout.watch_view, this, true);
28 |
29 | ViewGroup l = (ViewGroup) getChildAt(0);
30 | final BrowseView watch = (BrowseView) l.getChildAt(0);
31 | final BrowseView download = (BrowseView) l.getChildAt(1);
32 | final ImageView remove = watch.getLeftButton();
33 | remove.setVisibility(View.VISIBLE);
34 | remove.setImageDrawable(getResources().getDrawable(R.drawable.trash));
35 | remove.setOnClickListener(new OnClickListener() {
36 | @Override
37 | public void onClick(View v) {
38 | Prefs prefs = Utils.getActivity(v).getPrefs();
39 | watch.getPath().setOnFocusChangeListener(null);
40 | download.getPath().setOnFocusChangeListener(null);
41 | prefs.removeString(Prefs.K.WATCH_DIR, index);
42 | prefs.removeString(Prefs.K.DOWNLOAD_DIR, index);
43 | }
44 | });
45 | }
46 |
47 | public void setIndex(int index) {
48 | this.index = index;
49 | Prefs prefs = Utils.getActivity(this).getPrefs();
50 | ViewGroup l = (ViewGroup) getChildAt(0);
51 | BrowseView w = (BrowseView) l.getChildAt(0);
52 | BrowseView d = (BrowseView) l.getChildAt(1);
53 | w.setPath(prefs.getString(Prefs.K.WATCH_DIR, index, ""));
54 | w.setPref(Prefs.K.WATCH_DIR, index);
55 | d.setPath(prefs.getString(Prefs.K.DOWNLOAD_DIR, index, ""));
56 | d.setPref(Prefs.K.DOWNLOAD_DIR, index);
57 | }
58 |
59 | public void update(Prefs prefs) {
60 | ViewGroup l = (ViewGroup) getChildAt(0);
61 | BrowseView w = (BrowseView) l.getChildAt(0);
62 | BrowseView d = (BrowseView) l.getChildAt(1);
63 | w.setPath(prefs.getString(Prefs.K.WATCH_DIR, index, ""));
64 | d.setPath(prefs.getString(Prefs.K.DOWNLOAD_DIR, index, ""));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/res/layout/path_view.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
15 |
16 |
26 |
27 |
30 |
31 |
44 |
45 |
46 |
47 |
58 |
59 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/main/res/layout/torrent_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
32 |
33 |
44 |
45 |
56 |
57 |
64 |
65 |
72 |
--------------------------------------------------------------------------------
/src/main/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
25 |
26 |
32 |
33 |
41 |
42 |
50 |
51 |
52 |
57 |
58 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/views/PageFragment.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.views;
2 |
3 | import android.databinding.DataBindingUtil;
4 | import android.databinding.ViewDataBinding;
5 | import android.os.Bundle;
6 | import android.support.annotation.NonNull;
7 | import android.support.annotation.Nullable;
8 | import android.support.v4.app.Fragment;
9 | import android.support.v4.app.FragmentManager;
10 | import android.support.v4.app.FragmentPagerAdapter;
11 | import android.util.Log;
12 | import android.view.LayoutInflater;
13 | import android.view.View;
14 | import android.view.ViewGroup;
15 |
16 | import com.ap.transmission.btc.activities.ActivityBase;
17 | import com.ap.transmission.btc.BR;
18 | import com.ap.transmission.btc.BindingHelper;
19 | import com.ap.transmission.btc.Prefs;
20 | import com.ap.transmission.btc.Utils;
21 |
22 | /**
23 | * @author Andrey Pavlenko
24 | */
25 | public class PageFragment extends Fragment {
26 | private static final String TAG = PageFragment.class.getName();
27 | private static final String ARG_LAYOUT_ID = "layoutId";
28 | private int layoutId;
29 |
30 | static PageFragment newInstance(int layoutId) {
31 | PageFragment pageFragment = new PageFragment();
32 | Bundle arguments = new Bundle();
33 | arguments.putInt(ARG_LAYOUT_ID, layoutId);
34 | pageFragment.setArguments(arguments);
35 | return pageFragment;
36 | }
37 |
38 | @Override
39 | public void onCreate(Bundle savedInstanceState) {
40 | super.onCreate(savedInstanceState);
41 | Bundle args = getArguments();
42 | layoutId = (args == null) ? 0 : args.getInt(ARG_LAYOUT_ID);
43 | }
44 |
45 | @Override
46 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
47 | @Nullable Bundle savedInstanceState) {
48 | if (container == null) {
49 | Log.w(TAG, "container is null");
50 | return null;
51 | }
52 |
53 | ViewDataBinding b = DataBindingUtil.inflate(getLayoutInflater(), layoutId, container, false);
54 | ActivityBase a = Utils.getActivity(container);
55 |
56 | if (a == null) {
57 | Log.w(TAG, "Activity not found");
58 | return null;
59 | }
60 |
61 | Prefs prefs = a.getPrefs();
62 | BindingHelper h = new BindingHelper(a, b);
63 | b.setVariable(BR.h, h);
64 | b.setVariable(BR.p, prefs);
65 | return b.getRoot();
66 | }
67 |
68 | public static class Adapter extends FragmentPagerAdapter {
69 | private final TabInfo[] tabs;
70 |
71 | public Adapter(FragmentManager fm, TabInfo[] tabs) {
72 | super(fm);
73 | this.tabs = tabs;
74 | }
75 |
76 | @Override
77 | public Fragment getItem(int position) {
78 | return PageFragment.newInstance(tabs[position].getLayout());
79 | }
80 |
81 | @Override
82 | public int getCount() {
83 | return tabs.length;
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/http/handlers/StaticResourceHandler.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.http.handlers;
2 |
3 | import com.ap.transmission.btc.Utils;
4 | import com.ap.transmission.btc.http.HttpServer;
5 | import com.ap.transmission.btc.http.Method;
6 | import com.ap.transmission.btc.http.Range;
7 | import com.ap.transmission.btc.http.Request;
8 | import com.ap.transmission.btc.http.RequestHandler;
9 | import com.ap.transmission.btc.http.Response;
10 |
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.io.OutputStream;
14 | import java.net.Socket;
15 | import java.nio.ByteBuffer;
16 |
17 | /**
18 | * @author Andrey Pavlenko
19 | */
20 | public abstract class StaticResourceHandler implements RequestHandler {
21 | private int contentLen = -1;
22 |
23 | protected abstract Handler createHandler(HttpServer server, Socket socket);
24 |
25 | @Override
26 | public void handle(HttpServer server, Request req, Socket socket) {
27 | createHandler(server, socket).handle(req);
28 | }
29 |
30 | protected abstract class Handler extends HandlerBase {
31 |
32 | protected Handler(String logTag, HttpServer server, Socket socket) {
33 | super(logTag, server, socket);
34 | }
35 |
36 | protected abstract InputStream open() throws IOException;
37 |
38 | protected abstract String getContentType();
39 |
40 | @Override
41 | protected void doHandle(Request req) throws IOException {
42 | InputStream in;
43 | OutputStream out = null;
44 |
45 | try {
46 | in = open();
47 | } catch (IOException ex) {
48 | fail(Response.NotFound.instance, ex, "Failed to open file");
49 | return;
50 | }
51 |
52 | if (in == null) {
53 | fail(Response.NotFound.instance, "File not found");
54 | return;
55 | }
56 |
57 | try {
58 | ByteBuffer buf = null;
59 | int off = 0;
60 | int len = contentLen;
61 | Range range = req.getRange();
62 |
63 | if (contentLen == -1) {
64 | buf = Utils.readAll(in, 8192, Integer.MAX_VALUE);
65 | contentLen = len = buf.remaining();
66 | }
67 |
68 | if (range == null) {
69 | out = responseOk(getContentType(), len, true);
70 | } else {
71 | range.allign(len);
72 |
73 | if (range.isSatisfiable(len)) {
74 | out = responsePartial(getContentType(), range, len);
75 | off = (int) range.getStart();
76 | len = (int) (range.getEnd() - off + 1);
77 | } else {
78 | responseNotSatisfiable(range, len);
79 | return;
80 | }
81 | }
82 |
83 | if (req.getMethod() != Method.HEAD) {
84 | if (buf == null) {
85 | Utils.transfer(in, out, off, len);
86 | } else {
87 | out.write(buf.array(), off, len);
88 | }
89 | }
90 | } finally {
91 | Utils.close(in, out);
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/res/layout/download_torrent.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
24 |
25 |
32 |
33 |
38 |
39 |
44 |
45 |
46 |
47 |
56 |
57 |
63 |
64 |
70 |
71 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/main/res/layout/watch_dirs.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
24 |
25 |
41 |
42 |
46 |
47 |
51 |
52 |
59 |
60 |
69 |
70 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/src/main/cpp/commons.h:
--------------------------------------------------------------------------------
1 | #ifndef TRANSMISSIONBTC_COMMONS_H
2 | #define TRANSMISSIONBTC_COMMONS_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | extern "C" {
15 |
16 | #define LOG_TAG "transmissionbtc"
17 | #define CLASS_IOEX "java/io/IOException"
18 | #define CLASS_IAEX "java/lang/IllegalArgumentException"
19 | #define CLASS_NSTEX "com/ap/transmission/btc/torrent/NoSuchTorrentException"
20 | #define CLASS_DUPEX "com/ap/transmission/btc/torrent/DuplicateTorrentException"
21 |
22 | #define CATCH __onException__
23 |
24 | #define logErr(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
25 | #define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
26 | #define throwEX(env, className, ...) { throwException(__FILENAME__, __LINE__, env, className, __VA_ARGS__); goto CATCH; }
27 | #define throwIOEX(env, ...) throwEX(env, CLASS_IOEX, __VA_ARGS__)
28 | #define throwOrLog(env, className, throw, ...) \
29 | if (throw) { throwEX(env, className, __VA_ARGS__); } \
30 | else { logErr(__VA_ARGS__); goto CATCH; }
31 |
32 | typedef struct Err {
33 | bool isSet;
34 |
35 | void (*set)(struct Err *err, const char *ex, const char *msg, ...);
36 | } Err;
37 | #define errCheck(err) if (err->isSet) goto CATCH
38 |
39 | jint throwException(const char *, int, JNIEnv *, const char *, const char *, ...);
40 |
41 | size_t cp(JNIEnv *env, const char *, const char *);
42 |
43 | #define ctorFromFileEx(env, jsession, jpath) \
44 | ctorFromFile(env, jsession, jpath, true); \
45 | if (env->ExceptionCheck()) goto CATCH
46 |
47 | tr_ctor *ctorFromFile(JNIEnv *, jlong, jstring, bool);
48 |
49 | #define infoFromFileEx(env, jsession, jpath, info) \
50 | if ((infoFromFile(env, jsession, jpath, info, true) != TR_PARSE_OK) || env->ExceptionCheck()) \
51 | goto CATCH
52 |
53 | tr_parse_result infoFromFile(JNIEnv *, jlong, jstring, tr_info *, bool);
54 |
55 | #define findTorrentByHashFunc ((void *(*)(tr_session *, void *, Err *)) findTorrentByHash)
56 |
57 | int findTorrentByHash(tr_session *session, uint8_t *hash, Err *err);
58 |
59 | #define findTorrentByIdEx(session, id, err) findTorrentById(session, id, err); errCheck(err)
60 |
61 | tr_torrent *findTorrentById(tr_session *session, int id, Err *err);
62 |
63 | #define getFileInfoEx(tor, idx, err) getFileInfo(tor, idx, err); errCheck(err)
64 |
65 | const tr_file *getFileInfo(tr_torrent *tor, uint32_t idx, Err *err);
66 |
67 | #define getWantedFileInfoEx(tor, idx, err) getWantedFileInfo(tor, idx, err); errCheck(err)
68 |
69 | const tr_file *getWantedFileInfo(tr_torrent *tor, uint32_t idx, Err *err);
70 |
71 | #define runInTransmissionThreadEx(env, jsession, func, data) \
72 | runInTransmissionThread(__FILENAME__, __LINE__, env, jsession, func, data);\
73 | if (env->ExceptionCheck()) goto CATCH
74 |
75 | void *runInTransmissionThread(const char *file, int line, JNIEnv *env, jlong jsession,
76 | void *(*func)(tr_session *session, void *userData, Err *err),
77 | void *userData);
78 |
79 | } // extern "C"
80 | #endif //TRANSMISSIONBTC_COMMONS_H
81 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.4.1)
2 | project(transmissionbtc)
3 |
4 | if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
5 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type: [Debug|Release]" FORCE)
6 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release")
7 | endif ()
8 |
9 | set(EXT_C_FLAGS "-DANDROID -fno-unwind-tables -no-canonical-prefixes -D_FORTIFY_SOURCE=1 \
10 | -D_FILE_OFFSET_BITS=64 -D_LARGE_FILES=1")
11 | set(EXT_CXX_FLAGS "${EXT_C_FLAGS} -fno-exceptions -frtti -std=gnu++17")
12 | set(EXT_EXE_LINKER_FLAGS "-Wl,--exclude-libs,libgcc.a -Wl,--exclude-libs,libgcc_real.a \
13 | -Wl,--exclude-libs,libatomic.a -Wl,--gc-sections -static-libstdc++")
14 |
15 | if (CMAKE_BUILD_TYPE STREQUAL "Debug")
16 | set(EXT_C_FLAGS "-g -O0 ${EXT_C_FLAGS}")
17 | set(EXT_EXE_LINKER_FLAGS "-O0 ${EXT_EXE_LINKER_FLAGS}")
18 | set(CMAKE_C_FLAGS_DEBUG ${EXT_C_FLAGS})
19 | set(CMAKE_EXE_LINKER_FLAGS_DEBUG ${EXT_EXE_LINKER_FLAGS})
20 | else ()
21 | set(EXT_C_FLAGS "-O3 -DNDEBUG -flto -fvisibility=hidden -fdata-sections -ffunction-sections ${EXT_C_FLAGS}")
22 | set(EXT_CXX_FLAGS "-O3 -DNDEBUG -flto -fvisibility=hidden -fdata-sections -ffunction-sections ${EXT_CXX_FLAGS}")
23 | set(EXT_EXE_LINKER_FLAGS "-fuse-ld=gold -flto -O3 -Wl,--strip-all ${EXT_EXE_LINKER_FLAGS}")
24 | set(CMAKE_C_FLAGS_RELEASE ${EXT_C_FLAGS})
25 | set(CMAKE_CXX_FLAGS_RELEASE ${EXT_CXX_FLAGS})
26 | set(CMAKE_EXE_LINKER_FLAGS_RELEASE ${EXT_EXE_LINKER_FLAGS})
27 | endif ()
28 |
29 | set(EXT_CMAKE_ARGS -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
30 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
31 | -DANDROID_ABI=${ANDROID_ABI}
32 | -DANDROID_PLATFORM=${ANDROID_NATIVE_API_LEVEL}
33 | -DCMAKE_FIND_ROOT_PATH=${CMAKE_CURRENT_BINARY_DIR}
34 | -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=true
35 | -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=true
36 | -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=true
37 | -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=true
38 | -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH}
39 | -DCMAKE_DEBUG_POSTFIX=''
40 | -DCMAKE_C_FLAGS=${EXT_C_FLAGS}
41 | -DCMAKE_CXX_FLAGS=${EXT_CXX_FLAGS}
42 | -DCMAKE_EXE_LINKER_FLAGS=${EXT_EXE_LINKER_FLAGS}
43 | )
44 |
45 | add_library(${PROJECT_NAME} SHARED
46 | "${CMAKE_SOURCE_DIR}/src/main/cpp/native_to_java.cc"
47 | "${CMAKE_SOURCE_DIR}/src/main/cpp/env.cc"
48 | "${CMAKE_SOURCE_DIR}/src/main/cpp/sem.cc"
49 | "${CMAKE_SOURCE_DIR}/src/main/cpp/curl.cc"
50 | "${CMAKE_SOURCE_DIR}/src/main/cpp/hash.cc"
51 | "${CMAKE_SOURCE_DIR}/src/main/cpp/commons.cc"
52 | "${CMAKE_SOURCE_DIR}/src/main/cpp/torrent.cc"
53 | "${CMAKE_SOURCE_DIR}/src/main/cpp/transmission.cc"
54 | "${CMAKE_SOURCE_DIR}/src/main/cpp/stdredirect.cc"
55 | )
56 |
57 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
58 | include(OpenSSL)
59 | include(cURL)
60 | include(Event)
61 | include(Transmission)
62 |
63 | include_directories(SYSTEM
64 | ${OPENSSL_INCLUDE_DIR}
65 | ${CURL_INCLUDE_DIR}
66 | ${EVENT_INCLUDE_DIR}
67 | ${TR_INCLUDE_DIR})
68 |
69 | target_link_libraries(${PROJECT_NAME}
70 | ${TR_LIBRARIES}
71 | ${CURL_LIBRARIES}
72 | ${EVENT_LIBRARIES}
73 | ${OPENSSL_LIBRARIES}
74 | log
75 | z)
76 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/torrent/MediaInfo.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.torrent;
2 |
3 | import android.media.MediaMetadataRetriever;
4 |
5 | import com.ap.transmission.btc.Utils;
6 |
7 | import java.text.ParseException;
8 | import java.text.SimpleDateFormat;
9 | import java.util.Date;
10 | import java.util.Locale;
11 |
12 | import static java.util.concurrent.TimeUnit.HOURS;
13 | import static java.util.concurrent.TimeUnit.MILLISECONDS;
14 | import static java.util.concurrent.TimeUnit.MINUTES;
15 |
16 | /**
17 | * @author Andrey Pavlenko
18 | */
19 | public class MediaInfo {
20 | private final String title;
21 | private final String album;
22 | private final String artist;
23 | private final String genre;
24 | private final String mimeType;
25 | private final String date;
26 | private final String duration;
27 | private final String resolution;
28 |
29 | MediaInfo(MediaMetadataRetriever r) {
30 | String v;
31 | title = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
32 | artist = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
33 | album = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
34 | genre = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE);
35 | mimeType = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE);
36 |
37 | if ((v = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)) != null) {
38 | int idx = v.lastIndexOf('.');
39 | if (idx != -1) v = v.substring(0, idx);
40 |
41 | try {
42 | Date d = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US).parse(v);
43 | v = new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(d);
44 | } catch (ParseException e) {
45 | Utils.warn(getClass().getName(), "Failed to parse date: %s", v);
46 | v = null;
47 | }
48 |
49 | date = v;
50 | } else {
51 | date = null;
52 | }
53 |
54 | if ((v = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) != null) {
55 | long d = Long.parseLong(v);
56 | v = String.format(Locale.US, "%02d:%02d:%02d.000",
57 | MILLISECONDS.toHours(d),
58 | MILLISECONDS.toMinutes(d) - HOURS.toMinutes(MILLISECONDS.toHours(d)),
59 | MILLISECONDS.toSeconds(d) - MINUTES.toSeconds(MILLISECONDS.toMinutes(d)));
60 | duration = v;
61 | } else {
62 | duration = null;
63 | }
64 |
65 | if ((v = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)) != null) {
66 | String h = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
67 | resolution = v + 'x' + h;
68 | } else {
69 | resolution = null;
70 | }
71 |
72 | r.release();
73 | }
74 |
75 | public String getTitle() {
76 | return title;
77 | }
78 |
79 | public String getAlbum() {
80 | return album;
81 | }
82 |
83 | public String getArtist() {
84 | return artist;
85 | }
86 |
87 | public String getGenre() {
88 | return genre;
89 | }
90 |
91 | public String getMimeType() {
92 | return mimeType;
93 | }
94 |
95 | public String getDate() {
96 | return date;
97 | }
98 |
99 | public String getDuration() {
100 | return duration;
101 | }
102 |
103 | public String getResolution() {
104 | return resolution;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/activities/OpenTorrentActivity.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.activities;
2 |
3 | import android.os.AsyncTask;
4 | import android.view.View;
5 | import android.widget.ProgressBar;
6 |
7 | import com.ap.transmission.btc.R;
8 | import com.ap.transmission.btc.Utils;
9 | import com.ap.transmission.btc.func.Consumer;
10 | import com.ap.transmission.btc.func.Supplier;
11 | import com.ap.transmission.btc.http.handlers.torrent.TorrentHandler;
12 | import com.ap.transmission.btc.torrent.TorrentFile;
13 | import com.ap.transmission.btc.torrent.Transmission;
14 | import com.ap.transmission.btc.views.PathItem;
15 |
16 | import java.util.List;
17 | import java.util.concurrent.TimeoutException;
18 |
19 | import static com.ap.transmission.btc.Utils.showErr;
20 | import static com.ap.transmission.btc.services.TransmissionService.getTransmission;
21 |
22 | /**
23 | * @author Andrey Pavlenko
24 | */
25 | public class OpenTorrentActivity extends DownloadTorrentActivity {
26 | private WaitForTorrent waitFor;
27 |
28 | @Override
29 | public boolean isSequential() {
30 | return true;
31 | }
32 |
33 | @Override
34 | public boolean isIgnoreDuplicate() {
35 | return true;
36 | }
37 |
38 | @Override
39 | protected void finish(final String hash, final List items) {
40 | if (hash == null) return;
41 |
42 | final Transmission tr = getTransmission();
43 | int idx = 0;
44 |
45 | for (PathItem i : items) {
46 | if (i.isChecked()) {
47 | idx = i.getIndex();
48 | break;
49 | }
50 | }
51 |
52 | View listFiles = findViewById(R.id.list_files);
53 | ProgressBar progressBar = findViewById(R.id.progress_bar);
54 | listFiles.setVisibility(View.GONE);
55 | progressBar.setVisibility(View.VISIBLE);
56 |
57 | final int fileIndex = idx;
58 | Consumer callback = new Consumer() {
59 | @Override
60 | public void accept(Object o) {
61 | if (o == null) {
62 | // Canceled by user or timed out
63 | } else if (o instanceof TorrentFile) {
64 | TorrentFile trf = (TorrentFile) o;
65 |
66 | if (trf.open(OpenTorrentActivity.this, findViewById(R.id.button_download))) {
67 | OpenTorrentActivity.super.finish(hash, items);
68 | } else {
69 | finishWithDelay();
70 | }
71 | } else if (o instanceof TimeoutException) {
72 | TimeoutException ex = (TimeoutException) o;
73 | Utils.warn("OpenTorrentActivity", ex, "Timeout exceeded");
74 | showErr(findViewById(R.id.button_download), R.string.err_timeout_exceeded);
75 | } else if (o instanceof Throwable) {
76 | Throwable ex = (Throwable) o;
77 | Utils.warn("OpenTorrentActivity", ex, "Failed to open torrent");
78 | showErr(findViewById(R.id.button_download),
79 | R.string.err_failed_to_open_torrent, ex.getLocalizedMessage());
80 | } else {
81 | Utils.err("OpenTorrentActivity", "Must never get here: %s", o);
82 | }
83 | }
84 | };
85 |
86 | waitFor = new WaitForTorrent(callback, tr, hash, fileIndex);
87 | waitFor.execute((Void) null);
88 | }
89 |
90 | @Override
91 | public void finish() {
92 | if (waitFor != null) waitFor.cancel();
93 | super.finish();
94 | }
95 |
96 | private static final class WaitForTorrent extends AsyncTask {
97 | private final Consumer callback;
98 | private final Transmission tr;
99 | private final String hash;
100 | private final int fileIndex;
101 | volatile boolean isCanceled;
102 |
103 | private WaitForTorrent(Consumer callback, Transmission tr, String hash, int fileIndex) {
104 | this.callback = callback;
105 | this.tr = tr;
106 | this.hash = hash;
107 | this.fileIndex = fileIndex;
108 | }
109 |
110 |
111 | @Override
112 | protected Object doInBackground(Void... params) {
113 | try {
114 | return TorrentHandler.waitFor(tr, hash, fileIndex, new Supplier() {
115 | @Override
116 | public Boolean get() {
117 | return isCanceled;
118 | }
119 | }, "WaitForTorrent");
120 | } catch (Throwable ex) {
121 | return ex;
122 | }
123 | }
124 |
125 | @Override
126 | protected void onPostExecute(Object ex) {
127 | callback.accept(ex);
128 | }
129 |
130 | void cancel() {
131 | isCanceled = true;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/BindingHelper.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc;
2 |
3 | import android.content.Intent;
4 | import android.content.SharedPreferences;
5 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
6 | import android.databinding.ViewDataBinding;
7 | import android.net.Uri;
8 | import android.util.Log;
9 | import android.view.View;
10 | import android.widget.CompoundButton;
11 |
12 | import com.ap.transmission.btc.activities.ActivityBase;
13 | import com.ap.transmission.btc.services.TransmissionService;
14 | import com.ap.transmission.btc.torrent.Transmission;
15 |
16 | import java.io.IOException;
17 |
18 | /**
19 | * @author Andrey Pavlenko
20 | */
21 | public class BindingHelper implements OnSharedPreferenceChangeListener, Runnable {
22 | private final ActivityBase activity;
23 | private final ViewDataBinding dataBinding;
24 | private byte isServiceRunning = TransmissionService.isRunning() ? (byte) 1 : 0;
25 |
26 | public BindingHelper(ActivityBase activity, ViewDataBinding dataBinding) {
27 | this.activity = activity;
28 | this.dataBinding = dataBinding;
29 | activity.getPrefs().getPrefs().registerOnSharedPreferenceChangeListener(this);
30 | TransmissionService.addStateChangeListener(this);
31 | }
32 |
33 | public ActivityBase getActivity() {
34 | return activity;
35 | }
36 |
37 | public boolean and(boolean... bools) {
38 | for (boolean b : bools) if (!b) return false;
39 | return true;
40 | }
41 |
42 | public String getIp() {
43 | return Utils.getIPAddress(getActivity());
44 | }
45 |
46 | public boolean isServiceRunning() {
47 | return isServiceRunning == 1;
48 | }
49 |
50 | public boolean isServiceStarting() {
51 | return isServiceRunning == 2;
52 | }
53 |
54 | public boolean isSuspended() {
55 | if (isServiceRunning()) {
56 | Transmission tr = TransmissionService.getTransmission();
57 | return (tr != null) && tr.isSuspended();
58 | }
59 | return false;
60 | }
61 |
62 | public void startStopService(final View... disable) {
63 | final boolean running = isServiceRunning();
64 | for (View v : disable) v.setEnabled(false);
65 | Runnable callback = () -> {
66 | for (View v : disable) v.setEnabled(true);
67 | isServiceRunning = TransmissionService.isRunning() ? (byte) 1 : 0;
68 | invalidate();
69 |
70 | if (!running && !isServiceRunning()) {
71 | Utils.showErr(disable[0], R.string.err_failed_to_start_transmission);
72 | }
73 | };
74 |
75 | isServiceRunning = 2;
76 |
77 | if (running) {
78 | TransmissionService.stop(activity, callback);
79 | } else {
80 | TransmissionService.start(activity, callback);
81 | }
82 | }
83 |
84 | public void suspend(boolean suspend, Runnable callback) {
85 | Transmission tr = TransmissionService.getTransmission();
86 | if (tr != null) tr.suspend(suspend, true, callback);
87 | }
88 |
89 | public void openUrl(String scheme, String host, int port, String path) {
90 | String uri = scheme + "://" + host + ':' + port + "/" + path;
91 | Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
92 |
93 | try {
94 | activity.startActivity(i);
95 | } catch (Exception ex) {
96 | Utils.err(getClass().getName(), ex, "Failed to open web browser");
97 | Utils.showErr(dataBinding.getRoot(), R.string.err_failed_to_open_url, uri);
98 | }
99 | }
100 |
101 | public void checkRoot(View v) {
102 | CompoundButton cb = (CompoundButton) v;
103 | if (!cb.isChecked()) return;
104 |
105 | try {
106 | if (Utils.su(15000, "ls") == 0) return;
107 | } catch (IOException ex) {
108 | Log.e(getClass().getName(), "checkRoot failed", ex);
109 | }
110 |
111 | cb.setChecked(false);
112 | Utils.showErr(dataBinding.getRoot(), R.string.err_check_root_failed);
113 | }
114 |
115 | public void addWatchDir() {
116 | Prefs prefs = activity.getPrefs();
117 | int idx = prefs.getMaxIndex(Prefs.K.WATCH_DIR);
118 | prefs.set(Prefs.K.WATCH_DIR, prefs.getWatchDir(), idx);
119 | prefs.set(Prefs.K.DOWNLOAD_DIR, prefs.getDownloadDir(), idx);
120 | }
121 |
122 | @Override
123 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
124 | invalidate();
125 | }
126 |
127 | private void invalidate() {
128 | dataBinding.invalidateAll();
129 | }
130 |
131 | @Override
132 | public void run() {
133 | isServiceRunning = TransmissionService.isRunning() ? (byte) 1 : 0;
134 | dataBinding.invalidateAll();
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/http/handlers/upnp/DescriptorHandler.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.http.handlers.upnp;
2 |
3 | import android.content.res.AssetManager;
4 |
5 | import com.ap.transmission.btc.Prefs;
6 | import com.ap.transmission.btc.Utils;
7 | import com.ap.transmission.btc.http.HttpServer;
8 | import com.ap.transmission.btc.http.Request;
9 | import com.ap.transmission.btc.http.RequestHandler;
10 | import com.ap.transmission.btc.http.handlers.AssetHandler;
11 | import com.ap.transmission.btc.http.handlers.HandlerBase;
12 | import com.ap.transmission.btc.http.handlers.StaticResourceHandler;
13 | import com.ap.transmission.btc.torrent.Transmission;
14 |
15 | import java.io.IOException;
16 | import java.io.OutputStream;
17 | import java.net.Socket;
18 | import java.util.Map;
19 |
20 | import static com.ap.transmission.btc.BuildConfig.VERSION_NAME;
21 | import static com.ap.transmission.btc.Utils.ASCII;
22 |
23 | /**
24 | * @author Andrey Pavlenko
25 | */
26 | public class DescriptorHandler implements RequestHandler {
27 | public static final String PATH = "/upnp/descriptor.xml";
28 | private final Prefs prefs;
29 | private Content content;
30 |
31 | public DescriptorHandler(Prefs prefs) {
32 | this.prefs = prefs;
33 | }
34 |
35 | public static void addHandlers(Map handlers, Transmission transmission) {
36 | AssetManager amgr = transmission.getPrefs().getContext().getAssets();
37 | String contentType = "text/xml; charset=\"utf-8\"";
38 | StaticResourceHandler h1 = new AssetHandler(amgr, "upnp/ContentDirectoryScpd.xml", contentType);
39 | StaticResourceHandler h2 = new AssetHandler(amgr, "upnp/ConnectionManagerScpd.xml", contentType);
40 | handlers.put(PATH, new DescriptorHandler(transmission.getPrefs()));
41 | handlers.put("/upnp/ContentDirectory/scpd.xml", h1);
42 | handlers.put("/upnp/ConnectionManager/scpd.xml", h2);
43 | handlers.put(ContentDirectoryHandler.PATH, new ContentDirectoryHandler());
44 | }
45 |
46 | @Override
47 | public void handle(HttpServer server, Request req, Socket socket) {
48 | new HandlerBase("DescriptorHandler", server, socket) {
49 | @Override
50 | protected void doHandle(Request req) throws IOException {
51 | byte[] content = getContent();
52 | OutputStream out = responseOk("text/xml; charset=\"utf-8\"", content.length,
53 | false);
54 | out.write(content);
55 | out.close();
56 | }
57 | }.handle(req);
58 | }
59 |
60 | private byte[] getContent() {
61 | Content cnt = content;
62 |
63 | if(cnt == null) {
64 | content = cnt = new Content(prefs);
65 | } else {
66 | String ip = Utils.getIPAddress(prefs.getContext());
67 |
68 | if (!((ip == null) ? (cnt.ip == null) : ip.equals(cnt.ip))) {
69 | content = cnt = new Content(prefs);
70 | }
71 | }
72 |
73 | return cnt.content;
74 | }
75 |
76 | private static final class Content {
77 | final String ip;
78 | final byte[] content;
79 |
80 | Content(Prefs prefs) {
81 | String uuid = prefs.getUUID();
82 | String suffix = ip = Utils.getIPAddress(prefs.getContext());
83 | if (suffix == null) suffix = uuid;
84 | String xml = "\n" +
85 | "\n" +
86 | " \n" +
87 | " 1 \n" +
88 | " 1 \n" +
89 | " \n" +
90 | " \n" +
91 | " urn:schemas-upnp-org:device:MediaServer:1 \n" +
92 | " Transmission BTC (" + suffix + ") \n" +
93 | " Andrey Pavlenko \n" +
94 | " http://apavlenko.com/ \n" +
95 | " Transmission Bit Torrent Client \n" +
96 | " Transmission BTC \n" +
97 | " " + VERSION_NAME + " \n" +
98 | " http://apavlenko.com/ \n" +
99 | " uuid:" + uuid + " \n" +
100 | " DMS-1.50 \n" +
101 | " \n" +
102 | " \n" +
103 | " urn:schemas-upnp-org:service:ContentDirectory:1 \n" +
104 | " urn:upnp-org:serviceId:ContentDirectory \n" +
105 | " /upnp/ContentDirectory/scpd.xml \n" +
106 | " /upnp/ContentDirectory/control.xml \n" +
107 | " /upnp/ContentDirectory/event.xml \n" +
108 | " \n" +
109 | " \n" +
110 | " urn:schemas-upnp-org:service:ConnectionManager:1 \n" +
111 | " urn:upnp-org:serviceId:ConnectionManager \n" +
112 | " /upnp/ConnectionManager/scpd.xml \n" +
113 | " /upnp/ConnectionManager/control.xml \n" +
114 | " /upnp/ConnectionManager/event.xml \n" +
115 | " \n" +
116 | " \n" +
117 | " \n" +
118 | " \n";
119 | content = xml.getBytes(ASCII);
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/views/TorrentsList.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.views;
2 |
3 | import android.content.Context;
4 | import android.os.AsyncTask;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 | import android.support.design.widget.TabLayout;
8 | import android.util.AttributeSet;
9 | import android.widget.LinearLayout;
10 | import android.widget.ProgressBar;
11 |
12 | import com.ap.transmission.btc.BindingHelper;
13 | import com.ap.transmission.btc.R;
14 | import com.ap.transmission.btc.Utils;
15 | import com.ap.transmission.btc.func.Consumer;
16 | import com.ap.transmission.btc.services.TransmissionService;
17 | import com.ap.transmission.btc.torrent.NoSuchTorrentException;
18 | import com.ap.transmission.btc.torrent.Torrent;
19 | import com.ap.transmission.btc.torrent.TorrentFile;
20 | import com.ap.transmission.btc.torrent.TorrentFs;
21 | import com.ap.transmission.btc.torrent.Transmission;
22 |
23 | import java.util.Collections;
24 | import java.util.List;
25 |
26 | import static android.os.AsyncTask.Status.FINISHED;
27 | import static com.ap.transmission.btc.Utils.getActivity;
28 |
29 | /**
30 | * @author Andrey Pavlenko
31 | */
32 | public class TorrentsList extends LinearLayout {
33 | private List torrents = Collections.emptyList();
34 | private boolean active;
35 | private UpdateTask update;
36 |
37 | public TorrentsList(Context context, AttributeSet attrs) {
38 | super(context, attrs);
39 | setOrientation(VERTICAL);
40 | setFocusable(true);
41 | TabLayout tabLayout = getActivity(this).findViewById(R.id.tabs);
42 | setActive(tabLayout.getSelectedTabPosition() == 0);
43 | }
44 |
45 | public void setHelper(@SuppressWarnings("unused") BindingHelper h) {
46 | update();
47 | }
48 |
49 | public void setActive(boolean active) {
50 | if (this.active == active) return;
51 | this.active = active;
52 | update();
53 | }
54 |
55 | private void update() {
56 | if (update != null) return;
57 | Transmission tr = TransmissionService.getTransmission();
58 |
59 | if (!active || (tr == null) || !tr.isRunning()) {
60 | if (torrents != Collections.EMPTY_LIST) {
61 | torrents = Collections.emptyList();
62 | updateList();
63 | }
64 | } else {
65 | final UpdateTask upd = update = new UpdateTask(new Consumer>() {
66 | @Override
67 | public void accept(List torrents) {
68 | update = null;
69 | updateList(torrents);
70 | }
71 | });
72 | upd.executeOnExecutor(tr.getExecutor(), tr);
73 |
74 | if (getChildCount() == 0) {
75 | new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
76 | @Override
77 | public void run() {
78 | if (upd.getStatus() != FINISHED) addView(new ProgressBar(getContext()));
79 | }
80 | }, 1000);
81 | }
82 | }
83 | }
84 |
85 | private void updateList(List ls) {
86 | if (!active) {
87 | update();
88 | return;
89 | }
90 |
91 | if (!compare(torrents, ls)) {
92 | torrents = ls;
93 | updateList();
94 | }
95 |
96 | try {
97 | int count = getChildCount();
98 |
99 | if ((count > 0) && (getChildAt(0) instanceof ProgressBar)) {
100 | removeViewAt(0);
101 | count--;
102 | }
103 |
104 | for (int i = 0; i < count; i++) {
105 | ((TorrentView) getChildAt(i)).update();
106 | }
107 | } catch (IllegalArgumentException ex) { // Torrent removed?
108 | Utils.err(getClass().getName(), ex, "Failed to update TorrentViews");
109 | }
110 |
111 | final Handler handler = new Handler();
112 | handler.postDelayed(new Runnable() {
113 | @Override
114 | public void run() {
115 | update();
116 | }
117 | }, 1000);
118 | }
119 |
120 | private void updateList() {
121 | Context ctx = getContext();
122 | removeAllViews();
123 | int margin = Utils.toPx(10);
124 |
125 | for (Torrent tor : torrents) {
126 | TorrentView v = new TorrentView(ctx, null);
127 | v.setTorrent(tor);
128 | addView(v);
129 | ((MarginLayoutParams) v.getLayoutParams()).setMargins(0, margin, 0, 0);
130 | }
131 | }
132 |
133 | private static boolean compare(List l1, List l2) {
134 | return (l1 == l2);
135 | }
136 |
137 | private static final class UpdateTask extends AsyncTask> {
138 | private final Consumer> callback;
139 |
140 | UpdateTask(Consumer> callback) { this.callback = callback; }
141 |
142 | @Override
143 | protected List doInBackground(Transmission... tr) {
144 | try {
145 | for (TorrentFs fs = tr[0].getTorrentFs(); ; ) {
146 | try {
147 | @SuppressWarnings("unchecked") List ls = (List) fs.ls();
148 | if (!ls.isEmpty()) {
149 | ls.get(0).getStat(true); // Update torrent stat
150 |
151 | for (Torrent tor : ls) {
152 | for (TorrentFile f : tor.lsFiles()) {
153 | f.isComplete(); // Update file stat
154 | }
155 | }
156 | }
157 | return ls;
158 | } catch (NoSuchTorrentException ex) {
159 | fs.reportNoSuchTorrent(ex);
160 | }
161 | }
162 | } catch (IllegalStateException ex) { // Service stopped
163 | return Collections.emptyList();
164 | }
165 | }
166 |
167 | @Override
168 | protected void onPostExecute(List torrents) {
169 | callback.accept(torrents);
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/main/assets/upnp/ConnectionManagerScpd.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 0
6 |
7 |
8 |
9 | GetCurrentConnectionInfo
10 |
11 |
12 | ConnectionID
13 | in
14 | A_ARG_TYPE_ConnectionID
15 |
16 |
17 | RcsID
18 | out
19 | A_ARG_TYPE_RcsID
20 |
21 |
22 | AVTransportID
23 | out
24 | A_ARG_TYPE_AVTransportID
25 |
26 |
27 | ProtocolInfo
28 | out
29 | A_ARG_TYPE_ProtocolInfo
30 |
31 |
32 | PeerConnectionManager
33 | out
34 | A_ARG_TYPE_ConnectionManager
35 |
36 |
37 | PeerConnectionID
38 | out
39 | A_ARG_TYPE_ConnectionID
40 |
41 |
42 | Direction
43 | out
44 | A_ARG_TYPE_Direction
45 |
46 |
47 | Status
48 | out
49 | A_ARG_TYPE_ConnectionStatus
50 |
51 |
52 |
53 |
54 | GetProtocolInfo
55 |
56 |
57 | Source
58 | out
59 | SourceProtocolInfo
60 |
61 |
62 | Sink
63 | out
64 | SinkProtocolInfo
65 |
66 |
67 |
68 |
69 | GetCurrentConnectionIDs
70 |
71 |
72 | ConnectionIDs
73 | out
74 | CurrentConnectionIDs
75 |
76 |
77 |
78 |
79 |
80 |
81 | A_ARG_TYPE_ProtocolInfo
82 | string
83 |
84 |
85 | A_ARG_TYPE_ConnectionStatus
86 | string
87 |
88 | OK
89 | ContentFormatMismatch
90 | InsufficientBandwidth
91 | UnreliableChannel
92 | Unknown
93 |
94 |
95 |
96 | A_ARG_TYPE_AVTransportID
97 | i4
98 |
99 |
100 | A_ARG_TYPE_RcsID
101 | i4
102 |
103 |
104 | A_ARG_TYPE_ConnectionID
105 | i4
106 |
107 |
108 | A_ARG_TYPE_ConnectionManager
109 | string
110 |
111 |
112 | SourceProtocolInfo
113 | string
114 |
115 |
116 | SinkProtocolInfo
117 | string
118 |
119 |
120 | A_ARG_TYPE_Direction
121 | string
122 |
123 | Input
124 | Output
125 |
126 |
127 |
128 | CurrentConnectionIDs
129 | string
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/src/main/cpp/commons.cc:
--------------------------------------------------------------------------------
1 | #include "commons.h"
2 | #include
3 | #include "transmission-private.h"
4 |
5 | extern "C" {
6 |
7 | jint throwException(const char *file, int line, JNIEnv *env, const char *className,
8 | const char *format, ...) {
9 | int len;
10 | char msg[256];
11 | char *fmt = (char *) format;
12 |
13 | #ifndef NDEBUG
14 | char dfmt[1024];
15 | len = snprintf(dfmt, sizeof(dfmt), "[%s:%d] %s", file, line, format);
16 | if (len > 0) fmt = dfmt;
17 | #endif
18 |
19 | va_list args;
20 | va_start(args, format);
21 | len = vsnprintf(msg, sizeof(msg), (const char *) fmt, args);
22 | va_end(args);
23 | if (len > 0) fmt = msg;
24 |
25 | jclass c = env->FindClass(className);
26 | if (c == NULL) env->FindClass("java/lang/Error");
27 |
28 | if (env->ExceptionCheck()) {
29 | logErr("Exception: %s", fmt);
30 | } else {
31 | env->ThrowNew(c, fmt);
32 | }
33 |
34 | return 0;
35 | }
36 |
37 | size_t cp(JNIEnv *env, const char *fromPath, const char *toPath) {
38 | FILE *from = NULL, *to = NULL;
39 | size_t count = 0;
40 |
41 | if ((from = fopen(fromPath, "rb")) == NULL) {
42 | throwIOEX(env, "Failed to open source file %s", fromPath);
43 | }
44 |
45 | if ((to = fopen(toPath, "wb")) == NULL) {
46 | fclose(from);
47 | throwIOEX(env, "Failed to open destination file %s", toPath);
48 | }
49 |
50 | size_t n;
51 | char buffer[BUFSIZ];
52 |
53 | while ((n = fread(buffer, sizeof(char), sizeof(buffer), from)) > 0) {
54 | if (fwrite(buffer, sizeof(char), n, to) != n) {
55 | throwIOEX(env, "Error writing to destination file %s", toPath);
56 | } else {
57 | count += n;
58 | }
59 | }
60 |
61 | CATCH:
62 | if (from != NULL) fclose(from);
63 | if (to != NULL) fclose(to);
64 | return count;
65 | }
66 |
67 | tr_ctor *ctorFromFile(JNIEnv *env, jlong jsession, jstring jpath, bool throwErr) {
68 | const tr_session *session = (const tr_session *) jsession;
69 | const char *path = env->GetStringUTFChars(jpath, 0);
70 | tr_ctor *ctor = tr_ctorNew(session);
71 |
72 | if (tr_ctorSetMetainfoFromFile(ctor, path)) {
73 | tr_ctorFree(ctor);
74 | ctor = NULL;
75 | throwOrLog(env, CLASS_IOEX, throwErr, "Invalid torrent file: %s", path);
76 | }
77 |
78 | CATCH:
79 | env->ReleaseStringUTFChars(jpath, path);
80 | return ctor;
81 | }
82 |
83 | tr_parse_result
84 | infoFromFile(JNIEnv *env, jlong jsession, jstring jpath, tr_info *info, bool throwErr) {
85 | const char *path = NULL;
86 | tr_parse_result result = TR_PARSE_ERR;
87 | tr_ctor *ctor = ctorFromFileEx(env, jsession, jpath);
88 | result = tr_torrentParse(ctor, info);
89 | tr_ctorFree(ctor);
90 |
91 | if (result != TR_PARSE_OK) {
92 | path = env->GetStringUTFChars(jpath, 0);
93 | throwOrLog(env, CLASS_IOEX, throwErr, "Failed to parse torrent file: %s", path);
94 | }
95 |
96 | CATCH:
97 | if (path != NULL) env->ReleaseStringUTFChars(jpath, path);
98 | return result;
99 | }
100 |
101 | int findTorrentByHash(tr_session *session, uint8_t *hash, Err *err) {
102 | tr_torrent *tor = tr_torrentFindFromHash(session, hash);
103 |
104 | if (tor == NULL) {
105 | char hashString[1 + 2 * SHA_DIGEST_LENGTH];
106 | tr_binary_to_hex(hash, hashString, SHA_DIGEST_LENGTH);
107 | err->set(err, CLASS_NSTEX, "No such torrent: hash=%s", hashString);
108 | return -1;
109 | }
110 |
111 | return tr_torrentId(tor);
112 | }
113 |
114 | tr_torrent *findTorrentById(tr_session *session, int id, Err *err) {
115 | tr_torrent *tor = tr_torrentFindFromId(session, id);
116 | if (tor == NULL) err->set(err, CLASS_NSTEX, "No such torrent: id=%d", id);
117 | return tor;
118 | }
119 |
120 | const tr_file *getFileInfo(tr_torrent *tor, uint32_t idx, Err *err) {
121 | const tr_info *info = tr_torrentInfo(tor);
122 | if (idx >= info->fileCount) {
123 | err->set(err, CLASS_IAEX, "Invalid file index: %d", idx);
124 | return NULL;
125 | } else {
126 | return &info->files[idx];
127 | }
128 | }
129 |
130 | const tr_file *getWantedFileInfo(tr_torrent *tor, uint32_t idx, Err *err) {
131 | const tr_file *f = getFileInfoEx(tor, idx, err);
132 | if (f->dnd != 0) {
133 | err->set(err, CLASS_IAEX, "File #%d is unwanted for download: %s", idx, f->name);
134 | return NULL;
135 | }
136 | CATCH:
137 | return f;
138 | }
139 |
140 | struct Future {
141 | Err err;
142 | sem_t sem;
143 | tr_session *session;
144 | void *userData;
145 | void *result;
146 | const char *ex;
147 | char *exMsg;
148 | uint16_t exMsgBufLen;
149 |
150 | void *(*func)(tr_session *session, void *userData, Err *err);
151 | };
152 |
153 | static void setError(struct Err *err, const char *ex, const char *msg, ...) {
154 | struct Future *f = (struct Future *) err;
155 |
156 | if (f->exMsg == NULL) {
157 | f->exMsgBufLen = 512;
158 | f->exMsg = (char *) calloc(f->exMsgBufLen, sizeof(char));
159 | }
160 |
161 | va_list args;
162 | va_start(args, msg);
163 | vsnprintf(f->exMsg, f->exMsgBufLen, msg, args);
164 | va_end(args);
165 | f->ex = ex;
166 | f->err.isSet = true;
167 | }
168 |
169 | static void runInEventThread(void *data) {
170 | struct Future *f = (struct Future *) data;
171 | f->result = f->func(f->session, f->userData, &(f->err));
172 | sem_post(&(f->sem));
173 | }
174 |
175 | void *runInTransmissionThread(const char *file, int line, JNIEnv *env, jlong jsession,
176 | void *(*func)(tr_session *, void *, Err *),
177 | void *userData) {
178 | struct Future f;
179 | sem_t *sem = &(f.sem);
180 | memset(sem, 0, sizeof(sem_t));
181 | sem_init(sem, 0, 0);
182 | f.session = (tr_session *) jsession;
183 | f.userData = userData;
184 | f.ex = NULL;
185 | f.exMsg = NULL;
186 | f.err.isSet = false;
187 | f.err.set = setError;
188 | f.func = func;
189 |
190 | tr_runInEventThread(f.session, runInEventThread, &f);
191 | while ((sem_wait(sem) == -1) && (errno == EINTR));
192 | sem_destroy(sem);
193 |
194 | if (f.err.isSet) {
195 | throwException(file, line, env, f.ex, f.exMsg);
196 | free(f.exMsg);
197 | return NULL;
198 | } else {
199 | return f.result;
200 | }
201 | }
202 | } // extern "C"
--------------------------------------------------------------------------------
/src/main/cpp/native_to_java.cc:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "commons.h"
4 | #include "native_to_java.h"
5 | #include "libtransmission/file.h"
6 |
7 | #pragma clang diagnostic push
8 | #pragma clang diagnostic ignored "-Wconversion"
9 | #pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
10 |
11 | extern "C" {
12 |
13 | static JavaVM *jvm;
14 | static jclass classNative;
15 | static jclass classStorageAccess;
16 | static jmethodID addedOrChangedCallback;
17 | static jmethodID stoppedCallback;
18 | static jmethodID sessionChangedCallback;
19 | static jmethodID scheduledAltSpeedCallback;
20 | static jmethodID createDir;
21 | static jmethodID openFile;
22 | static jmethodID closeFileDescriptor;
23 | static jmethodID renamePath;
24 | static jmethodID removePath;
25 |
26 | #define JvmAttach() \
27 | void *__env__ = nullptr;\
28 | jint _detached = jvm->GetEnv(&__env__, JNI_VERSION_1_2);\
29 | JNIEnv *env = (JNIEnv*) __env__;\
30 | if ((_detached == JNI_EDETACHED) && (jvm->AttachCurrentThread(&env, NULL) != JNI_OK)) {\
31 | logErr("JavaVM->AttachCurrentThread() failed");\
32 | goto CATCH;\
33 | }
34 | #define JvmDetach() \
35 | if (env->ExceptionCheck()) logErr("Native to Java call failed");\
36 | if (_detached == JNI_EDETACHED) jvm->DetachCurrentThread()
37 |
38 | static void rndChars(char *str, int num);
39 |
40 | JNIEXPORT void JNICALL
41 | Java_com_ap_transmission_btc_Native_nativeToJavaInit(JNIEnv *env, jclass c) {
42 | env->GetJavaVM(&jvm);
43 | jclass sa = env->FindClass("com/ap/transmission/btc/StorageAccess");
44 | classNative = (jclass) env->NewGlobalRef(c);
45 | classStorageAccess = (jclass) env->NewGlobalRef(sa);
46 | addedOrChangedCallback = env->GetStaticMethodID(c, "torrentAddedOrChangedCallback",
47 | "()V");
48 | stoppedCallback = env->GetStaticMethodID(c, "torrentStoppedCallback", "()V");
49 | sessionChangedCallback = env->GetStaticMethodID(c, "sessionChangedCallback", "()V");
50 | scheduledAltSpeedCallback = env->GetStaticMethodID(c, "scheduledAltSpeedCallback", "()V");
51 | createDir = env->GetStaticMethodID(sa, "createDir", "(Ljava/lang/String;)Z");
52 | openFile = env->GetStaticMethodID(sa, "openFile", "(Ljava/lang/String;ZZZ)I");
53 | closeFileDescriptor = env->GetStaticMethodID(sa, "closeFileDescriptor", "(I)Z");
54 | renamePath = env->GetStaticMethodID(sa, "renamePath",
55 | "(Ljava/lang/String;Ljava/lang/String;)Z");
56 | removePath = env->GetStaticMethodID(sa, "removePath", "(Ljava/lang/String;)Z");
57 | }
58 |
59 | void callAddedOrChangedCallback() {
60 | JvmAttach();
61 | env->CallStaticVoidMethod(classNative, addedOrChangedCallback);
62 | JvmDetach();
63 | CATCH:;
64 | }
65 |
66 | void callStoppedCallback() {
67 | JvmAttach();
68 | env->CallStaticVoidMethod(classNative, stoppedCallback);
69 | JvmDetach();
70 | CATCH:;
71 | }
72 |
73 | void callSessionChangedCallback() {
74 | JvmAttach();
75 | env->CallStaticVoidMethod(classNative, sessionChangedCallback);
76 | JvmDetach();
77 | CATCH:;
78 | }
79 |
80 | void callScheduledAltSpeedCallback() {
81 | JvmAttach();
82 | env->CallStaticVoidMethod(classNative, scheduledAltSpeedCallback);
83 | JvmDetach();
84 | CATCH:;
85 | }
86 |
87 | } // extern "C"
88 |
89 | bool tr_android_dir_create(char const *path) {
90 | jboolean result = JNI_FALSE;
91 | {
92 | JvmAttach();
93 | jstring jpath = env->NewStringUTF(path);
94 | result = env->CallStaticBooleanMethod(classStorageAccess, createDir, jpath);
95 | JvmDetach();
96 | } CATCH:
97 | return result == JNI_TRUE;
98 | }
99 |
100 | tr_sys_file_t tr_android_file_open(char const *path, int flags) {
101 | int fd = -1;
102 | {
103 | JvmAttach();
104 | jboolean create = (flags & (TR_SYS_FILE_CREATE | TR_SYS_FILE_CREATE_NEW)) ? JNI_TRUE : JNI_FALSE;
105 | jboolean writable = JNI_TRUE;
106 | jboolean truncate = (flags & TR_SYS_FILE_TRUNCATE) ? JNI_TRUE : JNI_FALSE;
107 | jstring jpath = env->NewStringUTF(path);
108 | fd = env->CallStaticIntMethod(classStorageAccess, openFile, jpath,
109 | create, writable, truncate);
110 | JvmDetach();
111 | } CATCH:
112 | return (fd == -1) ? TR_BAD_SYS_FILE : fd;
113 | }
114 |
115 | tr_sys_file_t tr_android_file_open_temp(char *path_template) {
116 | int len = strlen(path_template);
117 | char *suffix = (path_template + len - 6);
118 |
119 | for (int i = 0; i < 100; i++) {
120 | rndChars(suffix, 6);
121 | if (access(path_template, F_OK) == -1) {
122 | return tr_android_file_open(path_template, TR_SYS_FILE_CREATE_NEW);
123 | }
124 | }
125 |
126 | strcpy(suffix, "XXXXXX\0");
127 | logErr("Failed to create temporary file: %s", path_template);
128 | return TR_BAD_SYS_FILE;
129 | }
130 |
131 | bool tr_android_file_close(tr_sys_file_t handle) {
132 | jboolean result = JNI_FALSE;
133 | {
134 | JvmAttach();
135 | result = env->CallStaticBooleanMethod(classStorageAccess, closeFileDescriptor,
136 | (jint) handle);
137 | JvmDetach();
138 | } CATCH:
139 | return result == JNI_TRUE;
140 | }
141 |
142 | bool tr_android_path_rename(char const *src_path, char const *dst_path) {
143 | jboolean result = JNI_FALSE;
144 | {
145 | JvmAttach();
146 | jstring src = env->NewStringUTF(src_path);
147 | jstring dst = env->NewStringUTF(dst_path);
148 | result = env->CallStaticBooleanMethod(classStorageAccess, renamePath, src, dst);
149 | JvmDetach();
150 | } CATCH:
151 | return result == JNI_TRUE;
152 | }
153 |
154 | bool tr_android_path_remove(char const *path) {
155 | jboolean result = JNI_FALSE;
156 | {
157 | JvmAttach();
158 | jstring jpath = env->NewStringUTF(path);
159 | result = env->CallStaticBooleanMethod(classStorageAccess, removePath, jpath);
160 | JvmDetach();
161 | } CATCH:
162 | return result == JNI_TRUE;
163 | }
164 |
165 | #define rndChar() "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[random() % 62]
166 |
167 | static void rndChars(char *str, int num) {
168 | for (; num > 0; num--, str++) *str = rndChar();
169 | }
170 |
171 | #pragma clang diagnostic pop
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/views/BrowseView.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.views;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.content.pm.PackageManager;
6 | import android.content.pm.ResolveInfo;
7 | import android.content.res.TypedArray;
8 | import android.support.annotation.RequiresApi;
9 | import android.util.AttributeSet;
10 | import android.view.LayoutInflater;
11 | import android.view.View;
12 | import android.widget.EditText;
13 | import android.widget.ImageView;
14 | import android.widget.RelativeLayout;
15 | import android.widget.TextView;
16 |
17 | import com.ap.transmission.btc.Adapters;
18 | import com.ap.transmission.btc.Prefs;
19 | import com.ap.transmission.btc.R;
20 | import com.ap.transmission.btc.Utils;
21 | import com.ap.transmission.btc.activities.ActivityBase;
22 | import com.ap.transmission.btc.activities.ActivityResultHandler;
23 | import com.ap.transmission.btc.activities.SelectFileActivity;
24 |
25 | import java.io.File;
26 |
27 | import static android.os.Build.VERSION.SDK_INT;
28 | import static android.os.Build.VERSION_CODES.LOLLIPOP;
29 | import static android.os.Build.VERSION_CODES.N;
30 | import static com.ap.transmission.btc.Utils.getRealDirPath;
31 |
32 | /**
33 | * @author Andrey Pavlenko
34 | */
35 | public class BrowseView extends RelativeLayout implements ActivityResultHandler {
36 | private static final int REQ_FILE = 10;
37 | private static byte isBrowseSupported;
38 | private boolean selectDir;
39 | private boolean selectFile;
40 | private boolean checkWritable;
41 | private Prefs.K pref;
42 | private int prefIndex = -1;
43 |
44 | public BrowseView(Context context, AttributeSet attrs) {
45 | super(context, attrs);
46 |
47 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BrowseView, 0, 0);
48 | String titleAttr = a.getString(R.styleable.BrowseView_title);
49 | String pathAttr = a.getString(R.styleable.BrowseView_path);
50 | boolean editable = a.getBoolean(R.styleable.BrowseView_editable, true);
51 | selectDir = a.getBoolean(R.styleable.BrowseView_select_dir, false);
52 | selectFile = a.getBoolean(R.styleable.BrowseView_select_file, false);
53 | checkWritable = a.getBoolean(R.styleable.BrowseView_writable, selectDir);
54 | a.recycle();
55 |
56 | LayoutInflater i = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
57 | if (i == null) throw new RuntimeException("Inflater is null");
58 | i.inflate(R.layout.browse_view, this, true);
59 | TextView title = getTitle();
60 | EditText path = getPath();
61 |
62 | title.setText(titleAttr);
63 | path.setText(pathAttr);
64 |
65 | if (!editable) {
66 | path.setKeyListener(null);
67 | path.setFocusable(false);
68 | path.setClickable(false);
69 | }
70 | }
71 |
72 | public TextView getTitle() {
73 | return (TextView) getChildAt(0);
74 | }
75 |
76 | public ImageView getLeftButton() {
77 | return (ImageView) getChildAt(1);
78 | }
79 |
80 | public ImageView getBrowseButton() {
81 | return (ImageView) getChildAt(2);
82 | }
83 |
84 | public void setTitle(String title) {
85 | getTitle().setText(title);
86 | }
87 |
88 | public EditText getPath() {
89 | return (EditText) getChildAt(3);
90 | }
91 |
92 | public void setPath(String path) {
93 | EditText t = getPath();
94 | String current = t.getText().toString();
95 | if (!current.equals(path)) t.setText(path);
96 | }
97 |
98 | public void setPref(Prefs.K pref) {
99 | this.pref = pref;
100 | Adapters.editTextPrefAdapter(getPath(), pref);
101 | setListener();
102 | }
103 |
104 | public void setPref(Prefs.K pref, int prefIndex) {
105 | this.pref = pref;
106 | this.prefIndex = prefIndex;
107 | Adapters.editTextPrefAdapter(getPath(), pref, prefIndex);
108 | setListener();
109 | }
110 |
111 | private void setListener() {
112 | getBrowseButton().setOnClickListener(new View.OnClickListener() {
113 | @Override
114 | @RequiresApi(api = LOLLIPOP)
115 | public void onClick(View v) {
116 | ActivityBase a = Utils.getActivity(v);
117 | File current = new File(getPath().getText().toString());
118 | Intent intent = new Intent(v.getContext(), SelectFileActivity.class);
119 | intent.putExtra(SelectFileActivity.REQUEST_INITIAL, current);
120 | intent.putExtra(SelectFileActivity.REQUEST_DIR, selectDir);
121 | intent.putExtra(SelectFileActivity.REQUEST_FILE, selectFile);
122 | intent.putExtra(SelectFileActivity.REQUEST_WRITABLE, checkWritable);
123 | a.setActivityResultHandler(BrowseView.this);
124 | a.startActivityForResult(intent, REQ_FILE);
125 | }
126 | });
127 | }
128 |
129 | @Override
130 | public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
131 | if ((data != null) && (pref != null)) {
132 | final File f;
133 |
134 | if (requestCode == REQ_FILE) {
135 | if (resultCode != SelectFileActivity.RESULT_OK) return true;
136 | f = (File) data.getSerializableExtra(SelectFileActivity.RESULT_FILE);
137 | } else {
138 | return false;
139 | }
140 |
141 | setResult(f);
142 | }
143 |
144 | return false;
145 | }
146 |
147 | private void setResult(File f) {
148 | setPath(f);
149 | }
150 |
151 | private void setPath(File f) {
152 | Prefs p = Utils.getActivity(this).getPrefs();
153 | p.set(pref, f.getAbsolutePath(), prefIndex);
154 | }
155 |
156 | @SuppressWarnings("unused")
157 | private boolean isBrowseSupported() {
158 | byte b = isBrowseSupported;
159 |
160 | if (b == 0) {
161 | if ((SDK_INT >= LOLLIPOP) && (SDK_INT < N)) {
162 | Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
163 | PackageManager pm = Utils.getActivity(this).getApplicationContext().getPackageManager();
164 | ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
165 | isBrowseSupported = b = (info != null) ? (byte) 1 : (byte) 2;
166 | } else {
167 | isBrowseSupported = b = 2;
168 | }
169 | }
170 | return b == 1;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/views/PathItem.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.views;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.view.View;
5 | import android.widget.CompoundButton;
6 |
7 | import com.ap.transmission.btc.R;
8 | import com.ap.transmission.btc.databinding.PathViewBinding;
9 |
10 | import java.util.ArrayList;
11 | import java.util.Collections;
12 | import java.util.Comparator;
13 | import java.util.HashMap;
14 | import java.util.List;
15 | import java.util.Map;
16 | import java.util.StringTokenizer;
17 |
18 | /**
19 | * @author Andrey Pavlenko
20 | */
21 | public class PathItem implements Comparable {
22 | private final PathItem parent;
23 | private final String name;
24 | private final String path;
25 | private final int index;
26 | private final int level;
27 | private final Map children = new HashMap<>();
28 | private boolean visible = true;
29 | private boolean checked = true;
30 | private boolean collapsed = false;
31 | private PathViewBinding binding;
32 |
33 | public PathItem(PathItem parent, String name, String path, int index, int level) {
34 | this.parent = parent;
35 | this.name = name;
36 | this.path = path;
37 | this.index = index;
38 | this.level = level;
39 | }
40 |
41 | public static Map split(String... items) {
42 | Map roots = new HashMap<>();
43 | StringBuilder sb = new StringBuilder();
44 |
45 | for (int i = 0; i < items.length; i++) {
46 | int level = 0;
47 | String item = items[i];
48 | PathItem parent = null;
49 | Map m = roots;
50 | sb.setLength(0);
51 | if (item.startsWith("/")) item = item.substring(1);
52 |
53 | for (StringTokenizer st = new StringTokenizer(item, "/"); st.hasMoreTokens(); ) {
54 | if (sb.length() != 0) sb.append('/');
55 | String name = st.nextToken();
56 | String path = sb.append(name).toString();
57 | PathItem pi = m.get(path);
58 |
59 | if (pi == null) {
60 | pi = new PathItem(parent, name, path, i, level);
61 | m.put(path, pi);
62 | }
63 |
64 | parent = pi;
65 | m = pi.children;
66 | level++;
67 | }
68 | }
69 |
70 | return roots;
71 | }
72 |
73 | public static List ls(Map roots) {
74 | return ls(roots, null);
75 | }
76 |
77 | public static List ls(Map roots, Comparator cmp) {
78 | List ls = new ArrayList<>(roots.size());
79 | ls(roots, ls, cmp);
80 | return ls;
81 | }
82 |
83 | private static void ls(Map roots, List ls, Comparator cmp) {
84 | List l = new ArrayList<>(roots.values());
85 | Collections.sort(l, cmp);
86 |
87 | for (PathItem i : l) {
88 | ls.add(i);
89 | ls(i.getChildren(), ls, cmp);
90 | }
91 | }
92 |
93 | public PathItem getParent() {
94 | return parent;
95 | }
96 |
97 | public String getName() {
98 | return name;
99 | }
100 |
101 | public String getLabelText() {
102 | return isDir() ? (" " + getName()) : getName();
103 | }
104 |
105 | public String getPath() {
106 | return path;
107 | }
108 |
109 | public int getIndex() {
110 | return index;
111 | }
112 |
113 | public int getLevel() {
114 | return level;
115 | }
116 |
117 | public Map getChildren() {
118 | return children;
119 | }
120 |
121 | public boolean isDir() {
122 | return !getChildren().isEmpty();
123 | }
124 |
125 | public int getIcon() {
126 | if (isDir()) {
127 | return isCollapsed() ? R.drawable.expand : R.drawable.collapse;
128 | } else {
129 | return -1;
130 | }
131 | }
132 |
133 | public boolean isVisible() {
134 | return visible;
135 | }
136 |
137 | public void setVisible(boolean visible) {
138 | this.visible = visible;
139 | if (!isCollapsed()) {
140 | for (PathItem i : getChildren().values()) i.setVisible(visible);
141 | }
142 | resetBinding();
143 | }
144 |
145 | public boolean isCollapsed() {
146 | return collapsed;
147 | }
148 |
149 | public void setCollapsed(boolean collapsed) {
150 | this.collapsed = collapsed;
151 | for (PathItem i : getChildren().values()) i.setVisible(!collapsed);
152 | resetBinding();
153 | }
154 |
155 | public boolean isChecked() {
156 | return checked;
157 | }
158 |
159 | public void setChecked(boolean checked) {
160 | this.checked = checked;
161 | for (PathItem i : getChildren().values()) i.setChecked(checked);
162 | resetBinding();
163 | }
164 |
165 | public void onClick(@SuppressWarnings("unused") View v) {
166 | if (isDir()) setCollapsed(!isCollapsed());
167 | else setChecked(!isChecked());
168 | }
169 |
170 | public void onCheckedChanged(@SuppressWarnings("unused") CompoundButton v, boolean isChecked) {
171 | setChecked(isChecked);
172 | }
173 |
174 | public void setBinding(PathViewBinding binding) {
175 | this.binding = binding;
176 | binding.setItem(this);
177 | }
178 |
179 | private void resetBinding() {
180 | if (binding != null) {
181 | binding.setItem(null);
182 | binding.setItem(this);
183 | }
184 | }
185 |
186 | @Override
187 | public int hashCode() {
188 | return getPath().hashCode();
189 | }
190 |
191 | @Override
192 | public boolean equals(Object obj) {
193 | if (obj == this) {
194 | return true;
195 | } else if (obj instanceof PathItem) {
196 | return getPath().equals(((PathItem) obj).getPath());
197 | } else {
198 | return false;
199 | }
200 | }
201 |
202 | @Override
203 | public String toString() {
204 | return isDir() ? (getPath() + '/') : getPath();
205 | }
206 |
207 | @Override
208 | public int compareTo(@NonNull PathItem i) {
209 | int l1 = getLevel();
210 | int l2 = i.getLevel();
211 |
212 | if (l1 == l2) {
213 | boolean d1 = isDir();
214 | boolean d2 = i.isDir();
215 |
216 | if (d1 == d2) {
217 | return getName().compareTo(i.getName());
218 | } else {
219 | return d1 ? -1 : 1;
220 | }
221 | } else {
222 | return (l1 < l2) ? -1 : 1;
223 | }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/src/main/assets/upnp/ContentDirectoryScpd.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 0
6 |
7 |
8 |
9 | Browse
10 |
11 |
12 | ObjectID
13 | in
14 | A_ARG_TYPE_ObjectID
15 |
16 |
17 | BrowseFlag
18 | in
19 | A_ARG_TYPE_BrowseFlag
20 |
21 |
22 | Filter
23 | in
24 | A_ARG_TYPE_Filter
25 |
26 |
27 | StartingIndex
28 | in
29 | A_ARG_TYPE_Index
30 |
31 |
32 | RequestedCount
33 | in
34 | A_ARG_TYPE_Count
35 |
36 |
37 | SortCriteria
38 | in
39 | A_ARG_TYPE_SortCriteria
40 |
41 |
42 | Result
43 | out
44 | A_ARG_TYPE_Result
45 |
46 |
47 | NumberReturned
48 | out
49 | A_ARG_TYPE_Count
50 |
51 |
52 | TotalMatches
53 | out
54 | A_ARG_TYPE_Count
55 |
56 |
57 | UpdateID
58 | out
59 | A_ARG_TYPE_UpdateID
60 |
61 |
62 |
63 |
64 | GetSortCapabilities
65 |
66 |
67 | SortCaps
68 | out
69 | SortCapabilities
70 |
71 |
72 |
73 |
74 | GetSystemUpdateID
75 |
76 |
77 | Id
78 | out
79 | SystemUpdateID
80 |
81 |
82 |
83 |
84 | GetSearchCapabilities
85 |
86 |
87 | SearchCaps
88 | out
89 | SearchCapabilities
90 |
91 |
92 |
93 |
94 |
95 |
96 | A_ARG_TYPE_BrowseFlag
97 | string
98 |
99 | BrowseMetadata
100 | BrowseDirectChildren
101 |
102 |
103 |
104 | ContainerUpdateIDs
105 | string
106 |
107 |
108 | SystemUpdateID
109 | ui4
110 |
111 |
112 | A_ARG_TYPE_Count
113 | ui4
114 |
115 |
116 | A_ARG_TYPE_SortCriteria
117 | string
118 |
119 |
120 | SortCapabilities
121 | string
122 |
123 |
124 | A_ARG_TYPE_Index
125 | ui4
126 |
127 |
128 | A_ARG_TYPE_ObjectID
129 | string
130 |
131 |
132 | A_ARG_TYPE_UpdateID
133 | ui4
134 |
135 |
136 | A_ARG_TYPE_Result
137 | string
138 |
139 |
140 | SearchCapabilities
141 | string
142 |
143 |
144 | A_ARG_TYPE_Filter
145 | string
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/torrent/TorrentDir.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.torrent;
2 |
3 | import android.app.Activity;
4 | import android.net.Uri;
5 | import android.view.View;
6 |
7 | import com.ap.transmission.btc.CompletedFuture;
8 | import com.ap.transmission.btc.Native;
9 | import com.ap.transmission.btc.R;
10 | import com.ap.transmission.btc.Utils;
11 | import com.ap.transmission.btc.http.HttpServer;
12 | import com.ap.transmission.btc.http.handlers.torrent.PlaylistHandler;
13 |
14 | import java.util.ArrayList;
15 | import java.util.Arrays;
16 | import java.util.Collections;
17 | import java.util.List;
18 | import java.util.concurrent.Callable;
19 | import java.util.concurrent.Future;
20 |
21 | import static com.ap.transmission.btc.Utils.showErr;
22 |
23 | /**
24 | * @author Andrey Pavlenko
25 | */
26 | public class TorrentDir implements TorrentItemContainer {
27 | private final TorrentItemContainer parent;
28 | private final String name;
29 | private final String fullName;
30 | private final int index;
31 | private String strId;
32 | private List children = new ArrayList<>();
33 |
34 | public TorrentDir(TorrentItemContainer parent, String name, String fullName, int index) {
35 | this.parent = parent;
36 | this.name = name;
37 | this.fullName = fullName;
38 | this.index = index;
39 | }
40 |
41 | public Torrent getTorrent() {
42 | return (parent instanceof Torrent) ? (Torrent) parent : ((TorrentDir) parent).getTorrent();
43 | }
44 |
45 | @Override
46 | public List ls() {
47 | return children;
48 | }
49 |
50 | public List lsFiles() {
51 | List ls = ls();
52 | List files = new ArrayList<>(ls.size());
53 | for (TorrentItem i : ls) {
54 | if (i instanceof TorrentFile) files.add((TorrentFile) i);
55 | }
56 | return files;
57 | }
58 |
59 | @Override
60 | public String getName() {
61 | return name;
62 | }
63 |
64 | @SuppressWarnings("unused")
65 | public String getFullName() {
66 | return fullName;
67 | }
68 |
69 | public int getIndex() {
70 | return index;
71 | }
72 |
73 | @Override
74 | public String getId() {
75 | String s = strId;
76 | if (s == null) strId = s = getTorrent().getHashString() + "-d" + getIndex();
77 | return s;
78 | }
79 |
80 | @Override
81 | public boolean isComplete() {
82 | for (TorrentItem i : ls()) {
83 | if (!i.isComplete()) return false;
84 | }
85 | return true;
86 | }
87 |
88 | @Override
89 | public boolean isDnd() {
90 | for (TorrentItem i : ls()) {
91 | if (!i.isDnd()) return false;
92 | }
93 | return true;
94 | }
95 |
96 | public Future setDnd(final boolean dnd) throws IllegalStateException, NoSuchTorrentException {
97 | final Torrent tor = getTorrent();
98 | List files = tor.lsFiles();
99 | int[] idx = new int[files.size()];
100 | int count = 0;
101 |
102 | for (TorrentFile f : files) {
103 | if (findFile(f.getIndex())) {
104 | idx[count++] = f.getIndex();
105 | }
106 | }
107 |
108 | if (count == 0) return CompletedFuture.VOID;
109 | if (count != idx.length) idx = Arrays.copyOf(idx, count);
110 | Future future;
111 |
112 | tor.readLock().lock();
113 | try {
114 | tor.checkValid();
115 | final int[] indexes = idx;
116 | future = tor.getTransmission().getExecutor().submit(new Callable() {
117 |
118 | @Override
119 | public Void call() throws Exception {
120 | tor.readLock().lock();
121 | try {
122 | tor.checkValid();
123 | getTorrent().checkValid();
124 | Native.torrentSetDnd(tor.getSessionId(), tor.getTorrentId(), indexes, dnd);
125 | return null;
126 | } catch (NoSuchTorrentException ex) {
127 | getFs().reportNoSuchTorrent(ex);
128 | throw ex;
129 | } finally {
130 | tor.readLock().unlock();
131 | }
132 | }
133 | });
134 |
135 | for (TorrentFile f : files) {
136 | int fidx = f.getIndex();
137 | for (int i : idx) {
138 | if (i == fidx) {
139 | f.setDndStat(dnd);
140 | break;
141 | }
142 | }
143 | }
144 | } finally {
145 | tor.readLock().unlock();
146 | }
147 |
148 | return future;
149 | }
150 |
151 | public boolean hasMediaFiles() {
152 | for (TorrentItem i : ls()) {
153 | if (i instanceof TorrentFile) {
154 | TorrentFile f = (TorrentFile) i;
155 | if (f.isVideo() || f.isAudio()) return true;
156 | }
157 | }
158 | return false;
159 | }
160 |
161 | public Uri getPlaylistUri() {
162 | try {
163 | Torrent tor = getTorrent();
164 | HttpServer http = tor.getTransmission().getHttpServer();
165 | return PlaylistHandler.createUri(http.getHostName(), http.getPort(), tor.getHashString(), getIndex());
166 | } catch (Exception ex) {
167 | Utils.err(getClass().getName(), ex, "Failed to create playlist uri", this);
168 | return null;
169 | }
170 | }
171 |
172 | public boolean play(Activity a, View v) {
173 | Uri uri = getPlaylistUri();
174 |
175 | if (uri == null) {
176 | showErr(v, R.string.err_failed_to_open_playlist);
177 | return false;
178 | } else {
179 | Utils.openUri(a, uri, PlaylistHandler.MIME_TYPE);
180 | return true;
181 | }
182 | }
183 |
184 | private boolean findFile(int idx) {
185 | for (TorrentItem i : ls()) {
186 | if (i instanceof TorrentFile) {
187 | if (((TorrentFile) i).getIndex() == idx) return true;
188 | } else if (((TorrentDir) i).findFile(idx)) {
189 | return true;
190 | }
191 | }
192 | return false;
193 | }
194 |
195 | @Override
196 | public TorrentItemContainer getParent() {
197 | return parent;
198 | }
199 |
200 | @Override
201 | public TorrentFs getFs() {
202 | return getParent().getFs();
203 | }
204 |
205 | @Override
206 | public String toString() {
207 | return getName();
208 | }
209 |
210 | void addChild(TorrentItem c) {
211 | children.add(c);
212 | }
213 |
214 | void compactChildren() {
215 | if (children.isEmpty()) {
216 | children = Collections.emptyList();
217 | } else {
218 | ((ArrayList) children).trimToSize();
219 | children = Collections.unmodifiableList(children);
220 | }
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/http/handlers/SoapHandler.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.http.handlers;
2 |
3 | import com.ap.transmission.btc.Baos;
4 | import com.ap.transmission.btc.Utils;
5 | import com.ap.transmission.btc.http.HttpServer;
6 | import com.ap.transmission.btc.http.Method;
7 | import com.ap.transmission.btc.http.Request;
8 | import com.ap.transmission.btc.http.RequestHandler;
9 | import com.ap.transmission.btc.http.Response;
10 |
11 | import org.w3c.dom.Document;
12 | import org.w3c.dom.Element;
13 | import org.w3c.dom.Node;
14 | import org.w3c.dom.NodeList;
15 |
16 | import java.io.IOException;
17 | import java.io.OutputStream;
18 | import java.net.Socket;
19 | import java.nio.ByteBuffer;
20 | import java.util.Map;
21 |
22 | import javax.xml.parsers.DocumentBuilder;
23 | import javax.xml.parsers.DocumentBuilderFactory;
24 | import javax.xml.parsers.ParserConfigurationException;
25 |
26 | import static com.ap.transmission.btc.Utils.UTF8;
27 | import static com.ap.transmission.btc.Utils.readXml;
28 | import static com.ap.transmission.btc.Utils.writeXml;
29 |
30 | /**
31 | * @author Andrey Pavlenko
32 | */
33 | public class SoapHandler implements RequestHandler {
34 | public static final String SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/";
35 | protected final Map handlers;
36 | protected final String logTag;
37 | protected final DocumentBuilder docBuilder;
38 | private int maxLen = 64;
39 |
40 | public SoapHandler(Map handlers, String logTag) {
41 | this.handlers = handlers;
42 | this.logTag = logTag;
43 |
44 | DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
45 | try {
46 | docBuilder = f.newDocumentBuilder();
47 | } catch (ParserConfigurationException ex) {
48 | throw new RuntimeException(ex);
49 | }
50 | }
51 |
52 | protected MessageHandler getHandler(String name) {
53 | return handlers.get(name);
54 | }
55 |
56 | @Override
57 | public void handle(HttpServer server, Request req, Socket socket) {
58 | new Handler(server, socket).handle(req);
59 | }
60 |
61 | public interface MessageHandler {
62 | void handle(Handler handler, Document reqDoc, Element reqBody,
63 | Document respDoc, Element respBody) throws Throwable;
64 | }
65 |
66 | protected class Handler extends HandlerBase {
67 | private Request request;
68 |
69 | protected Handler(HttpServer server, Socket socket) {
70 | super(logTag, server, socket);
71 | }
72 |
73 | public Request getRequest() {
74 | return request;
75 | }
76 |
77 | public void addFault(Document respDoc, Element respBody, String msg, Throwable ex) {
78 | warn(ex, "Failed to handle message: %s", msg);
79 | Element fault = respDoc.createElementNS(SOAP_NS, "s:Fault");
80 | Element faultcode = respDoc.createElementNS(SOAP_NS, "s:faultcode");
81 | Element faultstring = respDoc.createElementNS(SOAP_NS, "s:faultstring");
82 | faultcode.setTextContent("Server");
83 | faultstring.setTextContent(msg);
84 | respBody.appendChild(fault);
85 | fault.appendChild(faultcode);
86 | fault.appendChild(faultstring);
87 | }
88 |
89 | public Element findChild(Node n, String name) {
90 | NodeList list = n.getChildNodes();
91 | int count = list.getLength();
92 |
93 | for (int i = 0; i < count; i++) {
94 | Node c = list.item(i);
95 |
96 | if (c instanceof Element) {
97 | if (c.getLocalName().equals(name)) {
98 | return (Element) c;
99 | }
100 | }
101 | }
102 |
103 | return null;
104 | }
105 |
106 | @Override
107 | protected void doHandle(Request req) throws IOException {
108 | if (req.getMethod() != Method.POST) {
109 | fail(Response.BadRequest.instance, "Unexpected request method: %s", req.getMethod());
110 | return;
111 | }
112 |
113 | ByteBuffer payload = req.getPayload();
114 |
115 | if (payload == null) {
116 | fail(Response.BadRequest.instance, "Request payload is empty");
117 | return;
118 | }
119 |
120 | Document doc;
121 |
122 | try {
123 | doc = readXml(payload);
124 | } catch (Exception ex) {
125 | fail(Response.BadRequest.instance, ex, "Failed to parse request message");
126 | return;
127 | }
128 |
129 | if (isDebugEnabled()) debug("Handling request:\n%s", Utils.nodeToString(doc));
130 |
131 | Element envelope = findChild(doc, "Envelope");
132 |
133 | if (envelope == null) {
134 | fail(Response.BadRequest.instance, "No element");
135 | return;
136 | }
137 |
138 | Element body = findChild(envelope, "Body");
139 |
140 | if (body == null) {
141 | fail(Response.BadRequest.instance, "No element");
142 | return;
143 | }
144 |
145 | if (body.getFirstChild() == null) {
146 | fail(Response.BadRequest.instance, " element is empty");
147 | return;
148 | }
149 |
150 | request = req;
151 | handleMessage(doc, body);
152 | }
153 |
154 | private void handleMessage(Document reqDoc, Element reqBody) throws IOException {
155 | Document respDoc = docBuilder.newDocument();
156 | Element envelope = respDoc.createElementNS(SOAP_NS, "s:Envelope");
157 | Element respBody = respDoc.createElementNS(SOAP_NS, "s:Body");
158 | respDoc.appendChild(envelope);
159 | envelope.appendChild(respBody);
160 |
161 | String handlerName = reqBody.getFirstChild().getLocalName();
162 | MessageHandler h = getHandler(handlerName);
163 |
164 | if (h == null) {
165 | addFault(respDoc, respBody, "No such handler: " + handlerName, null);
166 | } else {
167 | try {
168 | h.handle(this, reqDoc, reqBody, respDoc, respBody);
169 | } catch (Throwable ex) {
170 | addFault(respDoc, respBody, "Handler failed: " + handlerName, ex);
171 | }
172 | }
173 |
174 | Baos baos = new Baos(maxLen);
175 |
176 | try {
177 | writeXml(respDoc, baos);
178 | } catch (Exception ex) {
179 | fail(Response.ServerError.instance, ex, "writeXml() failed");
180 | return;
181 | }
182 |
183 | ByteBuffer buf = baos.byteBuf();
184 | maxLen = Math.max(maxLen, buf.remaining());
185 |
186 | if (isDebugEnabled()) {
187 | debug("Sending response:\n%s", new String(buf.array(),
188 | buf.position(), buf.remaining(), UTF8));
189 | }
190 |
191 | OutputStream out = responseOk("text/xml; charset=\"utf-8\"", buf.remaining(), false);
192 | out.write(buf.array(), buf.position(), buf.remaining());
193 | }
194 | }
195 |
196 | private static boolean isDebugEnabled() {
197 | return false;
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/Adapters.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc;
2 |
3 | import android.content.res.Resources;
4 | import android.databinding.BindingAdapter;
5 | import android.databinding.BindingConversion;
6 | import android.graphics.drawable.Drawable;
7 | import android.os.Build;
8 | import android.text.Editable;
9 | import android.text.Html;
10 | import android.text.TextWatcher;
11 | import android.text.method.LinkMovementMethod;
12 | import android.view.View;
13 | import android.view.View.OnFocusChangeListener;
14 | import android.widget.AdapterView;
15 | import android.widget.ArrayAdapter;
16 | import android.widget.CheckBox;
17 | import android.widget.CompoundButton;
18 | import android.widget.EditText;
19 | import android.widget.Spinner;
20 | import android.widget.TextView;
21 |
22 | import java.util.ArrayList;
23 | import java.util.EnumSet;
24 | import java.util.List;
25 | import java.util.Set;
26 |
27 | import static com.ap.transmission.btc.Utils.getActivity;
28 |
29 | /**
30 | * @author Andrey Pavlenko
31 | */
32 | public class Adapters {
33 |
34 | @BindingAdapter("app:pref")
35 | public static void editTextPrefAdapter(final EditText view, final Prefs.K k) {
36 | editTextPrefAdapter(view, k, -1);
37 | }
38 |
39 | @BindingAdapter({"app:pref", "app:pref_index"})
40 | public static void editTextPrefAdapter(final EditText view, final Prefs.K k, final int index) {
41 | final Prefs p = getPrefs(view);
42 | Object value = p.get(k, index);
43 | String valueText = (value == null) ? "" : value.toString();
44 | String currentText = view.getText().toString();
45 | if (!currentText.equals(valueText)) view.setText(valueText);
46 |
47 | if (view.getTag(R.id.listener_tag) == null) {
48 | TextListener tl = new TextListener() {
49 | @Override
50 | public void onTextChanged(CharSequence s, int start, int before, int count) {
51 | p.set(k, s, index);
52 | }
53 | };
54 | OnFocusChangeListener fl = new OnFocusChangeListener() {
55 | @Override
56 | public void onFocusChange(View v, boolean hasFocus) {
57 | if (!hasFocus) {
58 | String current = view.getText().toString();
59 |
60 | if (current.isEmpty()) {
61 | final Object def = k.def(p, index);
62 | view.setText((def == null) ? "" : String.valueOf(def));
63 | }
64 | }
65 | }
66 | };
67 |
68 | final Object def = k.def(p, index);
69 | final String defText = (def == null) ? null : String.valueOf(def);
70 | if ((defText != null) && !defText.isEmpty()) view.setHint(defText);
71 |
72 | view.setTag(R.id.listener_tag, tl);
73 | view.addTextChangedListener(tl);
74 | view.setOnFocusChangeListener(fl);
75 | }
76 | }
77 |
78 | @BindingAdapter("app:pref")
79 | public static void checkBoxPropAdapter(final CheckBox view, final Prefs.K k) {
80 | final Prefs p = getPrefs(view);
81 | Boolean value = p.get(k);
82 | if (view.isChecked() != value) view.setChecked(value);
83 |
84 | if (view.getTag(R.id.listener_tag) == null) {
85 | CompoundButton.OnCheckedChangeListener l = new CompoundButton.OnCheckedChangeListener() {
86 | @Override
87 | public void onCheckedChanged(CompoundButton c, boolean isChecked) {
88 | p.set(k, isChecked);
89 | }
90 | };
91 | view.setTag(R.id.listener_tag, l);
92 | view.setOnCheckedChangeListener(l);
93 | }
94 | }
95 |
96 | @BindingAdapter("app:pref")
97 | public static void spinnerPropAdapter(final Spinner s, final Prefs.K k) {
98 | Resources res = s.getContext().getResources();
99 | final Prefs p = getPrefs(s);
100 | Object current = p.get(k);
101 | Set all = EnumSet.allOf((Class) current.getClass());
102 | List items = new ArrayList<>(all.size());
103 | int idx = 0;
104 | int i = 0;
105 |
106 | for (Object o : all) {
107 | String v = res.getText(((Localizable) o).getResourceId()).toString();
108 | if (o.equals(current)) idx = i;
109 | items.add(new SpinnerItem(o, v));
110 | i++;
111 | }
112 |
113 | ArrayAdapter adapter = new ArrayAdapter<>(s.getContext(),
114 | android.R.layout.simple_spinner_item, items);
115 | s.setAdapter(adapter);
116 | if (idx != s.getSelectedItemPosition()) s.setSelection(idx);
117 |
118 | if (s.getTag(R.id.listener_tag) == null) {
119 | AdapterView.OnItemSelectedListener l = new AdapterView.OnItemSelectedListener() {
120 | @Override
121 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
122 | p.set(k, ((SpinnerItem) s.getSelectedItem()).key);
123 | }
124 |
125 | @Override
126 | public void onNothingSelected(AdapterView> parent) {
127 | }
128 | };
129 | s.setTag(R.id.listener_tag, l);
130 | s.setOnItemSelectedListener(l);
131 | }
132 | }
133 |
134 | @BindingAdapter("app:html")
135 | public static void toHtml(TextView view, String html) {
136 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
137 | view.setText(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY));
138 | } else {
139 | view.setText(Html.fromHtml(html));
140 | }
141 |
142 | view.setClickable(true);
143 | view.setMovementMethod(LinkMovementMethod.getInstance());
144 | }
145 |
146 | @BindingAdapter("app:icon")
147 | public static void textIcon(TextView view, int res) {
148 | if (res < 0) return;
149 |
150 | Integer current = (Integer) view.getTag(R.id.icon_tag);
151 | if ((current != null) && (current == res)) return;
152 |
153 | Drawable d = view.getResources().getDrawable(res);
154 | int size = (int) (view.getTextSize() * view.getTextScaleX());
155 | d.setBounds(0, 0, size, size);
156 | view.setCompoundDrawables(d, null, null, null);
157 | view.setTag(R.id.icon_tag, res);
158 | }
159 |
160 | @BindingConversion
161 | public static int visibilityAdapter(boolean visible) {
162 | return visible ? View.VISIBLE : View.GONE;
163 | }
164 |
165 | private static Prefs getPrefs(View view) {
166 | return getActivity(view).getPrefs();
167 | }
168 |
169 | private static abstract class TextListener implements TextWatcher {
170 |
171 | @Override
172 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
173 | }
174 |
175 | @Override
176 | public void afterTextChanged(Editable s) {
177 | }
178 | }
179 |
180 | private static final class SpinnerItem {
181 | final Object key;
182 | final String value;
183 |
184 | SpinnerItem(Object key, String value) {
185 | this.key = key;
186 | this.value = value;
187 | }
188 |
189 | @Override
190 | public String toString() {
191 | return value;
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/main/res/layout/proxy.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
20 |
21 |
22 |
25 |
26 |
27 |
37 |
38 |
44 |
45 |
49 |
50 |
51 |
63 |
64 |
75 |
76 |
77 |
90 |
91 |
102 |
103 |
104 |
116 |
117 |
128 |
129 |
130 |
142 |
143 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/src/main/java/com/ap/transmission/btc/http/handlers/HandlerBase.java:
--------------------------------------------------------------------------------
1 | package com.ap.transmission.btc.http.handlers;
2 |
3 | import android.util.Log;
4 |
5 | import com.ap.transmission.btc.Utils;
6 | import com.ap.transmission.btc.http.HttpServer;
7 | import com.ap.transmission.btc.http.Range;
8 | import com.ap.transmission.btc.http.Request;
9 | import com.ap.transmission.btc.http.Response;
10 | import com.ap.transmission.btc.http.Response.ServerError;
11 | import com.ap.transmission.btc.torrent.Transmission;
12 |
13 | import java.io.ByteArrayOutputStream;
14 | import java.io.IOException;
15 | import java.io.OutputStream;
16 | import java.net.Socket;
17 | import java.net.SocketException;
18 |
19 | import static com.ap.transmission.btc.Utils.ASCII;
20 | import static com.ap.transmission.btc.Utils.isDebugEnabled;
21 |
22 | /**
23 | * @author Andrey Pavlenko
24 | */
25 | public abstract class HandlerBase {
26 | protected final String TAG;
27 | protected final HttpServer server;
28 | protected final Socket socket;
29 |
30 | protected HandlerBase(String logTag, HttpServer server, Socket socket) {
31 | TAG = "transmission." + logTag + " (" + socket.getRemoteSocketAddress() + ')';
32 | this.server = server;
33 | this.socket = socket;
34 | }
35 |
36 | public HttpServer getHttpServer() {
37 | return server;
38 | }
39 |
40 | public Socket getSocket() {
41 | return socket;
42 | }
43 |
44 | protected abstract void doHandle(Request req) throws Throwable;
45 |
46 | public void handle(Request req) {
47 | try {
48 | doHandle(req);
49 | } catch (SocketException ignore) {
50 | // Socket closed?
51 | } catch (IOException ex) {
52 | debug("Failed to handle request: %s", ex);
53 | } catch (Throwable ex) {
54 | fail(ServerError.instance, ex, "Handler failed: %s", this);
55 | } finally {
56 | Utils.close(socket);
57 | }
58 | }
59 |
60 | protected OutputStream responseOk(String contentType, long contentLen,
61 | @SuppressWarnings("SameParameterValue") boolean acceptRanges)
62 | throws IOException {
63 | OutputStream out = getOutputStream();
64 | out.write(Ok.data);
65 | out.write(Long.toString(contentLen).getBytes(ASCII));
66 |
67 | if (contentType != null) {
68 | out.write(ContentType.data);
69 | out.write(contentType.getBytes(ASCII));
70 | }
71 |
72 | if (acceptRanges) {
73 | out.write(AcceptRanges.data);
74 | }
75 |
76 | out.write(EOH.data);
77 | return flushOutputStream(out);
78 | }
79 |
80 | protected OutputStream responsePartial(String contentType, Range range, long totalLength)
81 | throws IOException {
82 | OutputStream out = getOutputStream();
83 | out.write(Partial.data);
84 | out.write(Long.toString(range.getLength()).getBytes(ASCII));
85 |
86 | out.write(ContentRange.data);
87 | out.write(Long.toString(range.getStart()).getBytes(ASCII));
88 | out.write('-');
89 | out.write(Long.toString(range.getEnd()).getBytes(ASCII));
90 | out.write('/');
91 | out.write(Long.toString(totalLength).getBytes(ASCII));
92 |
93 | if (contentType != null) {
94 | out.write(ContentType.data);
95 | out.write(contentType.getBytes(ASCII));
96 | }
97 |
98 | out.write(EOH.data);
99 | return flushOutputStream(out);
100 | }
101 |
102 | protected void responseNotSatisfiable(Range range, long contentLen)
103 | throws IOException {
104 | warn("Range is not satisfiable: range=%s, length=%d", range, contentLen);
105 | OutputStream out = getOutputStream();
106 | out.write(RangeNotSatisfiable.data);
107 | out.write(Long.toString(contentLen).getBytes(ASCII));
108 | out.write(EOH.data);
109 | flushOutputStream(out);
110 | }
111 |
112 | protected Transmission getTransmission() {
113 | return server.getTransmission();
114 | }
115 |
116 | protected void fail(Response resp, String msg, Object... args) {
117 | fail(resp, null, msg, args);
118 | }
119 |
120 | protected void fail(Response resp, Throwable err, String msg, Object... args) {
121 | if (msg != null) {
122 | if (err == null) Log.w(TAG, format(msg, args));
123 | else Log.w(TAG, format(msg, args), err);
124 | }
125 |
126 | if (resp != null) {
127 | try {
128 | OutputStream out = getOutputStream();
129 | resp.write(out);
130 | Utils.close(flushOutputStream(out));
131 | } catch (Throwable ex) {
132 | if (isDebugEnabled()) Log.d(TAG, "Failed to send response", ex);
133 | }
134 | }
135 | }
136 |
137 | public String getServerHost(Request req) {
138 | String host = req.getHost();
139 | if (host == null) return getHttpServer().getHostName();
140 | int idx = host.lastIndexOf(':');
141 | return (idx == -1) ? host : host.substring(0, idx);
142 | }
143 |
144 | public void debug(String msg, Object... args) {
145 | Utils.debug(TAG, msg, args);
146 | }
147 |
148 | public void debug(Throwable ex, String msg, Object... args) {
149 | Utils.debug(TAG, ex, msg, args);
150 | }
151 |
152 | public void warn(String msg, Object... args) {
153 | Utils.warn(TAG, msg, args);
154 | }
155 |
156 | public void warn(Throwable ex, String msg, Object... args) {
157 | Utils.warn(TAG, ex, msg, args);
158 | }
159 |
160 | private static String format(String msg, Object... args) {
161 | return (args != null) && (args.length > 0) ? String.format(msg, args) : msg;
162 | }
163 |
164 | private OutputStream getOutputStream() throws IOException {
165 | return isDebugEnabled() ? new ByteArrayOutputStream(1024) : socket.getOutputStream();
166 | }
167 |
168 | private OutputStream flushOutputStream(OutputStream out) throws IOException {
169 | if (isDebugEnabled()) {
170 | ByteArrayOutputStream baos = (ByteArrayOutputStream) out;
171 | byte[] bytes = baos.toByteArray();
172 | debug("Writing response:\n" + new String(bytes));
173 | out = socket.getOutputStream();
174 | out.write(bytes);
175 | return out;
176 | } else {
177 | return out;
178 | }
179 | }
180 |
181 | private static final class Ok {
182 | static final byte[] data = ("HTTP/1.1 200 OK\r\nConnection: close\r\n"
183 | + "Content-Length: ").getBytes(ASCII);
184 | }
185 |
186 | private static final class Partial {
187 | static final byte[] data = ("HTTP/1.1 206 Partial Content\r\nConnection: close\r\n"
188 | + "Content-Length: ").getBytes(ASCII);
189 | }
190 |
191 | private static final class RangeNotSatisfiable {
192 | static final byte[] data = ("HTTP/1.1 416 Range Not Satisfiable\r\nConnection: close\r\n" +
193 | "Content-Range: bytes */").getBytes(ASCII);
194 | }
195 |
196 | private static final class AcceptRanges {
197 | static final byte[] data = "\r\nAccept-Ranges: bytes".getBytes(ASCII);
198 | }
199 |
200 | private static final class ContentType {
201 | static final byte[] data = "\r\nContent-Type: ".getBytes(ASCII);
202 | }
203 |
204 | private static final class ContentRange {
205 | static final byte[] data = "\r\nContent-Range: bytes ".getBytes(ASCII);
206 | }
207 |
208 | private static final class EOH {
209 | static final byte[] data = "\r\n\r\n".getBytes(ASCII);
210 | }
211 | }
212 |
--------------------------------------------------------------------------------