Java tutorial
/* * Copyright (C) 2016 Yaroslav Pronin <proninyaroslav@mail.ru> * * This file is part of LibreTorrent. * * LibreTorrent is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LibreTorrent is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LibreTorrent. If not, see <http://www.gnu.org/licenses/>. */ package org.proninyaroslav.libretorrent.core; import android.content.Context; import android.text.TextUtils; import android.util.Log; import com.frostwire.jlibtorrent.AlertListener; import com.frostwire.jlibtorrent.Priority; import com.frostwire.jlibtorrent.Session; import com.frostwire.jlibtorrent.SettingsPack; import com.frostwire.jlibtorrent.Sha1Hash; import com.frostwire.jlibtorrent.TorrentHandle; import com.frostwire.jlibtorrent.TorrentInfo; import com.frostwire.jlibtorrent.alerts.Alert; import com.frostwire.jlibtorrent.alerts.AlertType; import com.frostwire.jlibtorrent.alerts.TorrentAlert; import com.frostwire.jlibtorrent.swig.ip_filter; import com.frostwire.jlibtorrent.swig.settings_pack; import org.apache.commons.io.FileUtils; import org.proninyaroslav.libretorrent.core.storage.TorrentStorage; import org.proninyaroslav.libretorrent.core.utils.TorrentUtils; import java.io.File; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.locks.ReentrantLock; /* * This class is designed for seeding, downloading and management of torrents. */ public class TorrentEngine implements TorrentEngineInterface { @SuppressWarnings("unused") private static final String TAG = TorrentEngine.class.getSimpleName(); public static final int DEFAULT_CACHE_SIZE = 256; public static final int DEFAULT_ACTIVE_DOWNLOADS = 4; public static final int DEFAULT_ACTIVE_SEEDS = 4; public static final int DEFAULT_MAX_PEER_LIST_SIZE = 200; public static final int DEFAULT_TICK_INTERVAL = 1000; public static final int DEFAULT_INACTIVITY_TIMEOUT = 60; public static final int MIN_CONNECTIONS_LIMIT = 2; public static final int DEFAULT_CONNECTIONS_LIMIT = 200; public static final int DEFAULT_ACTIVE_LIMIT = 6; public static final int DEFAULT_PORT = 6881; public static final int DEFAULT_PROXY_PORT = 8080; public static final int MAX_PORT_NUMBER = 65534; public static final int MIN_PORT_NUMBER = 49160; public static final boolean DEFAULT_DHT_ENABLED = true; public static final boolean DEFAULT_LSD_ENABLED = true; public static final boolean DEFAULT_UTP_ENABLED = true; public static final boolean DEFAULT_UPNP_ENABLED = true; public static final boolean DEFAULT_NATPMP_ENABLED = true; public static final boolean DEFAULT_ENCRYPT_IN_CONNECTIONS = true; public static final boolean DEFAULT_ENCRYPT_OUT_CONNECTIONS = true; private Context context; private Session session; private TorrentEngineCallback callback; private Queue<LoadTorrentTask> loadTorrentsQueue = new LinkedList<>(); private ExecutorService loadTorrentsExec; private Map<String, Torrent> addTorrentsQueue = new HashMap<>(); private ReentrantLock sync; public TorrentEngine(Context context, TorrentEngineCallback callback) throws Exception { this.context = context; sync = new ReentrantLock(); loadTorrentsExec = Executors.newCachedThreadPool(); this.callback = callback; } @Override public void start() { sync.lock(); try { if (isStarted()) { return; } session = new Session(); loadSettings(); setListener(); } finally { if (callback != null) { callback.onEngineStarted(); } sync.unlock(); } } @Override public void stop() { sync.lock(); try { if (session == null) { return; } saveSettings(); session.abort(); session = null; } finally { sync.unlock(); } } @Override public void restart() { sync.lock(); try { stop(); /* Allow some time to release native resources */ Thread.sleep(1000); start(); } catch (InterruptedException e) { /* Ignore */ } finally { sync.unlock(); } } @Override public void pause() { if (session != null && !session.isPaused()) { session.pause(); } } @Override public void resume() { if (session != null) { session.resume(); } } private void setListener() { if (session == null) { return; } session.addListener(new AlertListener() { @Override public int[] types() { return new int[] { AlertType.ADD_TORRENT.swig() }; } @Override public void alert(Alert<?> alert) { switch (alert.type()) { case ADD_TORRENT: TorrentAlert<?> torrentAlert = (TorrentAlert<?>) alert; Sha1Hash sha1hash = torrentAlert.handle().getInfoHash(); Torrent torrent = addTorrentsQueue.get(sha1hash.toString()); if (torrent != null) { TorrentHandle handle = session.findTorrent(sha1hash); handle.setSequentialDownload(torrent.isSequentialDownload()); if (torrent.isPaused()) { handle.pause(); } else { handle.resume(); } TorrentDownload task = new TorrentDownload(context, TorrentEngine.this, handle, torrent, callback); if (callback != null) { callback.onTorrentAdded(torrent.getId(), task); } } runNextLoadTorrentTask(); break; } } }); } @Override public void asyncDownload(Collection<Torrent> torrents) { if (session == null || torrents == null) { return; } for (Torrent torrent : torrents) { TorrentInfo ti = new TorrentInfo(new File(torrent.getTorrentFilePath())); Priority[] priorities = new Priority[ti.numFiles()]; List<Integer> torrentPriorities = torrent.getFilePriorities(); if (torrentPriorities.size() != ti.numFiles()) { continue; } for (int i = 0; i < torrentPriorities.size(); i++) { priorities[i] = Priority.fromSwig(torrentPriorities.get(i)); } File saveDir = new File(torrent.getDownloadPath()); String dataDir = TorrentUtils.findTorrentDataDir(context, torrent.getId()); File resumeFile = null; if (dataDir != null) { File file = new File(dataDir, TorrentStorage.Model.DATA_TORRENT_RESUME_FILE_NAME); if (file.exists()) { resumeFile = file; } } addTorrentsQueue.put(ti.infoHash().toString(), torrent); loadTorrentsQueue.add( new LoadTorrentTask(new File(torrent.getTorrentFilePath()), saveDir, resumeFile, priorities)); } runNextLoadTorrentTask(); } private void runNextLoadTorrentTask() { LoadTorrentTask task; try { task = loadTorrentsQueue.poll(); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); return; } if (task != null) { loadTorrentsExec.execute(task); } } @Override public Session getSession() { return session; } private void prioritizeFiles(TorrentHandle handle, Priority[] priorities) { if (handle == null) { return; } if (priorities != null) { /* Priorities for all files, priorities list for some selected files not supported */ if (handle.getTorrentInfo().numFiles() != priorities.length) { return; } handle.prioritizeFiles(priorities); } else { /* Did they just add the entire torrent (therefore not selecting any priorities) */ final Priority[] wholeTorrentPriorities = Priority.array(Priority.NORMAL, handle.getTorrentInfo().numFiles()); handle.prioritizeFiles(wholeTorrentPriorities); } } @Override public TorrentDownload download(Torrent torrent) { TorrentInfo ti = new TorrentInfo(new File(torrent.getTorrentFilePath())); Priority[] priorities = new Priority[ti.numFiles()]; List<Integer> torrentPriorities = torrent.getFilePriorities(); if (torrentPriorities.size() != ti.numFiles()) { return null; } for (int i = 0; i < torrentPriorities.size(); i++) { priorities[i] = Priority.fromSwig(torrentPriorities.get(i)); } TorrentHandle handle = session.findTorrent(ti.infoHash()); if (handle == null) { File saveDir = new File(torrent.getDownloadPath()); String dataDir = TorrentUtils.findTorrentDataDir(context, torrent.getId()); File resumeFile = null; if (dataDir != null) { File file = new File(dataDir, TorrentStorage.Model.DATA_TORRENT_RESUME_FILE_NAME); if (file.exists()) { resumeFile = file; } } /* New download */ handle = session.addTorrent(ti, saveDir, priorities, resumeFile); handle.setSequentialDownload(torrent.isSequentialDownload()); if (torrent.isPaused()) { handle.pause(); } else { handle.resume(); } return new TorrentDownload(context, this, handle, torrent, callback); } else { /* Found a download with the same hash, just adjust the priorities if needed */ prioritizeFiles(handle, priorities); } return null; } @Override public void saveSettings() { if (session == null) { return; } try { TorrentUtils.saveSession(context, session.saveState()); } catch (Exception e) { Log.e(TAG, "Error saving session state: "); Log.e(TAG, Log.getStackTraceString(e)); } } @Override public void loadSettings() { if (session == null) { return; } try { String sessionPath = TorrentUtils.findSessionFile(context); if (sessionPath == null) { return; } File sessionFile = new File(sessionPath); if (sessionFile.exists()) { byte[] data = FileUtils.readFileToByteArray(sessionFile); session.loadState(data); } else { revertToDefaultConfiguration(); } } catch (Exception e) { Log.e(TAG, "Error loading session state: "); Log.e(TAG, Log.getStackTraceString(e)); } } @Override public void revertToDefaultConfiguration() { if (session == null) { return; } SettingsPack sp = session.getSettingsPack(); sp.broadcastLSD(true); int maxQueuedDiskBytes = sp.maxQueuedDiskBytes(); sp.setMaxQueuedDiskBytes(maxQueuedDiskBytes / 2); int sendBufferWatermark = sp.sendBufferWatermark(); sp.setSendBufferWatermark(sendBufferWatermark / 2); sp.setCacheSize(DEFAULT_CACHE_SIZE); sp.activeDownloads(DEFAULT_ACTIVE_DOWNLOADS); sp.activeSeeds(DEFAULT_ACTIVE_SEEDS); sp.activeLimit(DEFAULT_ACTIVE_LIMIT); sp.setMaxPeerlistSize(DEFAULT_MAX_PEER_LIST_SIZE); sp.setGuidedReadCache(true); sp.setTickInterval(DEFAULT_TICK_INTERVAL); sp.setInactivityTimeout(DEFAULT_INACTIVITY_TIMEOUT); sp.setSeedingOutgoingConnections(false); sp.setConnectionsLimit(DEFAULT_CONNECTIONS_LIMIT); setSettings(sp); } @Override public void setSettings(SettingsPack sp) { if (sp == null || session == null) { return; } session.applySettings(sp); saveSettings(); } @Override public SettingsPack getSettings() { if (session == null) { return null; } return session.getSettingsPack(); } @Override public void setDownloadSpeedLimit(int limit) { if (session == null) { return; } SettingsPack settingsPack = session.getSettingsPack(); settingsPack.setDownloadRateLimit(limit); setSettings(settingsPack); } @Override public int getDownloadSpeedLimit() { if (session == null) { return 0; } return session.getSettingsPack().downloadRateLimit(); } @Override public void setUploadSpeedLimit(int limit) { if (session == null) { return; } SettingsPack settingsPack = session.getSettingsPack(); settingsPack.setUploadRateLimit(limit); session.applySettings(settingsPack); setSettings(settingsPack); } @Override public int getUploadSpeedLimit() { if (session == null) { return 0; } return session.getSettingsPack().uploadRateLimit(); } @Override public long getDownloadRate() { if (session == null) { return 0; } return session.getStats().downloadRate(); } @Override public long getUploadRate() { if (session == null) { return 0; } return session.getStats().uploadRate(); } @Override public long getTotalDownload() { if (session == null) { return 0; } return session.getStats().download(); } @Override public long getTotalUpload() { if (session == null) { return 0; } return session.getStats().upload(); } @Override public int getDownloadRateLimit() { if (session == null) { return 0; } return session.getSettingsPack().downloadRateLimit(); } @Override public int getUploadRateLimit() { if (session == null) { return 0; } return session.getSettingsPack().uploadRateLimit(); } @Override public int getPort() { if (session == null) { return -1; } return session.getListenPort(); } @Override public void setPort(int port) { if (session == null || port == -1) { return; } SettingsPack sp = session.getSettingsPack(); sp.setString(settings_pack.string_types.listen_interfaces.swigValue(), "0.0.0.0:" + port); setSettings(sp); } @Override public void setRandomPort() { int randomPort = MIN_PORT_NUMBER + (int) (Math.random() * ((MAX_PORT_NUMBER - MIN_PORT_NUMBER) + 1)); setPort(randomPort); } @Override public void enableIpFilter(String path) { if (path == null) { return; } IPFilterParser parser = new IPFilterParser(path); parser.setOnParsedListener(new IPFilterParser.OnParsedListener() { @Override public void onParsed(ip_filter filter, boolean success) { if (success && session != null) { session.swig().set_ip_filter(filter); } if (callback != null) { callback.onIpFilterParsed(success); } } }); parser.parse(); } @Override public void disableIpFilter() { if (session == null) { return; } session.swig().set_ip_filter(new ip_filter()); } @Override public void setProxy(Context context, ProxySettingsPack proxy) { if (context == null || proxy == null || session == null) { return; } SettingsPack sp = session.getSettingsPack(); settings_pack.proxy_type_t type = settings_pack.proxy_type_t.none; switch (proxy.getType()) { case SOCKS4: type = settings_pack.proxy_type_t.socks4; break; case SOCKS5: type = (TextUtils.isEmpty(proxy.getAddress()) ? settings_pack.proxy_type_t.socks5 : settings_pack.proxy_type_t.socks5_pw); break; case HTTP: type = (TextUtils.isEmpty(proxy.getAddress()) ? settings_pack.proxy_type_t.http : settings_pack.proxy_type_t.http_pw); break; } sp.setInteger(settings_pack.int_types.proxy_type.swigValue(), type.swigValue()); sp.setInteger(settings_pack.int_types.proxy_port.swigValue(), proxy.getPort()); sp.setString(settings_pack.string_types.proxy_hostname.swigValue(), proxy.getAddress()); sp.setString(settings_pack.string_types.proxy_username.swigValue(), proxy.getLogin()); sp.setString(settings_pack.string_types.proxy_password.swigValue(), proxy.getPassword()); sp.setBoolean(settings_pack.bool_types.proxy_peer_connections.swigValue(), proxy.isProxyPeersToo()); if (proxy.getType() != ProxySettingsPack.ProxyType.NONE) { sp.setBoolean(settings_pack.bool_types.force_proxy.swigValue(), proxy.isForceProxy()); } else { sp.setBoolean(settings_pack.bool_types.force_proxy.swigValue(), false); } setSettings(sp); } @Override public ProxySettingsPack getProxy() { ProxySettingsPack proxy = new ProxySettingsPack(); SettingsPack sp = session.getSettingsPack(); ProxySettingsPack.ProxyType type; String swigType = sp.getString(settings_pack.int_types.proxy_type.swigValue()); if (swigType.equals(settings_pack.proxy_type_t.none.toString())) { type = ProxySettingsPack.ProxyType.NONE; } else if (swigType.equals(settings_pack.proxy_type_t.socks4.toString())) { type = ProxySettingsPack.ProxyType.SOCKS4; } else if (swigType.equals(settings_pack.proxy_type_t.socks5.toString())) { type = ProxySettingsPack.ProxyType.SOCKS5; } else if (swigType.equals(settings_pack.proxy_type_t.http.toString())) { type = ProxySettingsPack.ProxyType.HTTP; } else { type = ProxySettingsPack.ProxyType.TOR; } proxy.setType(type); proxy.setPort(sp.getInteger(settings_pack.int_types.proxy_port.swigValue())); proxy.setAddress(sp.getString(settings_pack.string_types.proxy_hostname.swigValue())); proxy.setLogin(sp.getString(settings_pack.string_types.proxy_username.swigValue())); proxy.setPassword(sp.getString(settings_pack.string_types.proxy_password.swigValue())); proxy.setProxyPeersToo(sp.getBoolean(settings_pack.bool_types.proxy_peer_connections.swigValue())); proxy.setForceProxy(sp.getBoolean(settings_pack.bool_types.force_proxy.swigValue())); return proxy; } @Override public void disableProxy(Context context) { setProxy(context, new ProxySettingsPack()); } @Override public boolean isListening() { return session != null && session.isListening(); } @Override public boolean isStarted() { return session != null; } public boolean isPaused() { return session != null && session.isPaused(); } @Override public boolean isDHTEnabled() { return session != null && session.getSettingsPack().enableDht(); } @Override public boolean isPeXEnabled() { /* PeX enabled by default in session_handle.session_flags_t::add_default_plugins */ return session != null; } @Override public boolean isLSDEnabled() { return session != null && session.getSettingsPack().broadcastLSD(); } private final class LoadTorrentTask implements Runnable { private final File torrent; private final File saveDir; private final File resume; private final Priority[] priorities; LoadTorrentTask(File torrent, File saveDir, File resume, Priority[] priorities) { this.torrent = torrent; this.saveDir = saveDir; this.resume = resume; this.priorities = priorities; } @Override public void run() { try { session.asyncAddTorrent(new TorrentInfo(torrent), saveDir, priorities, resume); } catch (Throwable e) { Log.e(TAG, "Unable to restore torrent from previous session. (" + torrent.getAbsolutePath() + "): ", e); } } } }