merged upstream/dev, changes for peertube support

This commit is contained in:
Ritvik Saraf 2019-03-10 01:02:25 +05:30
commit d90b1ca5be
187 changed files with 6831 additions and 3208 deletions

View File

@ -19,7 +19,7 @@ hasn't been reported/requested before
* We use English for development. Issues in other languages will be closed and ignored. * We use English for development. Issues in other languages will be closed and ignored.
* Please only add *one* issue at a time. Do not put multiple issues into one thread. * Please only add *one* issue at a time. Do not put multiple issues into one thread.
* When reporting a bug please give us a context, and a description how to reproduce it. * When reporting a bug please give us a context, and a description how to reproduce it.
* Issues that only contain a generated bug report, but no describtion might be closed. * Issues that only contain a generated bug report, but no description might be closed.
## Bug Fixing ## Bug Fixing
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to * If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to

View File

@ -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> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p> <p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#updates">Updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p> <p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <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!

View File

@ -8,18 +8,20 @@ android {
applicationId "org.schabi.newpipe" applicationId "org.schabi.newpipe"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 28 targetSdkVersion 28
versionCode 69 versionCode 720
versionName "0.14.2" 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.yausername:NewPipeExtractor:b1a77fa' implementation 'com.github.yausername:NewPipeExtractor:c220700'
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'

View File

@ -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);
}
}

View File

@ -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"/>

View File

@ -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;
}
} }

View File

@ -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();
}
}

View File

@ -50,7 +50,6 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BackPressable;
@ -303,6 +302,7 @@ public class MainActivity extends AppCompatActivity {
drawerItems.getMenu() drawerItems.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title) .add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
.setIcon(ServiceHelper.getIcon(s.getServiceId())); .setIcon(ServiceHelper.getIcon(s.getServiceId()));
} }
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true); drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
} }
@ -367,6 +367,7 @@ public class MainActivity extends AppCompatActivity {
String selectedServiceName = NewPipe.getService( String selectedServiceName = NewPipe.getService(
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName(); ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
headerServiceView.setText(selectedServiceName); headerServiceView.setText(selectedServiceName);
headerServiceView.post(() -> headerServiceView.setSelected(true));
} catch (Exception e) { } catch (Exception e) {
ErrorActivity.reportUiError(this, e); ErrorActivity.reportUiError(this, e);
} }

View File

@ -36,12 +36,12 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -81,10 +81,13 @@ public class RouterActivity extends AppCompatActivity {
protected int selectedPreviously = -1; protected int selectedPreviously = -1;
protected String currentUrl; protected String currentUrl;
protected boolean internalRoute = false;
protected final CompositeDisposable disposables = new CompositeDisposable(); protected final CompositeDisposable disposables = new CompositeDisposable();
private boolean selectionIsDownload = false; private boolean selectionIsDownload = false;
public static final String internalRouteKey = "internalRoute";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -94,11 +97,13 @@ public class RouterActivity extends AppCompatActivity {
currentUrl = getUrl(getIntent()); currentUrl = getUrl(getIntent());
if (TextUtils.isEmpty(currentUrl)) { if (TextUtils.isEmpty(currentUrl)) {
Toast.makeText(this, R.string.invalid_url_toast, Toast.LENGTH_LONG).show(); handleText();
finish(); finish();
} }
} }
internalRoute = getIntent().getBooleanExtra(internalRouteKey, false);
setTheme(ThemeHelper.isLightThemeSelected(this) setTheme(ThemeHelper.isLightThemeSelected(this)
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
} }
@ -112,7 +117,7 @@ public class RouterActivity extends AppCompatActivity {
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
handleUrl(currentUrl); handleUrl(currentUrl);
} }
@ -353,6 +358,15 @@ public class RouterActivity extends AppCompatActivity {
positiveButton.setEnabled(state); positiveButton.setEnabled(state);
} }
private void handleText(){
String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT);
int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0);
Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
NavigationHelper.openSearch(getThemeWrapperContext(),serviceId,searchString);
}
private void handleChoice(final String selectedChoiceKey) { private void handleChoice(final String selectedChoiceKey) {
final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list)); final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
if (validChoicesList.contains(selectedChoiceKey)) { if (validChoicesList.contains(selectedChoiceKey)) {
@ -383,8 +397,10 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> { .subscribe(intent -> {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if(!internalRoute){
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
startActivity(intent); startActivity(intent);
finish(); finish();

View File

@ -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;
} }

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -52,6 +52,7 @@ 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.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
@ -64,6 +65,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
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.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.info_list.InfoItemDialog; import org.schabi.newpipe.info_list.InfoItemDialog;
@ -100,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
@ -177,6 +180,7 @@ public class VideoDetailFragment
private static final String COMMENTS_TAB_TAG = "COMMENTS"; private static final String COMMENTS_TAB_TAG = "COMMENTS";
private static final String RELATED_TAB_TAG = "NEXT VIDEO"; private static final String RELATED_TAB_TAG = "NEXT VIDEO";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private AppBarLayout appBarLayout; private AppBarLayout appBarLayout;
private ViewPager viewPager; private ViewPager viewPager;
@ -365,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;
@ -623,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;
} }
@ -667,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() {
@ -695,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");
@ -736,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;
} }
@ -756,10 +771,10 @@ public class VideoDetailFragment
} }
public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) { public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) {
if (DEBUG) if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = ["
Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); + 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(); initTabs();
@ -811,6 +826,10 @@ public class VideoDetailFragment
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG); pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG);
} }
if(pageAdapter.getCount() == 0){
pageAdapter.addFragment(new EmptyFragment(), EMPTY_TAB_TAG);
}
pageAdapter.notifyDataSetUpdate(); pageAdapter.notifyDataSetUpdate();
if(pageAdapter.getCount() < 2){ if(pageAdapter.getCount() < 2){
@ -822,7 +841,10 @@ public class VideoDetailFragment
private boolean shouldShowComments() { private boolean shouldShowComments() {
try { try {
return showComments && NewPipe.getService(serviceId).isCommentsSupported(); return showComments && NewPipe.getService(serviceId)
.getServiceInfo()
.getMediaCapabilities()
.contains(COMMENTS);
} catch (ExtractionException e) { } catch (ExtractionException e) {
return false; return false;
} }
@ -1038,7 +1060,7 @@ public class VideoDetailFragment
} }
} }
pushToStack(serviceId, url, name); //pushToStack(serviceId, url, name);
animateView(thumbnailPlayButton, true, 200); animateView(thumbnailPlayButton, true, 200);
videoTitleTextView.setText(name); videoTitleTextView.setText(name);
@ -1089,11 +1111,13 @@ 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);
@ -1150,20 +1174,28 @@ public class VideoDetailFragment
public void openDownloadDialog() { public void openDownloadDialog() {
try { try {
DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams); downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
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);
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View File

@ -31,6 +31,7 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
@ -233,10 +234,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);
@ -487,12 +488,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
protected boolean onError(Throwable exception) { protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true; if (super.onError(exception)) return true;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; if(exception instanceof ContentNotAvailableException){
onUnrecoverableError(exception, showError(getString(R.string.content_not_available), false);
UserAction.REQUESTED_CHANNEL, }else{
NewPipe.getNameOfService(serviceId), int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
url, onUnrecoverableError(exception,
errorId); UserAction.REQUESTED_CHANNEL,
NewPipe.getNameOfService(serviceId),
url,
errorId);
}
return true; return true;
} }

View File

@ -305,6 +305,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view -> headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
headerPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue());
return true;
});
headerBackgroundButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue());
return true;
});
} }
private PlayQueue getPlayQueue() { private PlayQueue getPlayQueue() {

View File

@ -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);
}
} }

View File

@ -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);
} }

View File

@ -1,7 +1,7 @@
package org.schabi.newpipe.info_list.holder; package org.schabi.newpipe.info_list.holder;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils; import android.text.util.Linkify;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
@ -11,9 +11,13 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
public class CommentsMiniInfoItemHolder extends InfoItemHolder { public class CommentsMiniInfoItemHolder extends InfoItemHolder {
@ -26,6 +30,25 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final int commentDefaultLines = 2; private static final int commentDefaultLines = 2;
private static final int commentExpandedLines = 1000; private static final int commentExpandedLines = 1000;
private String commentText;
private String streamUrl;
private static final Pattern pattern = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
@Override
public String transformUrl(Matcher match, String url) {
int timestamp = 0;
String hours = match.group(1);
String minutes = match.group(2);
String seconds = match.group(3);
if(hours != null) timestamp += (Integer.parseInt(hours.replace(":", ""))*3600);
if(minutes != null) timestamp += (Integer.parseInt(minutes.replace(":", ""))*60);
if(seconds != null) timestamp += (Integer.parseInt(seconds));
return streamUrl + url.replace(match.group(0), "&t=" + String.valueOf(timestamp));
}
};
CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent); super(infoItemBuilder, layoutId, parent);
@ -66,34 +89,59 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
} }
}); });
// ellipsize if not already ellipsized streamUrl = item.getUrl();
if (null == itemContentView.getEllipsize()) {
itemContentView.setEllipsize(TextUtils.TruncateAt.END); itemContentView.setMaxLines(commentDefaultLines);
itemContentView.setMaxLines(commentDefaultLines); commentText = item.getCommentText();
itemContentView.setText(commentText);
linkify();
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
if(itemContentView.getLineCount() == 0){
itemContentView.post(() -> ellipsize());
}else{
ellipsize();
} }
itemContentView.setText(item.getCommentText());
if (null != item.getLikeCount()) { if (null != item.getLikeCount()) {
itemLikesCountView.setText(String.valueOf(item.getLikeCount())); itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
} }
itemPublishedTime.setText(item.getPublishedTime()); itemPublishedTime.setText(item.getPublishedTime());
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
toggleEllipsize(item.getCommentText()); toggleEllipsize();
if (itemBuilder.getOnCommentsSelectedListener() != null) { if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item); itemBuilder.getOnCommentsSelectedListener().selected(item);
} }
}); });
} }
private void toggleEllipsize(String text) { private void ellipsize() {
// toggle ellipsize if (itemContentView.getLineCount() > commentDefaultLines){
if (null == itemContentView.getEllipsize()) { int endOfLastLine = itemContentView.getLayout().getLineEnd(commentDefaultLines - 1);
itemContentView.setEllipsize(TextUtils.TruncateAt.END); String newVal = itemContentView.getText().subSequence(0, endOfLastLine - 3) + "...";
itemContentView.setMaxLines(commentDefaultLines); itemContentView.setText(newVal);
} else { linkify();
itemContentView.setEllipsize(null);
itemContentView.setMaxLines(commentExpandedLines);
} }
} }
private void toggleEllipsize() {
if (itemContentView.getText().toString().equals(commentText)) {
ellipsize();
} else {
expand();
}
}
private void expand() {
itemContentView.setMaxLines(commentExpandedLines);
itemContentView.setText(commentText);
linkify();
}
private void linkify(){
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
Linkify.addLinks(itemContentView, pattern, null, null, timestampLink);
itemContentView.setMovementMethod(null);
}
} }

View File

@ -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
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////

View File

@ -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);
}
} }

View File

@ -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);
}
}

View File

@ -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;
@ -270,6 +275,8 @@ public final class BackgroundPlayer extends Service {
protected class BasePlayerImpl extends BasePlayer { protected class BasePlayerImpl extends BasePlayer {
@NonNull final private AudioPlaybackResolver resolver; @NonNull final private AudioPlaybackResolver resolver;
private int cachedDuration;
private String cachedDurationString;
BasePlayerImpl(Context context) { BasePlayerImpl(Context context) {
super(context); super(context);
@ -344,10 +351,14 @@ public final class BackgroundPlayer extends Service {
if (!shouldUpdateOnProgress) return; if (!shouldUpdateOnProgress) return;
resetNotification(); resetNotification();
if(Build.VERSION.SDK_INT >= 26 /*Oreo*/) updateNotificationThumbnail(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail();
if (bigNotRemoteView != null) { if (bigNotRemoteView != null) {
if(cachedDuration != duration) {
cachedDuration = duration;
cachedDurationString = getTimeString(duration);
}
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration)); bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + cachedDurationString);
} }
if (notRemoteView != null) { if (notRemoteView != null) {
notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);

View File

@ -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
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View File

@ -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();

View File

@ -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) {

View File

@ -70,10 +70,10 @@ public class PlayerHelper {
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
public static String getTimeString(int milliSeconds) { public static String getTimeString(int milliSeconds) {
long seconds = (milliSeconds % 60000L) / 1000L; int seconds = (milliSeconds % 60000) / 1000;
long minutes = (milliSeconds % 3600000L) / 60000L; int minutes = (milliSeconds % 3600000) / 60000;
long hours = (milliSeconds % 86400000L) / 3600000L; int hours = (milliSeconds % 86400000) / 3600000;
long days = (milliSeconds % (86400000L * 7L)) / 86400000L; int days = (milliSeconds % (86400000 * 7)) / 86400000;
stringBuilder.setLength(0); stringBuilder.setLength(0);
return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString() return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString()

View File

@ -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);

View File

@ -94,15 +94,17 @@ public class VideoPlaybackResolver implements PlaybackResolver {
// Below are auxiliary media sources // Below are auxiliary media sources
// Create subtitle sources // Create subtitle sources
for (final SubtitlesStream subtitle : info.getSubtitles()) { if(info.getSubtitles() != null) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); for (final SubtitlesStream subtitle : info.getSubtitles()) {
if (mimeType == null) continue; final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) continue;
final Format textFormat = Format.createTextSampleFormat(null, mimeType, final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = dataSource.getSampleMediaSourceFactory() final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
.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) {

View File

@ -9,6 +9,7 @@ import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.preference.EditTextPreference;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
@ -16,14 +17,15 @@ import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils; import com.nononsenseapps.filepicker.Utils;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.utils.Localization; import org.schabi.newpipe.extractor.utils.Localization;
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.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ZipHelper; import org.schabi.newpipe.util.ZipHelper;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
@ -42,6 +44,9 @@ import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
public class ContentSettingsFragment extends BasePreferenceFragment { public class ContentSettingsFragment extends BasePreferenceFragment {
@ -58,6 +63,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private String thumbnailLoadToggleKey; private String thumbnailLoadToggleKey;
private CompositeDisposable disposables = new CompositeDisposable();
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -135,30 +142,39 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
peerTubeInstance.setSummary(sharedPreferences.getString(getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl())); peerTubeInstance.setSummary(sharedPreferences.getString(getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl()));
peerTubeInstance.setOnPreferenceChangeListener((Preference p, Object newInstance) -> { peerTubeInstance.setOnPreferenceChangeListener((Preference p, Object newInstance) -> {
EditTextPreference pEt = (EditTextPreference) p;
String url = (String) newInstance; String url = (String) newInstance;
if(!url.startsWith("https://")){ if (!url.startsWith("https://")) {
Toast.makeText(getActivity(), "instance url should start with https://", Toast.makeText(getActivity(), "instance url should start with https://",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
return false; return false;
}else{ } else {
boolean shouldUpdate = Single.fromCallable(() -> { pEt.setSummary("fetching instance details..");
Disposable disposable = Single.fromCallable(() -> {
ServiceList.PeerTube.setInstance(url); ServiceList.PeerTube.setInstance(url);
return true; return true;
}).subscribeOn(Schedulers.io()) }).subscribeOn(Schedulers.io())
.onErrorReturnItem(false) .observeOn(AndroidSchedulers.mainThread())
.blockingGet(); .subscribe(result -> {
if (result) {
if (shouldUpdate) { pEt.setSummary(url);
p.setSummary(url); pEt.setText(url);
SharedPreferences.Editor editor = sharedPreferences.edit(); SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName()).apply(); editor.putString(App.getApp().getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName()).apply();
editor.putString(getString(R.string.current_service_key), ServiceList.PeerTube.getServiceInfo().getName()).apply(); editor.putString(App.getApp().getString(R.string.current_service_key), ServiceList.PeerTube.getServiceInfo().getName()).apply();
editor.putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply(); NavigationHelper.openMainActivity(App.getApp());
}else{ } else {
Toast.makeText(getActivity(), "unable to update instance", pEt.setSummary(ServiceList.PeerTube.getBaseUrl());
Toast.LENGTH_SHORT).show(); Toast.makeText(getActivity(), "unable to update instance",
} Toast.LENGTH_SHORT).show();
return shouldUpdate; }
}, error -> {
pEt.setSummary(ServiceList.PeerTube.getBaseUrl());
Toast.makeText(getActivity(), "unable to update instance",
Toast.LENGTH_SHORT).show();
});
disposables.add(disposable);
return false;
} }
}); });
} }
@ -218,7 +234,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
e.printStackTrace(); e.printStackTrace();
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
}finally { } finally {
try { try {
if (output != null) { if (output != null) {
output.flush(); output.flush();
@ -242,7 +258,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
} finally { } finally {
try { try {
zipFile.close(); zipFile.close();
} catch (Exception ignored){} } catch (Exception ignored) {
}
} }
try { try {
@ -265,7 +282,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
} }
//If settings file exist, ask if it should be imported. //If settings file exist, ask if it should be imported.
if(ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) { if (ZipHelper.extractFileFromZip(filePath, newpipe_settings.getPath(), "newpipe.settings")) {
AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); AlertDialog.Builder alert = new AlertDialog.Builder(getContext());
alert.setTitle(R.string.import_settings); alert.setTitle(R.string.import_settings);
@ -320,7 +337,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
e.printStackTrace(); e.printStackTrace();
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
e.printStackTrace(); e.printStackTrace();
}finally { } finally {
try { try {
if (input != null) { if (input != null) {
input.close(); input.close();
@ -343,4 +360,5 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash)); "none", "", R.string.app_ui_crash));
} }
} }

View File

@ -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);

View File

@ -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;
};
}

View File

@ -0,0 +1,128 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public class CommentTextOnTouchListener implements View.OnTouchListener {
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
private static final Pattern timestampPattern = Pattern.compile(".*&t=(\\d+)");
@Override
public boolean onTouch(View v, MotionEvent event) {
if(!(v instanceof TextView)){
return false;
}
TextView widget = (TextView) v;
Object text = widget.getText();
if (text instanceof Spanned) {
Spannable buffer = (Spannable) text;
int action = event.getAction();
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off,
ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
boolean handled = false;
if(link[0] instanceof URLSpan){
handled = handleUrl(v.getContext(), (URLSpan) link[0]);
}
if(!handled) link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
}
}
}
return false;
}
private boolean handleUrl(Context context, URLSpan urlSpan) {
String url = urlSpan.getURL();
StreamingService service;
StreamingService.LinkType linkType;
try {
service = NewPipe.getServiceByUrl(url);
linkType = service.getLinkTypeByUrl(url);
} catch (ExtractionException e) {
return false;
}
if(linkType == StreamingService.LinkType.NONE){
return false;
}
Matcher matcher = timestampPattern.matcher(url);
if(linkType == StreamingService.LinkType.STREAM && matcher.matches()){
int seconds = Integer.parseInt(matcher.group(1));
return playOnPopup(context, url, service, seconds);
}else{
NavigationHelper.openRouterActivity(context, url);
return true;
}
}
private boolean playOnPopup(Context context, String url, StreamingService service, int seconds) {
LinkHandlerFactory factory = service.getStreamLHFactory();
String cleanUrl = null;
try {
cleanUrl = factory.getUrl(factory.getId(url));
} catch (ParsingException e) {
return false;
}
Single single = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info);
((StreamInfo) info).setStartPosition(seconds);
NavigationHelper.enqueueOnPopupPlayer(context, playQueue, true);
});
return true;
}
}

View File

@ -32,6 +32,7 @@ import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem; 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.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
@ -47,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;
@ -96,10 +98,13 @@ 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,

View 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);
}
}

View File

@ -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;
} }
@ -48,6 +50,8 @@ public class KioskTranslator {
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local); return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local);
case "Recently added": case "Recently added":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent); return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent);
case "conferences":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
default: default:
return 0; return 0;
} }

View File

@ -21,6 +21,7 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.about.AboutActivity; import org.schabi.newpipe.about.AboutActivity;
import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
@ -34,11 +35,11 @@ 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.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
import org.schabi.newpipe.local.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
import org.schabi.newpipe.local.feed.FeedFragment;
import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment;
@ -422,6 +423,13 @@ public class NavigationHelper {
context.startActivity(mIntent); context.startActivity(mIntent);
} }
public static void openRouterActivity(Context context, String url) {
Intent mIntent = new Intent(context, RouterActivity.class);
mIntent.setData(Uri.parse(url));
mIntent.putExtra(RouterActivity.internalRouteKey, true);
context.startActivity(mIntent);
}
public static void openAbout(Context context) { public static void openAbout(Context context) {
Intent intent = new Intent(context, AboutActivity.class); Intent intent = new Intent(context, AboutActivity.class);
context.startActivity(intent); context.startActivity(intent);

View File

@ -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:

View File

@ -11,9 +11,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.io.IOException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
@ -30,6 +28,8 @@ public class ServiceHelper {
return R.drawable.place_holder_cloud; return R.drawable.place_holder_cloud;
case 2: case 2:
return R.drawable.place_holder_peertube; return R.drawable.place_holder_peertube;
case 3:
return R.drawable.place_holder_gadse;
default: default:
return R.drawable.place_holder_circle; return R.drawable.place_holder_circle;
} }
@ -43,6 +43,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;
} }
} }
@ -134,7 +136,7 @@ public class ServiceHelper {
} }
public static void initService(Context context, int serviceId) { public static void initService(Context context, int serviceId) {
if(serviceId == 2){ if(serviceId == ServiceList.PeerTube.getServiceId()){
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
String peerTubeInstanceUrl = sharedPreferences.getString(context.getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl()); String peerTubeInstanceUrl = sharedPreferences.getString(context.getString(R.string.peertube_instance_url_key), ServiceList.PeerTube.getBaseUrl());
String peerTubeInstanceName = sharedPreferences.getString(context.getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName()); String peerTubeInstanceName = sharedPreferences.getString(context.getString(R.string.peertube_instance_name_key), ServiceList.PeerTube.getServiceInfo().getName());

View File

@ -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);

View File

@ -30,6 +30,7 @@ import android.view.ContextThemeWrapper;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@ -136,15 +137,16 @@ public class ThemeHelper {
else if (selectedTheme.equals(blackTheme)) themeName = "BlackTheme"; else if (selectedTheme.equals(blackTheme)) themeName = "BlackTheme";
else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme"; else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme";
switch (serviceId) { if(serviceId == ServiceList.PeerTube.getServiceId()){
case 2: //service name for peertube depends on the instance
//service name for peertube depends on the instance themeName += ".PeerTube";
themeName += ".PeerTube"; }else{
break; themeName += "." + service.getServiceInfo().getName();
default:
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;

View File

@ -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;
} }

View File

@ -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);
for (Thread thread : threads) joinForThread(thread); if (threads != null)
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;
notifyPostProcessing(1);
notifyProgress(0);
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
Exception exception = null;
try { try {
notifyPostProcessing(true); Postprocessing
notifyProgress(0); .getAlgorithm(postprocessingName, this)
.run();
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, this);
algorithm.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() {

View File

@ -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

View 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;
}
}

View File

@ -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,71 +52,84 @@ 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;
try { mission.done = 0;
int i = 0; mission.length = file.length();
for (; i < sources.length - 1; i++) {
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
}
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
int[] idx = {0}; if (worksOnSameFile) {
CircularFile.OffsetChecker checker = () -> { ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
while (idx[0] < sources.length) { try {
/* int i = 0;
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks) for (; i < sources.length - 1; i++) {
* or the CircularFile can lead to unexpected results sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
*/ }
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) { sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
idx[0]++;
continue;// the selected source is not used anymore int[] idx = {0};
CircularFile.OffsetChecker checker = () -> {
while (idx[0] < sources.length) {
/*
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
* or the CircularFile can lead to unexpected results
*/
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
idx[0]++;
continue;// the selected source is not used anymore
}
return sources[idx[0]].getFilePointer() - 1;
} }
return sources[idx[0]].getFilePointer() - 1; return -1;
};
out = new CircularFile(file, 0, this::progressReport, checker);
result = process(out, sources);
if (result == OK_RESULT)
finalLength = out.finalizeFile();
} finally {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
source.dispose();
}
} }
if (out != null) {
return -1; out.dispose();
};
out = new CircularFile(file, 0, this::progressReport, checker);
mission.done = 0;
mission.length = file.length();
int result = process(out, sources);
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 {
for (SharpStream source : sources) {
if (source != null && !source.isDisposed()) {
source.dispose();
} }
} }
if (out != null) { } else {
out.dispose(); 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;

View File

@ -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;
}
}

View File

@ -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
@ -41,7 +40,7 @@ class TttmlConverter extends Postprocessing {
out, out,
getArgumentAt(1, "true").equals("true"), getArgumentAt(1, "true").equals("true"),
getArgumentAt(2, "true").equals("true") getArgumentAt(2, "true").equals("true")
); );
} catch (Exception err) { } catch (Exception err) {
Log.e(TAG, "subtitle parse failed", err); Log.e(TAG, "subtitle parse failed", err);
@ -56,7 +55,7 @@ class TttmlConverter extends Postprocessing {
} else if (err instanceof XPathExpressionException) { } else if (err instanceof XPathExpressionException) {
return 7; return 7;
} }
return 8; return 8;
} }

View File

@ -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

View File

@ -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()) {
// Incomplete post-processing results in a corrupted download file if (mis.postprocessingThis) {
// because the selected algorithm works on the same file to save space. // Incomplete post-processing results in a corrupted download file
if (!dl.delete()) { // because the selected algorithm works on the same file to save space.
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); if (exists && dl.isFile() && !dl.delete())
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
exists = true;
} }
exists = true;
mis.postprocessingRunning = false; mis.postprocessingState = 0;
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_FAILED; 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() {

View File

@ -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 {

View File

@ -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();
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Some files were not shown because too many files have changed in this diff Show More