parent
e475348d39
commit
5c27292543
|
@ -160,6 +160,7 @@ dependencies {
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.browser:browser:1.3.0'
|
implementation 'androidx.browser:browser:1.3.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
|
implementation project(path: ':torrentStream')
|
||||||
testImplementation 'junit:junit:4.13.1'
|
testImplementation 'junit:junit:4.13.1'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
|
@ -181,7 +182,6 @@ dependencies {
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||||
implementation 'com.github.mancj:MaterialSearchBar:0.8.5'
|
implementation 'com.github.mancj:MaterialSearchBar:0.8.5'
|
||||||
|
|
||||||
implementation "com.github.TorrentStream:TorrentStream-Android:2.7.0"
|
|
||||||
|
|
||||||
implementation "io.github.kobakei:ratethisapp:1.2.0"
|
implementation "io.github.kobakei:ratethisapp:1.2.0"
|
||||||
implementation 'com.github.vkay94:DoubleTapPlayerView:1.0.0'
|
implementation 'com.github.vkay94:DoubleTapPlayerView:1.0.0'
|
||||||
|
|
|
@ -75,6 +75,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.frostwire.jlibtorrent.SessionManager;
|
||||||
import com.github.se_bastiaan.torrentstream.StreamStatus;
|
import com.github.se_bastiaan.torrentstream.StreamStatus;
|
||||||
import com.github.se_bastiaan.torrentstream.Torrent;
|
import com.github.se_bastiaan.torrentstream.Torrent;
|
||||||
import com.github.se_bastiaan.torrentstream.TorrentOptions;
|
import com.github.se_bastiaan.torrentstream.TorrentOptions;
|
||||||
|
@ -119,6 +120,8 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -239,7 +242,8 @@ public class PeertubeActivity extends AppCompatActivity implements CommentListAd
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStreamStarted(Torrent torrent) {
|
public void onStreamStarted(Torrent torrent) {
|
||||||
startStream(peertube, torrent.getVideoFile().getAbsolutePath(), null, autoPlay, -1, null, null, true);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -249,11 +253,44 @@ public class PeertubeActivity extends AppCompatActivity implements CommentListAd
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStreamReady(Torrent torrent) {
|
public void onStreamReady(Torrent torrent) {
|
||||||
|
if (torrent.getVideoFile() != null) {
|
||||||
|
for (int i = 0; i < torrent.getFileNames().length; i++) {
|
||||||
|
torrent.getTorrentHandle().renameFile(i, torrent.getFileNames()[0].replaceAll("[^a-zA-Z0-9/.-]", "_"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startStream(peertube, torrent.getVideoFile().getAbsolutePath().replaceAll("[^a-zA-Z0-9/.-]", "_"), null, autoPlay, -1, null, null, true);
|
||||||
|
PlayerControlView controlView = binding.doubleTapPlayerView.findViewById(R.id.exo_controller);
|
||||||
|
ConstraintLayout torrent_info = controlView.findViewById(R.id.torrent_info);
|
||||||
|
TextView dowload_rate = controlView.findViewById(R.id.dowload_rate);
|
||||||
|
TextView upload_rate = controlView.findViewById(R.id.upload_rate);
|
||||||
|
torrent_info.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
new Timer().scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SessionManager sessionManager = torrentStream.getSessionManager();
|
||||||
|
if (sessionManager != null) {
|
||||||
|
long upload = sessionManager.uploadRate();
|
||||||
|
long download = sessionManager.downloadRate();
|
||||||
|
int seeds = sessionManager.maxActiveSeeds();
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
dowload_rate.setText(String.format("▼ %s", Helper.rateSize(PeertubeActivity.this, download)));
|
||||||
|
upload_rate.setText(String.format("▲ %s", Helper.rateSize(PeertubeActivity.this, upload)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0, 1000);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStreamProgress(Torrent torrent, StreamStatus status) {
|
public void onStreamProgress(Torrent torrent, StreamStatus status) {
|
||||||
|
if (binding != null) {
|
||||||
|
PlayerControlView controlView = binding.doubleTapPlayerView.findViewById(R.id.exo_controller);
|
||||||
|
TextView peers_number = controlView.findViewById(R.id.peers_number);
|
||||||
|
peers_number.setText(getString(R.string.peers, status.peers));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -278,7 +315,7 @@ public class PeertubeActivity extends AppCompatActivity implements CommentListAd
|
||||||
}
|
}
|
||||||
isRemote = false;
|
isRemote = false;
|
||||||
TorrentOptions torrentOptions = new TorrentOptions.Builder()
|
TorrentOptions torrentOptions = new TorrentOptions.Builder()
|
||||||
.saveLocation(getCacheDir())
|
.saveLocation(getCacheDir() + "/torrent/")
|
||||||
.autoDownload(true)
|
.autoDownload(true)
|
||||||
.removeFilesAfterStop(true)
|
.removeFilesAfterStop(true)
|
||||||
.build();
|
.build();
|
||||||
|
@ -1344,14 +1381,16 @@ public class PeertubeActivity extends AppCompatActivity implements CommentListAd
|
||||||
SingleSampleMediaSource subtitleSource = null;
|
SingleSampleMediaSource subtitleSource = null;
|
||||||
DataSource.Factory dataSourceFactory = null;
|
DataSource.Factory dataSourceFactory = null;
|
||||||
if (localTorrentUrl != null) {
|
if (localTorrentUrl != null) {
|
||||||
|
|
||||||
|
java.io.File localFile = new java.io.File(localTorrentUrl);
|
||||||
DataSpec dataSpec = new DataSpec(Uri.fromFile(new java.io.File(localTorrentUrl)));
|
DataSpec dataSpec = new DataSpec(Uri.fromFile(new java.io.File(localTorrentUrl)));
|
||||||
FileDataSource fileDataSource = new FileDataSource();
|
FileDataSource fileDataSource = new FileDataSource();
|
||||||
try {
|
try {
|
||||||
fileDataSource.open(dataSpec);
|
fileDataSource.open(dataSpec);
|
||||||
|
dataSourceFactory = () -> fileDataSource;
|
||||||
} catch (FileDataSource.FileDataSourceException e) {
|
} catch (FileDataSource.FileDataSourceException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
dataSourceFactory = () -> fileDataSource;
|
|
||||||
}
|
}
|
||||||
if (video_cache == 0 || dataSourceFactory != null) {
|
if (video_cache == 0 || dataSourceFactory != null) {
|
||||||
if (dataSourceFactory == null) {
|
if (dataSourceFactory == null) {
|
||||||
|
|
|
@ -628,4 +628,25 @@ public class Helper {
|
||||||
return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.mb));
|
return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.mb));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String rateSize(Context context, long size) {
|
||||||
|
if (size > 1000000000) {
|
||||||
|
float rounded = (float) size / 1000000000;
|
||||||
|
DecimalFormat df = new DecimalFormat("#.#");
|
||||||
|
return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.gb));
|
||||||
|
} else if (size > 1000000) {
|
||||||
|
float rounded = (float) size / 1000000;
|
||||||
|
DecimalFormat df = new DecimalFormat("#.#");
|
||||||
|
return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.mb));
|
||||||
|
} else if (size > 1000) {
|
||||||
|
float rounded = (float) size / 1000000;
|
||||||
|
DecimalFormat df = new DecimalFormat("#.#");
|
||||||
|
return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.kb));
|
||||||
|
} else {
|
||||||
|
float rounded = (float) size / 1000000;
|
||||||
|
DecimalFormat df = new DecimalFormat("#.#");
|
||||||
|
return String.format(Locale.getDefault(), "%s%s", df.format(rounded), context.getString(R.string.b));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,44 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/torrent_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/seekbar_controller"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dowload_rate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:textColor="#FFBEBEBE"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/upload_rate"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/upload_rate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="5dp"
|
||||||
|
android:textColor="#FFBEBEBE"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/peers_number"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/peers_number"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:textColor="#FFBEBEBE"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/seekbar_controller"
|
android:id="@+id/seekbar_controller"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|
|
@ -170,6 +170,10 @@
|
||||||
<string name="action_mute">Mute</string>
|
<string name="action_mute">Mute</string>
|
||||||
|
|
||||||
<string name="unlimited">Unlimited</string>
|
<string name="unlimited">Unlimited</string>
|
||||||
|
|
||||||
|
<string name="peers">%1$d Peers</string>
|
||||||
|
<string name="b">B</string>
|
||||||
|
<string name="kb">KB</string>
|
||||||
<string name="mb">MB</string>
|
<string name="mb">MB</string>
|
||||||
<string name="gb">GB</string>
|
<string name="gb">GB</string>
|
||||||
<string name="total_video_quota">Total video quota</string>
|
<string name="total_video_quota">Total video quota</string>
|
||||||
|
@ -449,4 +453,5 @@
|
||||||
<string name="instance_not_availabe">Instance is not available!</string>
|
<string name="instance_not_availabe">Instance is not available!</string>
|
||||||
<string name="max_tag_size">The video should not have more than 5 tags!</string>
|
<string name="max_tag_size">The video should not have more than 5 tags!</string>
|
||||||
<string name="watermark">Watermark</string>
|
<string name="watermark">Watermark</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -9,6 +9,8 @@ buildscript {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||||
def nav_version = "2.3.0"
|
def nav_version = "2.3.0"
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||||
|
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
|
||||||
|
classpath 'de.undercouch:gradle-download-task:4.0.4'
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
|
include ':torrentStream'
|
||||||
include ':app'
|
include ':app'
|
||||||
rootProject.name = "Fedilab Tube"
|
rootProject.name = "Fedilab Tube"
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'com.github.dcendents.android-maven'
|
||||||
|
apply plugin: 'de.undercouch.download'
|
||||||
|
|
||||||
|
group='com.github.TorrentStream'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 29
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 15
|
||||||
|
targetSdkVersion 29
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
libtorrentVersion = '1.2.11.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom task which downloads the appropriate version of JAR files for jlibtorrent
|
||||||
|
task downloadDependencies(type: Download) {
|
||||||
|
def baseUrl = "https://github.com/frostwire/frostwire-jlibtorrent" +
|
||||||
|
"/releases/download/release%2F$libtorrentVersion"
|
||||||
|
|
||||||
|
def platforms = ['arm', 'arm64', 'x86', 'x86_64']
|
||||||
|
def urls = platforms.collect { "$baseUrl/jlibtorrent-android-$it-${libtorrentVersion}.jar" }
|
||||||
|
urls.add("$baseUrl/jlibtorrent-${libtorrentVersion}.jar")
|
||||||
|
|
||||||
|
src urls
|
||||||
|
dest 'libs'
|
||||||
|
overwrite false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add our custom task as a dependency to the build
|
||||||
|
// You may need to run gradle sync for IDE warnings to disappear
|
||||||
|
preBuild.dependsOn(downloadDependencies)
|
||||||
|
|
||||||
|
// Add deletion of libs folder to clean task
|
||||||
|
clean {
|
||||||
|
delete 'libs'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,17 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in /Users/Sebastiaan/Development/Android/SDK/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<manifest package="com.github.se_bastiaan.torrentstream">
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream;
|
||||||
|
|
||||||
|
public class StreamStatus {
|
||||||
|
public final float progress;
|
||||||
|
public final int bufferProgress;
|
||||||
|
public final int seeds;
|
||||||
|
public final int peers;
|
||||||
|
public final int downloadSpeed;
|
||||||
|
|
||||||
|
StreamStatus(float progress, int bufferProgress, int seeds, int peers, int downloadSpeed) {
|
||||||
|
this.progress = progress;
|
||||||
|
this.bufferProgress = bufferProgress;
|
||||||
|
this.seeds = seeds;
|
||||||
|
this.peers = peers;
|
||||||
|
this.downloadSpeed = downloadSpeed;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,527 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream;
|
||||||
|
|
||||||
|
import com.frostwire.jlibtorrent.AlertListener;
|
||||||
|
import com.frostwire.jlibtorrent.FileStorage;
|
||||||
|
import com.frostwire.jlibtorrent.Priority;
|
||||||
|
import com.frostwire.jlibtorrent.TorrentFlags;
|
||||||
|
import com.frostwire.jlibtorrent.TorrentHandle;
|
||||||
|
import com.frostwire.jlibtorrent.TorrentInfo;
|
||||||
|
import com.frostwire.jlibtorrent.TorrentStatus;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.Alert;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.AlertType;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.BlockFinishedAlert;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.PieceFinishedAlert;
|
||||||
|
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Torrent implements AlertListener {
|
||||||
|
|
||||||
|
private final static Integer MAX_PREPARE_COUNT = 20;
|
||||||
|
private final static Integer MIN_PREPARE_COUNT = 2;
|
||||||
|
private final static Integer DEFAULT_PREPARE_COUNT = 5;
|
||||||
|
private final static Integer SEQUENTIAL_CONCURRENT_PIECES_COUNT = 5;
|
||||||
|
private final List<WeakReference<TorrentInputStream>> torrentStreamReferences;
|
||||||
|
private final TorrentHandle torrentHandle;
|
||||||
|
private final TorrentListener listener;
|
||||||
|
private final Long prepareSize;
|
||||||
|
private Integer piecesToPrepare;
|
||||||
|
private Integer lastPieceIndex;
|
||||||
|
private Integer firstPieceIndex;
|
||||||
|
private Integer selectedFileIndex = -1;
|
||||||
|
private Integer interestedPieceIndex = 0;
|
||||||
|
private Double prepareProgress = 0d;
|
||||||
|
private Double progressStep = 0d;
|
||||||
|
private List<Integer> preparePieces;
|
||||||
|
private Boolean[] hasPieces;
|
||||||
|
private State state = State.RETRIEVING_META;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The constructor for a new Torrent
|
||||||
|
* <p/>
|
||||||
|
* First the largest file in the download is selected as the file for playback
|
||||||
|
* <p/>
|
||||||
|
* After setting this priority, the first and last index of the pieces that make up this file are determined.
|
||||||
|
* And last: amount of pieces that are needed for playback are calculated (needed for playback means: make up 10 megabyte of the file)
|
||||||
|
*
|
||||||
|
* @param torrentHandle jlibtorrent TorrentHandle
|
||||||
|
*/
|
||||||
|
public Torrent(TorrentHandle torrentHandle, TorrentListener listener, Long prepareSize) {
|
||||||
|
this.torrentHandle = torrentHandle;
|
||||||
|
this.listener = listener;
|
||||||
|
|
||||||
|
this.prepareSize = prepareSize;
|
||||||
|
|
||||||
|
torrentStreamReferences = new ArrayList<>();
|
||||||
|
|
||||||
|
if (selectedFileIndex == -1) {
|
||||||
|
setLargestFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.listener != null) {
|
||||||
|
this.listener.onStreamPrepared(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset piece priorities of selected file to normal
|
||||||
|
*/
|
||||||
|
private void resetPriorities() {
|
||||||
|
Priority[] priorities = torrentHandle.piecePriorities();
|
||||||
|
for (int i = 0; i < priorities.length; i++) {
|
||||||
|
if (i >= firstPieceIndex && i <= lastPieceIndex) {
|
||||||
|
torrentHandle.piecePriority(i, Priority.NORMAL);
|
||||||
|
} else {
|
||||||
|
torrentHandle.piecePriority(i, Priority.IGNORE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get LibTorrent torrent handle of this torrent
|
||||||
|
*
|
||||||
|
* @return {@link TorrentHandle}
|
||||||
|
*/
|
||||||
|
public TorrentHandle getTorrentHandle() {
|
||||||
|
return torrentHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getVideoFile() {
|
||||||
|
|
||||||
|
return new File(torrentHandle.savePath() + "/" + torrentHandle.torrentFile().files().filePath(selectedFileIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an InputStream for the video file.
|
||||||
|
* Read is be blocked until the requested piece(s) is downloaded.
|
||||||
|
*
|
||||||
|
* @return {@link InputStream}
|
||||||
|
*/
|
||||||
|
public InputStream getVideoStream() throws FileNotFoundException {
|
||||||
|
File file = getVideoFile();
|
||||||
|
TorrentInputStream inputStream = new TorrentInputStream(this, new FileInputStream(file));
|
||||||
|
torrentStreamReferences.add(new WeakReference<>(inputStream));
|
||||||
|
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the location of the file that is being downloaded
|
||||||
|
*
|
||||||
|
* @return {@link File} The file location
|
||||||
|
*/
|
||||||
|
public File getSaveLocation() {
|
||||||
|
return new File(torrentHandle.savePath() + "/" + torrentHandle.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume the torrent download
|
||||||
|
*/
|
||||||
|
public void resume() {
|
||||||
|
torrentHandle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the torrent download
|
||||||
|
*/
|
||||||
|
public void pause() {
|
||||||
|
torrentHandle.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the selected file index to the largest file in the torrent
|
||||||
|
*/
|
||||||
|
public void setLargestFile() {
|
||||||
|
setSelectedFileIndex(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the index of the file that should be downloaded
|
||||||
|
* If the given index is -1, then the largest file is chosen
|
||||||
|
*
|
||||||
|
* @param selectedFileIndex {@link Integer} Index of the file
|
||||||
|
*/
|
||||||
|
public void setSelectedFileIndex(Integer selectedFileIndex) {
|
||||||
|
TorrentInfo torrentInfo = torrentHandle.torrentFile();
|
||||||
|
FileStorage fileStorage = torrentInfo.files();
|
||||||
|
|
||||||
|
if (selectedFileIndex == -1) {
|
||||||
|
long highestFileSize = 0;
|
||||||
|
int selectedFile = -1;
|
||||||
|
for (int i = 0; i < fileStorage.numFiles(); i++) {
|
||||||
|
long fileSize = fileStorage.fileSize(i);
|
||||||
|
if (highestFileSize < fileSize) {
|
||||||
|
highestFileSize = fileSize;
|
||||||
|
torrentHandle.filePriority(selectedFile, Priority.IGNORE);
|
||||||
|
selectedFile = i;
|
||||||
|
torrentHandle.filePriority(i, Priority.NORMAL);
|
||||||
|
} else {
|
||||||
|
torrentHandle.filePriority(i, Priority.IGNORE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedFileIndex = selectedFile;
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < fileStorage.numFiles(); i++) {
|
||||||
|
if (i == selectedFileIndex) {
|
||||||
|
torrentHandle.filePriority(i, Priority.NORMAL);
|
||||||
|
} else {
|
||||||
|
torrentHandle.filePriority(i, Priority.IGNORE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.selectedFileIndex = selectedFileIndex;
|
||||||
|
|
||||||
|
Priority[] piecePriorities = torrentHandle.piecePriorities();
|
||||||
|
int firstPieceIndexLocal = -1;
|
||||||
|
int lastPieceIndexLocal = -1;
|
||||||
|
for (int i = 0; i < piecePriorities.length; i++) {
|
||||||
|
if (piecePriorities[i] != Priority.IGNORE) {
|
||||||
|
if (firstPieceIndexLocal == -1) {
|
||||||
|
firstPieceIndexLocal = i;
|
||||||
|
}
|
||||||
|
piecePriorities[i] = Priority.IGNORE;
|
||||||
|
} else {
|
||||||
|
if (firstPieceIndexLocal != -1 && lastPieceIndexLocal == -1) {
|
||||||
|
lastPieceIndexLocal = i - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastPieceIndexLocal == -1) {
|
||||||
|
lastPieceIndexLocal = piecePriorities.length - 1;
|
||||||
|
}
|
||||||
|
int pieceCount = lastPieceIndexLocal - firstPieceIndexLocal + 1;
|
||||||
|
int pieceLength = torrentHandle.torrentFile().pieceLength();
|
||||||
|
int activePieceCount;
|
||||||
|
if (pieceLength > 0) {
|
||||||
|
activePieceCount = (int) (prepareSize / pieceLength);
|
||||||
|
if (activePieceCount < MIN_PREPARE_COUNT) {
|
||||||
|
activePieceCount = MIN_PREPARE_COUNT;
|
||||||
|
} else if (activePieceCount > MAX_PREPARE_COUNT) {
|
||||||
|
activePieceCount = MAX_PREPARE_COUNT;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activePieceCount = DEFAULT_PREPARE_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pieceCount < activePieceCount) {
|
||||||
|
activePieceCount = pieceCount / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.firstPieceIndex = firstPieceIndexLocal;
|
||||||
|
this.interestedPieceIndex = this.firstPieceIndex;
|
||||||
|
this.lastPieceIndex = lastPieceIndexLocal;
|
||||||
|
piecesToPrepare = activePieceCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filenames of the files in the torrent
|
||||||
|
*
|
||||||
|
* @return {@link String[]}
|
||||||
|
*/
|
||||||
|
public String[] getFileNames() {
|
||||||
|
FileStorage fileStorage = torrentHandle.torrentFile().files();
|
||||||
|
String[] fileNames = new String[fileStorage.numFiles()];
|
||||||
|
for (int i = 0; i < fileStorage.numFiles(); i++) {
|
||||||
|
fileNames[i] = fileStorage.fileName(i);
|
||||||
|
}
|
||||||
|
return fileNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare torrent for playback. Prioritize the first {@code piecesToPrepare} pieces and the last {@code piecesToPrepare} pieces
|
||||||
|
* from {@code firstPieceIndex} and {@code lastPieceIndex}. Ignore all other pieces.
|
||||||
|
*/
|
||||||
|
public void startDownload() {
|
||||||
|
if (state == State.STREAMING || state == State.STARTING) return;
|
||||||
|
state = State.STARTING;
|
||||||
|
|
||||||
|
List<Integer> indices = new ArrayList<>();
|
||||||
|
|
||||||
|
Priority[] priorities = torrentHandle.piecePriorities();
|
||||||
|
for (int i = 0; i < priorities.length; i++) {
|
||||||
|
if (priorities[i] != Priority.IGNORE) {
|
||||||
|
torrentHandle.piecePriority(i, Priority.NORMAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < piecesToPrepare; i++) {
|
||||||
|
indices.add(lastPieceIndex - i);
|
||||||
|
torrentHandle.piecePriority(lastPieceIndex - i, Priority.SEVEN);
|
||||||
|
torrentHandle.setPieceDeadline(lastPieceIndex - i, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < piecesToPrepare; i++) {
|
||||||
|
indices.add(firstPieceIndex + i);
|
||||||
|
torrentHandle.piecePriority(firstPieceIndex + i, Priority.SEVEN);
|
||||||
|
torrentHandle.setPieceDeadline(firstPieceIndex + i, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
preparePieces = indices;
|
||||||
|
|
||||||
|
hasPieces = new Boolean[lastPieceIndex - firstPieceIndex + 1];
|
||||||
|
Arrays.fill(hasPieces, false);
|
||||||
|
|
||||||
|
TorrentInfo torrentInfo = torrentHandle.torrentFile();
|
||||||
|
TorrentStatus status = torrentHandle.status();
|
||||||
|
|
||||||
|
double blockCount = indices.size() * torrentInfo.pieceLength() / status.blockSize();
|
||||||
|
|
||||||
|
progressStep = 100 / blockCount;
|
||||||
|
|
||||||
|
torrentStreamReferences.clear();
|
||||||
|
|
||||||
|
torrentHandle.resume();
|
||||||
|
|
||||||
|
listener.onStreamStarted(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the piece that contains the specified bytes were downloaded already
|
||||||
|
*
|
||||||
|
* @param bytes The bytes you're interested in
|
||||||
|
* @return {@code true} if downloaded, {@code false} if not
|
||||||
|
*/
|
||||||
|
public boolean hasBytes(long bytes) {
|
||||||
|
if (hasPieces == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pieceIndex = (int) (bytes / torrentHandle.torrentFile().pieceLength());
|
||||||
|
return hasPieces[pieceIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the bytes of the selected file that you're interested in
|
||||||
|
* The piece of that specific offset is selected and that piece plus the 1 preceding and the 3 after it.
|
||||||
|
* These pieces will then be prioritised, which results in continuing the sequential download after that piece
|
||||||
|
*
|
||||||
|
* @param bytes The bytes you're interested in
|
||||||
|
*/
|
||||||
|
public void setInterestedBytes(long bytes) {
|
||||||
|
if (hasPieces == null && bytes >= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pieceIndex = (int) (bytes / torrentHandle.torrentFile().pieceLength());
|
||||||
|
interestedPieceIndex = pieceIndex;
|
||||||
|
if (!hasPieces[pieceIndex] && torrentHandle.piecePriority(pieceIndex + firstPieceIndex) != Priority.SEVEN) {
|
||||||
|
interestedPieceIndex = pieceIndex;
|
||||||
|
int pieces = 5;
|
||||||
|
for (int i = pieceIndex; i < hasPieces.length; i++) {
|
||||||
|
// Set full priority to first found piece that is not confirmed finished
|
||||||
|
if (!hasPieces[i]) {
|
||||||
|
torrentHandle.piecePriority(i + firstPieceIndex, Priority.SEVEN);
|
||||||
|
torrentHandle.setPieceDeadline(i + firstPieceIndex, 1000);
|
||||||
|
pieces--;
|
||||||
|
if (pieces == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the interesting pieces are downloaded already
|
||||||
|
*
|
||||||
|
* @return {@code true} if the 5 pieces that were selected using `setInterestedBytes` are all reported complete including the `nextPieces`, {@code false} if not
|
||||||
|
*/
|
||||||
|
public boolean hasInterestedBytes(int nextPieces) {
|
||||||
|
for (int i = 0; i < 5 + nextPieces; i++) {
|
||||||
|
int index = interestedPieceIndex + i;
|
||||||
|
if (hasPieces.length <= index || index < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPieces[interestedPieceIndex + i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the interesting pieces are downloaded already
|
||||||
|
*
|
||||||
|
* @return {@code true} if the 5 pieces that were selected using `setInterestedBytes` are all reported complete, {@code false} if not
|
||||||
|
*/
|
||||||
|
public boolean hasInterestedBytes() {
|
||||||
|
return hasInterestedBytes(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the piece we're currently interested in
|
||||||
|
*
|
||||||
|
* @return Interested piece index
|
||||||
|
*/
|
||||||
|
public int getInterestedPieceIndex() {
|
||||||
|
return interestedPieceIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get amount of pieces to prepare
|
||||||
|
*
|
||||||
|
* @return Amount of pieces to prepare
|
||||||
|
*/
|
||||||
|
public Integer getPiecesToPrepare() {
|
||||||
|
return piecesToPrepare;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start sequential mode downloading
|
||||||
|
*/
|
||||||
|
private void startSequentialMode() {
|
||||||
|
resetPriorities();
|
||||||
|
|
||||||
|
if (hasPieces == null) {
|
||||||
|
torrentHandle.setFlags(torrentHandle.flags().and_(TorrentFlags.SEQUENTIAL_DOWNLOAD));
|
||||||
|
} else {
|
||||||
|
for (int i = firstPieceIndex + piecesToPrepare; i < firstPieceIndex + piecesToPrepare + SEQUENTIAL_CONCURRENT_PIECES_COUNT; i++) {
|
||||||
|
torrentHandle.piecePriority(i, Priority.SEVEN);
|
||||||
|
torrentHandle.setPieceDeadline(i, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current torrent state
|
||||||
|
*
|
||||||
|
* @return {@link State}
|
||||||
|
*/
|
||||||
|
public State getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Piece finished
|
||||||
|
*
|
||||||
|
* @param alert
|
||||||
|
*/
|
||||||
|
private void pieceFinished(PieceFinishedAlert alert) {
|
||||||
|
if (state == State.STREAMING && hasPieces != null) {
|
||||||
|
int pieceIndex = alert.pieceIndex() - firstPieceIndex;
|
||||||
|
hasPieces[pieceIndex] = true;
|
||||||
|
|
||||||
|
if (pieceIndex >= interestedPieceIndex) {
|
||||||
|
for (int i = pieceIndex; i < hasPieces.length; i++) {
|
||||||
|
// Set full priority to first found piece that is not confirmed finished
|
||||||
|
if (!hasPieces[i]) {
|
||||||
|
torrentHandle.piecePriority(i + firstPieceIndex, Priority.SEVEN);
|
||||||
|
torrentHandle.setPieceDeadline(i + firstPieceIndex, 1000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Iterator<Integer> piecesIterator = preparePieces.iterator();
|
||||||
|
while (piecesIterator.hasNext()) {
|
||||||
|
int index = piecesIterator.next();
|
||||||
|
if (index == alert.pieceIndex()) {
|
||||||
|
piecesIterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPieces != null) {
|
||||||
|
hasPieces[alert.pieceIndex() - firstPieceIndex] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preparePieces.size() == 0) {
|
||||||
|
startSequentialMode();
|
||||||
|
|
||||||
|
prepareProgress = 100d;
|
||||||
|
sendStreamProgress();
|
||||||
|
state = State.STREAMING;
|
||||||
|
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onStreamReady(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void blockFinished(BlockFinishedAlert alert) {
|
||||||
|
for (Integer index : preparePieces) {
|
||||||
|
if (index == alert.pieceIndex()) {
|
||||||
|
prepareProgress += progressStep;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStreamProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendStreamProgress() {
|
||||||
|
TorrentStatus status = torrentHandle.status();
|
||||||
|
float progress = status.progress() * 100;
|
||||||
|
int seeds = status.numSeeds();
|
||||||
|
int peers = status.numPeers();
|
||||||
|
int downloadSpeed = status.downloadPayloadRate();
|
||||||
|
|
||||||
|
if (listener != null && prepareProgress >= 1) {
|
||||||
|
listener.onStreamProgress(this, new StreamStatus(progress, prepareProgress.intValue(), seeds, peers, downloadSpeed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] types() {
|
||||||
|
return new int[]{
|
||||||
|
AlertType.PIECE_FINISHED.swig(),
|
||||||
|
AlertType.BLOCK_FINISHED.swig()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void alert(Alert<?> alert) {
|
||||||
|
switch (alert.type()) {
|
||||||
|
case PIECE_FINISHED:
|
||||||
|
pieceFinished((PieceFinishedAlert) alert);
|
||||||
|
break;
|
||||||
|
case BLOCK_FINISHED:
|
||||||
|
blockFinished((BlockFinishedAlert) alert);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterator<WeakReference<TorrentInputStream>> i = torrentStreamReferences.iterator();
|
||||||
|
|
||||||
|
while (i.hasNext()) {
|
||||||
|
WeakReference<TorrentInputStream> reference = i.next();
|
||||||
|
TorrentInputStream inputStream = reference.get();
|
||||||
|
|
||||||
|
if (inputStream == null) {
|
||||||
|
i.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputStream.alert(alert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum State {UNKNOWN, RETRIEVING_META, STARTING, STREAMING}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package com.github.se_bastiaan.torrentstream;
|
||||||
|
|
||||||
|
import com.frostwire.jlibtorrent.AlertListener;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.Alert;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.AlertType;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
class TorrentInputStream extends FilterInputStream implements AlertListener {
|
||||||
|
private final Torrent torrent;
|
||||||
|
private boolean stopped;
|
||||||
|
private long location;
|
||||||
|
|
||||||
|
TorrentInputStream(Torrent torrent, InputStream inputStream) {
|
||||||
|
super(inputStream);
|
||||||
|
|
||||||
|
this.torrent = torrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void finalize() throws Throwable {
|
||||||
|
synchronized (this) {
|
||||||
|
stopped = true;
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean waitForPiece(long offset) {
|
||||||
|
while (!Thread.currentThread().isInterrupted() && !stopped) {
|
||||||
|
try {
|
||||||
|
if (torrent.hasBytes(offset)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
wait();
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int read() throws IOException {
|
||||||
|
if (!waitForPiece(location)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location++;
|
||||||
|
|
||||||
|
return super.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int read(byte[] buffer, int offset, int length) throws IOException {
|
||||||
|
int pieceLength = torrent.getTorrentHandle().torrentFile().pieceLength();
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i += pieceLength) {
|
||||||
|
if (!waitForPiece(location + i)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location += length;
|
||||||
|
|
||||||
|
return super.read(buffer, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
synchronized (this) {
|
||||||
|
stopped = true;
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized long skip(long n) throws IOException {
|
||||||
|
location += n;
|
||||||
|
return super.skip(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void pieceFinished() {
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] types() {
|
||||||
|
return new int[]{
|
||||||
|
AlertType.PIECE_FINISHED.swig(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void alert(Alert<?> alert) {
|
||||||
|
switch (alert.type()) {
|
||||||
|
case PIECE_FINISHED:
|
||||||
|
pieceFinished();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public final class TorrentOptions {
|
||||||
|
|
||||||
|
String saveLocation = "/";
|
||||||
|
String proxyHost;
|
||||||
|
String proxyUsername;
|
||||||
|
String proxyPassword;
|
||||||
|
String peerFingerprint;
|
||||||
|
Integer maxDownloadSpeed = 0;
|
||||||
|
Integer maxUploadSpeed = 0;
|
||||||
|
Integer maxConnections = 200;
|
||||||
|
Integer maxDht = 88;
|
||||||
|
Integer listeningPort = -1;
|
||||||
|
Boolean removeFiles = false;
|
||||||
|
Boolean anonymousMode = false;
|
||||||
|
Boolean autoDownload = true;
|
||||||
|
Long prepareSize = 15 * 1024L * 1024L;
|
||||||
|
|
||||||
|
private TorrentOptions() {
|
||||||
|
// Unused
|
||||||
|
}
|
||||||
|
|
||||||
|
private TorrentOptions(TorrentOptions torrentOptions) {
|
||||||
|
this.saveLocation = torrentOptions.saveLocation;
|
||||||
|
this.proxyHost = torrentOptions.proxyHost;
|
||||||
|
this.proxyUsername = torrentOptions.proxyUsername;
|
||||||
|
this.proxyPassword = torrentOptions.proxyPassword;
|
||||||
|
this.peerFingerprint = torrentOptions.peerFingerprint;
|
||||||
|
this.maxDownloadSpeed = torrentOptions.maxDownloadSpeed;
|
||||||
|
this.maxUploadSpeed = torrentOptions.maxUploadSpeed;
|
||||||
|
this.maxConnections = torrentOptions.maxConnections;
|
||||||
|
this.maxDht = torrentOptions.maxDht;
|
||||||
|
this.listeningPort = torrentOptions.listeningPort;
|
||||||
|
this.removeFiles = torrentOptions.removeFiles;
|
||||||
|
this.anonymousMode = torrentOptions.anonymousMode;
|
||||||
|
this.autoDownload = torrentOptions.autoDownload;
|
||||||
|
this.prepareSize = torrentOptions.prepareSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder toBuilder() {
|
||||||
|
return new Builder(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private TorrentOptions torrentOptions;
|
||||||
|
|
||||||
|
public Builder() {
|
||||||
|
torrentOptions = new TorrentOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder(TorrentOptions torrentOptions) {
|
||||||
|
torrentOptions = new TorrentOptions(torrentOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder saveLocation(String saveLocation) {
|
||||||
|
torrentOptions.saveLocation = saveLocation;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder saveLocation(File saveLocation) {
|
||||||
|
torrentOptions.saveLocation = saveLocation.getAbsolutePath();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder maxUploadSpeed(Integer maxUploadSpeed) {
|
||||||
|
torrentOptions.maxUploadSpeed = maxUploadSpeed;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder maxDownloadSpeed(Integer maxDownloadSpeed) {
|
||||||
|
torrentOptions.maxDownloadSpeed = maxDownloadSpeed;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder maxConnections(Integer maxConnections) {
|
||||||
|
torrentOptions.maxConnections = maxConnections;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder maxActiveDHT(Integer maxActiveDHT) {
|
||||||
|
torrentOptions.maxDht = maxActiveDHT;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder removeFilesAfterStop(Boolean b) {
|
||||||
|
torrentOptions.removeFiles = b;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder prepareSize(Long prepareSize) {
|
||||||
|
torrentOptions.prepareSize = prepareSize;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder listeningPort(Integer port) {
|
||||||
|
torrentOptions.listeningPort = port;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder proxy(String host, String username, String password) {
|
||||||
|
torrentOptions.proxyHost = host;
|
||||||
|
torrentOptions.proxyUsername = username;
|
||||||
|
torrentOptions.proxyPassword = password;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder peerFingerprint(String peerId) {
|
||||||
|
torrentOptions.peerFingerprint = peerId;
|
||||||
|
torrentOptions.anonymousMode = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder anonymousMode(Boolean enable) {
|
||||||
|
torrentOptions.anonymousMode = enable;
|
||||||
|
if (torrentOptions.anonymousMode)
|
||||||
|
torrentOptions.peerFingerprint = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder autoDownload(Boolean enable) {
|
||||||
|
torrentOptions.autoDownload = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TorrentOptions build() {
|
||||||
|
return torrentOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,542 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
|
||||||
|
import com.frostwire.jlibtorrent.Priority;
|
||||||
|
import com.frostwire.jlibtorrent.SessionManager;
|
||||||
|
import com.frostwire.jlibtorrent.SessionParams;
|
||||||
|
import com.frostwire.jlibtorrent.SettingsPack;
|
||||||
|
import com.frostwire.jlibtorrent.TorrentHandle;
|
||||||
|
import com.frostwire.jlibtorrent.TorrentInfo;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.AddTorrentAlert;
|
||||||
|
import com.frostwire.jlibtorrent.swig.settings_pack;
|
||||||
|
import com.github.se_bastiaan.torrentstream.exceptions.DirectoryModifyException;
|
||||||
|
import com.github.se_bastiaan.torrentstream.exceptions.NotInitializedException;
|
||||||
|
import com.github.se_bastiaan.torrentstream.exceptions.TorrentInfoException;
|
||||||
|
import com.github.se_bastiaan.torrentstream.listeners.DHTStatsAlertListener;
|
||||||
|
import com.github.se_bastiaan.torrentstream.listeners.TorrentAddedAlertListener;
|
||||||
|
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener;
|
||||||
|
import com.github.se_bastiaan.torrentstream.utils.FileUtils;
|
||||||
|
import com.github.se_bastiaan.torrentstream.utils.ThreadUtils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
|
public final class TorrentStream {
|
||||||
|
|
||||||
|
private static final String LIBTORRENT_THREAD_NAME = "TORRENTSTREAM_LIBTORRENT", STREAMING_THREAD_NAME = "TORRENTSTREAMER_STREAMING";
|
||||||
|
private static TorrentStream sThis;
|
||||||
|
private final List<TorrentListener> listeners = new ArrayList<>();
|
||||||
|
private CountDownLatch initialisingLatch;
|
||||||
|
private SessionManager torrentSession;
|
||||||
|
private Boolean initialising = false, initialised = false, isStreaming = false, isCanceled = false;
|
||||||
|
private TorrentOptions torrentOptions;
|
||||||
|
private Torrent currentTorrent;
|
||||||
|
private final TorrentAddedAlertListener torrentAddedAlertListener = new TorrentAddedAlertListener() {
|
||||||
|
@Override
|
||||||
|
public void torrentAdded(AddTorrentAlert alert) {
|
||||||
|
InternalTorrentListener listener = new InternalTorrentListener();
|
||||||
|
TorrentHandle th = torrentSession.find(alert.handle().infoHash());
|
||||||
|
currentTorrent = new Torrent(th, listener, torrentOptions.prepareSize);
|
||||||
|
|
||||||
|
torrentSession.addListener(currentTorrent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private String currentTorrentUrl;
|
||||||
|
private Integer dhtNodes = 0;
|
||||||
|
private final DHTStatsAlertListener dhtStatsAlertListener = new DHTStatsAlertListener() {
|
||||||
|
@Override
|
||||||
|
public void stats(int totalDhtNodes) {
|
||||||
|
dhtNodes = totalDhtNodes;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private HandlerThread libTorrentThread, streamingThread;
|
||||||
|
private Handler libTorrentHandler, streamingHandler;
|
||||||
|
|
||||||
|
private TorrentStream(TorrentOptions options) {
|
||||||
|
torrentOptions = options;
|
||||||
|
initialise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TorrentStream init(TorrentOptions options) {
|
||||||
|
sThis = new TorrentStream(options);
|
||||||
|
return sThis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TorrentStream getInstance() throws NotInitializedException {
|
||||||
|
if (sThis == null)
|
||||||
|
throw new NotInitializedException();
|
||||||
|
|
||||||
|
return sThis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain internal session manager
|
||||||
|
*
|
||||||
|
* @return {@link SessionManager}
|
||||||
|
*/
|
||||||
|
public SessionManager getSessionManager() {
|
||||||
|
return torrentSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialise() {
|
||||||
|
if (libTorrentThread != null && torrentSession != null) {
|
||||||
|
resumeSession();
|
||||||
|
} else {
|
||||||
|
if ((initialising || initialised) && libTorrentThread != null) {
|
||||||
|
libTorrentThread.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialising = true;
|
||||||
|
initialised = false;
|
||||||
|
initialisingLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
libTorrentThread = new HandlerThread(LIBTORRENT_THREAD_NAME);
|
||||||
|
libTorrentThread.start();
|
||||||
|
libTorrentHandler = new Handler(libTorrentThread.getLooper());
|
||||||
|
libTorrentHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
torrentSession = new SessionManager();
|
||||||
|
setOptions(torrentOptions);
|
||||||
|
|
||||||
|
torrentSession.addListener(dhtStatsAlertListener);
|
||||||
|
torrentSession.startDht();
|
||||||
|
|
||||||
|
initialising = false;
|
||||||
|
initialised = true;
|
||||||
|
initialisingLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume TorrentSession
|
||||||
|
*/
|
||||||
|
public void resumeSession() {
|
||||||
|
if (libTorrentThread != null && torrentSession != null) {
|
||||||
|
libTorrentHandler.removeCallbacksAndMessages(null);
|
||||||
|
|
||||||
|
//resume torrent session if needed
|
||||||
|
if (torrentSession.isPaused()) {
|
||||||
|
libTorrentHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
torrentSession.resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrentSession.isDhtRunning()) {
|
||||||
|
libTorrentHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
torrentSession.startDht();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause TorrentSession
|
||||||
|
*/
|
||||||
|
public void pauseSession() {
|
||||||
|
if (!isStreaming)
|
||||||
|
libTorrentHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
torrentSession.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get torrent metadata, either by downloading the .torrent or fetching the magnet
|
||||||
|
*
|
||||||
|
* @param torrentUrl {@link String} URL to .torrent or magnet link
|
||||||
|
* @return {@link TorrentInfo}
|
||||||
|
*/
|
||||||
|
private TorrentInfo getTorrentInfo(String torrentUrl) throws TorrentInfoException {
|
||||||
|
if (torrentUrl.startsWith("magnet")) {
|
||||||
|
byte[] data = torrentSession.fetchMagnet(torrentUrl, 30000);
|
||||||
|
if (data != null)
|
||||||
|
try {
|
||||||
|
return TorrentInfo.bdecode(data);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new TorrentInfoException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (torrentUrl.startsWith("http") || torrentUrl.startsWith("https")) {
|
||||||
|
try {
|
||||||
|
URL url = new URL(torrentUrl);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setInstanceFollowRedirects(true);
|
||||||
|
connection.connect();
|
||||||
|
|
||||||
|
InputStream inputStream = connection.getInputStream();
|
||||||
|
|
||||||
|
byte[] responseByteArray = new byte[0];
|
||||||
|
|
||||||
|
if (connection.getResponseCode() == 200) {
|
||||||
|
responseByteArray = getBytesFromInputStream(inputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputStream.close();
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
|
if (responseByteArray.length > 0) {
|
||||||
|
return TorrentInfo.bdecode(responseByteArray);
|
||||||
|
}
|
||||||
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
|
throw new TorrentInfoException(e);
|
||||||
|
}
|
||||||
|
} else if (torrentUrl.startsWith("file")) {
|
||||||
|
Uri path = Uri.parse(torrentUrl);
|
||||||
|
File file = new File(path.getPath());
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileInputStream fileInputStream = new FileInputStream(file);
|
||||||
|
byte[] responseByteArray = getBytesFromInputStream(fileInputStream);
|
||||||
|
fileInputStream.close();
|
||||||
|
|
||||||
|
if (responseByteArray.length > 0) {
|
||||||
|
return TorrentInfo.bdecode(responseByteArray);
|
||||||
|
}
|
||||||
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
|
throw new TorrentInfoException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getBytesFromInputStream(InputStream inputStream) throws IOException {
|
||||||
|
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
int bufferSize = 1024;
|
||||||
|
byte[] buffer = new byte[bufferSize];
|
||||||
|
|
||||||
|
int len = 0;
|
||||||
|
while ((len = inputStream.read(buffer)) != -1) {
|
||||||
|
byteBuffer.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return byteBuffer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start stream download for specified torrent
|
||||||
|
*
|
||||||
|
* @param torrentUrl {@link String} .torrent or magnet link
|
||||||
|
*/
|
||||||
|
public void startStream(final String torrentUrl) {
|
||||||
|
if (!initialising && !initialised)
|
||||||
|
initialise();
|
||||||
|
|
||||||
|
if (libTorrentHandler == null || isStreaming) return;
|
||||||
|
|
||||||
|
isCanceled = false;
|
||||||
|
|
||||||
|
streamingThread = new HandlerThread(STREAMING_THREAD_NAME);
|
||||||
|
streamingThread.start();
|
||||||
|
streamingHandler = new Handler(streamingThread.getLooper());
|
||||||
|
|
||||||
|
streamingHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
isStreaming = true;
|
||||||
|
|
||||||
|
if (initialisingLatch != null) {
|
||||||
|
try {
|
||||||
|
initialisingLatch.await();
|
||||||
|
initialisingLatch = null;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
isStreaming = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTorrentUrl = torrentUrl;
|
||||||
|
|
||||||
|
File saveDirectory = new File(torrentOptions.saveLocation);
|
||||||
|
if (!saveDirectory.isDirectory() && !saveDirectory.mkdirs()) {
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamError(null, new DirectoryModifyException());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isStreaming = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSession.removeListener(torrentAddedAlertListener);
|
||||||
|
TorrentInfo torrentInfo = null;
|
||||||
|
try {
|
||||||
|
torrentInfo = getTorrentInfo(torrentUrl);
|
||||||
|
} catch (final TorrentInfoException e) {
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamError(null, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
torrentSession.addListener(torrentAddedAlertListener);
|
||||||
|
|
||||||
|
if (torrentInfo == null) {
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamError(null, new TorrentInfoException(null));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isStreaming = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Priority[] priorities = new Priority[torrentInfo.numFiles()];
|
||||||
|
for (int i = 0; i < priorities.length; i++) {
|
||||||
|
priorities[i] = Priority.IGNORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentTorrentUrl.equals(torrentUrl) || isCanceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSession.download(torrentInfo, saveDirectory, null, priorities, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop current torrent stream
|
||||||
|
*/
|
||||||
|
public void stopStream() {
|
||||||
|
//remove all callbacks from handler
|
||||||
|
if (libTorrentHandler != null)
|
||||||
|
libTorrentHandler.removeCallbacksAndMessages(null);
|
||||||
|
if (streamingHandler != null)
|
||||||
|
streamingHandler.removeCallbacksAndMessages(null);
|
||||||
|
|
||||||
|
isCanceled = true;
|
||||||
|
isStreaming = false;
|
||||||
|
if (currentTorrent != null) {
|
||||||
|
final File saveLocation = currentTorrent.getSaveLocation();
|
||||||
|
|
||||||
|
currentTorrent.pause();
|
||||||
|
torrentSession.removeListener(currentTorrent);
|
||||||
|
torrentSession.remove(currentTorrent.getTorrentHandle());
|
||||||
|
currentTorrent = null;
|
||||||
|
|
||||||
|
if (torrentOptions.removeFiles) {
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int tries = 0;
|
||||||
|
while (!FileUtils.recursiveDelete(saveLocation) && tries < 5) {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000); // If deleted failed then something is still using the file, wait and then retry
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamError(currentTorrent, new DirectoryModifyException());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamingThread != null)
|
||||||
|
streamingThread.interrupt();
|
||||||
|
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamStopped();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TorrentOptions getOptions() {
|
||||||
|
return torrentOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOptions(TorrentOptions options) {
|
||||||
|
torrentOptions = options;
|
||||||
|
|
||||||
|
SettingsPack settingsPack = new SettingsPack()
|
||||||
|
.anonymousMode(torrentOptions.anonymousMode)
|
||||||
|
.connectionsLimit(torrentOptions.maxConnections)
|
||||||
|
.downloadRateLimit(torrentOptions.maxDownloadSpeed)
|
||||||
|
.uploadRateLimit(torrentOptions.maxUploadSpeed)
|
||||||
|
.activeDhtLimit(torrentOptions.maxDht);
|
||||||
|
|
||||||
|
if (torrentOptions.listeningPort != -1) {
|
||||||
|
String ifStr = String.format(Locale.ENGLISH, "%s:%d", "0.0.0.0", torrentOptions.listeningPort);
|
||||||
|
settingsPack.setString(settings_pack.string_types.listen_interfaces.swigValue(), ifStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrentOptions.proxyHost != null) {
|
||||||
|
settingsPack.setString(settings_pack.string_types.proxy_hostname.swigValue(), torrentOptions.proxyHost);
|
||||||
|
if (torrentOptions.proxyUsername != null) {
|
||||||
|
settingsPack.setString(settings_pack.string_types.proxy_username.swigValue(), torrentOptions.proxyUsername);
|
||||||
|
if (torrentOptions.proxyPassword != null) {
|
||||||
|
settingsPack.setString(settings_pack.string_types.proxy_password.swigValue(), torrentOptions.proxyPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrentOptions.peerFingerprint != null) {
|
||||||
|
settingsPack.setString(settings_pack.string_types.peer_fingerprint.swigValue(), torrentOptions.peerFingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrentSession.isRunning()) {
|
||||||
|
SessionParams sessionParams = new SessionParams(settingsPack);
|
||||||
|
torrentSession.start(sessionParams);
|
||||||
|
} else {
|
||||||
|
torrentSession.applySettings(settingsPack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStreaming() {
|
||||||
|
return isStreaming;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentTorrentUrl() {
|
||||||
|
return currentTorrentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTotalDhtNodes() {
|
||||||
|
return dhtNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Torrent getCurrentTorrent() {
|
||||||
|
return currentTorrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(TorrentListener listener) {
|
||||||
|
if (listener != null)
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(TorrentListener listener) {
|
||||||
|
if (listener != null)
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class InternalTorrentListener implements TorrentListener {
|
||||||
|
|
||||||
|
public void onStreamStarted(final Torrent torrent) {
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamStarted(torrent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onStreamError(final Torrent torrent, final Exception e) {
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamError(torrent, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onStreamReady(final Torrent torrent) {
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamReady(torrent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onStreamProgress(final Torrent torrent, final StreamStatus status) {
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamProgress(torrent, status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStreamStopped() {
|
||||||
|
// Not used
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStreamPrepared(final Torrent torrent) {
|
||||||
|
if (torrentOptions.autoDownload) {
|
||||||
|
torrent.startDownload();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final TorrentListener listener : listeners) {
|
||||||
|
ThreadUtils.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onStreamPrepared(torrent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream.exceptions;
|
||||||
|
|
||||||
|
public class DirectoryModifyException extends Exception {
|
||||||
|
|
||||||
|
public DirectoryModifyException() {
|
||||||
|
super("Could not create or delete save directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream.exceptions;
|
||||||
|
|
||||||
|
public class NotInitializedException extends Exception {
|
||||||
|
|
||||||
|
public NotInitializedException() {
|
||||||
|
super("TorrentStreamer is not initialized. Call init() first before getting an instance.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream.exceptions;
|
||||||
|
|
||||||
|
public class TorrentInfoException extends Exception {
|
||||||
|
|
||||||
|
public TorrentInfoException(Throwable cause) {
|
||||||
|
super("No torrent info could be found or read", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This file is part of TorrentStreamer-Android.
|
||||||
|
* *
|
||||||
|
* * TorrentStreamer-Android is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * TorrentStreamer-Android 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 Lesser General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* * along with TorrentStreamer-Android. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream.listeners;
|
||||||
|
|
||||||
|
import com.frostwire.jlibtorrent.AlertListener;
|
||||||
|
import com.frostwire.jlibtorrent.DhtRoutingBucket;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.Alert;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.AlertType;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.DhtStatsAlert;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public abstract class DHTStatsAlertListener implements AlertListener {
|
||||||
|
@Override
|
||||||
|
public int[] types() {
|
||||||
|
return new int[]{AlertType.DHT_STATS.swig()};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void alert(Alert<?> alert) {
|
||||||
|
if (alert instanceof DhtStatsAlert) {
|
||||||
|
DhtStatsAlert dhtAlert = (DhtStatsAlert) alert;
|
||||||
|
stats(countTotalDHTNodes(dhtAlert));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void stats(int totalDhtNodes);
|
||||||
|
|
||||||
|
private int countTotalDHTNodes(DhtStatsAlert alert) {
|
||||||
|
final ArrayList<DhtRoutingBucket> routingTable = alert.routingTable();
|
||||||
|
|
||||||
|
int totalNodes = 0;
|
||||||
|
if (routingTable != null) {
|
||||||
|
for (DhtRoutingBucket bucket : routingTable) {
|
||||||
|
totalNodes += bucket.numNodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalNodes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream.listeners;
|
||||||
|
|
||||||
|
import com.frostwire.jlibtorrent.AlertListener;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.AddTorrentAlert;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.Alert;
|
||||||
|
import com.frostwire.jlibtorrent.alerts.AlertType;
|
||||||
|
|
||||||
|
public abstract class TorrentAddedAlertListener implements AlertListener {
|
||||||
|
@Override
|
||||||
|
public int[] types() {
|
||||||
|
return new int[]{AlertType.ADD_TORRENT.swig()};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void alert(Alert<?> alert) {
|
||||||
|
switch (alert.type()) {
|
||||||
|
case ADD_TORRENT:
|
||||||
|
torrentAdded((AddTorrentAlert) alert);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void torrentAdded(AddTorrentAlert alert);
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015-2018 Sébastiaan (github.com/se-bastiaan)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream.listeners;
|
||||||
|
|
||||||
|
import com.github.se_bastiaan.torrentstream.StreamStatus;
|
||||||
|
import com.github.se_bastiaan.torrentstream.Torrent;
|
||||||
|
|
||||||
|
public interface TorrentListener {
|
||||||
|
void onStreamPrepared(Torrent torrent);
|
||||||
|
|
||||||
|
void onStreamStarted(Torrent torrent);
|
||||||
|
|
||||||
|
void onStreamError(Torrent torrent, Exception e);
|
||||||
|
|
||||||
|
void onStreamReady(Torrent torrent);
|
||||||
|
|
||||||
|
void onStreamProgress(Torrent torrent, StreamStatus status);
|
||||||
|
|
||||||
|
void onStreamStopped();
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This file is part of TorrentStreamer-Android.
|
||||||
|
* *
|
||||||
|
* * TorrentStreamer-Android is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * TorrentStreamer-Android 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 Lesser General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* * along with TorrentStreamer-Android. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream.utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public final class FileUtils {
|
||||||
|
|
||||||
|
private FileUtils() throws InstantiationException {
|
||||||
|
throw new InstantiationException("This class is not created for instantiation");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete every item below the File location
|
||||||
|
*
|
||||||
|
* @param file Location
|
||||||
|
* @return {@code true} when successful delete
|
||||||
|
*/
|
||||||
|
public static boolean recursiveDelete(File file) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
String[] children = file.list();
|
||||||
|
if (children == null) return false;
|
||||||
|
for (String child : children) {
|
||||||
|
recursiveDelete(new File(file, child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* * This file is part of TorrentStreamer-Android.
|
||||||
|
* *
|
||||||
|
* * TorrentStreamer-Android is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * TorrentStreamer-Android 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 Lesser General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* * along with TorrentStreamer-Android. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.se_bastiaan.torrentstream.utils;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
public final class ThreadUtils {
|
||||||
|
|
||||||
|
private ThreadUtils() throws InstantiationException {
|
||||||
|
throw new InstantiationException("This class is not created for instantiation");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the given {@link Runnable} on the ui thread.
|
||||||
|
*
|
||||||
|
* @param runnable The runnable to execute.
|
||||||
|
*/
|
||||||
|
public static void runOnUiThread(Runnable runnable) {
|
||||||
|
Thread uiThread = Looper.getMainLooper().getThread();
|
||||||
|
if (Thread.currentThread() != uiThread) new Handler(Looper.getMainLooper()).post(runnable);
|
||||||
|
else runnable.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue