Merge branch 'dev' into enqueue-playlist
20
README.md
|
@ -12,11 +12,13 @@
|
||||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#updates">Updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||||
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
<p align="center"><a href="https://newpipe.schabi.org">Website</a> • <a href="https://newpipe.schabi.org/blog/">Blog</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<b>WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||||
|
|
||||||
|
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
@ -73,6 +75,20 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
|
||||||
* Show comments
|
* Show comments
|
||||||
* … and many more
|
* … and many more
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
||||||
|
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||||
|
* Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||||
|
* Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
||||||
|
|
||||||
|
When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. The signing key issue is being discussed in issue [#1981](https://github.com/TeamNewPipe/NewPipe/issues/1981), and may be fixed by setting up our own repository on F-Droid.
|
||||||
|
|
||||||
|
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
|
||||||
|
1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists
|
||||||
|
2. Uninstall NewPipe
|
||||||
|
3. Download the APK from the new source and install it
|
||||||
|
4. Import the data from step 1 via "Settings>Content>Import Database"
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||||
The more is done the better it gets!
|
The more is done the better it gets!
|
||||||
|
|
|
@ -8,18 +8,20 @@ android {
|
||||||
applicationId "org.schabi.newpipe"
|
applicationId "org.schabi.newpipe"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 70
|
versionCode 720
|
||||||
versionName "0.15.0"
|
versionName "0.16.0"
|
||||||
|
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
|
|
||||||
debug {
|
debug {
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
debuggable true
|
debuggable true
|
||||||
|
@ -33,6 +35,7 @@ android {
|
||||||
// but continue the build even when errors are found:
|
// but continue the build even when errors are found:
|
||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
@ -54,7 +57,7 @@ dependencies {
|
||||||
exclude module: 'support-annotations'
|
exclude module: 'support-annotations'
|
||||||
})
|
})
|
||||||
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:99915e4527c0'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:e7e411dc296d8633'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.test.ApplicationTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
|
||||||
*/
|
|
||||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
|
||||||
public ApplicationTest() {
|
|
||||||
super(Application.class);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -35,12 +35,6 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".player.old.PlayVideoActivity"
|
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
|
||||||
android:theme="@style/OldVideoPlayerTheme"
|
|
||||||
tools:ignore="UnusedAttribute"/>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".player.BackgroundPlayer"
|
android:name=".player.BackgroundPlayer"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
|
@ -119,7 +113,6 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".ReCaptchaActivity"
|
android:name=".ReCaptchaActivity"
|
||||||
android:label="@string/reCaptchaActivity"/>
|
android:label="@string/reCaptchaActivity"/>
|
||||||
<activity android:name=".download.ExtSDDownloadFailedActivity" />
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
|
@ -184,6 +177,19 @@
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
<data android:host="www.youtube-nocookie.com"/>
|
||||||
|
<data android:pathPrefix="/embed/"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
<data android:scheme="vnd.youtube"/>
|
<data android:scheme="vnd.youtube"/>
|
||||||
<data android:scheme="vnd.youtube.launch"/>
|
<data android:scheme="vnd.youtube.launch"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -210,6 +216,29 @@
|
||||||
<data android:pathPrefix="/user/"/>
|
<data android:pathPrefix="/user/"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Invidious filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
<data android:host="invidio.us"/>
|
||||||
|
<data android:host="www.invidio.us"/>
|
||||||
|
<!-- video prefix -->
|
||||||
|
<data android:pathPrefix="/embed/"/>
|
||||||
|
<data android:pathPrefix="/watch"/>
|
||||||
|
<!-- channel prefix -->
|
||||||
|
<data android:pathPrefix="/channel/"/>
|
||||||
|
<data android:pathPrefix="/user/"/>
|
||||||
|
<!-- playlist prefix -->
|
||||||
|
<data android:pathPrefix="/playlist"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Soundcloud filter -->
|
<!-- Soundcloud filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
package android.support.design.widget;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.design.animation.AnimationUtils;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
// check this https://github.com/ToDou/appbarlayout-spring-behavior/blob/master/appbarspring/src/main/java/android/support/design/widget/AppBarFlingFixBehavior.java
|
||||||
|
public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
|
|
||||||
|
private ValueAnimator mOffsetAnimator;
|
||||||
|
private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
|
||||||
|
|
||||||
|
public FlingBehavior() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public FlingBehavior(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
|
||||||
|
if (dy != 0) {
|
||||||
|
int val = child.getBottom();
|
||||||
|
if (val != 0) {
|
||||||
|
int min, max;
|
||||||
|
if (dy < 0) {
|
||||||
|
// We're scrolling down
|
||||||
|
} else {
|
||||||
|
// We're scrolling up
|
||||||
|
if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
|
||||||
|
mOffsetAnimator.cancel();
|
||||||
|
}
|
||||||
|
min = -child.getUpNestedPreScrollRange();
|
||||||
|
max = 0;
|
||||||
|
consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull AppBarLayout child, @NonNull View target, float velocityX, float velocityY) {
|
||||||
|
|
||||||
|
if (velocityY != 0) {
|
||||||
|
if (velocityY < 0) {
|
||||||
|
// We're flinging down
|
||||||
|
int val = child.getBottom();
|
||||||
|
if (val != 0) {
|
||||||
|
final int targetScroll =
|
||||||
|
+child.getDownNestedPreScrollRange();
|
||||||
|
animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// We're flinging up
|
||||||
|
int val = child.getBottom();
|
||||||
|
if (val != 0) {
|
||||||
|
final int targetScroll = -child.getUpNestedPreScrollRange();
|
||||||
|
if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
|
||||||
|
animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
|
||||||
|
final AppBarLayout child, final int offset, float velocity) {
|
||||||
|
final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);
|
||||||
|
|
||||||
|
final int duration;
|
||||||
|
velocity = Math.abs(velocity);
|
||||||
|
if (velocity > 0) {
|
||||||
|
duration = 3 * Math.round(1000 * (distance / velocity));
|
||||||
|
} else {
|
||||||
|
final float distanceRatio = (float) distance / child.getHeight();
|
||||||
|
duration = (int) ((distanceRatio + 1) * 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout,
|
||||||
|
final AppBarLayout child, final int offset, final int duration) {
|
||||||
|
final int currentOffset = getTopBottomOffsetForScrollingSibling();
|
||||||
|
if (currentOffset == offset) {
|
||||||
|
if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
|
||||||
|
mOffsetAnimator.cancel();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOffsetAnimator == null) {
|
||||||
|
mOffsetAnimator = new ValueAnimator();
|
||||||
|
mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
|
||||||
|
mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(ValueAnimator animator) {
|
||||||
|
setHeaderTopBottomOffset(coordinatorLayout, child,
|
||||||
|
(Integer) animator.getAnimatedValue());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mOffsetAnimator.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
|
||||||
|
mOffsetAnimator.setIntValues(currentOffset, offset);
|
||||||
|
mOffsetAnimator.start();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
|
@ -65,6 +66,7 @@ import io.reactivex.plugins.RxJavaPlugins;
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
protected static final String TAG = App.class.toString();
|
protected static final String TAG = App.class.toString();
|
||||||
private RefWatcher refWatcher;
|
private RefWatcher refWatcher;
|
||||||
|
private static App app;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static final Class<? extends ReportSenderFactory>[]
|
private static final Class<? extends ReportSenderFactory>[]
|
||||||
|
@ -88,6 +90,8 @@ public class App extends Application {
|
||||||
}
|
}
|
||||||
refWatcher = installLeakCanary();
|
refWatcher = installLeakCanary();
|
||||||
|
|
||||||
|
app = this;
|
||||||
|
|
||||||
// Initialize settings first because others inits can use its values
|
// Initialize settings first because others inits can use its values
|
||||||
SettingsActivity.initSettings(this);
|
SettingsActivity.initSettings(this);
|
||||||
|
|
||||||
|
@ -100,6 +104,9 @@ public class App extends Application {
|
||||||
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||||
|
|
||||||
configureRxJavaErrorHandler();
|
configureRxJavaErrorHandler();
|
||||||
|
|
||||||
|
// Check for new version
|
||||||
|
new CheckForNewAppVersionTask().execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Downloader getDownloader() {
|
protected Downloader getDownloader() {
|
||||||
|
@ -211,6 +218,31 @@ public class App extends Application {
|
||||||
NotificationManager mNotificationManager =
|
NotificationManager mNotificationManager =
|
||||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
mNotificationManager.createNotificationChannel(mChannel);
|
mNotificationManager.createNotificationChannel(mChannel);
|
||||||
|
|
||||||
|
setUpUpdateNotificationChannel(importance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up notification channel for app update.
|
||||||
|
* @param importance
|
||||||
|
*/
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
private void setUpUpdateNotificationChannel(int importance) {
|
||||||
|
|
||||||
|
final String appUpdateId
|
||||||
|
= getString(R.string.app_update_notification_channel_id);
|
||||||
|
final CharSequence appUpdateName
|
||||||
|
= getString(R.string.app_update_notification_channel_name);
|
||||||
|
final String appUpdateDescription
|
||||||
|
= getString(R.string.app_update_notification_channel_description);
|
||||||
|
|
||||||
|
NotificationChannel appUpdateChannel
|
||||||
|
= new NotificationChannel(appUpdateId, appUpdateName, importance);
|
||||||
|
appUpdateChannel.setDescription(appUpdateDescription);
|
||||||
|
|
||||||
|
NotificationManager appUpdateNotificationManager
|
||||||
|
= (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -226,4 +258,8 @@ public class App extends Application {
|
||||||
protected boolean isDisposedRxExceptionsReported() {
|
protected boolean isDisposedRxExceptionsReported() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static App getApp() {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,242 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.Signature;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
import android.support.v4.app.NotificationManagerCompat;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
|
||||||
|
* If there is a newer version we show a notification, informing the user. On tapping
|
||||||
|
* the notification, the user will be directed to the download link.
|
||||||
|
*/
|
||||||
|
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||||
|
|
||||||
|
private static final Application app = App.getApp();
|
||||||
|
private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||||
|
private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
|
||||||
|
private static final int timeoutPeriod = 30;
|
||||||
|
|
||||||
|
private SharedPreferences mPrefs;
|
||||||
|
private OkHttpClient client;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
|
||||||
|
mPrefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||||
|
|
||||||
|
// Check if user has enabled/ disabled update checking
|
||||||
|
// and if the current apk is a github one or not.
|
||||||
|
if (!mPrefs.getBoolean(app.getString(R.string.update_app_key), true)
|
||||||
|
|| !isGithubApk()) {
|
||||||
|
this.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(Void... voids) {
|
||||||
|
|
||||||
|
if(isCancelled() || !isConnected()) return null;
|
||||||
|
|
||||||
|
// Make a network request to get latest NewPipe data.
|
||||||
|
if (client == null) {
|
||||||
|
|
||||||
|
client = new OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.readTimeout(timeoutPeriod, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(newPipeApiUrl)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Response response = client.newCall(request).execute();
|
||||||
|
return response.body().string();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
ErrorActivity.reportError(app, ex, null, null,
|
||||||
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
|
"app update API fail", R.string.app_ui_crash));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String response) {
|
||||||
|
|
||||||
|
// Parse the json from the response.
|
||||||
|
if (response != null) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject mainObject = new JSONObject(response);
|
||||||
|
JSONObject flavoursObject = mainObject.getJSONObject("flavors");
|
||||||
|
JSONObject githubObject = flavoursObject.getJSONObject("github");
|
||||||
|
JSONObject githubStableObject = githubObject.getJSONObject("stable");
|
||||||
|
|
||||||
|
String versionName = githubStableObject.getString("version");
|
||||||
|
String versionCode = githubStableObject.getString("version_code");
|
||||||
|
String apkLocationUrl = githubStableObject.getString("apk");
|
||||||
|
|
||||||
|
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
||||||
|
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
ErrorActivity.reportError(app, ex, null, null,
|
||||||
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
|
"could not parse app update JSON data", R.string.app_ui_crash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to compare the current and latest available app version.
|
||||||
|
* If a newer version is available, we show the update notification.
|
||||||
|
* @param versionName
|
||||||
|
* @param apkLocationUrl
|
||||||
|
*/
|
||||||
|
private void compareAppVersionAndShowNotification(String versionName,
|
||||||
|
String apkLocationUrl,
|
||||||
|
String versionCode) {
|
||||||
|
|
||||||
|
int NOTIFICATION_ID = 2000;
|
||||||
|
|
||||||
|
if (BuildConfig.VERSION_CODE < Integer.valueOf(versionCode)) {
|
||||||
|
|
||||||
|
// A pending intent to open the apk location url in the browser.
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||||
|
PendingIntent pendingIntent
|
||||||
|
= PendingIntent.getActivity(app, 0, intent, 0);
|
||||||
|
|
||||||
|
NotificationCompat.Builder notificationBuilder = new NotificationCompat
|
||||||
|
.Builder(app, app.getString(R.string.app_update_notification_channel_id))
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentTitle(app.getString(R.string.app_update_notification_content_title))
|
||||||
|
.setContentText(app.getString(R.string.app_update_notification_content_text)
|
||||||
|
+ " " + versionName);
|
||||||
|
|
||||||
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app);
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to get the apk's SHA1 key.
|
||||||
|
* https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133
|
||||||
|
*/
|
||||||
|
private static String getCertificateSHA1Fingerprint() {
|
||||||
|
|
||||||
|
PackageManager pm = app.getPackageManager();
|
||||||
|
String packageName = app.getPackageName();
|
||||||
|
int flags = PackageManager.GET_SIGNATURES;
|
||||||
|
PackageInfo packageInfo = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
packageInfo = pm.getPackageInfo(packageName, flags);
|
||||||
|
} catch (PackageManager.NameNotFoundException ex) {
|
||||||
|
ErrorActivity.reportError(app, ex, null, null,
|
||||||
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
|
"Could not find package info", R.string.app_ui_crash));
|
||||||
|
}
|
||||||
|
|
||||||
|
Signature[] signatures = packageInfo.signatures;
|
||||||
|
byte[] cert = signatures[0].toByteArray();
|
||||||
|
InputStream input = new ByteArrayInputStream(cert);
|
||||||
|
|
||||||
|
CertificateFactory cf = null;
|
||||||
|
X509Certificate c = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
cf = CertificateFactory.getInstance("X509");
|
||||||
|
c = (X509Certificate) cf.generateCertificate(input);
|
||||||
|
} catch (CertificateException ex) {
|
||||||
|
ErrorActivity.reportError(app, ex, null, null,
|
||||||
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
|
"Certificate error", R.string.app_ui_crash));
|
||||||
|
}
|
||||||
|
|
||||||
|
String hexString = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||||
|
byte[] publicKey = md.digest(c.getEncoded());
|
||||||
|
hexString = byte2HexFormatted(publicKey);
|
||||||
|
} catch (NoSuchAlgorithmException ex1) {
|
||||||
|
ErrorActivity.reportError(app, ex1, null, null,
|
||||||
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
|
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||||
|
} catch (CertificateEncodingException ex2) {
|
||||||
|
ErrorActivity.reportError(app, ex2, null, null,
|
||||||
|
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||||
|
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String byte2HexFormatted(byte[] arr) {
|
||||||
|
|
||||||
|
StringBuilder str = new StringBuilder(arr.length * 2);
|
||||||
|
|
||||||
|
for (int i = 0; i < arr.length; i++) {
|
||||||
|
String h = Integer.toHexString(arr[i]);
|
||||||
|
int l = h.length();
|
||||||
|
if (l == 1) h = "0" + h;
|
||||||
|
if (l > 2) h = h.substring(l - 2, l);
|
||||||
|
str.append(h.toUpperCase());
|
||||||
|
if (i < (arr.length - 1)) str.append(':');
|
||||||
|
}
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGithubApk() {
|
||||||
|
|
||||||
|
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConnected() {
|
||||||
|
|
||||||
|
ConnectivityManager cm =
|
||||||
|
(ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
return cm.getActiveNetworkInfo() != null
|
||||||
|
&& cm.getActiveNetworkInfo().isConnected();
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,18 +3,24 @@ package org.schabi.newpipe;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.DownloadRequest;
|
||||||
|
import org.schabi.newpipe.extractor.DownloadResponse;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.extractor.utils.Localization;
|
import org.schabi.newpipe.extractor.utils.Localization;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okhttp3.MediaType;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
@ -139,13 +145,16 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||||
|
|
||||||
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||||
final Request.Builder requestBuilder = new Request.Builder()
|
final Request.Builder requestBuilder = new Request.Builder()
|
||||||
.method("GET", null).url(siteUrl)
|
.method("GET", null).url(siteUrl);
|
||||||
.addHeader("User-Agent", USER_AGENT);
|
|
||||||
|
|
||||||
for (Map.Entry<String, String> header : customProperties.entrySet()) {
|
for (Map.Entry<String, String> header : customProperties.entrySet()) {
|
||||||
requestBuilder.addHeader(header.getKey(), header.getValue());
|
requestBuilder.addHeader(header.getKey(), header.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!customProperties.containsKey("User-Agent")) {
|
||||||
|
requestBuilder.header("User-Agent", USER_AGENT);
|
||||||
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(mCookies)) {
|
if (!TextUtils.isEmpty(mCookies)) {
|
||||||
requestBuilder.addHeader("Cookie", mCookies);
|
requestBuilder.addHeader("Cookie", mCookies);
|
||||||
}
|
}
|
||||||
|
@ -177,4 +186,96 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||||
return download(siteUrl, Collections.emptyMap());
|
return download(siteUrl, Collections.emptyMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DownloadResponse get(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
|
||||||
|
final Request.Builder requestBuilder = new Request.Builder()
|
||||||
|
.method("GET", null).url(siteUrl);
|
||||||
|
|
||||||
|
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
|
||||||
|
// set custom headers in request
|
||||||
|
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
|
||||||
|
for(String value : pair.getValue()){
|
||||||
|
requestBuilder.addHeader(pair.getKey(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestHeaders.containsKey("User-Agent")) {
|
||||||
|
requestBuilder.header("User-Agent", USER_AGENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(mCookies)) {
|
||||||
|
requestBuilder.addHeader("Cookie", mCookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Request okRequest = requestBuilder.build();
|
||||||
|
final Response response = client.newCall(okRequest).execute();
|
||||||
|
final ResponseBody body = response.body();
|
||||||
|
|
||||||
|
if (response.code() == 429) {
|
||||||
|
throw new ReCaptchaException("reCaptcha Challenge requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body == null) {
|
||||||
|
response.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DownloadResponse(body.string(), response.headers().toMultimap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DownloadResponse get(String siteUrl) throws IOException, ReCaptchaException {
|
||||||
|
return get(siteUrl, DownloadRequest.emptyRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DownloadResponse post(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
|
||||||
|
|
||||||
|
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
|
||||||
|
if(null == requestHeaders.get("Content-Type") || requestHeaders.get("Content-Type").isEmpty()){
|
||||||
|
// content type header is required. maybe throw an exception here
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = requestHeaders.get("Content-Type").get(0);
|
||||||
|
|
||||||
|
RequestBody okRequestBody = null;
|
||||||
|
if(null != request.getRequestBody()){
|
||||||
|
okRequestBody = RequestBody.create(MediaType.parse(contentType), request.getRequestBody());
|
||||||
|
}
|
||||||
|
final Request.Builder requestBuilder = new Request.Builder()
|
||||||
|
.method("POST", okRequestBody).url(siteUrl);
|
||||||
|
|
||||||
|
// set custom headers in request
|
||||||
|
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
|
||||||
|
for(String value : pair.getValue()){
|
||||||
|
requestBuilder.addHeader(pair.getKey(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestHeaders.containsKey("User-Agent")) {
|
||||||
|
requestBuilder.header("User-Agent", USER_AGENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(mCookies)) {
|
||||||
|
requestBuilder.addHeader("Cookie", mCookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Request okRequest = requestBuilder.build();
|
||||||
|
final Response response = client.newCall(okRequest).execute();
|
||||||
|
final ResponseBody body = response.body();
|
||||||
|
|
||||||
|
if (response.code() == 429) {
|
||||||
|
throw new ReCaptchaException("reCaptcha Challenge requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body == null) {
|
||||||
|
response.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DownloadResponse(body.string(), response.headers().toMultimap());
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -457,7 +457,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
break;
|
break;
|
||||||
case R.id.subtitle_button:
|
case R.id.subtitle_button:
|
||||||
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together
|
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
|
||||||
kind = 's';
|
kind = 's';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -477,7 +477,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
final String finalFileName = fileName;
|
final String finalFileName = fileName;
|
||||||
|
|
||||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
|
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
|
||||||
// should be safe run the following code without "getActivity().runOnUiThread()"
|
|
||||||
if (listed) {
|
if (listed) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
builder.setTitle(R.string.download_dialog_title)
|
builder.setTitle(R.string.download_dialog_title)
|
||||||
|
@ -511,11 +510,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
|
|
||||||
if (secondaryStream != null) {
|
if (secondaryStream != null) {
|
||||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
|
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||||
psArgs = null;
|
psArgs = null;
|
||||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||||
|
|
||||||
// set nearLength, only, if both sizes are fetched or known. this probably does not work on weak internet connections
|
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
|
||||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
package org.schabi.newpipe.download;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
|
|
||||||
public class ExtSDDownloadFailedActivity extends AppCompatActivity {
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
new AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.download_to_sdcard_error_title)
|
|
||||||
.setMessage(R.string.download_to_sdcard_error_message)
|
|
||||||
.setPositiveButton(R.string.yes, (DialogInterface dialogInterface, int i) -> {
|
|
||||||
NewPipeSettings.resetDownloadFolders(this);
|
|
||||||
finish();
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.cancel, (DialogInterface dialogInterface, int i) -> {
|
|
||||||
dialogInterface.dismiss();
|
|
||||||
finish();
|
|
||||||
})
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.BaseFragment;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
|
public class EmptyFragment extends BaseFragment {
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_empty, container, false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.app.FragmentManager;
|
||||||
|
import android.support.v4.app.FragmentPagerAdapter;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TabAdaptor extends FragmentPagerAdapter {
|
||||||
|
|
||||||
|
private final List<Fragment> mFragmentList = new ArrayList<>();
|
||||||
|
private final List<String> mFragmentTitleList = new ArrayList<>();
|
||||||
|
private final FragmentManager fragmentManager;
|
||||||
|
|
||||||
|
public TabAdaptor(FragmentManager fm) {
|
||||||
|
super(fm);
|
||||||
|
this.fragmentManager = fm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Fragment getItem(int position) {
|
||||||
|
return mFragmentList.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return mFragmentList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addFragment(Fragment fragment, String title) {
|
||||||
|
mFragmentList.add(fragment);
|
||||||
|
mFragmentTitleList.add(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAllItems() {
|
||||||
|
mFragmentList.clear();
|
||||||
|
mFragmentTitleList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeItem(int position){
|
||||||
|
mFragmentList.remove(position == 0 ? 0 : position - 1);
|
||||||
|
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateItem(int position, Fragment fragment){
|
||||||
|
mFragmentList.set(position, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateItem(String title, Fragment fragment){
|
||||||
|
int index = mFragmentTitleList.indexOf(title);
|
||||||
|
if(index != -1){
|
||||||
|
updateItem(index, fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemPosition(Object object) {
|
||||||
|
if (mFragmentList.contains(object)) return mFragmentList.indexOf(object);
|
||||||
|
else return POSITION_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyDataSetUpdate(){
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||||
|
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,11 +10,13 @@ import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.DrawableRes;
|
import android.support.annotation.DrawableRes;
|
||||||
import android.support.annotation.FloatRange;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.design.widget.AppBarLayout;
|
||||||
|
import android.support.design.widget.TabLayout;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
import android.support.v4.view.ViewPager;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
@ -25,7 +27,6 @@ import android.text.method.LinkMovementMethod;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
|
@ -33,19 +34,15 @@ import android.view.MenuItem;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewParent;
|
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.nirhart.parallaxscroll.views.ParallaxScrollView;
|
|
||||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||||
|
@ -55,7 +52,9 @@ import org.schabi.newpipe.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.download.DownloadDialog;
|
import org.schabi.newpipe.download.DownloadDialog;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
@ -63,21 +62,22 @@ import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.fragments.EmptyFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.MainVideoPlayer;
|
import org.schabi.newpipe.player.MainVideoPlayer;
|
||||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
|
@ -85,11 +85,9 @@ import org.schabi.newpipe.util.InfoCache;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -104,6 +102,7 @@ import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class VideoDetailFragment
|
public class VideoDetailFragment
|
||||||
|
@ -114,27 +113,27 @@ public class VideoDetailFragment
|
||||||
View.OnLongClickListener {
|
View.OnLongClickListener {
|
||||||
public static final String AUTO_PLAY = "auto_play";
|
public static final String AUTO_PLAY = "auto_play";
|
||||||
|
|
||||||
// Amount of videos to show on start
|
|
||||||
private static final int INITIAL_RELATED_VIDEOS = 8;
|
|
||||||
|
|
||||||
private InfoItemBuilder infoItemBuilder = null;
|
|
||||||
|
|
||||||
private int updateFlags = 0;
|
private int updateFlags = 0;
|
||||||
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
|
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
|
||||||
private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2;
|
private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2;
|
||||||
private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4;
|
private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4;
|
||||||
|
private static final int COMMENTS_UPDATE_FLAG = 0x4;
|
||||||
|
|
||||||
private boolean autoPlayEnabled;
|
private boolean autoPlayEnabled;
|
||||||
private boolean showRelatedStreams;
|
private boolean showRelatedStreams;
|
||||||
private boolean wasRelatedStreamsExpanded = false;
|
private boolean showComments;
|
||||||
|
|
||||||
@State protected int serviceId = Constants.NO_SERVICE_ID;
|
@State
|
||||||
@State protected String name;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@State protected String url;
|
@State
|
||||||
|
protected String name;
|
||||||
|
@State
|
||||||
|
protected String url;
|
||||||
|
|
||||||
private StreamInfo currentInfo;
|
private StreamInfo currentInfo;
|
||||||
private Disposable currentWorker;
|
private Disposable currentWorker;
|
||||||
@NonNull private CompositeDisposable disposables = new CompositeDisposable();
|
@NonNull
|
||||||
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
private List<VideoStream> sortedVideoStreams;
|
private List<VideoStream> sortedVideoStreams;
|
||||||
private int selectedVideoStreamIndex = -1;
|
private int selectedVideoStreamIndex = -1;
|
||||||
|
@ -147,7 +146,6 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
private Spinner spinnerToolbar;
|
private Spinner spinnerToolbar;
|
||||||
|
|
||||||
private ParallaxScrollView parallaxScrollRootView;
|
|
||||||
private LinearLayout contentRootLayoutHiding;
|
private LinearLayout contentRootLayoutHiding;
|
||||||
|
|
||||||
private View thumbnailBackgroundButton;
|
private View thumbnailBackgroundButton;
|
||||||
|
@ -156,7 +154,6 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
private View videoTitleRoot;
|
private View videoTitleRoot;
|
||||||
private TextView videoTitleTextView;
|
private TextView videoTitleTextView;
|
||||||
@Nullable
|
|
||||||
private ImageView videoTitleToggleArrow;
|
private ImageView videoTitleToggleArrow;
|
||||||
private TextView videoCountView;
|
private TextView videoCountView;
|
||||||
|
|
||||||
|
@ -181,10 +178,15 @@ public class VideoDetailFragment
|
||||||
private ImageView thumbsDownImageView;
|
private ImageView thumbsDownImageView;
|
||||||
private TextView thumbsDisabledTextView;
|
private TextView thumbsDisabledTextView;
|
||||||
|
|
||||||
private TextView nextStreamTitle;
|
private static final String COMMENTS_TAB_TAG = "COMMENTS";
|
||||||
private LinearLayout relatedStreamRootLayout;
|
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
|
||||||
private LinearLayout relatedStreamsView;
|
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||||
private ImageButton relatedStreamExpandButton;
|
|
||||||
|
private AppBarLayout appBarLayout;
|
||||||
|
private ViewPager viewPager;
|
||||||
|
private TabAdaptor pageAdapter;
|
||||||
|
private TabLayout tabLayout;
|
||||||
|
private FrameLayout relatedStreamsLayout;
|
||||||
|
|
||||||
|
|
||||||
/*////////////////////////////////////////////////////////////////////////*/
|
/*////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -200,12 +202,17 @@ public class VideoDetailFragment
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void
|
||||||
|
onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity)
|
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(getString(R.string.show_next_video_key), true);
|
.getBoolean(getString(R.string.show_next_video_key), true);
|
||||||
|
|
||||||
|
showComments = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
.getBoolean(getString(R.string.show_comments_key), true);
|
||||||
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.registerOnSharedPreferenceChangeListener(this);
|
.registerOnSharedPreferenceChangeListener(this);
|
||||||
}
|
}
|
||||||
|
@ -227,14 +234,16 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
if (updateFlags != 0) {
|
if (updateFlags != 0) {
|
||||||
if (!isLoading.get() && currentInfo != null) {
|
if (!isLoading.get() && currentInfo != null) {
|
||||||
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentInfo);
|
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) startLoading(false);
|
||||||
if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo);
|
if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo);
|
||||||
|
if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) startLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0
|
if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0
|
||||||
&& menu != null) {
|
&& menu != null) {
|
||||||
updateMenuItemVisibility();
|
updateMenuItemVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFlags = 0;
|
updateFlags = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,6 +300,9 @@ public class VideoDetailFragment
|
||||||
updateFlags |= RESOLUTIONS_MENU_UPDATE_FLAG;
|
updateFlags |= RESOLUTIONS_MENU_UPDATE_FLAG;
|
||||||
} else if (key.equals(getString(R.string.show_play_with_kodi_key))) {
|
} else if (key.equals(getString(R.string.show_play_with_kodi_key))) {
|
||||||
updateFlags |= TOOLBAR_ITEMS_UPDATE_FLAG;
|
updateFlags |= TOOLBAR_ITEMS_UPDATE_FLAG;
|
||||||
|
} else if (key.equals(getString(R.string.show_comments_key))) {
|
||||||
|
showComments = sharedPreferences.getBoolean(key, true);
|
||||||
|
updateFlags |= COMMENTS_UPDATE_FLAG;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,7 +312,6 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
private static final String INFO_KEY = "info_key";
|
private static final String INFO_KEY = "info_key";
|
||||||
private static final String STACK_KEY = "stack_key";
|
private static final String STACK_KEY = "stack_key";
|
||||||
private static final String WAS_RELATED_EXPANDED_KEY = "was_related_expanded_key";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
@ -309,10 +320,6 @@ public class VideoDetailFragment
|
||||||
// Check if the next video label and video is visible,
|
// Check if the next video label and video is visible,
|
||||||
// if it is, include the two elements in the next check
|
// if it is, include the two elements in the next check
|
||||||
int nextCount = currentInfo != null && currentInfo.getNextVideo() != null ? 2 : 0;
|
int nextCount = currentInfo != null && currentInfo.getNextVideo() != null ? 2 : 0;
|
||||||
if (relatedStreamsView != null
|
|
||||||
&& relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) {
|
|
||||||
outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoading.get() && currentInfo != null && isVisible()) {
|
if (!isLoading.get() && currentInfo != null && isVisible()) {
|
||||||
outState.putSerializable(INFO_KEY, currentInfo);
|
outState.putSerializable(INFO_KEY, currentInfo);
|
||||||
|
@ -325,12 +332,11 @@ public class VideoDetailFragment
|
||||||
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
|
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
|
||||||
super.onRestoreInstanceState(savedState);
|
super.onRestoreInstanceState(savedState);
|
||||||
|
|
||||||
wasRelatedStreamsExpanded = savedState.getBoolean(WAS_RELATED_EXPANDED_KEY, false);
|
|
||||||
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||||
if (serializable instanceof StreamInfo) {
|
if (serializable instanceof StreamInfo) {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
currentInfo = (StreamInfo) serializable;
|
currentInfo = (StreamInfo) serializable;
|
||||||
InfoCache.getInstance().putInfo(serviceId, url, currentInfo);
|
InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
serializable = savedState.getSerializable(STACK_KEY);
|
serializable = savedState.getSerializable(STACK_KEY);
|
||||||
|
@ -338,6 +344,7 @@ public class VideoDetailFragment
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
stack.addAll((Collection<? extends StackItem>) serializable);
|
stack.addAll((Collection<? extends StackItem>) serializable);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -362,7 +369,8 @@ public class VideoDetailFragment
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.detail_controls_download:
|
case R.id.detail_controls_download:
|
||||||
if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
if (PermissionHelper.checkStoragePermissions(activity,
|
||||||
|
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||||
this.openDownloadDialog();
|
this.openDownloadDialog();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -392,9 +400,6 @@ public class VideoDetailFragment
|
||||||
case R.id.detail_title_root_layout:
|
case R.id.detail_title_root_layout:
|
||||||
toggleTitleAndDescription();
|
toggleTitleAndDescription();
|
||||||
break;
|
break;
|
||||||
case R.id.detail_related_streams_expand:
|
|
||||||
toggleExpandRelatedVideos(currentInfo);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -418,7 +423,6 @@ public class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleTitleAndDescription() {
|
private void toggleTitleAndDescription() {
|
||||||
if (videoTitleToggleArrow != null) { //it is null for tablets
|
|
||||||
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
|
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
|
||||||
videoTitleTextView.setMaxLines(1);
|
videoTitleTextView.setMaxLines(1);
|
||||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||||
|
@ -429,33 +433,6 @@ public class VideoDetailFragment
|
||||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
|
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleExpandRelatedVideos(StreamInfo info) {
|
|
||||||
if (DEBUG) Log.d(TAG, "toggleExpandRelatedVideos() called with: info = [" + info + "]");
|
|
||||||
if (!showRelatedStreams) return;
|
|
||||||
|
|
||||||
int nextCount = info.getNextVideo() != null ? 2 : 0;
|
|
||||||
int initialCount = INITIAL_RELATED_VIDEOS + nextCount;
|
|
||||||
|
|
||||||
if (relatedStreamsView.getChildCount() > initialCount) {
|
|
||||||
relatedStreamsView.removeViews(initialCount,
|
|
||||||
relatedStreamsView.getChildCount() - (initialCount));
|
|
||||||
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(
|
|
||||||
activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Log.d(TAG, "toggleExpandRelatedVideos() called with: info = [" + info + "], from = [" + INITIAL_RELATED_VIDEOS + "]");
|
|
||||||
for (int i = INITIAL_RELATED_VIDEOS; i < info.getRelatedStreams().size(); i++) {
|
|
||||||
InfoItem item = info.getRelatedStreams().get(i);
|
|
||||||
//Log.d(TAG, "i = " + i);
|
|
||||||
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item));
|
|
||||||
}
|
|
||||||
relatedStreamExpandButton.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(activity,
|
|
||||||
ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Init
|
// Init
|
||||||
|
@ -466,8 +443,6 @@ public class VideoDetailFragment
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
|
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
|
||||||
|
|
||||||
parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content);
|
|
||||||
|
|
||||||
thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout);
|
thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout);
|
||||||
thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view);
|
thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view);
|
||||||
thumbnailPlayButton = rootView.findViewById(R.id.detail_thumbnail_play_button);
|
thumbnailPlayButton = rootView.findViewById(R.id.detail_thumbnail_play_button);
|
||||||
|
@ -503,30 +478,23 @@ public class VideoDetailFragment
|
||||||
uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view);
|
uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view);
|
||||||
uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view);
|
uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view);
|
||||||
|
|
||||||
relatedStreamRootLayout = rootView.findViewById(R.id.detail_related_streams_root_layout);
|
appBarLayout = rootView.findViewById(R.id.appbarlayout);
|
||||||
nextStreamTitle = rootView.findViewById(R.id.detail_next_stream_title);
|
viewPager = rootView.findViewById(R.id.viewpager);
|
||||||
relatedStreamsView = rootView.findViewById(R.id.detail_related_streams_view);
|
pageAdapter = new TabAdaptor(getChildFragmentManager());
|
||||||
|
viewPager.setAdapter(pageAdapter);
|
||||||
|
tabLayout = rootView.findViewById(R.id.tablayout);
|
||||||
|
tabLayout.setupWithViewPager(viewPager);
|
||||||
|
|
||||||
relatedStreamExpandButton = rootView.findViewById(R.id.detail_related_streams_expand);
|
relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout);
|
||||||
|
|
||||||
infoItemBuilder = new InfoItemBuilder(activity);
|
|
||||||
setHeightThumbnail();
|
setHeightThumbnail();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
infoItemBuilder.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
|
||||||
@Override
|
|
||||||
public void selected(StreamInfoItem selectedItem) {
|
|
||||||
selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void held(StreamInfoItem selectedItem) {
|
|
||||||
showStreamDialog(selectedItem);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
videoTitleRoot.setOnClickListener(this);
|
videoTitleRoot.setOnClickListener(this);
|
||||||
uploaderRootLayout.setOnClickListener(this);
|
uploaderRootLayout.setOnClickListener(this);
|
||||||
|
@ -536,7 +504,6 @@ public class VideoDetailFragment
|
||||||
detailControlsAddToPlaylist.setOnClickListener(this);
|
detailControlsAddToPlaylist.setOnClickListener(this);
|
||||||
detailControlsDownload.setOnClickListener(this);
|
detailControlsDownload.setOnClickListener(this);
|
||||||
detailControlsDownload.setOnLongClickListener(this);
|
detailControlsDownload.setOnLongClickListener(this);
|
||||||
relatedStreamExpandButton.setOnClickListener(this);
|
|
||||||
|
|
||||||
detailControlsBackground.setLongClickable(true);
|
detailControlsBackground.setLongClickable(true);
|
||||||
detailControlsPopup.setLongClickable(true);
|
detailControlsPopup.setLongClickable(true);
|
||||||
|
@ -619,44 +586,6 @@ public class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initRelatedVideos(StreamInfo info) {
|
|
||||||
if (relatedStreamsView.getChildCount() > 0) relatedStreamsView.removeAllViews();
|
|
||||||
|
|
||||||
if (info.getNextVideo() != null && showRelatedStreams) {
|
|
||||||
nextStreamTitle.setVisibility(View.VISIBLE);
|
|
||||||
relatedStreamsView.addView(
|
|
||||||
infoItemBuilder.buildView(relatedStreamsView, info.getNextVideo()));
|
|
||||||
relatedStreamsView.addView(getSeparatorView());
|
|
||||||
setRelatedStreamsVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
nextStreamTitle.setVisibility(View.GONE);
|
|
||||||
setRelatedStreamsVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.getRelatedStreams() != null
|
|
||||||
&& !info.getRelatedStreams().isEmpty() && showRelatedStreams) {
|
|
||||||
//long first = System.nanoTime(), each;
|
|
||||||
int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS
|
|
||||||
? INITIAL_RELATED_VIDEOS
|
|
||||||
: info.getRelatedStreams().size();
|
|
||||||
for (int i = 0; i < to; i++) {
|
|
||||||
InfoItem item = info.getRelatedStreams().get(i);
|
|
||||||
//each = System.nanoTime();
|
|
||||||
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item));
|
|
||||||
//if (DEBUG) Log.d(TAG, "each took " + ((System.nanoTime() - each) / 1000000L) + "ms");
|
|
||||||
}
|
|
||||||
//if (DEBUG) Log.d(TAG, "Total time " + ((System.nanoTime() - first) / 1000000L) + "ms");
|
|
||||||
|
|
||||||
setRelatedStreamsVisibility(View.VISIBLE);
|
|
||||||
relatedStreamExpandButton.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(
|
|
||||||
activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
|
|
||||||
} else {
|
|
||||||
if (info.getNextVideo() == null) setRelatedStreamsVisibility(View.GONE);
|
|
||||||
relatedStreamExpandButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Menu
|
// Menu
|
||||||
|
@ -699,13 +628,13 @@ public class VideoDetailFragment
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case R.id.menu_item_share: {
|
case R.id.menu_item_share: {
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
shareUrl(currentInfo.getName(), currentInfo.getUrl());
|
shareUrl(currentInfo.getName(), currentInfo.getOriginalUrl());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case R.id.menu_item_openInBrowser: {
|
case R.id.menu_item_openInBrowser: {
|
||||||
if (currentInfo != null) {
|
if (currentInfo != null) {
|
||||||
openUrlInBrowser(currentInfo.getUrl());
|
openUrlInBrowser(currentInfo.getOriginalUrl());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -728,7 +657,8 @@ public class VideoDetailFragment
|
||||||
builder.setMessage(R.string.kore_not_found)
|
builder.setMessage(R.string.kore_not_found)
|
||||||
.setPositiveButton(R.string.install, (DialogInterface dialog, int which) ->
|
.setPositiveButton(R.string.install, (DialogInterface dialog, int which) ->
|
||||||
NavigationHelper.installKore(context))
|
NavigationHelper.installKore(context))
|
||||||
.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {});
|
.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
|
||||||
|
});
|
||||||
builder.create().show();
|
builder.create().show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -742,10 +672,16 @@ public class VideoDetailFragment
|
||||||
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
|
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
||||||
|
|
||||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
|
||||||
|
activity,
|
||||||
|
info.getVideoStreams(),
|
||||||
|
info.getVideoOnlyStreams(),
|
||||||
|
false);
|
||||||
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||||
|
|
||||||
final StreamItemAdapter<VideoStream, Stream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled);
|
final StreamItemAdapter<VideoStream, Stream> streamsAdapter =
|
||||||
|
new StreamItemAdapter<>(activity,
|
||||||
|
new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled);
|
||||||
spinnerToolbar.setAdapter(streamsAdapter);
|
spinnerToolbar.setAdapter(streamsAdapter);
|
||||||
spinnerToolbar.setSelection(selectedVideoStreamIndex);
|
spinnerToolbar.setSelection(selectedVideoStreamIndex);
|
||||||
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
|
@ -770,17 +706,17 @@ public class VideoDetailFragment
|
||||||
*/
|
*/
|
||||||
protected final LinkedList<StackItem> stack = new LinkedList<>();
|
protected final LinkedList<StackItem> stack = new LinkedList<>();
|
||||||
|
|
||||||
public void clearHistory() {
|
|
||||||
stack.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void pushToStack(int serviceId, String videoUrl, String name) {
|
public void pushToStack(int serviceId, String videoUrl, String name) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "pushToStack() called with: serviceId = [" + serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]");
|
Log.d(TAG, "pushToStack() called with: serviceId = ["
|
||||||
|
+ serviceId + "], videoUrl = [" + videoUrl + "], name = [" + name + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stack.size() > 0 && stack.peek().getServiceId() == serviceId && stack.peek().getUrl().equals(videoUrl)) {
|
if (stack.size() > 0
|
||||||
Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]");
|
&& stack.peek().getServiceId() == serviceId
|
||||||
|
&& stack.peek().getUrl().equals(videoUrl)) {
|
||||||
|
Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = ["
|
||||||
|
+ serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "pushToStack() wasn't equal");
|
Log.d(TAG, "pushToStack() wasn't equal");
|
||||||
|
@ -811,7 +747,11 @@ public class VideoDetailFragment
|
||||||
// Get stack item from the new top
|
// Get stack item from the new top
|
||||||
StackItem peek = stack.peek();
|
StackItem peek = stack.peek();
|
||||||
|
|
||||||
selectAndLoadVideo(peek.getServiceId(), peek.getUrl(), !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : "");
|
selectAndLoadVideo(peek.getServiceId(),
|
||||||
|
peek.getUrl(),
|
||||||
|
!TextUtils.isEmpty(peek.getTitle())
|
||||||
|
? peek.getTitle()
|
||||||
|
: "");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -831,28 +771,22 @@ public class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) {
|
public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) {
|
||||||
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
|
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = ["
|
||||||
|
+ info + "], scrollToTop = [" + scrollToTop + "]");
|
||||||
|
|
||||||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||||
pushToStack(serviceId, url, name);
|
pushToStack(serviceId, url, name);
|
||||||
showLoading();
|
showLoading();
|
||||||
|
initTabs();
|
||||||
|
|
||||||
Log.d(TAG, "prepareAndHandleInfo() called parallaxScrollRootView.getScrollY(): "
|
if (scrollToTop) appBarLayout.setExpanded(true, true);
|
||||||
+ parallaxScrollRootView.getScrollY());
|
|
||||||
final boolean greaterThanThreshold = parallaxScrollRootView.getScrollY() > (int)
|
|
||||||
(getResources().getDisplayMetrics().heightPixels * .1f);
|
|
||||||
|
|
||||||
if (scrollToTop) parallaxScrollRootView.smoothScrollTo(0, 0);
|
|
||||||
animateView(contentRootLayoutHiding,
|
|
||||||
false,
|
|
||||||
greaterThanThreshold ? 250 : 0, 0, () -> {
|
|
||||||
handleResult(info);
|
handleResult(info);
|
||||||
showContentWithAnimation(120, 0, .01f);
|
showContent();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void prepareAndLoadInfo() {
|
protected void prepareAndLoadInfo() {
|
||||||
parallaxScrollRootView.smoothScrollTo(0, 0);
|
appBarLayout.setExpanded(true, true);
|
||||||
pushToStack(serviceId, url, name);
|
pushToStack(serviceId, url, name);
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -861,6 +795,7 @@ public class VideoDetailFragment
|
||||||
public void startLoading(boolean forceLoad) {
|
public void startLoading(boolean forceLoad) {
|
||||||
super.startLoading(forceLoad);
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
|
initTabs();
|
||||||
currentInfo = null;
|
currentInfo = null;
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) currentWorker.dispose();
|
||||||
|
|
||||||
|
@ -870,12 +805,49 @@ public class VideoDetailFragment
|
||||||
.subscribe((@NonNull StreamInfo result) -> {
|
.subscribe((@NonNull StreamInfo result) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
currentInfo = result;
|
currentInfo = result;
|
||||||
showContentWithAnimation(120, 0, 0);
|
|
||||||
handleResult(result);
|
handleResult(result);
|
||||||
|
showContent();
|
||||||
}, (@NonNull Throwable throwable) -> {
|
}, (@NonNull Throwable throwable) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
onError(throwable);
|
onError(throwable);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initTabs() {
|
||||||
|
pageAdapter.clearAllItems();
|
||||||
|
|
||||||
|
if(shouldShowComments()){
|
||||||
|
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showRelatedStreams && null == relatedStreamsLayout){
|
||||||
|
//temp empty fragment. will be updated in handleResult
|
||||||
|
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pageAdapter.getCount() == 0){
|
||||||
|
pageAdapter.addFragment(new EmptyFragment(), EMPTY_TAB_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
pageAdapter.notifyDataSetUpdate();
|
||||||
|
|
||||||
|
if(pageAdapter.getCount() < 2){
|
||||||
|
tabLayout.setVisibility(View.GONE);
|
||||||
|
}else{
|
||||||
|
tabLayout.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldShowComments() {
|
||||||
|
try {
|
||||||
|
return showComments && NewPipe.getService(serviceId)
|
||||||
|
.getServiceInfo()
|
||||||
|
.getMediaCapabilities()
|
||||||
|
.contains(COMMENTS);
|
||||||
|
} catch (ExtractionException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -995,24 +967,6 @@ public class VideoDetailFragment
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private View getSeparatorView() {
|
|
||||||
View separator = new View(activity);
|
|
||||||
LinearLayout.LayoutParams params =
|
|
||||||
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1);
|
|
||||||
int m8 = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
|
|
||||||
int m5 = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
|
|
||||||
params.setMargins(m8, m5, m8, m5);
|
|
||||||
separator.setLayoutParams(params);
|
|
||||||
|
|
||||||
TypedValue typedValue = new TypedValue();
|
|
||||||
activity.getTheme().resolveAttribute(R.attr.separator_color, typedValue, true);
|
|
||||||
separator.setBackgroundColor(typedValue.data);
|
|
||||||
|
|
||||||
return separator;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setHeightThumbnail() {
|
private void setHeightThumbnail() {
|
||||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||||
boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||||
|
@ -1024,49 +978,8 @@ public class VideoDetailFragment
|
||||||
thumbnailImageView.setMinimumHeight(height);
|
thumbnailImageView.setMinimumHeight(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showContentWithAnimation(long duration,
|
private void showContent() {
|
||||||
long delay,
|
AnimationUtils.slideUp(contentRootLayoutHiding,120, 96, 0.06f);
|
||||||
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
|
|
||||||
int translationY = (int) (getResources().getDisplayMetrics().heightPixels *
|
|
||||||
(translationPercent > 0.0f ? translationPercent : .06f));
|
|
||||||
|
|
||||||
contentRootLayoutHiding.animate().setListener(null).cancel();
|
|
||||||
contentRootLayoutHiding.setAlpha(0f);
|
|
||||||
contentRootLayoutHiding.setTranslationY(translationY);
|
|
||||||
contentRootLayoutHiding.setVisibility(View.VISIBLE);
|
|
||||||
contentRootLayoutHiding.animate()
|
|
||||||
.alpha(1f)
|
|
||||||
.translationY(0)
|
|
||||||
.setStartDelay(delay)
|
|
||||||
.setDuration(duration)
|
|
||||||
.setInterpolator(new FastOutSlowInInterpolator())
|
|
||||||
.start();
|
|
||||||
|
|
||||||
uploaderRootLayout.animate().setListener(null).cancel();
|
|
||||||
uploaderRootLayout.setAlpha(0f);
|
|
||||||
uploaderRootLayout.setTranslationY(translationY);
|
|
||||||
uploaderRootLayout.setVisibility(View.VISIBLE);
|
|
||||||
uploaderRootLayout.animate()
|
|
||||||
.alpha(1f)
|
|
||||||
.translationY(0)
|
|
||||||
.setStartDelay((long) (duration * .5f) + delay)
|
|
||||||
.setDuration(duration)
|
|
||||||
.setInterpolator(new FastOutSlowInInterpolator())
|
|
||||||
.start();
|
|
||||||
|
|
||||||
if (showRelatedStreams) {
|
|
||||||
relatedStreamRootLayout.animate().setListener(null).cancel();
|
|
||||||
relatedStreamRootLayout.setAlpha(0f);
|
|
||||||
relatedStreamRootLayout.setTranslationY(translationY);
|
|
||||||
relatedStreamRootLayout.setVisibility(View.VISIBLE);
|
|
||||||
relatedStreamRootLayout.animate()
|
|
||||||
.alpha(1f)
|
|
||||||
.translationY(0)
|
|
||||||
.setStartDelay((long) (duration * .8f) + delay)
|
|
||||||
.setDuration(duration)
|
|
||||||
.setInterpolator(new FastOutSlowInInterpolator())
|
|
||||||
.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setInitialData(int serviceId, String url, String name) {
|
protected void setInitialData(int serviceId, String url, String name) {
|
||||||
|
@ -1101,7 +1014,7 @@ public class VideoDetailFragment
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
|
|
||||||
animateView(contentRootLayoutHiding, false, 200);
|
contentRootLayoutHiding.setVisibility(View.INVISIBLE);
|
||||||
animateView(spinnerToolbar, false, 200);
|
animateView(spinnerToolbar, false, 200);
|
||||||
animateView(thumbnailPlayButton, false, 50);
|
animateView(thumbnailPlayButton, false, 50);
|
||||||
animateView(detailDurationView, false, 100);
|
animateView(detailDurationView, false, 100);
|
||||||
|
@ -1111,18 +1024,18 @@ public class VideoDetailFragment
|
||||||
animateView(videoTitleTextView, true, 0);
|
animateView(videoTitleTextView, true, 0);
|
||||||
|
|
||||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||||
if (videoTitleToggleArrow != null) { //phone
|
|
||||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||||
videoTitleToggleArrow.setVisibility(View.GONE);
|
videoTitleToggleArrow.setVisibility(View.GONE);
|
||||||
} else { //tablet
|
|
||||||
final View related = (View) relatedStreamRootLayout.getParent();
|
|
||||||
//don`t need to hide it if related streams are disabled
|
|
||||||
if (related.getVisibility() == View.VISIBLE) {
|
|
||||||
related.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
videoTitleRoot.setClickable(false);
|
videoTitleRoot.setClickable(false);
|
||||||
|
|
||||||
|
if(relatedStreamsLayout != null){
|
||||||
|
if(showRelatedStreams){
|
||||||
|
relatedStreamsLayout.setVisibility(View.INVISIBLE);
|
||||||
|
}else{
|
||||||
|
relatedStreamsLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
imageLoader.cancelDisplayTask(thumbnailImageView);
|
imageLoader.cancelDisplayTask(thumbnailImageView);
|
||||||
imageLoader.cancelDisplayTask(uploaderThumb);
|
imageLoader.cancelDisplayTask(uploaderThumb);
|
||||||
thumbnailImageView.setImageBitmap(null);
|
thumbnailImageView.setImageBitmap(null);
|
||||||
|
@ -1134,7 +1047,20 @@ public class VideoDetailFragment
|
||||||
super.handleResult(info);
|
super.handleResult(info);
|
||||||
|
|
||||||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||||
pushToStack(serviceId, url, name);
|
|
||||||
|
if(showRelatedStreams){
|
||||||
|
if(null == relatedStreamsLayout){ //phone
|
||||||
|
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo));
|
||||||
|
pageAdapter.notifyDataSetUpdate();
|
||||||
|
}else{ //tablet
|
||||||
|
getChildFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(currentInfo))
|
||||||
|
.commitNow();
|
||||||
|
relatedStreamsLayout.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//pushToStack(serviceId, url, name);
|
||||||
|
|
||||||
animateView(thumbnailPlayButton, true, 200);
|
animateView(thumbnailPlayButton, true, 200);
|
||||||
videoTitleTextView.setText(name);
|
videoTitleTextView.setText(name);
|
||||||
|
@ -1185,25 +1111,23 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
if (info.getDuration() > 0) {
|
if (info.getDuration() > 0) {
|
||||||
detailDurationView.setText(Localization.getDurationString(info.getDuration()));
|
detailDurationView.setText(Localization.getDurationString(info.getDuration()));
|
||||||
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.duration_background_color));
|
detailDurationView.setBackgroundColor(
|
||||||
|
ContextCompat.getColor(activity, R.color.duration_background_color));
|
||||||
animateView(detailDurationView, true, 100);
|
animateView(detailDurationView, true, 100);
|
||||||
} else if (info.getStreamType() == StreamType.LIVE_STREAM) {
|
} else if (info.getStreamType() == StreamType.LIVE_STREAM) {
|
||||||
detailDurationView.setText(R.string.duration_live);
|
detailDurationView.setText(R.string.duration_live);
|
||||||
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.live_duration_background_color));
|
detailDurationView.setBackgroundColor(
|
||||||
|
ContextCompat.getColor(activity, R.color.live_duration_background_color));
|
||||||
animateView(detailDurationView, true, 100);
|
animateView(detailDurationView, true, 100);
|
||||||
} else {
|
} else {
|
||||||
detailDurationView.setVisibility(View.GONE);
|
detailDurationView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
videoDescriptionView.setVisibility(View.GONE);
|
videoDescriptionView.setVisibility(View.GONE);
|
||||||
if (videoTitleToggleArrow != null) {
|
|
||||||
videoTitleRoot.setClickable(true);
|
videoTitleRoot.setClickable(true);
|
||||||
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
||||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||||
videoDescriptionRootLayout.setVisibility(View.GONE);
|
videoDescriptionRootLayout.setVisibility(View.GONE);
|
||||||
} else {
|
|
||||||
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(info.getUploadDate())) {
|
if (!TextUtils.isEmpty(info.getUploadDate())) {
|
||||||
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
|
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
|
||||||
}
|
}
|
||||||
|
@ -1212,11 +1136,6 @@ public class VideoDetailFragment
|
||||||
animateView(spinnerToolbar, true, 500);
|
animateView(spinnerToolbar, true, 500);
|
||||||
setupActionBar(info);
|
setupActionBar(info);
|
||||||
initThumbnailViews(info);
|
initThumbnailViews(info);
|
||||||
initRelatedVideos(info);
|
|
||||||
if (wasRelatedStreamsExpanded) {
|
|
||||||
toggleExpandRelatedVideos(currentInfo);
|
|
||||||
wasRelatedStreamsExpanded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName());
|
setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName());
|
||||||
setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||||
|
@ -1251,11 +1170,6 @@ public class VideoDetailFragment
|
||||||
// Only auto play in the first open
|
// Only auto play in the first open
|
||||||
autoPlayEnabled = false;
|
autoPlayEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ViewParent related = relatedStreamRootLayout.getParent();
|
|
||||||
if (related instanceof ScrollView) {
|
|
||||||
((ScrollView) related).scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1269,10 +1183,18 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Toast.makeText(activity,
|
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||||
R.string.could_not_setup_download_menu,
|
ServiceList.all()
|
||||||
Toast.LENGTH_LONG).show();
|
.get(currentInfo
|
||||||
e.printStackTrace();
|
.getServiceId())
|
||||||
|
.getServiceInfo()
|
||||||
|
.getName(), "",
|
||||||
|
R.string.could_not_setup_download_menu);
|
||||||
|
|
||||||
|
ErrorActivity.reportError(getActivity(),
|
||||||
|
e,
|
||||||
|
getActivity().getClass(),
|
||||||
|
getActivity().findViewById(android.R.id.content), info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1314,13 +1236,4 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
|
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRelatedStreamsVisibility(int visibility) {
|
|
||||||
final ViewParent parent = relatedStreamRootLayout.getParent();
|
|
||||||
if (parent instanceof ScrollView) {
|
|
||||||
((ScrollView) parent).setVisibility(visibility);
|
|
||||||
} else {
|
|
||||||
relatedStreamRootLayout.setVisibility(visibility);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.view.View;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
|
@ -220,6 +221,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
||||||
|
@Override
|
||||||
|
public void selected(CommentsInfoItem selectedItem) {
|
||||||
|
onItemSelected(selectedItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
itemsList.clearOnScrollListeners();
|
itemsList.clearOnScrollListeners();
|
||||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -233,10 +233,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
openRssFeed();
|
openRssFeed();
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_openInBrowser:
|
case R.id.menu_item_openInBrowser:
|
||||||
openUrlInBrowser(url);
|
openUrlInBrowser(currentInfo.getOriginalUrl());
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_share:
|
case R.id.menu_item_share:
|
||||||
shareUrl(name, url);
|
shareUrl(name, currentInfo.getOriginalUrl());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.comments;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||||
|
|
||||||
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Views
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private boolean mIsVisibleToUser = false;
|
||||||
|
|
||||||
|
public static CommentsFragment getInstance(int serviceId, String url, String name) {
|
||||||
|
CommentsFragment instance = new CommentsFragment();
|
||||||
|
instance.setInitialData(serviceId, url, name);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||||
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
|
mIsVisibleToUser = isVisibleToUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Load and handle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
|
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<CommentsInfo> loadResult(boolean forceLoad) {
|
||||||
|
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Contract
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showLoading() {
|
||||||
|
super.showLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResult(@NonNull CommentsInfo result) {
|
||||||
|
super.handleResult(result);
|
||||||
|
|
||||||
|
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
|
||||||
|
|
||||||
|
if (!result.getErrors().isEmpty()) {
|
||||||
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||||
|
super.handleNextItems(result);
|
||||||
|
|
||||||
|
if (!result.getErrors().isEmpty()) {
|
||||||
|
showSnackBarError(result.getErrors(),
|
||||||
|
UserAction.REQUESTED_COMMENTS,
|
||||||
|
NewPipe.getNameOfService(serviceId),
|
||||||
|
"Get next page of: " + url,
|
||||||
|
R.string.general_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// OnError
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onError(Throwable exception) {
|
||||||
|
if (super.onError(exception)) return true;
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTitle(String title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isGridLayout() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.TooltipCompat;
|
import android.support.v7.widget.TooltipCompat;
|
||||||
|
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
@ -39,15 +40,15 @@ import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||||
|
import org.schabi.newpipe.util.FireTvUtils;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.Constants;
|
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
|
@ -72,8 +73,8 @@ import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import io.reactivex.subjects.PublishSubject;
|
import io.reactivex.subjects.PublishSubject;
|
||||||
|
|
||||||
|
import static android.support.v7.widget.helper.ItemTouchHelper.Callback.makeMovementFlags;
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class SearchFragment
|
public class SearchFragment
|
||||||
|
@ -104,8 +105,13 @@ public class SearchFragment
|
||||||
// this three represet the current search query
|
// this three represet the current search query
|
||||||
@State
|
@State
|
||||||
protected String searchString;
|
protected String searchString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No content filter should add like contentfilter = all
|
||||||
|
* be aware of this when implementing an extractor.
|
||||||
|
*/
|
||||||
@State
|
@State
|
||||||
protected String[] contentFilter;
|
protected String[] contentFilter = new String[0];
|
||||||
@State
|
@State
|
||||||
protected String sortFilter;
|
protected String sortFilter;
|
||||||
|
|
||||||
|
@ -292,7 +298,23 @@ public class SearchFragment
|
||||||
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
|
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
|
||||||
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
|
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
|
||||||
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
|
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
|
||||||
suggestionsRecyclerView.setLayoutManager(new LayoutManagerSmoothScroller(activity));
|
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||||
|
@Override
|
||||||
|
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
return getSuggestionMovementFlags(recyclerView, viewHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
|
||||||
|
@NonNull RecyclerView.ViewHolder viewHolder1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
|
||||||
|
onSuggestionItemSwiped(viewHolder, i);
|
||||||
|
}
|
||||||
|
}).attachToRecyclerView(suggestionsRecyclerView);
|
||||||
|
|
||||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||||
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||||
|
@ -335,7 +357,7 @@ public class SearchFragment
|
||||||
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||||
search(!TextUtils.isEmpty(searchString)
|
search(!TextUtils.isEmpty(searchString)
|
||||||
? searchString
|
? searchString
|
||||||
: searchEditText.getText().toString(), new String[0], "");
|
: searchEditText.getText().toString(), this.contentFilter, "");
|
||||||
} else {
|
} else {
|
||||||
if (searchEditText != null) {
|
if (searchEditText != null) {
|
||||||
searchEditText.setText("");
|
searchEditText.setText("");
|
||||||
|
@ -449,6 +471,9 @@ public class SearchFragment
|
||||||
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||||
showSuggestionsPanel();
|
showSuggestionsPanel();
|
||||||
}
|
}
|
||||||
|
if(FireTvUtils.isFireTv()){
|
||||||
|
showKeyboardSearch();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
||||||
|
@ -499,7 +524,9 @@ public class SearchFragment
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||||
}
|
}
|
||||||
if (event != null
|
if(actionId == EditorInfo.IME_ACTION_PREVIOUS){
|
||||||
|
hideKeyboardSearch();
|
||||||
|
} else if (event != null
|
||||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||||
search(searchEditText.getText().toString(), new String[0], "");
|
search(searchEditText.getText().toString(), new String[0], "");
|
||||||
|
@ -541,7 +568,7 @@ public class SearchFragment
|
||||||
if (searchEditText.requestFocus()) {
|
if (searchEditText.requestFocus()) {
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
||||||
Context.INPUT_METHOD_SERVICE);
|
Context.INPUT_METHOD_SERVICE);
|
||||||
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
|
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,8 +578,7 @@ public class SearchFragment
|
||||||
|
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
||||||
Context.INPUT_METHOD_SERVICE);
|
Context.INPUT_METHOD_SERVICE);
|
||||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
|
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
||||||
InputMethodManager.HIDE_NOT_ALWAYS);
|
|
||||||
|
|
||||||
searchEditText.clearFocus();
|
searchEditText.clearFocus();
|
||||||
}
|
}
|
||||||
|
@ -736,6 +762,7 @@ public class SearchFragment
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void loadMoreItems() {
|
protected void loadMoreItems() {
|
||||||
|
if(nextPageUrl == null || nextPageUrl.isEmpty()) return;
|
||||||
isLoading.set(true);
|
isLoading.set(true);
|
||||||
showListFooter(true);
|
showListFooter(true);
|
||||||
if (searchDisposable != null) searchDisposable.dispose();
|
if (searchDisposable != null) searchDisposable.dispose();
|
||||||
|
@ -890,4 +917,28 @@ public class SearchFragment
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Suggestion item touch helper
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
public int getSuggestionMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
final int position = viewHolder.getAdapterPosition();
|
||||||
|
final SuggestionItem item = suggestionListAdapter.getItem(position);
|
||||||
|
return item.fromHistory ? makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSuggestionItemSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
|
||||||
|
final int position = viewHolder.getAdapterPosition();
|
||||||
|
final String query = suggestionListAdapter.getItem(position).query;
|
||||||
|
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
howManyDeleted -> suggestionPublisher
|
||||||
|
.onNext(searchEditText.getText().toString()),
|
||||||
|
throwable -> showSnackBarError(throwable,
|
||||||
|
UserAction.DELETE_FROM_HISTORY, "none",
|
||||||
|
"Deleting item failed", R.string.general_error));
|
||||||
|
disposables.add(onDelete);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private SuggestionItem getItem(int position) {
|
SuggestionItem getItem(int position) {
|
||||||
return items.get(position);
|
return items.get(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
package org.schabi.newpipe.fragments.list.videos;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.Switch;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
|
import org.schabi.newpipe.util.RelatedStreamInfo;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
|
||||||
|
|
||||||
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
private RelatedStreamInfo relatedStreamInfo;
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Views
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
private View headerRootLayout;
|
||||||
|
private Switch aSwitch;
|
||||||
|
|
||||||
|
private boolean mIsVisibleToUser = false;
|
||||||
|
|
||||||
|
public static RelatedVideosFragment getInstance(StreamInfo info) {
|
||||||
|
RelatedVideosFragment instance = new RelatedVideosFragment();
|
||||||
|
instance.setInitialData(info);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||||
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
|
mIsVisibleToUser = isVisibleToUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_related_streams, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected View getListHeader(){
|
||||||
|
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
|
||||||
|
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
|
||||||
|
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
|
||||||
|
|
||||||
|
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||||
|
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||||
|
aSwitch.setChecked(autoplay);
|
||||||
|
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||||
|
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
||||||
|
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
|
||||||
|
prefEdit.apply();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return headerRootLayout;
|
||||||
|
}else{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
|
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<RelatedStreamInfo> loadResult(boolean forceLoad) {
|
||||||
|
return Single.fromCallable(() -> relatedStreamInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Contract
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showLoading() {
|
||||||
|
super.showLoading();
|
||||||
|
if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResult(@NonNull RelatedStreamInfo result) {
|
||||||
|
|
||||||
|
super.handleResult(result);
|
||||||
|
|
||||||
|
if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE);
|
||||||
|
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
|
||||||
|
|
||||||
|
if (!result.getErrors().isEmpty()) {
|
||||||
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||||
|
super.handleNextItems(result);
|
||||||
|
|
||||||
|
if (!result.getErrors().isEmpty()) {
|
||||||
|
showSnackBarError(result.getErrors(),
|
||||||
|
UserAction.REQUESTED_STREAM,
|
||||||
|
NewPipe.getNameOfService(serviceId),
|
||||||
|
"Get next page of: " + url,
|
||||||
|
R.string.general_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// OnError
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onError(Throwable exception) {
|
||||||
|
if (super.onError(exception)) return true;
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTitle(String title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInitialData(StreamInfo info) {
|
||||||
|
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||||
|
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static final String INFO_KEY = "related_info_key";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.putSerializable(INFO_KEY, relatedStreamInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
|
||||||
|
super.onRestoreInstanceState(savedState);
|
||||||
|
if (savedState != null) {
|
||||||
|
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||||
|
if(serializable instanceof RelatedStreamInfo){
|
||||||
|
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
|
||||||
|
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||||
|
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||||
|
if(null != aSwitch) aSwitch.setChecked(autoplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isGridLayout() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,10 +10,13 @@ import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
|
@ -50,6 +53,7 @@ public class InfoItemBuilder {
|
||||||
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
||||||
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
||||||
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
||||||
|
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
|
||||||
|
|
||||||
public InfoItemBuilder(Context context) {
|
public InfoItemBuilder(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
@ -73,6 +77,8 @@ public class InfoItemBuilder {
|
||||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
|
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
|
||||||
case PLAYLIST:
|
case PLAYLIST:
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
|
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
|
||||||
|
case COMMENT:
|
||||||
|
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent);
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Trollolo");
|
Log.e(TAG, "Trollolo");
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||||
|
@ -111,4 +117,12 @@ public class InfoItemBuilder {
|
||||||
this.onPlaylistSelectedListener = listener;
|
this.onPlaylistSelectedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
|
||||||
|
return onCommentsSelectedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
|
||||||
|
this.onCommentsSelectedListener = onCommentsSelectedListener;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,13 @@ import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||||
|
@ -63,6 +66,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
|
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
|
||||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||||
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
||||||
|
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
|
||||||
|
private static final int COMMENT_HOLDER_TYPE = 0x401;
|
||||||
|
|
||||||
private final InfoItemBuilder infoItemBuilder;
|
private final InfoItemBuilder infoItemBuilder;
|
||||||
private final ArrayList<InfoItem> infoItemList;
|
private final ArrayList<InfoItem> infoItemList;
|
||||||
|
@ -98,6 +103,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
infoItemBuilder.setOnPlaylistSelectedListener(listener);
|
infoItemBuilder.setOnPlaylistSelectedListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> listener) {
|
||||||
|
infoItemBuilder.setOnCommentsSelectedListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
public void useMiniItemVariants(boolean useMiniVariant) {
|
public void useMiniItemVariants(boolean useMiniVariant) {
|
||||||
this.useMiniVariant = useMiniVariant;
|
this.useMiniVariant = useMiniVariant;
|
||||||
}
|
}
|
||||||
|
@ -223,6 +232,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
||||||
case PLAYLIST:
|
case PLAYLIST:
|
||||||
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
|
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
|
||||||
|
case COMMENT:
|
||||||
|
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Trollolo");
|
Log.e(TAG, "Trollolo");
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -231,7 +242,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case HEADER_TYPE:
|
case HEADER_TYPE:
|
||||||
return new HFHolder(header);
|
return new HFHolder(header);
|
||||||
|
@ -255,6 +267,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||||
case GRID_PLAYLIST_HOLDER_TYPE:
|
case GRID_PLAYLIST_HOLDER_TYPE:
|
||||||
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
|
||||||
|
case MINI_COMMENT_HOLDER_TYPE:
|
||||||
|
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
|
case COMMENT_HOLDER_TYPE:
|
||||||
|
return new CommentsInfoItemHolder(infoItemBuilder, parent);
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Trollolo");
|
Log.e(TAG, "Trollolo");
|
||||||
return new FallbackViewHolder(new View(parent.getContext()));
|
return new FallbackViewHolder(new View(parent.getContext()));
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Christian Schabesberger on 12.02.17.
|
||||||
|
*
|
||||||
|
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||||
|
* ChannelInfoItemHolder .java is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe 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.
|
||||||
|
*
|
||||||
|
* NewPipe 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
||||||
|
|
||||||
|
public final TextView itemTitleView;
|
||||||
|
|
||||||
|
public CommentsInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, R.layout.list_comments_item, parent);
|
||||||
|
|
||||||
|
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final InfoItem infoItem) {
|
||||||
|
super.updateFromItem(infoItem);
|
||||||
|
|
||||||
|
if (!(infoItem instanceof CommentsInfoItem)) return;
|
||||||
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
|
itemTitleView.setText(item.getAuthorName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
import de.hdodenhof.circleimageview.CircleImageView;
|
||||||
|
|
||||||
|
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
public final CircleImageView itemThumbnailView;
|
||||||
|
private final TextView itemContentView;
|
||||||
|
private final TextView itemLikesCountView;
|
||||||
|
private final TextView itemDislikesCountView;
|
||||||
|
private final TextView itemPublishedTime;
|
||||||
|
|
||||||
|
private static final int commentDefaultLines = 2;
|
||||||
|
private static final int commentExpandedLines = 1000;
|
||||||
|
|
||||||
|
CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
|
||||||
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
|
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
|
||||||
|
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
|
||||||
|
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
|
||||||
|
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final InfoItem infoItem) {
|
||||||
|
if (!(infoItem instanceof CommentsInfoItem)) return;
|
||||||
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
|
itemBuilder.getImageLoader()
|
||||||
|
.displayImage(item.getAuthorThumbnail(),
|
||||||
|
itemThumbnailView,
|
||||||
|
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
|
||||||
|
itemThumbnailView.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
try {
|
||||||
|
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
||||||
|
NavigationHelper.openChannelFragment(
|
||||||
|
activity.getSupportFragmentManager(),
|
||||||
|
item.getServiceId(),
|
||||||
|
item.getAuthorEndpoint(),
|
||||||
|
item.getAuthorName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ellipsize if not already ellipsized
|
||||||
|
if (null == itemContentView.getEllipsize()) {
|
||||||
|
itemContentView.setEllipsize(TextUtils.TruncateAt.END);
|
||||||
|
itemContentView.setMaxLines(commentDefaultLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
itemContentView.setText(item.getCommentText());
|
||||||
|
if (null != item.getLikeCount()) {
|
||||||
|
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
|
||||||
|
}
|
||||||
|
itemPublishedTime.setText(item.getPublishedTime());
|
||||||
|
|
||||||
|
itemView.setOnClickListener(view -> {
|
||||||
|
toggleEllipsize(item.getCommentText());
|
||||||
|
if (itemBuilder.getOnCommentsSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnCommentsSelectedListener().selected(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleEllipsize(String text) {
|
||||||
|
// toggle ellipsize
|
||||||
|
if (null == itemContentView.getEllipsize()) {
|
||||||
|
itemContentView.setEllipsize(TextUtils.TruncateAt.END);
|
||||||
|
itemContentView.setMaxLines(commentDefaultLines);
|
||||||
|
} else {
|
||||||
|
itemContentView.setEllipsize(null);
|
||||||
|
itemContentView.setMaxLines(commentExpandedLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,11 @@ import android.os.Parcelable;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
@ -21,11 +25,13 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.settings.SettingsActivity;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
@ -104,6 +110,12 @@ public class StatisticsPlaylistFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
inflater.inflate(R.menu.menu_history, menu);
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment LifeCycle - Views
|
// Fragment LifeCycle - Views
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -155,6 +167,53 @@ public class StatisticsPlaylistFragment
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.action_history_clear:
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.delete_view_history_alert)
|
||||||
|
.setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||||
|
.setPositiveButton(R.string.delete, ((dialog, which) -> {
|
||||||
|
final Disposable onDelete = recordManager.deleteWholeStreamHistory()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
howManyDeleted -> Toast.makeText(getContext(),
|
||||||
|
R.string.view_history_deleted,
|
||||||
|
Toast.LENGTH_SHORT).show(),
|
||||||
|
throwable -> ErrorActivity.reportError(getContext(),
|
||||||
|
throwable,
|
||||||
|
SettingsActivity.class, null,
|
||||||
|
ErrorActivity.ErrorInfo.make(
|
||||||
|
UserAction.DELETE_FROM_HISTORY,
|
||||||
|
"none",
|
||||||
|
"Delete view history",
|
||||||
|
R.string.general_error)));
|
||||||
|
|
||||||
|
final Disposable onClearOrphans = recordManager.removeOrphanedRecords()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
howManyDeleted -> {},
|
||||||
|
throwable -> ErrorActivity.reportError(getContext(),
|
||||||
|
throwable,
|
||||||
|
SettingsActivity.class, null,
|
||||||
|
ErrorActivity.ErrorInfo.make(
|
||||||
|
UserAction.DELETE_FROM_HISTORY,
|
||||||
|
"none",
|
||||||
|
"Delete search history",
|
||||||
|
R.string.general_error)));
|
||||||
|
disposables.add(onClearOrphans);
|
||||||
|
disposables.add(onDelete);
|
||||||
|
}))
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment LifeCycle - Loading
|
// Fragment LifeCycle - Loading
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -147,11 +147,16 @@ public class SubscriptionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
|
private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
|
||||||
return info.getUrl().equals(entity.getUrl()) &&
|
return equalsAndNotNull(info.getUrl(), entity.getUrl()) &&
|
||||||
info.getServiceId() == entity.getServiceId() &&
|
info.getServiceId() == entity.getServiceId() &&
|
||||||
info.getName().equals(entity.getName()) &&
|
info.getName().equals(entity.getName()) &&
|
||||||
info.getAvatarUrl().equals(entity.getAvatarUrl()) &&
|
equalsAndNotNull(info.getAvatarUrl(), entity.getAvatarUrl()) &&
|
||||||
info.getDescription().equals(entity.getDescription()) &&
|
equalsAndNotNull(info.getDescription(), entity.getDescription()) &&
|
||||||
info.getSubscriberCount() == entity.getSubscriberCount();
|
info.getSubscriberCount() == entity.getSubscriberCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean equalsAndNotNull(final Object o1, final Object o2) {
|
||||||
|
return (o1 != null && o2 != null)
|
||||||
|
&& o1.equals(o2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.ContextWrapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixes a leak caused by AudioManager using an Activity context.
|
||||||
|
* Tracked at https://android-review.googlesource.com/#/c/140481/1 and
|
||||||
|
* https://github.com/square/leakcanary/issues/205
|
||||||
|
* Source:
|
||||||
|
* https://gist.github.com/jankovd/891d96f476f7a9ce24e2
|
||||||
|
*/
|
||||||
|
public class AudioServiceLeakFix extends ContextWrapper {
|
||||||
|
|
||||||
|
AudioServiceLeakFix(Context base) {
|
||||||
|
super(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ContextWrapper preventLeakOf(Context base) {
|
||||||
|
return new AudioServiceLeakFix(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getSystemService(String name) {
|
||||||
|
if (Context.AUDIO_SERVICE.equals(name)) {
|
||||||
|
return getApplicationContext().getSystemService(name);
|
||||||
|
}
|
||||||
|
return super.getSystemService(name);
|
||||||
|
}
|
||||||
|
}
|
|
@ -130,6 +130,11 @@ public final class BackgroundPlayer extends Service {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attachBaseContext(Context base) {
|
||||||
|
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
return mBinder;
|
return mBinder;
|
||||||
|
|
|
@ -241,6 +241,11 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||||
isBackPressed = false;
|
isBackPressed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attachBaseContext(Context newBase) {
|
||||||
|
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase));
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// State Saving
|
// State Saving
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
|
@ -181,6 +181,11 @@ public final class PopupVideoPlayer extends Service {
|
||||||
closePopup();
|
closePopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attachBaseContext(Context base) {
|
||||||
|
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
return mBinder;
|
return mBinder;
|
||||||
|
@ -626,6 +631,7 @@ public final class PopupVideoPlayer extends Service {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||||
|
if (playerImpl == null) return;
|
||||||
// rebuild notification here since remote view does not release bitmaps,
|
// rebuild notification here since remote view does not release bitmaps,
|
||||||
// causing memory leaks
|
// causing memory leaks
|
||||||
resetNotification();
|
resetNotification();
|
||||||
|
|
|
@ -131,7 +131,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
|
||||||
private void onAudioFocusLossCanDuck() {
|
private void onAudioFocusLossCanDuck() {
|
||||||
Log.d(TAG, "onAudioFocusLossCanDuck() called");
|
Log.d(TAG, "onAudioFocusLossCanDuck() called");
|
||||||
// Set the volume to 1/10 on ducking
|
// Set the volume to 1/10 on ducking
|
||||||
animateAudio(player.getVolume(), DUCK_AUDIO_TO);
|
player.setVolume(DUCK_AUDIO_TO);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void animateAudio(final float from, final float to) {
|
private void animateAudio(final float from, final float to) {
|
||||||
|
|
|
@ -45,7 +45,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD
|
||||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||||
|
|
||||||
public class PlayerHelper {
|
public class PlayerHelper {
|
||||||
private PlayerHelper() {}
|
private PlayerHelper() {}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package org.schabi.newpipe.player.playback;
|
package org.schabi.newpipe.player.playback;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
|
|
||||||
import org.schabi.newpipe.player.BasePlayer;
|
import org.schabi.newpipe.player.BasePlayer;
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||||
|
@ -54,6 +56,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
|
||||||
.setTitle(item.getTitle())
|
.setTitle(item.getTitle())
|
||||||
.setSubtitle(item.getUploader());
|
.setSubtitle(item.getUploader());
|
||||||
|
|
||||||
|
// set additional metadata for A2DP/AVRCP
|
||||||
|
Bundle additionalMetadata = new Bundle();
|
||||||
|
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
||||||
|
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration());
|
||||||
|
descriptionBuilder.setExtras(additionalMetadata);
|
||||||
|
|
||||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
||||||
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);
|
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
@ -93,6 +94,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
// Below are auxiliary media sources
|
// Below are auxiliary media sources
|
||||||
|
|
||||||
// Create subtitle sources
|
// Create subtitle sources
|
||||||
|
if(info.getSubtitles() != null) {
|
||||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
||||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
||||||
if (mimeType == null) continue;
|
if (mimeType == null) continue;
|
||||||
|
@ -103,6 +105,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||||
mediaSources.add(textSource);
|
mediaSources.add(textSource);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaSources.size() == 1) {
|
if (mediaSources.size() == 1) {
|
||||||
return mediaSources.get(0);
|
return mediaSources.get(0);
|
||||||
|
|
|
@ -15,6 +15,7 @@ public enum UserAction {
|
||||||
REQUESTED_CHANNEL("requested channel"),
|
REQUESTED_CHANNEL("requested channel"),
|
||||||
REQUESTED_PLAYLIST("requested playlist"),
|
REQUESTED_PLAYLIST("requested playlist"),
|
||||||
REQUESTED_KIOSK("requested kiosk"),
|
REQUESTED_KIOSK("requested kiosk"),
|
||||||
|
REQUESTED_COMMENTS("requested comments"),
|
||||||
DELETE_FROM_HISTORY("delete from history"),
|
DELETE_FROM_HISTORY("delete from history"),
|
||||||
PLAY_STREAM("Play stream");
|
PLAY_STREAM("Play stream");
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Bundle;
|
||||||
import android.support.v7.preference.Preference;
|
import android.support.v7.preference.Preference;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
|
import org.schabi.newpipe.CheckForNewAppVersionTask;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
public class MainSettingsFragment extends BasePreferenceFragment {
|
public class MainSettingsFragment extends BasePreferenceFragment {
|
||||||
|
@ -13,6 +14,13 @@ public class MainSettingsFragment extends BasePreferenceFragment {
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
addPreferencesFromResource(R.xml.main_settings);
|
addPreferencesFromResource(R.xml.main_settings);
|
||||||
|
|
||||||
|
if (!CheckForNewAppVersionTask.isGithubApk()) {
|
||||||
|
final Preference update = findPreference(getString(R.string.update_pref_screen_key));
|
||||||
|
getPreferenceScreen().removePreference(update);
|
||||||
|
|
||||||
|
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply();
|
||||||
|
}
|
||||||
|
|
||||||
if (!DEBUG) {
|
if (!DEBUG) {
|
||||||
final Preference debug = findPreference(getString(R.string.debug_pref_screen_key));
|
final Preference debug = findPreference(getString(R.string.debug_pref_screen_key));
|
||||||
getPreferenceScreen().removePreference(debug);
|
getPreferenceScreen().removePreference(debug);
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.preference.Preference;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.CheckForNewAppVersionTask;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
|
public class UpdateSettingsFragment extends BasePreferenceFragment {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
String updateToggleKey = getString(R.string.update_app_key);
|
||||||
|
findPreference(updateToggleKey).setOnPreferenceChangeListener(updatePreferenceChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
addPreferencesFromResource(R.xml.update_settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Preference.OnPreferenceChangeListener updatePreferenceChange
|
||||||
|
= (preference, newValue) -> {
|
||||||
|
|
||||||
|
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key),
|
||||||
|
(boolean) newValue).apply();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import android.animation.ArgbEvaluator;
|
||||||
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.support.annotation.ColorInt;
|
import android.support.annotation.ColorInt;
|
||||||
|
import android.support.annotation.FloatRange;
|
||||||
import android.support.v4.view.ViewCompat;
|
import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -363,4 +364,24 @@ public class AnimationUtils {
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void slideUp(final View view,
|
||||||
|
long duration,
|
||||||
|
long delay,
|
||||||
|
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
|
||||||
|
int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels *
|
||||||
|
(translationPercent));
|
||||||
|
|
||||||
|
view.animate().setListener(null).cancel();
|
||||||
|
view.setAlpha(0f);
|
||||||
|
view.setTranslationY(translationY);
|
||||||
|
view.setVisibility(View.VISIBLE);
|
||||||
|
view.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.translationY(0)
|
||||||
|
.setStartDelay(delay)
|
||||||
|
.setDuration(duration)
|
||||||
|
.setInterpolator(new FastOutSlowInInterpolator())
|
||||||
|
.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,12 @@ import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.ReCaptchaActivity;
|
import org.schabi.newpipe.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.extractor.Info;
|
import org.schabi.newpipe.extractor.Info;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.SuggestionExtractor;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
|
@ -46,6 +48,7 @@ import org.schabi.newpipe.report.UserAction;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.Maybe;
|
import io.reactivex.Maybe;
|
||||||
|
@ -95,17 +98,20 @@ public final class ExtractorHelper {
|
||||||
public static Single<List<String>> suggestionsFor(final int serviceId,
|
public static Single<List<String>> suggestionsFor(final int serviceId,
|
||||||
final String query) {
|
final String query) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return Single.fromCallable(() ->
|
return Single.fromCallable(() -> {
|
||||||
NewPipe.getService(serviceId)
|
SuggestionExtractor extractor = NewPipe.getService(serviceId)
|
||||||
.getSuggestionExtractor()
|
.getSuggestionExtractor();
|
||||||
.suggestionList(query));
|
return extractor != null
|
||||||
|
? extractor.suggestionList(query)
|
||||||
|
: Collections.emptyList();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Single<StreamInfo> getStreamInfo(final int serviceId,
|
public static Single<StreamInfo> getStreamInfo(final int serviceId,
|
||||||
final String url,
|
final String url,
|
||||||
boolean forceLoad) {
|
boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
|
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM, Single.fromCallable(() ->
|
||||||
StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
|
StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +119,7 @@ public final class ExtractorHelper {
|
||||||
final String url,
|
final String url,
|
||||||
boolean forceLoad) {
|
boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
|
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL, Single.fromCallable(() ->
|
||||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,11 +131,27 @@ public final class ExtractorHelper {
|
||||||
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
|
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
|
||||||
|
final String url,
|
||||||
|
boolean forceLoad) {
|
||||||
|
checkServiceId(serviceId);
|
||||||
|
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT, Single.fromCallable(() ->
|
||||||
|
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Single<InfoItemsPage> getMoreCommentItems(final int serviceId,
|
||||||
|
final CommentsInfo info,
|
||||||
|
final String nextPageUrl) {
|
||||||
|
checkServiceId(serviceId);
|
||||||
|
return Single.fromCallable(() ->
|
||||||
|
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPageUrl));
|
||||||
|
}
|
||||||
|
|
||||||
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
|
||||||
final String url,
|
final String url,
|
||||||
boolean forceLoad) {
|
boolean forceLoad) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
|
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
|
||||||
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +166,7 @@ public final class ExtractorHelper {
|
||||||
public static Single<KioskInfo> getKioskInfo(final int serviceId,
|
public static Single<KioskInfo> getKioskInfo(final int serviceId,
|
||||||
final String url,
|
final String url,
|
||||||
boolean forceLoad) {
|
boolean forceLoad) {
|
||||||
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
|
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
|
||||||
KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,16 +190,17 @@ public final class ExtractorHelper {
|
||||||
private static <I extends Info> Single<I> checkCache(boolean forceLoad,
|
private static <I extends Info> Single<I> checkCache(boolean forceLoad,
|
||||||
int serviceId,
|
int serviceId,
|
||||||
String url,
|
String url,
|
||||||
|
InfoItem.InfoType infoType,
|
||||||
Single<I> loadFromNetwork) {
|
Single<I> loadFromNetwork) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info));
|
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info, infoType));
|
||||||
|
|
||||||
Single<I> load;
|
Single<I> load;
|
||||||
if (forceLoad) {
|
if (forceLoad) {
|
||||||
cache.removeInfo(serviceId, url);
|
cache.removeInfo(serviceId, url, infoType);
|
||||||
load = loadFromNetwork;
|
load = loadFromNetwork;
|
||||||
} else {
|
} else {
|
||||||
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url),
|
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
|
||||||
loadFromNetwork.toMaybe())
|
loadFromNetwork.toMaybe())
|
||||||
.firstElement() //Take the first valid
|
.firstElement() //Take the first valid
|
||||||
.toSingle();
|
.toSingle();
|
||||||
|
@ -189,11 +212,11 @@ public final class ExtractorHelper {
|
||||||
/**
|
/**
|
||||||
* Default implementation uses the {@link InfoCache} to get cached results
|
* Default implementation uses the {@link InfoCache} to get cached results
|
||||||
*/
|
*/
|
||||||
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url) {
|
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url, InfoItem.InfoType infoType) {
|
||||||
checkServiceId(serviceId);
|
checkServiceId(serviceId);
|
||||||
return Maybe.defer(() -> {
|
return Maybe.defer(() -> {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
I info = (I) cache.getFromKey(serviceId, url);
|
I info = (I) cache.getFromKey(serviceId, url, infoType);
|
||||||
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
|
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
|
||||||
|
|
||||||
// Only return info if it's not null (it is cached)
|
// Only return info if it's not null (it is cached)
|
||||||
|
|
10
app/src/main/java/org/schabi/newpipe/util/FireTvUtils.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
|
|
||||||
|
public class FireTvUtils {
|
||||||
|
public static boolean isFireTv(){
|
||||||
|
final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv";
|
||||||
|
return App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import android.util.Log;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.extractor.Info;
|
import org.schabi.newpipe.extractor.Info;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -52,27 +53,27 @@ public final class InfoCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public Info getFromKey(int serviceId, @NonNull String url) {
|
public Info getFromKey(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
|
||||||
if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
|
if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||||
synchronized (lruCache) {
|
synchronized (lruCache) {
|
||||||
return getInfo(keyOf(serviceId, url));
|
return getInfo(keyOf(serviceId, url, infoType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) {
|
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info, @NonNull InfoItem.InfoType infoType) {
|
||||||
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
|
||||||
|
|
||||||
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
|
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
|
||||||
synchronized (lruCache) {
|
synchronized (lruCache) {
|
||||||
final CacheData data = new CacheData(info, expirationMillis);
|
final CacheData data = new CacheData(info, expirationMillis);
|
||||||
lruCache.put(keyOf(serviceId, url), data);
|
lruCache.put(keyOf(serviceId, url, infoType), data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeInfo(int serviceId, @NonNull String url) {
|
public void removeInfo(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
|
||||||
if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
|
if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
|
||||||
synchronized (lruCache) {
|
synchronized (lruCache) {
|
||||||
lruCache.remove(keyOf(serviceId, url));
|
lruCache.remove(keyOf(serviceId, url, infoType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,8 +99,8 @@ public final class InfoCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static String keyOf(final int serviceId, @NonNull final String url) {
|
private static String keyOf(final int serviceId, @NonNull final String url, @NonNull InfoItem.InfoType infoType) {
|
||||||
return serviceId + url;
|
return serviceId + url + infoType.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void removeStaleCache() {
|
private static void removeStaleCache() {
|
||||||
|
|
|
@ -31,6 +31,8 @@ public class KioskTranslator {
|
||||||
return c.getString(R.string.top_50);
|
return c.getString(R.string.top_50);
|
||||||
case "New & hot":
|
case "New & hot":
|
||||||
return c.getString(R.string.new_and_hot);
|
return c.getString(R.string.new_and_hot);
|
||||||
|
case "conferences":
|
||||||
|
return c.getString(R.string.conferences);
|
||||||
default:
|
default:
|
||||||
return kioskId;
|
return kioskId;
|
||||||
}
|
}
|
||||||
|
@ -44,6 +46,8 @@ public class KioskTranslator {
|
||||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||||
case "New & hot":
|
case "New & hot":
|
||||||
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||||
|
case "conferences":
|
||||||
|
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
|
||||||
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
||||||
import org.schabi.newpipe.local.feed.FeedFragment;
|
import org.schabi.newpipe.local.feed.FeedFragment;
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||||
|
@ -309,6 +310,18 @@ public class NavigationHelper {
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void openCommentsFragment(
|
||||||
|
FragmentManager fragmentManager,
|
||||||
|
int serviceId,
|
||||||
|
String url,
|
||||||
|
String name) {
|
||||||
|
if (name == null) name = "";
|
||||||
|
fragmentManager.beginTransaction().setCustomAnimations(R.anim.switch_service_in, R.anim.switch_service_out)
|
||||||
|
.replace(R.id.fragment_holder, CommentsFragment.getInstance(serviceId, url, name))
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
public static void openPlaylistFragment(FragmentManager fragmentManager,
|
public static void openPlaylistFragment(FragmentManager fragmentManager,
|
||||||
int serviceId,
|
int serviceId,
|
||||||
String url,
|
String url,
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RelatedStreamInfo extends ListInfo<InfoItem> {
|
||||||
|
|
||||||
|
private StreamInfoItem nextStream;
|
||||||
|
|
||||||
|
public RelatedStreamInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) {
|
||||||
|
super(serviceId, listUrlIdHandler, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RelatedStreamInfo getInfo(StreamInfo info) {
|
||||||
|
ListLinkHandler handler = new ListLinkHandler(info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null);
|
||||||
|
RelatedStreamInfo relatedStreamInfo = new RelatedStreamInfo(info.getServiceId(), handler, info.getName());
|
||||||
|
List<InfoItem> streams = new ArrayList<>();
|
||||||
|
if(info.getNextVideo() != null){
|
||||||
|
streams.add(info.getNextVideo());
|
||||||
|
}
|
||||||
|
streams.addAll(info.getRelatedStreams());
|
||||||
|
relatedStreamInfo.setRelatedItems(streams);
|
||||||
|
relatedStreamInfo.setNextStream(info.getNextVideo());
|
||||||
|
return relatedStreamInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamInfoItem getNextStream() {
|
||||||
|
return nextStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNextStream(StreamInfoItem nextStream) {
|
||||||
|
this.nextStream = nextStream;
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,6 @@ public class SecondaryStreamHelper<T extends Stream> {
|
||||||
* @return selected audio stream or null if a candidate was not found
|
* @return selected audio stream or null if a candidate was not found
|
||||||
*/
|
*/
|
||||||
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
|
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
|
||||||
// TODO: check if m4v and m4a selected streams are DASH compliant
|
|
||||||
switch (videoStream.getFormat()) {
|
switch (videoStream.getFormat()) {
|
||||||
case WEBM:
|
case WEBM:
|
||||||
case MPEG_4:
|
case MPEG_4:
|
||||||
|
|
|
@ -24,9 +24,11 @@ public class ServiceHelper {
|
||||||
case 0:
|
case 0:
|
||||||
return R.drawable.place_holder_youtube;
|
return R.drawable.place_holder_youtube;
|
||||||
case 1:
|
case 1:
|
||||||
return R.drawable.place_holder_circle;
|
return R.drawable.place_holder_cloud;
|
||||||
|
case 2:
|
||||||
|
return R.drawable.place_holder_gadse;
|
||||||
default:
|
default:
|
||||||
return R.drawable.service;
|
return R.drawable.place_holder_circle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +40,8 @@ public class ServiceHelper {
|
||||||
case "playlists": return c.getString(R.string.playlists);
|
case "playlists": return c.getString(R.string.playlists);
|
||||||
case "tracks": return c.getString(R.string.tracks);
|
case "tracks": return c.getString(R.string.tracks);
|
||||||
case "users": return c.getString(R.string.users);
|
case "users": return c.getString(R.string.users);
|
||||||
|
case "conferences" : return c.getString(R.string.conferences);
|
||||||
|
case "events" : return c.getString(R.string.events);
|
||||||
default: return filter;
|
default: return filter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,8 @@ import io.reactivex.schedulers.Schedulers;
|
||||||
import us.shandian.giga.util.Utility;
|
import us.shandian.giga.util.Utility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}.
|
* A list adapter for a list of {@link Stream streams},
|
||||||
|
* currently supporting {@link VideoStream}, {@link AudioStream} and {@link SubtitlesStream}
|
||||||
*/
|
*/
|
||||||
public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseAdapter {
|
public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseAdapter {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
@ -110,7 +111,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (stream instanceof AudioStream) {
|
} else if (stream instanceof AudioStream) {
|
||||||
qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps";
|
AudioStream audioStream = ((AudioStream) stream);
|
||||||
|
qualityString = audioStream.getAverageBitrate() > 0
|
||||||
|
? audioStream.getAverageBitrate() + "kbps"
|
||||||
|
: audioStream.getFormat().getName();
|
||||||
} else if (stream instanceof SubtitlesStream) {
|
} else if (stream instanceof SubtitlesStream) {
|
||||||
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
|
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
|
||||||
if (((SubtitlesStream) stream).isAutoGenerated()) {
|
if (((SubtitlesStream) stream).isAutoGenerated()) {
|
||||||
|
@ -154,8 +158,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
|
||||||
private final long[] streamSizes;
|
private final long[] streamSizes;
|
||||||
private final String unknownSize;
|
private final String unknownSize;
|
||||||
|
|
||||||
public StreamSizeWrapper(List<T> streamsList, Context context) {
|
public StreamSizeWrapper(List<T> sL, Context context) {
|
||||||
this.streamsList = streamsList;
|
this.streamsList = sL != null
|
||||||
|
? sL
|
||||||
|
: Collections.emptyList();
|
||||||
this.streamSizes = new long[streamsList.size()];
|
this.streamSizes = new long[streamsList.size()];
|
||||||
this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content);
|
this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content);
|
||||||
|
|
||||||
|
|
|
@ -137,7 +137,9 @@ public class ThemeHelper {
|
||||||
else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme";
|
else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme";
|
||||||
|
|
||||||
themeName += "." + service.getServiceInfo().getName();
|
themeName += "." + service.getServiceInfo().getName();
|
||||||
int resourceId = context.getResources().getIdentifier(themeName, "style", context.getPackageName());
|
int resourceId = context
|
||||||
|
.getResources()
|
||||||
|
.getIdentifier(themeName, "style", context.getPackageName());
|
||||||
|
|
||||||
if (resourceId > 0) {
|
if (resourceId > 0) {
|
||||||
return resourceId;
|
return resourceId;
|
||||||
|
|
|
@ -156,7 +156,6 @@ public class DownloadInitializer extends Thread {
|
||||||
|
|
||||||
if (retryCount++ > mMission.maxRetry) {
|
if (retryCount++ > mMission.maxRetry) {
|
||||||
Log.e(TAG, "initializer failed", e);
|
Log.e(TAG, "initializer failed", e);
|
||||||
mMission.running = false;
|
|
||||||
mMission.notifyError(e);
|
mMission.notifyError(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ public class DownloadMission extends Mission {
|
||||||
public static final int ERROR_SSL_EXCEPTION = 1004;
|
public static final int ERROR_SSL_EXCEPTION = 1004;
|
||||||
public static final int ERROR_UNKNOWN_HOST = 1005;
|
public static final int ERROR_UNKNOWN_HOST = 1005;
|
||||||
public static final int ERROR_CONNECT_HOST = 1006;
|
public static final int ERROR_CONNECT_HOST = 1006;
|
||||||
public static final int ERROR_POSTPROCESSING_FAILED = 1007;
|
public static final int ERROR_POSTPROCESSING = 1007;
|
||||||
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
||||||
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
||||||
|
|
||||||
|
@ -79,9 +79,12 @@ public class DownloadMission extends Mission {
|
||||||
public String postprocessingName;
|
public String postprocessingName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the post-processing algorithm is actually running, used to detect corrupt downloads
|
* Indicates if the post-processing state:
|
||||||
|
* 0: ready
|
||||||
|
* 1: running
|
||||||
|
* 2: completed
|
||||||
*/
|
*/
|
||||||
public boolean postprocessingRunning;
|
public int postprocessingState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate if the post-processing algorithm works on the same file
|
* Indicate if the post-processing algorithm works on the same file
|
||||||
|
@ -356,7 +359,7 @@ public class DownloadMission extends Mission {
|
||||||
finishCount++;
|
finishCount++;
|
||||||
|
|
||||||
if (finishCount == currentThreadCount) {
|
if (finishCount == currentThreadCount) {
|
||||||
if (errCode > ERROR_NOTHING) return;
|
if (errCode != ERROR_NOTHING) return;
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onFinish" + (current + 1) + "/" + urls.length);
|
Log.d(TAG, "onFinish" + (current + 1) + "/" + urls.length);
|
||||||
|
@ -382,19 +385,26 @@ public class DownloadMission extends Mission {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyPostProcessing(boolean processing) {
|
private void notifyPostProcessing(int state) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, (processing ? "enter" : "exit") + " postprocessing on " + location + File.separator + name);
|
String action;
|
||||||
|
switch (state) {
|
||||||
|
case 1:
|
||||||
|
action = "Running";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
action = "Completed";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
action = "Failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (blockState) {
|
synchronized (blockState) {
|
||||||
if (!processing) {
|
|
||||||
postprocessingName = null;
|
|
||||||
postprocessingArgs = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't return without fully write the current state
|
// don't return without fully write the current state
|
||||||
postprocessingRunning = processing;
|
postprocessingState = state;
|
||||||
Utility.writeToFile(metadata, DownloadMission.this);
|
Utility.writeToFile(metadata, DownloadMission.this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -403,16 +413,30 @@ public class DownloadMission extends Mission {
|
||||||
* Start downloading with multiple threads.
|
* Start downloading with multiple threads.
|
||||||
*/
|
*/
|
||||||
public void start() {
|
public void start() {
|
||||||
if (running || current >= urls.length) return;
|
if (running || isFinished()) return;
|
||||||
|
|
||||||
// ensure that the previous state is completely paused.
|
// ensure that the previous state is completely paused.
|
||||||
joinForThread(init);
|
joinForThread(init);
|
||||||
|
if (threads != null)
|
||||||
for (Thread thread : threads) joinForThread(thread);
|
for (Thread thread : threads) joinForThread(thread);
|
||||||
|
|
||||||
enqueued = false;
|
enqueued = false;
|
||||||
running = true;
|
running = true;
|
||||||
errCode = ERROR_NOTHING;
|
errCode = ERROR_NOTHING;
|
||||||
|
|
||||||
|
if (current >= urls.length && postprocessingName != null) {
|
||||||
|
runAsync(1, () -> {
|
||||||
|
if (doPostprocessing()) {
|
||||||
|
running = false;
|
||||||
|
deleteThisFromFile();
|
||||||
|
|
||||||
|
notify(DownloadManagerService.MESSAGE_FINISHED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (blocks < 0) {
|
if (blocks < 0) {
|
||||||
initializer();
|
initializer();
|
||||||
return;
|
return;
|
||||||
|
@ -420,7 +444,7 @@ public class DownloadMission extends Mission {
|
||||||
|
|
||||||
init = null;
|
init = null;
|
||||||
|
|
||||||
if (threads.length < 1) {
|
if (threads == null || threads.length < 1) {
|
||||||
threads = new Thread[currentThreadCount];
|
threads = new Thread[currentThreadCount];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,18 +468,18 @@ public class DownloadMission extends Mission {
|
||||||
public synchronized void pause() {
|
public synchronized void pause() {
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
|
|
||||||
running = false;
|
if (isPsRunning()) {
|
||||||
recovered = true;
|
|
||||||
enqueued = false;
|
|
||||||
|
|
||||||
if (postprocessingRunning) {
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.w(TAG, "pause during post-processing is not applicable.");
|
Log.w(TAG, "pause during post-processing is not applicable.");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (init != null && init.isAlive()) {
|
running = false;
|
||||||
|
recovered = true;
|
||||||
|
enqueued = false;
|
||||||
|
|
||||||
|
if (init != null && Thread.currentThread() != init && init.isAlive()) {
|
||||||
init.interrupt();
|
init.interrupt();
|
||||||
synchronized (blockState) {
|
synchronized (blockState) {
|
||||||
resetState();
|
resetState();
|
||||||
|
@ -532,13 +556,36 @@ public class DownloadMission extends Mission {
|
||||||
mWritingToFile = false;
|
mWritingToFile = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the download if fully finished
|
||||||
|
*
|
||||||
|
* @return true, otherwise, false
|
||||||
|
*/
|
||||||
public boolean isFinished() {
|
public boolean isFinished() {
|
||||||
return current >= urls.length && postprocessingName == null;
|
return current >= urls.length && (postprocessingName == null || postprocessingState == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the download file is corrupt due a failed post-processing
|
||||||
|
*
|
||||||
|
* @return {@code true} if this mission is unrecoverable
|
||||||
|
*/
|
||||||
|
public boolean isPsFailed() {
|
||||||
|
return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if a post-processing algorithm is running
|
||||||
|
*
|
||||||
|
* @return true, otherwise, false
|
||||||
|
*/
|
||||||
|
public boolean isPsRunning() {
|
||||||
|
return postprocessingName != null && postprocessingState == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getLength() {
|
public long getLength() {
|
||||||
long calculated;
|
long calculated;
|
||||||
if (postprocessingRunning) {
|
if (postprocessingState == 1) {
|
||||||
calculated = length;
|
calculated = length;
|
||||||
} else {
|
} else {
|
||||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||||
|
@ -550,16 +597,19 @@ public class DownloadMission extends Mission {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean doPostprocessing() {
|
private boolean doPostprocessing() {
|
||||||
if (postprocessingName == null) return true;
|
if (postprocessingName == null || postprocessingState == 2) return true;
|
||||||
|
|
||||||
try {
|
notifyPostProcessing(1);
|
||||||
notifyPostProcessing(true);
|
|
||||||
notifyProgress(0);
|
notifyProgress(0);
|
||||||
|
|
||||||
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
|
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
|
||||||
|
|
||||||
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, this);
|
Exception exception = null;
|
||||||
algorithm.run();
|
|
||||||
|
try {
|
||||||
|
Postprocessing
|
||||||
|
.getAlgorithm(postprocessingName, this)
|
||||||
|
.run();
|
||||||
} catch (Exception err) {
|
} catch (Exception err) {
|
||||||
StringBuilder args = new StringBuilder(" ");
|
StringBuilder args = new StringBuilder(" ");
|
||||||
if (postprocessingArgs != null) {
|
if (postprocessingArgs != null) {
|
||||||
|
@ -571,15 +621,21 @@ public class DownloadMission extends Mission {
|
||||||
}
|
}
|
||||||
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
|
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
|
||||||
|
|
||||||
notifyError(ERROR_POSTPROCESSING_FAILED, err);
|
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
||||||
return false;
|
|
||||||
|
exception = err;
|
||||||
} finally {
|
} finally {
|
||||||
notifyPostProcessing(false);
|
notifyPostProcessing(errCode == ERROR_NOTHING ? 2 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errCode != ERROR_NOTHING) notify(DownloadManagerService.MESSAGE_ERROR);
|
if (errCode != ERROR_NOTHING) {
|
||||||
|
if (exception == null) exception = errObject;
|
||||||
|
notifyError(ERROR_POSTPROCESSING, exception);
|
||||||
|
|
||||||
return errCode == ERROR_NOTHING;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean deleteThisFromFile() {
|
private boolean deleteThisFromFile() {
|
||||||
|
|
|
@ -13,9 +13,7 @@ import us.shandian.giga.get.DownloadMission;
|
||||||
class Mp4DashMuxer extends Postprocessing {
|
class Mp4DashMuxer extends Postprocessing {
|
||||||
|
|
||||||
Mp4DashMuxer(DownloadMission mission) {
|
Mp4DashMuxer(DownloadMission mission) {
|
||||||
super(mission);
|
super(mission, 15360 * 1024/* 15 MiB */, true);
|
||||||
recommendedReserve = 15360 * 1024;// 15 MiB
|
|
||||||
worksOnSameFile = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
136
app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
|
import android.media.MediaCodec.BufferInfo;
|
||||||
|
import android.media.MediaExtractor;
|
||||||
|
import android.media.MediaMuxer;
|
||||||
|
import android.media.MediaMuxer.OutputFormat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import us.shandian.giga.get.DownloadMission;
|
||||||
|
|
||||||
|
|
||||||
|
class Mp4Muxer extends Postprocessing {
|
||||||
|
private static final String TAG = "Mp4Muxer";
|
||||||
|
private static final int NOTIFY_BYTES_INTERVAL = 128 * 1024;// 128 KiB
|
||||||
|
|
||||||
|
Mp4Muxer(DownloadMission mission) {
|
||||||
|
super(mission, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||||
|
File dlFile = mission.getDownloadedFile();
|
||||||
|
File tmpFile = new File(mission.location, mission.name.concat(".tmp"));
|
||||||
|
|
||||||
|
if (tmpFile.exists())
|
||||||
|
if (!tmpFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
|
||||||
|
|
||||||
|
if (!tmpFile.createNewFile()) return DownloadMission.ERROR_FILE_CREATION;
|
||||||
|
|
||||||
|
FileInputStream source = null;
|
||||||
|
MediaMuxer muxer = null;
|
||||||
|
|
||||||
|
//noinspection TryFinallyCanBeTryWithResources
|
||||||
|
try {
|
||||||
|
source = new FileInputStream(dlFile);
|
||||||
|
MediaExtractor tracks[] = {
|
||||||
|
getMediaExtractor(source, mission.offsets[0], mission.offsets[1] - mission.offsets[0]),
|
||||||
|
getMediaExtractor(source, mission.offsets[1], mission.length - mission.offsets[1])
|
||||||
|
};
|
||||||
|
|
||||||
|
muxer = new MediaMuxer(tmpFile.getAbsolutePath(), OutputFormat.MUXER_OUTPUT_MPEG_4);
|
||||||
|
|
||||||
|
int tracksIndex[] = {
|
||||||
|
muxer.addTrack(tracks[0].getTrackFormat(0)),
|
||||||
|
muxer.addTrack(tracks[1].getTrackFormat(0))
|
||||||
|
};
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(512 * 1024);// 512 KiB
|
||||||
|
BufferInfo info = new BufferInfo();
|
||||||
|
|
||||||
|
long written = 0;
|
||||||
|
long nextReport = NOTIFY_BYTES_INTERVAL;
|
||||||
|
|
||||||
|
muxer.start();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
int done = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < tracks.length; i++) {
|
||||||
|
if (tracksIndex[i] < 0) continue;
|
||||||
|
|
||||||
|
info.set(0,
|
||||||
|
tracks[i].readSampleData(buffer, 0),
|
||||||
|
tracks[i].getSampleTime(),
|
||||||
|
tracks[i].getSampleFlags()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (info.size >= 0) {
|
||||||
|
muxer.writeSampleData(tracksIndex[i], buffer, info);
|
||||||
|
written += info.size;
|
||||||
|
done++;
|
||||||
|
}
|
||||||
|
if (!tracks[i].advance()) {
|
||||||
|
// EOF reached
|
||||||
|
tracks[i].release();
|
||||||
|
tracksIndex[i] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (written > nextReport) {
|
||||||
|
nextReport = written + NOTIFY_BYTES_INTERVAL;
|
||||||
|
super.progressReport(written);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (done < 1) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this part should not fail
|
||||||
|
if (!dlFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
|
||||||
|
if (!tmpFile.renameTo(dlFile)) return DownloadMission.ERROR_FILE_CREATION;
|
||||||
|
|
||||||
|
return OK_RESULT;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (muxer != null) {
|
||||||
|
muxer.stop();
|
||||||
|
muxer.release();
|
||||||
|
}
|
||||||
|
} catch (Exception err) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.e(TAG, "muxer stop/release failed", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source != null) {
|
||||||
|
try {
|
||||||
|
source.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the operation fails, delete the temporal file
|
||||||
|
if (tmpFile.exists()) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
tmpFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaExtractor getMediaExtractor(FileInputStream source, long offset, long length) throws IOException {
|
||||||
|
MediaExtractor extractor = new MediaExtractor();
|
||||||
|
extractor.setDataSource(source.getFD(), offset, length);
|
||||||
|
extractor.selectTrack(0);
|
||||||
|
|
||||||
|
return extractor;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,21 +18,21 @@ public abstract class Postprocessing {
|
||||||
|
|
||||||
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||||
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
|
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
|
||||||
|
public static final String ALGORITHM_MP4_MUXER = "mp4";
|
||||||
public static final String ALGORITHM_WEBM_MUXER = "webm";
|
public static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||||
private static final String ALGORITHM_TEST_ALGO = "test";
|
|
||||||
|
|
||||||
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
|
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
|
||||||
if (null == algorithmName) {
|
if (null == algorithmName) {
|
||||||
throw new NullPointerException("algorithmName");
|
throw new NullPointerException("algorithmName");
|
||||||
} else switch (algorithmName) {
|
} else switch (algorithmName) {
|
||||||
case ALGORITHM_TTML_CONVERTER:
|
case ALGORITHM_TTML_CONVERTER:
|
||||||
return new TttmlConverter(mission);
|
return new TtmlConverter(mission);
|
||||||
case ALGORITHM_MP4_DASH_MUXER:
|
case ALGORITHM_MP4_DASH_MUXER:
|
||||||
return new Mp4DashMuxer(mission);
|
return new Mp4DashMuxer(mission);
|
||||||
|
case ALGORITHM_MP4_MUXER:
|
||||||
|
return new Mp4Muxer(mission);
|
||||||
case ALGORITHM_WEBM_MUXER:
|
case ALGORITHM_WEBM_MUXER:
|
||||||
return new WebMMuxer(mission);
|
return new WebMMuxer(mission);
|
||||||
case ALGORITHM_TEST_ALGO:
|
|
||||||
return new TestAlgo(mission);
|
|
||||||
/*case "example-algorithm":
|
/*case "example-algorithm":
|
||||||
return new ExampleAlgorithm(mission);*/
|
return new ExampleAlgorithm(mission);*/
|
||||||
default:
|
default:
|
||||||
|
@ -52,17 +52,28 @@ public abstract class Postprocessing {
|
||||||
*/
|
*/
|
||||||
public int recommendedReserve;
|
public int recommendedReserve;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the download to post-process
|
||||||
|
*/
|
||||||
protected DownloadMission mission;
|
protected DownloadMission mission;
|
||||||
|
|
||||||
Postprocessing(DownloadMission mission) {
|
Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
|
||||||
this.mission = mission;
|
this.mission = mission;
|
||||||
|
this.recommendedReserve = recommendedReserve;
|
||||||
|
this.worksOnSameFile = worksOnSameFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void run() throws IOException {
|
public void run() throws IOException {
|
||||||
File file = mission.getDownloadedFile();
|
File file = mission.getDownloadedFile();
|
||||||
CircularFile out = null;
|
CircularFile out = null;
|
||||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
int result;
|
||||||
|
long finalLength = -1;
|
||||||
|
|
||||||
|
mission.done = 0;
|
||||||
|
mission.length = file.length();
|
||||||
|
|
||||||
|
if (worksOnSameFile) {
|
||||||
|
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||||
try {
|
try {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (; i < sources.length - 1; i++) {
|
for (; i < sources.length - 1; i++) {
|
||||||
|
@ -87,27 +98,12 @@ public abstract class Postprocessing {
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
out = new CircularFile(file, 0, this::progressReport, checker);
|
out = new CircularFile(file, 0, this::progressReport, checker);
|
||||||
|
|
||||||
mission.done = 0;
|
result = process(out, sources);
|
||||||
mission.length = file.length();
|
|
||||||
|
|
||||||
int result = process(out, sources);
|
if (result == OK_RESULT)
|
||||||
|
finalLength = out.finalizeFile();
|
||||||
if (result == OK_RESULT) {
|
|
||||||
long finalLength = out.finalizeFile();
|
|
||||||
mission.done = finalLength;
|
|
||||||
mission.length = finalLength;
|
|
||||||
} else {
|
|
||||||
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
|
||||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result != OK_RESULT && worksOnSameFile) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
new File(mission.location, mission.name).delete();
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
for (SharpStream source : sources) {
|
for (SharpStream source : sources) {
|
||||||
if (source != null && !source.isDisposed()) {
|
if (source != null && !source.isDisposed()) {
|
||||||
|
@ -118,6 +114,23 @@ public abstract class Postprocessing {
|
||||||
out.dispose();
|
out.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
result = process(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == OK_RESULT) {
|
||||||
|
if (finalLength < 0) finalLength = file.length();
|
||||||
|
mission.done = finalLength;
|
||||||
|
mission.length = finalLength;
|
||||||
|
} else {
|
||||||
|
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||||
|
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != OK_RESULT && worksOnSameFile) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,7 +151,7 @@ public abstract class Postprocessing {
|
||||||
return mission.postprocessingArgs[index];
|
return mission.postprocessingArgs[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void progressReport(long done) {
|
void progressReport(long done) {
|
||||||
mission.done = done;
|
mission.done = done;
|
||||||
if (mission.length < mission.done) mission.length = mission.done;
|
if (mission.length < mission.done) mission.length = mission.done;
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
package us.shandian.giga.postprocessing;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Algorithm for testing proposes
|
|
||||||
*/
|
|
||||||
class TestAlgo extends Postprocessing {
|
|
||||||
|
|
||||||
public TestAlgo(DownloadMission mission) {
|
|
||||||
super(mission);
|
|
||||||
|
|
||||||
worksOnSameFile = true;
|
|
||||||
recommendedReserve = 4096 * 1024;// 4 KiB
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
|
||||||
|
|
||||||
int written = 0;
|
|
||||||
int size = 5 * 1024 * 1024;// 5 MiB
|
|
||||||
byte[] buffer = new byte[8 * 1024];//8 KiB
|
|
||||||
mission.length = size;
|
|
||||||
|
|
||||||
Random rnd = new Random();
|
|
||||||
|
|
||||||
// only write random data
|
|
||||||
sources[0].dispose();
|
|
||||||
|
|
||||||
while (written < size) {
|
|
||||||
rnd.nextBytes(buffer);
|
|
||||||
|
|
||||||
int read = Math.min(buffer.length, size - written);
|
|
||||||
out.write(buffer, 0, read);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep((int) (Math.random() * 10));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
written += read;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Postprocessing.OK_RESULT;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,13 +18,12 @@ import us.shandian.giga.postprocessing.io.SharpInputStream;
|
||||||
/**
|
/**
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
class TttmlConverter extends Postprocessing {
|
class TtmlConverter extends Postprocessing {
|
||||||
private static final String TAG = "TttmlConverter";
|
private static final String TAG = "TtmlConverter";
|
||||||
|
|
||||||
TttmlConverter(DownloadMission mission) {
|
TtmlConverter(DownloadMission mission) {
|
||||||
super(mission);
|
// due how XmlPullParser works, the xml is fully loaded on the ram
|
||||||
recommendedReserve = 0;// due how XmlPullParser works, the xml is fully loaded on the ram
|
super(mission, 0, true);
|
||||||
worksOnSameFile = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
|
@ -15,9 +15,7 @@ import us.shandian.giga.get.DownloadMission;
|
||||||
class WebMMuxer extends Postprocessing {
|
class WebMMuxer extends Postprocessing {
|
||||||
|
|
||||||
WebMMuxer(DownloadMission mission) {
|
WebMMuxer(DownloadMission mission) {
|
||||||
super(mission);
|
super(mission, 2048 * 1024/* 2 MiB */, true);
|
||||||
recommendedReserve = 2048 * 1024;// 2 MiB
|
|
||||||
worksOnSameFile = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -141,15 +141,18 @@ public class DownloadManager {
|
||||||
File dl = mis.getDownloadedFile();
|
File dl = mis.getDownloadedFile();
|
||||||
boolean exists = dl.exists();
|
boolean exists = dl.exists();
|
||||||
|
|
||||||
if (mis.postprocessingRunning && mis.postprocessingThis) {
|
if (mis.isPsRunning()) {
|
||||||
|
if (mis.postprocessingThis) {
|
||||||
// Incomplete post-processing results in a corrupted download file
|
// Incomplete post-processing results in a corrupted download file
|
||||||
// because the selected algorithm works on the same file to save space.
|
// because the selected algorithm works on the same file to save space.
|
||||||
if (!dl.delete()) {
|
if (exists && dl.isFile() && !dl.delete())
|
||||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||||
}
|
|
||||||
exists = true;
|
exists = true;
|
||||||
mis.postprocessingRunning = false;
|
}
|
||||||
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_FAILED;
|
|
||||||
|
mis.postprocessingState = 0;
|
||||||
|
mis.errCode = DownloadMission.ERROR_POSTPROCESSING;
|
||||||
mis.errObject = new RuntimeException("stopped unexpectedly");
|
mis.errObject = new RuntimeException("stopped unexpectedly");
|
||||||
} else if (exists && !dl.isFile()) {
|
} else if (exists && !dl.isFile()) {
|
||||||
// probably a folder, this should never happens
|
// probably a folder, this should never happens
|
||||||
|
@ -332,7 +335,7 @@ public class DownloadManager {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (mission.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && !mission.isFinished())
|
if (mission.running && !mission.isFinished() && !mission.isPsFailed())
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -471,7 +474,7 @@ public class DownloadManager {
|
||||||
boolean flag = false;
|
boolean flag = false;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (mission.running && mission.isFinished() && !mission.postprocessingRunning) {
|
if (mission.running && !mission.isFinished() && !mission.isPsRunning()) {
|
||||||
flag = true;
|
flag = true;
|
||||||
mission.pause();
|
mission.pause();
|
||||||
}
|
}
|
||||||
|
@ -528,6 +531,8 @@ public class DownloadManager {
|
||||||
ArrayList<Object> current;
|
ArrayList<Object> current;
|
||||||
ArrayList<Mission> hidden;
|
ArrayList<Mission> hidden;
|
||||||
|
|
||||||
|
boolean hasFinished = false;
|
||||||
|
|
||||||
private MissionIterator() {
|
private MissionIterator() {
|
||||||
hidden = new ArrayList<>(2);
|
hidden = new ArrayList<>(2);
|
||||||
current = null;
|
current = null;
|
||||||
|
@ -563,6 +568,7 @@ public class DownloadManager {
|
||||||
list.addAll(finished);
|
list.addAll(finished);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasFinished = finished.size() > 0;
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
@ -637,6 +643,10 @@ public class DownloadManager {
|
||||||
hidden.remove(mission);
|
hidden.remove(mission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasFinishedMissions() {
|
||||||
|
return hasFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getOldListSize() {
|
public int getOldListSize() {
|
||||||
|
|
|
@ -50,6 +50,7 @@ import us.shandian.giga.ui.common.Deleter;
|
||||||
import us.shandian.giga.ui.common.ProgressDrawable;
|
import us.shandian.giga.ui.common.ProgressDrawable;
|
||||||
import us.shandian.giga.util.Utility;
|
import us.shandian.giga.util.Utility;
|
||||||
|
|
||||||
|
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||||
import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
|
import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
|
||||||
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
|
import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
|
||||||
|
@ -59,7 +60,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
|
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
|
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_FAILED;
|
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
|
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
|
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
|
||||||
|
@ -67,7 +68,8 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST;
|
||||||
public class MissionAdapter extends Adapter<ViewHolder> {
|
public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
|
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
|
||||||
private static final String TAG = "MissionAdapter";
|
private static final String TAG = "MissionAdapter";
|
||||||
private static final String UNDEFINED_SPEED = "--.-%";
|
private static final String UNDEFINED_PROGRESS = "--.-%";
|
||||||
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ALGORITHMS.put(R.id.md5, "MD5");
|
ALGORITHMS.put(R.id.md5, "MD5");
|
||||||
|
@ -158,7 +160,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
str = R.string.missions_header_pending;
|
str = R.string.missions_header_pending;
|
||||||
} else {
|
} else {
|
||||||
str = R.string.missions_header_finished;
|
str = R.string.missions_header_finished;
|
||||||
setClearButtonVisibility(true);
|
if (mClear != null) mClear.setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
((ViewHolderHeader) view).header.setText(str);
|
((ViewHolderHeader) view).header.setText(str);
|
||||||
|
@ -178,7 +180,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
if (h.item.mission instanceof DownloadMission) {
|
if (h.item.mission instanceof DownloadMission) {
|
||||||
DownloadMission mission = (DownloadMission) item.mission;
|
DownloadMission mission = (DownloadMission) item.mission;
|
||||||
String length = Utility.formatBytes(mission.getLength());
|
String length = Utility.formatBytes(mission.getLength());
|
||||||
if (mission.running && !mission.postprocessingRunning) length += " --.- kB/s";
|
if (mission.running && !mission.isPsRunning()) length += " --.- kB/s";
|
||||||
|
|
||||||
h.size.setText(length);
|
h.size.setText(length);
|
||||||
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
|
h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause);
|
||||||
|
@ -238,11 +240,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
if (Float.isNaN(progress) || Float.isInfinite(progress))
|
h.progress.setProgress(isNotFinite(progress) ? 1f : progress);
|
||||||
h.progress.setProgress(1f);
|
|
||||||
h.status.setText(R.string.msg_error);
|
h.status.setText(R.string.msg_error);
|
||||||
} else if (Float.isNaN(progress) || Float.isInfinite(progress)) {
|
} else if (isNotFinite(progress)) {
|
||||||
h.status.setText(UNDEFINED_SPEED);
|
h.status.setText(UNDEFINED_PROGRESS);
|
||||||
} else {
|
} else {
|
||||||
h.status.setText(String.format("%.2f%%", progress * 100));
|
h.status.setText(String.format("%.2f%%", progress * 100));
|
||||||
h.progress.setProgress(progress);
|
h.progress.setProgress(progress);
|
||||||
|
@ -251,11 +252,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
long length = mission.getLength();
|
long length = mission.getLength();
|
||||||
|
|
||||||
int state;
|
int state;
|
||||||
if (mission.errCode == ERROR_POSTPROCESSING_FAILED) {
|
if (mission.isPsFailed()) {
|
||||||
state = 0;
|
state = 0;
|
||||||
} else if (!mission.running) {
|
} else if (!mission.running) {
|
||||||
state = mission.enqueued ? 1 : 2;
|
state = mission.enqueued ? 1 : 2;
|
||||||
} else if (mission.postprocessingRunning) {
|
} else if (mission.isPsRunning()) {
|
||||||
state = 3;
|
state = 3;
|
||||||
} else {
|
} else {
|
||||||
state = 0;
|
state = 0;
|
||||||
|
@ -322,6 +323,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
|
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||||
}
|
}
|
||||||
|
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
|
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
}
|
||||||
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
Log.v(TAG, "Starting intent: " + intent);
|
Log.v(TAG, "Starting intent: " + intent);
|
||||||
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
|
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
|
||||||
|
@ -406,7 +410,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
case ERROR_CONNECT_HOST:
|
case ERROR_CONNECT_HOST:
|
||||||
str.append(mContext.getString(R.string.error_connect_host));
|
str.append(mContext.getString(R.string.error_connect_host));
|
||||||
break;
|
break;
|
||||||
case ERROR_POSTPROCESSING_FAILED:
|
case ERROR_POSTPROCESSING:
|
||||||
str.append(mContext.getString(R.string.error_postprocessing_failed));
|
str.append(mContext.getString(R.string.error_postprocessing_failed));
|
||||||
case ERROR_UNKNOWN_EXCEPTION:
|
case ERROR_UNKNOWN_EXCEPTION:
|
||||||
break;
|
break;
|
||||||
|
@ -437,7 +441,6 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
public void clearFinishedDownloads() {
|
public void clearFinishedDownloads() {
|
||||||
mDownloadManager.forgetFinishedDownloads();
|
mDownloadManager.forgetFinishedDownloads();
|
||||||
applyChanges();
|
applyChanges();
|
||||||
setClearButtonVisibility(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) {
|
private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) {
|
||||||
|
@ -447,6 +450,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
if (mission != null) {
|
if (mission != null) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case R.id.start:
|
case R.id.start:
|
||||||
|
h.status.setText(UNDEFINED_PROGRESS);
|
||||||
h.state = -1;
|
h.state = -1;
|
||||||
h.size.setText(Utility.formatBytes(mission.getLength()));
|
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||||
mDownloadManager.resumeMission(mission);
|
mDownloadManager.resumeMission(mission);
|
||||||
|
@ -506,11 +510,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
mIterator.end();
|
mIterator.end();
|
||||||
|
|
||||||
checkEmptyMessageVisibility();
|
checkEmptyMessageVisibility();
|
||||||
|
if (mClear != null) mClear.setVisible(mIterator.hasFinishedMissions());
|
||||||
if (mIterator.getOldListSize() > 0) {
|
|
||||||
int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1);
|
|
||||||
setClearButtonVisibility(lastItemType == DownloadManager.SPECIAL_FINISHED);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void forceUpdate() {
|
public void forceUpdate() {
|
||||||
|
@ -529,17 +529,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClearButton(MenuItem clearButton) {
|
public void setClearButton(MenuItem clearButton) {
|
||||||
if (mClear == null) {
|
if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions());
|
||||||
int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1);
|
|
||||||
clearButton.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED);
|
|
||||||
}
|
|
||||||
mClear = clearButton;
|
mClear = clearButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setClearButtonVisibility(boolean flag) {
|
|
||||||
mClear.setVisible(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkEmptyMessageVisibility() {
|
private void checkEmptyMessageVisibility() {
|
||||||
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
|
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
|
||||||
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
|
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
|
||||||
|
@ -596,6 +589,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isNotFinite(Float value) {
|
||||||
|
return Float.isNaN(value) || Float.isInfinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ViewHolderItem extends RecyclerView.ViewHolder {
|
class ViewHolderItem extends RecyclerView.ViewHolder {
|
||||||
DownloadManager.MissionItem item;
|
DownloadManager.MissionItem item;
|
||||||
|
@ -667,7 +664,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
|
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
|
||||||
|
|
||||||
if (mission != null) {
|
if (mission != null) {
|
||||||
if (!mission.postprocessingRunning) {
|
if (!mission.isPsRunning()) {
|
||||||
if (mission.running) {
|
if (mission.running) {
|
||||||
pause.setVisible(true);
|
pause.setVisible(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -678,8 +675,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
||||||
queue.setChecked(mission.enqueued);
|
queue.setChecked(mission.enqueued);
|
||||||
|
|
||||||
delete.setVisible(true);
|
delete.setVisible(true);
|
||||||
start.setVisible(mission.errCode != ERROR_POSTPROCESSING_FAILED);
|
|
||||||
queue.setVisible(mission.errCode != ERROR_POSTPROCESSING_FAILED);
|
boolean flag = !mission.isPsFailed();
|
||||||
|
start.setVisible(flag);
|
||||||
|
queue.setVisible(flag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import us.shandian.giga.service.DownloadManager;
|
import us.shandian.giga.service.DownloadManager;
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
|
@ -40,7 +41,7 @@ public class MissionsFragment extends Fragment {
|
||||||
private MissionAdapter mAdapter;
|
private MissionAdapter mAdapter;
|
||||||
private GridLayoutManager mGridManager;
|
private GridLayoutManager mGridManager;
|
||||||
private LinearLayoutManager mLinearManager;
|
private LinearLayoutManager mLinearManager;
|
||||||
private Context mActivity;
|
private Context mContext;
|
||||||
|
|
||||||
private DMBinder mBinder;
|
private DMBinder mBinder;
|
||||||
private Bundle mBundle;
|
private Bundle mBundle;
|
||||||
|
@ -53,7 +54,7 @@ public class MissionsFragment extends Fragment {
|
||||||
mBinder = (DownloadManagerService.DMBinder) binder;
|
mBinder = (DownloadManagerService.DMBinder) binder;
|
||||||
mBinder.clearDownloadNotifications();
|
mBinder.clearDownloadNotifications();
|
||||||
|
|
||||||
mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty);
|
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty);
|
||||||
mAdapter.deleterLoad(mBundle, getView());
|
mAdapter.deleterLoad(mBundle, getView());
|
||||||
|
|
||||||
mBundle = null;
|
mBundle = null;
|
||||||
|
@ -79,17 +80,17 @@ public class MissionsFragment extends Fragment {
|
||||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
mLinear = mPrefs.getBoolean("linear", false);
|
mLinear = mPrefs.getBoolean("linear", false);
|
||||||
|
|
||||||
mActivity = getActivity();
|
//mContext = getActivity().getApplicationContext();
|
||||||
mBundle = savedInstanceState;
|
mBundle = savedInstanceState;
|
||||||
|
|
||||||
// Bind the service
|
// Bind the service
|
||||||
mActivity.bindService(new Intent(mActivity, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
mEmpty = v.findViewById(R.id.list_empty_view);
|
mEmpty = v.findViewById(R.id.list_empty_view);
|
||||||
mList = v.findViewById(R.id.mission_recycler);
|
mList = v.findViewById(R.id.mission_recycler);
|
||||||
|
|
||||||
// Init
|
// Init layouts managers
|
||||||
mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE);
|
mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE);
|
||||||
mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -103,7 +104,6 @@ public class MissionsFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mLinearManager = new LinearLayoutManager(getActivity());
|
mLinearManager = new LinearLayoutManager(getActivity());
|
||||||
|
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
@ -115,13 +115,13 @@ public class MissionsFragment extends Fragment {
|
||||||
* Added in API level 23.
|
* Added in API level 23.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context activity) {
|
public void onAttach(Context context) {
|
||||||
super.onAttach(activity);
|
super.onAttach(context);
|
||||||
|
|
||||||
// Bug: in api< 23 this is never called
|
// Bug: in api< 23 this is never called
|
||||||
// so mActivity=null
|
// so mActivity=null
|
||||||
// so app crashes with nullpointer exception
|
// so app crashes with null-pointer exception
|
||||||
mActivity = activity;
|
mContext = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment {
|
||||||
public void onAttach(Activity activity) {
|
public void onAttach(Activity activity) {
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
|
|
||||||
mActivity = activity;
|
mContext = activity.getApplicationContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ public class MissionsFragment extends Fragment {
|
||||||
|
|
||||||
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
||||||
mBinder.enableNotifications(true);
|
mBinder.enableNotifications(true);
|
||||||
mActivity.unbindService(mConnection);
|
mContext.unbindService(mConnection);
|
||||||
mAdapter.deleterDispose(null);
|
mAdapter.deleterDispose(null);
|
||||||
|
|
||||||
mBinder = null;
|
mBinder = null;
|
||||||
|
@ -189,7 +189,15 @@ public class MissionsFragment extends Fragment {
|
||||||
mList.setAdapter(mAdapter);
|
mList.setAdapter(mAdapter);
|
||||||
|
|
||||||
if (mSwitch != null) {
|
if (mSwitch != null) {
|
||||||
mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list);
|
boolean isLight = ThemeHelper.isLightThemeSelected(mContext);
|
||||||
|
int icon;
|
||||||
|
|
||||||
|
if (mLinear)
|
||||||
|
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
|
||||||
|
else
|
||||||
|
icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp;
|
||||||
|
|
||||||
|
mSwitch.setIcon(icon);
|
||||||
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
|
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
|
||||||
mPrefs.edit().putBoolean("linear", mLinear).apply();
|
mPrefs.edit().putBoolean("linear", mLinear).apply();
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_delete_black_24dp.png
Normal file
After Width: | Height: | Size: 317 B |
BIN
app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png
Normal file
After Width: | Height: | Size: 319 B |
BIN
app/src/main/res/drawable-hdpi/ic_grid_black_24dp.png
Normal file
After Width: | Height: | Size: 422 B |
BIN
app/src/main/res/drawable-hdpi/ic_grid_white_24dp.png
Normal file
After Width: | Height: | Size: 415 B |
BIN
app/src/main/res/drawable-hdpi/ic_list_black_24dp.png
Normal file
After Width: | Height: | Size: 265 B |
BIN
app/src/main/res/drawable-hdpi/ic_list_white_24dp.png
Normal file
After Width: | Height: | Size: 276 B |
BIN
app/src/main/res/drawable-hdpi/ic_newpipe_update.png
Executable file
After Width: | Height: | Size: 680 B |
BIN
app/src/main/res/drawable-hdpi/ic_settings_update_black.png
Executable file
After Width: | Height: | Size: 720 B |
BIN
app/src/main/res/drawable-hdpi/ic_settings_update_white.png
Executable file
After Width: | Height: | Size: 720 B |
Before Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/drawable-ldrtl-mdpi/ic_settings_update_black.png
Executable file
After Width: | Height: | Size: 512 B |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_delete_black_24dp.png
Normal file
After Width: | Height: | Size: 198 B |
BIN
app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png
Normal file
After Width: | Height: | Size: 198 B |
BIN
app/src/main/res/drawable-mdpi/ic_grid_black_24dp.png
Normal file
After Width: | Height: | Size: 270 B |
BIN
app/src/main/res/drawable-mdpi/ic_grid_white_24dp.png
Normal file
After Width: | Height: | Size: 279 B |
BIN
app/src/main/res/drawable-mdpi/ic_list_black_24dp.png
Normal file
After Width: | Height: | Size: 248 B |
BIN
app/src/main/res/drawable-mdpi/ic_list_white_24dp.png
Normal file
After Width: | Height: | Size: 254 B |
BIN
app/src/main/res/drawable-mdpi/ic_newpipe_update.png
Executable file
After Width: | Height: | Size: 401 B |
BIN
app/src/main/res/drawable-mdpi/ic_settings_update_black.png
Executable file
After Width: | Height: | Size: 379 B |
BIN
app/src/main/res/drawable-mdpi/ic_settings_update_white.png
Executable file
After Width: | Height: | Size: 562 B |
BIN
app/src/main/res/drawable-nodpi/place_holder_cloud.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
app/src/main/res/drawable-nodpi/place_holder_gadse.png
Normal file
After Width: | Height: | Size: 17 KiB |
12
app/src/main/res/drawable-v23/splash_background.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:drawable="@color/light_youtube_primary_color"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:width="80dp"
|
||||||
|
android:height="80dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:drawable="@drawable/splash_forground"/>
|
||||||
|
</layer-list>
|
BIN
app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png
Normal file
After Width: | Height: | Size: 270 B |
BIN
app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png
Normal file
After Width: | Height: | Size: 274 B |
BIN
app/src/main/res/drawable-xhdpi/ic_grid_black_24dp.png
Normal file
After Width: | Height: | Size: 276 B |
BIN
app/src/main/res/drawable-xhdpi/ic_grid_white_24dp.png
Normal file
After Width: | Height: | Size: 288 B |
BIN
app/src/main/res/drawable-xhdpi/ic_list_black_24dp.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
app/src/main/res/drawable-xhdpi/ic_list_white_24dp.png
Normal file
After Width: | Height: | Size: 249 B |
BIN
app/src/main/res/drawable-xhdpi/ic_newpipe_update.png
Executable file
After Width: | Height: | Size: 734 B |
BIN
app/src/main/res/drawable-xhdpi/ic_settings_update_black.png
Executable file
After Width: | Height: | Size: 954 B |
BIN
app/src/main/res/drawable-xhdpi/ic_settings_update_white.png
Executable file
After Width: | Height: | Size: 1,010 B |
Before Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png
Normal file
After Width: | Height: | Size: 506 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png
Normal file
After Width: | Height: | Size: 495 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_grid_black_24dp.png
Normal file
After Width: | Height: | Size: 427 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_grid_white_24dp.png
Normal file
After Width: | Height: | Size: 419 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_list_black_24dp.png
Normal file
After Width: | Height: | Size: 292 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png
Normal file
After Width: | Height: | Size: 295 B |