1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-10-26 12:57:39 +00:00

Merge branch 'dev' into enqueue-playlist

This commit is contained in:
Christian Schabesberger
2019-03-03 20:50:00 +01:00
committed by GitHub
191 changed files with 8270 additions and 3575 deletions

View File

@@ -1,5 +1,6 @@
package org.schabi.newpipe;
import android.annotation.TargetApi;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -65,6 +66,7 @@ import io.reactivex.plugins.RxJavaPlugins;
public class App extends Application {
protected static final String TAG = App.class.toString();
private RefWatcher refWatcher;
private static App app;
@SuppressWarnings("unchecked")
private static final Class<? extends ReportSenderFactory>[]
@@ -88,6 +90,8 @@ public class App extends Application {
}
refWatcher = installLeakCanary();
app = this;
// Initialize settings first because others inits can use its values
SettingsActivity.initSettings(this);
@@ -100,6 +104,9 @@ public class App extends Application {
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
configureRxJavaErrorHandler();
// Check for new version
new CheckForNewAppVersionTask().execute();
}
protected Downloader getDownloader() {
@@ -211,6 +218,31 @@ public class App extends Application {
NotificationManager mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
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
@@ -226,4 +258,8 @@ public class App extends Application {
protected boolean isDisposedRxExceptionsReported() {
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

@@ -3,18 +3,24 @@ package org.schabi.newpipe;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.schabi.newpipe.extractor.DownloadRequest;
import org.schabi.newpipe.extractor.DownloadResponse;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.utils.Localization;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
@@ -139,13 +145,16 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
final Request.Builder requestBuilder = new Request.Builder()
.method("GET", null).url(siteUrl)
.addHeader("User-Agent", USER_AGENT);
.method("GET", null).url(siteUrl);
for (Map.Entry<String, String> header : customProperties.entrySet()) {
requestBuilder.addHeader(header.getKey(), header.getValue());
}
if (!customProperties.containsKey("User-Agent")) {
requestBuilder.header("User-Agent", USER_AGENT);
}
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
}
@@ -177,4 +186,96 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
public String download(String siteUrl) throws IOException, ReCaptchaException {
return download(siteUrl, Collections.emptyMap());
}
}
@Override
public DownloadResponse get(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
final Request.Builder requestBuilder = new Request.Builder()
.method("GET", null).url(siteUrl);
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
// set custom headers in request
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
for(String value : pair.getValue()){
requestBuilder.addHeader(pair.getKey(), value);
}
}
if (!requestHeaders.containsKey("User-Agent")) {
requestBuilder.header("User-Agent", USER_AGENT);
}
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
}
final Request okRequest = requestBuilder.build();
final Response response = client.newCall(okRequest).execute();
final ResponseBody body = response.body();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested");
}
if (body == null) {
response.close();
return null;
}
return new DownloadResponse(body.string(), response.headers().toMultimap());
}
@Override
public DownloadResponse get(String siteUrl) throws IOException, ReCaptchaException {
return get(siteUrl, DownloadRequest.emptyRequest);
}
@Override
public DownloadResponse post(String siteUrl, DownloadRequest request) throws IOException, ReCaptchaException {
Map<String, List<String>> requestHeaders = request.getRequestHeaders();
if(null == requestHeaders.get("Content-Type") || requestHeaders.get("Content-Type").isEmpty()){
// content type header is required. maybe throw an exception here
return null;
}
String contentType = requestHeaders.get("Content-Type").get(0);
RequestBody okRequestBody = null;
if(null != request.getRequestBody()){
okRequestBody = RequestBody.create(MediaType.parse(contentType), request.getRequestBody());
}
final Request.Builder requestBuilder = new Request.Builder()
.method("POST", okRequestBody).url(siteUrl);
// set custom headers in request
for (Map.Entry<String, List<String>> pair : requestHeaders.entrySet()) {
for(String value : pair.getValue()){
requestBuilder.addHeader(pair.getKey(), value);
}
}
if (!requestHeaders.containsKey("User-Agent")) {
requestBuilder.header("User-Agent", USER_AGENT);
}
if (!TextUtils.isEmpty(mCookies)) {
requestBuilder.addHeader("Cookie", mCookies);
}
final Request okRequest = requestBuilder.build();
final Response response = client.newCall(okRequest).execute();
final ResponseBody body = response.body();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested");
}
if (body == null) {
response.close();
return null;
}
return new DownloadResponse(body.string(), response.headers().toMultimap());
}
}

View File

@@ -457,7 +457,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
break;
case R.id.subtitle_button:
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';
break;
default:
@@ -477,7 +477,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
final String finalFileName = fileName;
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
// should be safe run the following code without "getActivity().runOnUiThread()"
if (listed) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.download_dialog_title)
@@ -511,11 +510,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (secondaryStream != null) {
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;
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) {
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

@@ -0,0 +1,73 @@
package org.schabi.newpipe.fragments.detail;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public class TabAdaptor extends FragmentPagerAdapter {
private final List<Fragment> mFragmentList = new ArrayList<>();
private final List<String> mFragmentTitleList = new ArrayList<>();
private final FragmentManager fragmentManager;
public TabAdaptor(FragmentManager fm) {
super(fm);
this.fragmentManager = fm;
}
@Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
@Override
public int getCount() {
return mFragmentList.size();
}
public void addFragment(Fragment fragment, String title) {
mFragmentList.add(fragment);
mFragmentTitleList.add(title);
}
public void clearAllItems() {
mFragmentList.clear();
mFragmentTitleList.clear();
}
public void removeItem(int position){
mFragmentList.remove(position == 0 ? 0 : position - 1);
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
}
public void updateItem(int position, Fragment fragment){
mFragmentList.set(position, fragment);
}
public void updateItem(String title, Fragment fragment){
int index = mFragmentTitleList.indexOf(title);
if(index != -1){
updateItem(index, fragment);
}
}
@Override
public int getItemPosition(Object object) {
if (mFragmentList.contains(object)) return mFragmentList.indexOf(object);
else return POSITION_NONE;
}
public void notifyDataSetUpdate(){
notifyDataSetChanged();
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
}
}

View File

@@ -10,11 +10,13 @@ import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
@@ -25,7 +27,6 @@ import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -33,19 +34,15 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.nirhart.parallaxscroll.views.ParallaxScrollView;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
@@ -55,7 +52,9 @@ import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.InfoItem;
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.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream;
@@ -63,21 +62,22 @@ import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainVideoPlayer;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
@@ -85,11 +85,9 @@ import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.Serializable;
import java.util.Collection;
@@ -104,6 +102,7 @@ import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class VideoDetailFragment
@@ -114,27 +113,27 @@ public class VideoDetailFragment
View.OnLongClickListener {
public static final String AUTO_PLAY = "auto_play";
// Amount of videos to show on start
private static final int INITIAL_RELATED_VIDEOS = 8;
private InfoItemBuilder infoItemBuilder = null;
private int updateFlags = 0;
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2;
private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4;
private static final int COMMENTS_UPDATE_FLAG = 0x4;
private boolean autoPlayEnabled;
private boolean showRelatedStreams;
private boolean wasRelatedStreamsExpanded = false;
private boolean showComments;
@State protected int serviceId = Constants.NO_SERVICE_ID;
@State protected String name;
@State protected String url;
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
protected String name;
@State
protected String url;
private StreamInfo currentInfo;
private Disposable currentWorker;
@NonNull private CompositeDisposable disposables = new CompositeDisposable();
@NonNull
private CompositeDisposable disposables = new CompositeDisposable();
private List<VideoStream> sortedVideoStreams;
private int selectedVideoStreamIndex = -1;
@@ -147,7 +146,6 @@ public class VideoDetailFragment
private Spinner spinnerToolbar;
private ParallaxScrollView parallaxScrollRootView;
private LinearLayout contentRootLayoutHiding;
private View thumbnailBackgroundButton;
@@ -156,7 +154,6 @@ public class VideoDetailFragment
private View videoTitleRoot;
private TextView videoTitleTextView;
@Nullable
private ImageView videoTitleToggleArrow;
private TextView videoCountView;
@@ -181,10 +178,15 @@ public class VideoDetailFragment
private ImageView thumbsDownImageView;
private TextView thumbsDisabledTextView;
private TextView nextStreamTitle;
private LinearLayout relatedStreamRootLayout;
private LinearLayout relatedStreamsView;
private ImageButton relatedStreamExpandButton;
private static final String COMMENTS_TAB_TAG = "COMMENTS";
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private AppBarLayout appBarLayout;
private ViewPager viewPager;
private TabAdaptor pageAdapter;
private TabLayout tabLayout;
private FrameLayout relatedStreamsLayout;
/*////////////////////////////////////////////////////////////////////////*/
@@ -200,12 +202,17 @@ public class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(Bundle savedInstanceState) {
public void
onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(getString(R.string.show_next_video_key), true);
showComments = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(getString(R.string.show_comments_key), true);
PreferenceManager.getDefaultSharedPreferences(activity)
.registerOnSharedPreferenceChangeListener(this);
}
@@ -227,14 +234,16 @@ public class VideoDetailFragment
if (updateFlags != 0) {
if (!isLoading.get() && currentInfo != null) {
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentInfo);
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) startLoading(false);
if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo);
if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) startLoading(false);
}
if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0
&& menu != null) {
updateMenuItemVisibility();
}
updateFlags = 0;
}
@@ -291,6 +300,9 @@ public class VideoDetailFragment
updateFlags |= RESOLUTIONS_MENU_UPDATE_FLAG;
} else if (key.equals(getString(R.string.show_play_with_kodi_key))) {
updateFlags |= TOOLBAR_ITEMS_UPDATE_FLAG;
} else if (key.equals(getString(R.string.show_comments_key))) {
showComments = sharedPreferences.getBoolean(key, true);
updateFlags |= COMMENTS_UPDATE_FLAG;
}
}
@@ -300,7 +312,6 @@ public class VideoDetailFragment
private static final String INFO_KEY = "info_key";
private static final String STACK_KEY = "stack_key";
private static final String WAS_RELATED_EXPANDED_KEY = "was_related_expanded_key";
@Override
public void onSaveInstanceState(Bundle outState) {
@@ -309,10 +320,6 @@ public class VideoDetailFragment
// Check if the next video label and video is visible,
// if it is, include the two elements in the next check
int nextCount = currentInfo != null && currentInfo.getNextVideo() != null ? 2 : 0;
if (relatedStreamsView != null
&& relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) {
outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true);
}
if (!isLoading.get() && currentInfo != null && isVisible()) {
outState.putSerializable(INFO_KEY, currentInfo);
@@ -325,12 +332,11 @@ public class VideoDetailFragment
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
super.onRestoreInstanceState(savedState);
wasRelatedStreamsExpanded = savedState.getBoolean(WAS_RELATED_EXPANDED_KEY, false);
Serializable serializable = savedState.getSerializable(INFO_KEY);
if (serializable instanceof StreamInfo) {
//noinspection unchecked
currentInfo = (StreamInfo) serializable;
InfoCache.getInstance().putInfo(serviceId, url, currentInfo);
InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM);
}
serializable = savedState.getSerializable(STACK_KEY);
@@ -338,6 +344,7 @@ public class VideoDetailFragment
//noinspection unchecked
stack.addAll((Collection<? extends StackItem>) serializable);
}
}
/*//////////////////////////////////////////////////////////////////////////
@@ -362,7 +369,8 @@ public class VideoDetailFragment
}
break;
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();
}
break;
@@ -392,9 +400,6 @@ public class VideoDetailFragment
case R.id.detail_title_root_layout:
toggleTitleAndDescription();
break;
case R.id.detail_related_streams_expand:
toggleExpandRelatedVideos(currentInfo);
break;
}
}
@@ -418,45 +423,17 @@ public class VideoDetailFragment
}
private void toggleTitleAndDescription() {
if (videoTitleToggleArrow != null) { //it is null for tablets
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
videoTitleTextView.setMaxLines(1);
videoDescriptionRootLayout.setVisibility(View.GONE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
} else {
videoTitleTextView.setMaxLines(10);
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
}
if (videoDescriptionRootLayout.getVisibility() == View.VISIBLE) {
videoTitleTextView.setMaxLines(1);
videoDescriptionRootLayout.setVisibility(View.GONE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
} else {
videoTitleTextView.setMaxLines(10);
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_up);
}
}
private void toggleExpandRelatedVideos(StreamInfo info) {
if (DEBUG) Log.d(TAG, "toggleExpandRelatedVideos() called with: info = [" + info + "]");
if (!showRelatedStreams) return;
int nextCount = info.getNextVideo() != null ? 2 : 0;
int initialCount = INITIAL_RELATED_VIDEOS + nextCount;
if (relatedStreamsView.getChildCount() > initialCount) {
relatedStreamsView.removeViews(initialCount,
relatedStreamsView.getChildCount() - (initialCount));
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(
activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
return;
}
//Log.d(TAG, "toggleExpandRelatedVideos() called with: info = [" + info + "], from = [" + INITIAL_RELATED_VIDEOS + "]");
for (int i = INITIAL_RELATED_VIDEOS; i < info.getRelatedStreams().size(); i++) {
InfoItem item = info.getRelatedStreams().get(i);
//Log.d(TAG, "i = " + i);
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item));
}
relatedStreamExpandButton.setImageDrawable(
ContextCompat.getDrawable(activity,
ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse)));
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@@ -466,8 +443,6 @@ public class VideoDetailFragment
super.initViews(rootView, savedInstanceState);
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content);
thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout);
thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view);
thumbnailPlayButton = rootView.findViewById(R.id.detail_thumbnail_play_button);
@@ -503,30 +478,23 @@ public class VideoDetailFragment
uploaderTextView = rootView.findViewById(R.id.detail_uploader_text_view);
uploaderThumb = rootView.findViewById(R.id.detail_uploader_thumbnail_view);
relatedStreamRootLayout = rootView.findViewById(R.id.detail_related_streams_root_layout);
nextStreamTitle = rootView.findViewById(R.id.detail_next_stream_title);
relatedStreamsView = rootView.findViewById(R.id.detail_related_streams_view);
appBarLayout = rootView.findViewById(R.id.appbarlayout);
viewPager = rootView.findViewById(R.id.viewpager);
pageAdapter = new TabAdaptor(getChildFragmentManager());
viewPager.setAdapter(pageAdapter);
tabLayout = rootView.findViewById(R.id.tablayout);
tabLayout.setupWithViewPager(viewPager);
relatedStreamExpandButton = rootView.findViewById(R.id.detail_related_streams_expand);
relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout);
infoItemBuilder = new InfoItemBuilder(activity);
setHeightThumbnail();
}
@Override
protected void initListeners() {
super.initListeners();
infoItemBuilder.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
}
@Override
public void held(StreamInfoItem selectedItem) {
showStreamDialog(selectedItem);
}
});
videoTitleRoot.setOnClickListener(this);
uploaderRootLayout.setOnClickListener(this);
@@ -536,7 +504,6 @@ public class VideoDetailFragment
detailControlsAddToPlaylist.setOnClickListener(this);
detailControlsDownload.setOnClickListener(this);
detailControlsDownload.setOnLongClickListener(this);
relatedStreamExpandButton.setOnClickListener(this);
detailControlsBackground.setLongClickable(true);
detailControlsPopup.setLongClickable(true);
@@ -619,44 +586,6 @@ public class VideoDetailFragment
}
}
private void initRelatedVideos(StreamInfo info) {
if (relatedStreamsView.getChildCount() > 0) relatedStreamsView.removeAllViews();
if (info.getNextVideo() != null && showRelatedStreams) {
nextStreamTitle.setVisibility(View.VISIBLE);
relatedStreamsView.addView(
infoItemBuilder.buildView(relatedStreamsView, info.getNextVideo()));
relatedStreamsView.addView(getSeparatorView());
setRelatedStreamsVisibility(View.VISIBLE);
} else {
nextStreamTitle.setVisibility(View.GONE);
setRelatedStreamsVisibility(View.GONE);
}
if (info.getRelatedStreams() != null
&& !info.getRelatedStreams().isEmpty() && showRelatedStreams) {
//long first = System.nanoTime(), each;
int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS
? INITIAL_RELATED_VIDEOS
: info.getRelatedStreams().size();
for (int i = 0; i < to; i++) {
InfoItem item = info.getRelatedStreams().get(i);
//each = System.nanoTime();
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item));
//if (DEBUG) Log.d(TAG, "each took " + ((System.nanoTime() - each) / 1000000L) + "ms");
}
//if (DEBUG) Log.d(TAG, "Total time " + ((System.nanoTime() - first) / 1000000L) + "ms");
setRelatedStreamsVisibility(View.VISIBLE);
relatedStreamExpandButton.setVisibility(View.VISIBLE);
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(
activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
} else {
if (info.getNextVideo() == null) setRelatedStreamsVisibility(View.GONE);
relatedStreamExpandButton.setVisibility(View.GONE);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
@@ -690,7 +619,7 @@ public class VideoDetailFragment
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(isLoading.get()) {
if (isLoading.get()) {
// if is still loading block menu
return true;
}
@@ -699,13 +628,13 @@ public class VideoDetailFragment
switch (id) {
case R.id.menu_item_share: {
if (currentInfo != null) {
shareUrl(currentInfo.getName(), currentInfo.getUrl());
shareUrl(currentInfo.getName(), currentInfo.getOriginalUrl());
}
return true;
}
case R.id.menu_item_openInBrowser: {
if (currentInfo != null) {
openUrlInBrowser(currentInfo.getUrl());
openUrlInBrowser(currentInfo.getOriginalUrl());
}
return true;
}
@@ -714,7 +643,7 @@ public class VideoDetailFragment
NavigationHelper.playWithKore(activity, Uri.parse(
url.replace("https", "http")));
} catch (Exception e) {
if(DEBUG) Log.i(TAG, "Failed to start kore", e);
if (DEBUG) Log.i(TAG, "Failed to start kore", e);
showInstallKoreDialog(activity);
}
return true;
@@ -728,7 +657,8 @@ public class VideoDetailFragment
builder.setMessage(R.string.kore_not_found)
.setPositiveButton(R.string.install, (DialogInterface dialog, int which) ->
NavigationHelper.installKore(context))
.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {});
.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
});
builder.create().show();
}
@@ -742,10 +672,16 @@ public class VideoDetailFragment
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
.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);
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.setSelection(selectedVideoStreamIndex);
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@@ -770,17 +706,17 @@ public class VideoDetailFragment
*/
protected final LinkedList<StackItem> stack = new LinkedList<>();
public void clearHistory() {
stack.clear();
}
public void pushToStack(int serviceId, String videoUrl, String name) {
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)) {
Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" + serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]");
if (stack.size() > 0
&& stack.peek().getServiceId() == serviceId
&& stack.peek().getUrl().equals(videoUrl)) {
Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = ["
+ serviceId + "], videoUrl == peek.getUrl = [" + videoUrl + "]");
return;
} else {
Log.d(TAG, "pushToStack() wasn't equal");
@@ -811,7 +747,11 @@ public class VideoDetailFragment
// Get stack item from the new top
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;
}
@@ -831,28 +771,22 @@ public class VideoDetailFragment
}
public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) {
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = ["
+ info + "], scrollToTop = [" + scrollToTop + "]");
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
pushToStack(serviceId, url, name);
showLoading();
initTabs();
Log.d(TAG, "prepareAndHandleInfo() called parallaxScrollRootView.getScrollY(): "
+ parallaxScrollRootView.getScrollY());
final boolean greaterThanThreshold = parallaxScrollRootView.getScrollY() > (int)
(getResources().getDisplayMetrics().heightPixels * .1f);
if (scrollToTop) appBarLayout.setExpanded(true, true);
handleResult(info);
showContent();
if (scrollToTop) parallaxScrollRootView.smoothScrollTo(0, 0);
animateView(contentRootLayoutHiding,
false,
greaterThanThreshold ? 250 : 0, 0, () -> {
handleResult(info);
showContentWithAnimation(120, 0, .01f);
});
}
protected void prepareAndLoadInfo() {
parallaxScrollRootView.smoothScrollTo(0, 0);
appBarLayout.setExpanded(true, true);
pushToStack(serviceId, url, name);
startLoading(false);
}
@@ -861,6 +795,7 @@ public class VideoDetailFragment
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
initTabs();
currentInfo = null;
if (currentWorker != null) currentWorker.dispose();
@@ -870,12 +805,49 @@ public class VideoDetailFragment
.subscribe((@NonNull StreamInfo result) -> {
isLoading.set(false);
currentInfo = result;
showContentWithAnimation(120, 0, 0);
handleResult(result);
showContent();
}, (@NonNull Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
});
}
private void initTabs() {
pageAdapter.clearAllItems();
if(shouldShowComments()){
pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG);
}
if(showRelatedStreams && null == relatedStreamsLayout){
//temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(new Fragment(), RELATED_TAB_TAG);
}
if(pageAdapter.getCount() == 0){
pageAdapter.addFragment(new EmptyFragment(), EMPTY_TAB_TAG);
}
pageAdapter.notifyDataSetUpdate();
if(pageAdapter.getCount() < 2){
tabLayout.setVisibility(View.GONE);
}else{
tabLayout.setVisibility(View.VISIBLE);
}
}
private boolean shouldShowComments() {
try {
return showComments && NewPipe.getService(serviceId)
.getServiceInfo()
.getMediaCapabilities()
.contains(COMMENTS);
} catch (ExtractionException e) {
return false;
}
}
/*//////////////////////////////////////////////////////////////////////////
@@ -995,24 +967,6 @@ public class VideoDetailFragment
}));
}
private View getSeparatorView() {
View separator = new View(activity);
LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1);
int m8 = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
int m5 = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
params.setMargins(m8, m5, m8, m5);
separator.setLayoutParams(params);
TypedValue typedValue = new TypedValue();
activity.getTheme().resolveAttribute(R.attr.separator_color, typedValue, true);
separator.setBackgroundColor(typedValue.data);
return separator;
}
private void setHeightThumbnail() {
final DisplayMetrics metrics = getResources().getDisplayMetrics();
boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
@@ -1024,49 +978,8 @@ public class VideoDetailFragment
thumbnailImageView.setMinimumHeight(height);
}
private void showContentWithAnimation(long duration,
long delay,
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
int translationY = (int) (getResources().getDisplayMetrics().heightPixels *
(translationPercent > 0.0f ? translationPercent : .06f));
contentRootLayoutHiding.animate().setListener(null).cancel();
contentRootLayoutHiding.setAlpha(0f);
contentRootLayoutHiding.setTranslationY(translationY);
contentRootLayoutHiding.setVisibility(View.VISIBLE);
contentRootLayoutHiding.animate()
.alpha(1f)
.translationY(0)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
uploaderRootLayout.animate().setListener(null).cancel();
uploaderRootLayout.setAlpha(0f);
uploaderRootLayout.setTranslationY(translationY);
uploaderRootLayout.setVisibility(View.VISIBLE);
uploaderRootLayout.animate()
.alpha(1f)
.translationY(0)
.setStartDelay((long) (duration * .5f) + delay)
.setDuration(duration)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
if (showRelatedStreams) {
relatedStreamRootLayout.animate().setListener(null).cancel();
relatedStreamRootLayout.setAlpha(0f);
relatedStreamRootLayout.setTranslationY(translationY);
relatedStreamRootLayout.setVisibility(View.VISIBLE);
relatedStreamRootLayout.animate()
.alpha(1f)
.translationY(0)
.setStartDelay((long) (duration * .8f) + delay)
.setDuration(duration)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
}
private void showContent() {
AnimationUtils.slideUp(contentRootLayoutHiding,120, 96, 0.06f);
}
protected void setInitialData(int serviceId, String url, String name) {
@@ -1101,7 +1014,7 @@ public class VideoDetailFragment
public void showLoading() {
super.showLoading();
animateView(contentRootLayoutHiding, false, 200);
contentRootLayoutHiding.setVisibility(View.INVISIBLE);
animateView(spinnerToolbar, false, 200);
animateView(thumbnailPlayButton, false, 50);
animateView(detailDurationView, false, 100);
@@ -1111,17 +1024,17 @@ public class VideoDetailFragment
animateView(videoTitleTextView, true, 0);
videoDescriptionRootLayout.setVisibility(View.GONE);
if (videoTitleToggleArrow != null) { //phone
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoTitleToggleArrow.setVisibility(View.GONE);
} else { //tablet
final View related = (View) relatedStreamRootLayout.getParent();
//don`t need to hide it if related streams are disabled
if (related.getVisibility() == View.VISIBLE) {
related.setVisibility(View.INVISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoTitleToggleArrow.setVisibility(View.GONE);
videoTitleRoot.setClickable(false);
if(relatedStreamsLayout != null){
if(showRelatedStreams){
relatedStreamsLayout.setVisibility(View.INVISIBLE);
}else{
relatedStreamsLayout.setVisibility(View.GONE);
}
}
videoTitleRoot.setClickable(false);
imageLoader.cancelDisplayTask(thumbnailImageView);
imageLoader.cancelDisplayTask(uploaderThumb);
@@ -1134,7 +1047,20 @@ public class VideoDetailFragment
super.handleResult(info);
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
pushToStack(serviceId, url, name);
if(showRelatedStreams){
if(null == relatedStreamsLayout){ //phone
pageAdapter.updateItem(RELATED_TAB_TAG, RelatedVideosFragment.getInstance(currentInfo));
pageAdapter.notifyDataSetUpdate();
}else{ //tablet
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedStreamsLayout, RelatedVideosFragment.getInstance(currentInfo))
.commitNow();
relatedStreamsLayout.setVisibility(View.VISIBLE);
}
}
//pushToStack(serviceId, url, name);
animateView(thumbnailPlayButton, true, 200);
videoTitleTextView.setText(name);
@@ -1185,25 +1111,23 @@ public class VideoDetailFragment
if (info.getDuration() > 0) {
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);
} else if (info.getStreamType() == StreamType.LIVE_STREAM) {
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);
} else {
detailDurationView.setVisibility(View.GONE);
}
videoDescriptionView.setVisibility(View.GONE);
if (videoTitleToggleArrow != null) {
videoTitleRoot.setClickable(true);
videoTitleToggleArrow.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoDescriptionRootLayout.setVisibility(View.GONE);
} else {
videoDescriptionRootLayout.setVisibility(View.VISIBLE);
}
videoTitleRoot.setClickable(true);
videoTitleToggleArrow.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
videoDescriptionRootLayout.setVisibility(View.GONE);
if (!TextUtils.isEmpty(info.getUploadDate())) {
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
}
@@ -1212,11 +1136,6 @@ public class VideoDetailFragment
animateView(spinnerToolbar, true, 500);
setupActionBar(info);
initThumbnailViews(info);
initRelatedVideos(info);
if (wasRelatedStreamsExpanded) {
toggleExpandRelatedVideos(currentInfo);
wasRelatedStreamsExpanded = false;
}
setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName());
setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName());
@@ -1251,11 +1170,6 @@ public class VideoDetailFragment
// Only auto play in the first open
autoPlayEnabled = false;
}
final ViewParent related = relatedStreamRootLayout.getParent();
if (related instanceof ScrollView) {
((ScrollView) related).scrollTo(0, 0);
}
}
@@ -1269,10 +1183,18 @@ public class VideoDetailFragment
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
Toast.makeText(activity,
R.string.could_not_setup_download_menu,
Toast.LENGTH_LONG).show();
e.printStackTrace();
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
ServiceList.all()
.get(currentInfo
.getServiceId())
.getServiceInfo()
.getName(), "",
R.string.could_not_setup_download_menu);
ErrorActivity.reportError(getActivity(),
e,
getActivity().getClass(),
getActivity().findViewById(android.R.id.content), info);
}
}
@@ -1314,13 +1236,4 @@ public class VideoDetailFragment
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
}
private void setRelatedStreamsVisibility(int visibility) {
final ViewParent parent = relatedStreamRootLayout.getParent();
if (parent instanceof ScrollView) {
((ScrollView) parent).setVisibility(visibility);
} else {
relatedStreamRootLayout.setVisibility(visibility);
}
}
}

View File

@@ -22,6 +22,7 @@ import android.view.View;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
@@ -220,6 +221,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
}
});
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
@Override
public void selected(CommentsInfoItem selectedItem) {
onItemSelected(selectedItem);
}
});
itemsList.clearOnScrollListeners();
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
@Override

View File

@@ -233,10 +233,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
openRssFeed();
break;
case R.id.menu_item_openInBrowser:
openUrlInBrowser(url);
openUrlInBrowser(currentInfo.getOriginalUrl());
break;
case R.id.menu_item_share:
shareUrl(name, url);
shareUrl(name, currentInfo.getOriginalUrl());
break;
default:
return super.onOptionsItemSelected(item);

View File

@@ -0,0 +1,149 @@
package org.schabi.newpipe.fragments.list.comments;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private CompositeDisposable disposables = new CompositeDisposable();
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private boolean mIsVisibleToUser = false;
public static CommentsFragment getInstance(int serviceId, String url, String name) {
CommentsFragment instance = new CommentsFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_comments, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl);
}
@Override
protected Single<CommentsInfo> loadResult(boolean forceLoad) {
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
}
@Override
public void handleResult(@NonNull CommentsInfo result) {
super.handleResult(result);
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) disposables.clear();
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_COMMENTS,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
return;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
return;
}
@Override
protected boolean isGridLayout() {
return false;
}
}

View File

@@ -12,6 +12,7 @@ import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.TooltipCompat;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.Editable;
import android.text.TextUtils;
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.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.util.FireTvUtils;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
@@ -72,8 +73,8 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import static android.support.v7.widget.helper.ItemTouchHelper.Callback.makeMovementFlags;
import static java.util.Arrays.asList;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment
@@ -104,8 +105,13 @@ public class SearchFragment
// this three represet the current search query
@State
protected String searchString;
/**
* No content filter should add like contentfilter = all
* be aware of this when implementing an extractor.
*/
@State
protected String[] contentFilter;
protected String[] contentFilter = new String[0];
@State
protected String sortFilter;
@@ -292,7 +298,23 @@ public class SearchFragment
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
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);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
@@ -335,7 +357,7 @@ public class SearchFragment
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
search(!TextUtils.isEmpty(searchString)
? searchString
: searchEditText.getText().toString(), new String[0], "");
: searchEditText.getText().toString(), this.contentFilter, "");
} else {
if (searchEditText != null) {
searchEditText.setText("");
@@ -449,6 +471,9 @@ public class SearchFragment
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
if(FireTvUtils.isFireTv()){
showKeyboardSearch();
}
});
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
@@ -499,7 +524,9 @@ public class SearchFragment
if (DEBUG) {
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.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
search(searchEditText.getText().toString(), new String[0], "");
@@ -541,7 +568,7 @@ public class SearchFragment
if (searchEditText.requestFocus()) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
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(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
searchEditText.clearFocus();
}
@@ -736,6 +762,7 @@ public class SearchFragment
@Override
protected void loadMoreItems() {
if(nextPageUrl == null || nextPageUrl.isEmpty()) return;
isLoading.set(true);
showListFooter(true);
if (searchDisposable != null) searchDisposable.dispose();
@@ -890,4 +917,28 @@ public class SearchFragment
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);
}

View File

@@ -0,0 +1,208 @@
package org.schabi.newpipe.fragments.list.videos;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.Switch;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.RelatedStreamInfo;
import java.io.Serializable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
private CompositeDisposable disposables = new CompositeDisposable();
private RelatedStreamInfo relatedStreamInfo;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private Switch aSwitch;
private boolean mIsVisibleToUser = false;
public static RelatedVideosFragment getInstance(StreamInfo info) {
RelatedVideosFragment instance = new RelatedVideosFragment();
instance.setInitialData(info);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_related_streams, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
}
protected View getListHeader(){
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
aSwitch.setChecked(autoplay);
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
prefEdit.apply();
}
});
return headerRootLayout;
}else{
return null;
}
}
@Override
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
}
@Override
protected Single<RelatedStreamInfo> loadResult(boolean forceLoad) {
return Single.fromCallable(() -> relatedStreamInfo);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE);
}
@Override
public void handleResult(@NonNull RelatedStreamInfo result) {
super.handleResult(result);
if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE);
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) disposables.clear();
}
@Override
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_STREAM,
NewPipe.getNameOfService(serviceId),
"Get next page of: " + url,
R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
hideLoading();
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
return;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
return;
}
private void setInitialData(StreamInfo info) {
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
}
private static final String INFO_KEY = "related_info_key";
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, relatedStreamInfo);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
super.onRestoreInstanceState(savedState);
if (savedState != null) {
Serializable serializable = savedState.getSerializable(INFO_KEY);
if(serializable instanceof RelatedStreamInfo){
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
}
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
if(null != aSwitch) aSwitch.setChecked(autoplay);
}
@Override
protected boolean isGridLayout() {
return false;
}
}

View File

@@ -10,10 +10,13 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
@@ -50,6 +53,7 @@ public class InfoItemBuilder {
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
public InfoItemBuilder(Context context) {
this.context = context;
@@ -73,6 +77,8 @@ public class InfoItemBuilder {
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
case PLAYLIST:
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
case COMMENT:
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent);
default:
Log.e(TAG, "Trollolo");
throw new RuntimeException("InfoType not expected = " + infoType.name());
@@ -111,4 +117,12 @@ public class InfoItemBuilder {
this.onPlaylistSelectedListener = listener;
}
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
return onCommentsSelectedListener;
}
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
this.onCommentsSelectedListener = onCommentsSelectedListener;
}
}

View File

@@ -9,10 +9,13 @@ import android.view.ViewGroup;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
@@ -63,6 +66,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
private static final int MINI_COMMENT_HOLDER_TYPE = 0x400;
private static final int COMMENT_HOLDER_TYPE = 0x401;
private final InfoItemBuilder infoItemBuilder;
private final ArrayList<InfoItem> infoItemList;
@@ -98,6 +103,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
infoItemBuilder.setOnPlaylistSelectedListener(listener);
}
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> listener) {
infoItemBuilder.setOnCommentsSelectedListener(listener);
}
public void useMiniItemVariants(boolean useMiniVariant) {
this.useMiniVariant = useMiniVariant;
}
@@ -223,6 +232,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
case PLAYLIST:
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
case COMMENT:
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
default:
Log.e(TAG, "Trollolo");
return -1;
@@ -231,7 +242,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
if (DEBUG)
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
switch (type) {
case HEADER_TYPE:
return new HFHolder(header);
@@ -255,6 +267,10 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
case GRID_PLAYLIST_HOLDER_TYPE:
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
case MINI_COMMENT_HOLDER_TYPE:
return new CommentsMiniInfoItemHolder(infoItemBuilder, parent);
case COMMENT_HOLDER_TYPE:
return new CommentsInfoItemHolder(infoItemBuilder, parent);
default:
Log.e(TAG, "Trollolo");
return new FallbackViewHolder(new View(parent.getContext()));

View File

@@ -0,0 +1,53 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.Localization;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfoItemHolder .java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
public final TextView itemTitleView;
public CommentsInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comments_item, parent);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
super.updateFromItem(infoItem);
if (!(infoItem instanceof CommentsInfoItem)) return;
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemTitleView.setText(item.getAuthorName());
}
}

View File

@@ -0,0 +1,99 @@
package org.schabi.newpipe.info_list.holder;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.NavigationHelper;
import de.hdodenhof.circleimageview.CircleImageView;
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
public final CircleImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemDislikesCountView;
private final TextView itemPublishedTime;
private static final int commentDefaultLines = 2;
private static final int commentExpandedLines = 1000;
CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
}
public CommentsMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_comments_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
if (!(infoItem instanceof CommentsInfoItem)) return;
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemBuilder.getImageLoader()
.displayImage(item.getAuthorThumbnail(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemThumbnailView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
NavigationHelper.openChannelFragment(
activity.getSupportFragmentManager(),
item.getServiceId(),
item.getAuthorEndpoint(),
item.getAuthorName());
} catch (Exception e) {
ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e);
}
}
});
// ellipsize if not already ellipsized
if (null == itemContentView.getEllipsize()) {
itemContentView.setEllipsize(TextUtils.TruncateAt.END);
itemContentView.setMaxLines(commentDefaultLines);
}
itemContentView.setText(item.getCommentText());
if (null != item.getLikeCount()) {
itemLikesCountView.setText(String.valueOf(item.getLikeCount()));
}
itemPublishedTime.setText(item.getPublishedTime());
itemView.setOnClickListener(view -> {
toggleEllipsize(item.getCommentText());
if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
});
}
private void toggleEllipsize(String text) {
// toggle ellipsize
if (null == itemContentView.getEllipsize()) {
itemContentView.setEllipsize(TextUtils.TruncateAt.END);
itemContentView.setMaxLines(commentDefaultLines);
} else {
itemContentView.setEllipsize(null);
itemContentView.setMaxLines(commentExpandedLines);
}
}
}

View File

@@ -8,7 +8,11 @@ import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -21,11 +25,13 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.BaseLocalListFragment;
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.SinglePlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
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
///////////////////////////////////////////////////////////////////////////
@@ -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
///////////////////////////////////////////////////////////////////////////

View File

@@ -147,11 +147,16 @@ public class SubscriptionService {
}
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.getName().equals(entity.getName()) &&
info.getAvatarUrl().equals(entity.getAvatarUrl()) &&
info.getDescription().equals(entity.getDescription()) &&
equalsAndNotNull(info.getAvatarUrl(), entity.getAvatarUrl()) &&
equalsAndNotNull(info.getDescription(), entity.getDescription()) &&
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();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;

View File

@@ -241,6 +241,11 @@ public final class MainVideoPlayer extends AppCompatActivity
isBackPressed = false;
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase));
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/

View File

@@ -181,6 +181,11 @@ public final class PopupVideoPlayer extends Service {
closePopup();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
@@ -626,6 +631,7 @@ public final class PopupVideoPlayer extends Service {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
if (playerImpl == null) return;
// rebuild notification here since remote view does not release bitmaps,
// causing memory leaks
resetNotification();

View File

@@ -131,7 +131,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
private void onAudioFocusLossCanDuck() {
Log.d(TAG, "onAudioFocusLossCanDuck() called");
// 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) {

View File

@@ -45,7 +45,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
public class PlayerHelper {
private PlayerHelper() {}

View File

@@ -1,7 +1,9 @@
package org.schabi.newpipe.player.playback;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
@@ -54,6 +56,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
.setTitle(item.getTitle())
.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());
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);

View File

@@ -13,6 +13,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
@@ -93,15 +94,17 @@ public class VideoPlaybackResolver implements PlaybackResolver {
// Below are auxiliary media sources
// Create subtitle sources
for (final SubtitlesStream subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) continue;
if(info.getSubtitles() != null) {
for (final SubtitlesStream subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
if (mimeType == null) continue;
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
mediaSources.add(textSource);
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
mediaSources.add(textSource);
}
}
if (mediaSources.size() == 1) {

View File

@@ -15,6 +15,7 @@ public enum UserAction {
REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream");

View File

@@ -4,6 +4,7 @@ import android.os.Bundle;
import android.support.v7.preference.Preference;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.CheckForNewAppVersionTask;
import org.schabi.newpipe.R;
public class MainSettingsFragment extends BasePreferenceFragment {
@@ -13,6 +14,13 @@ public class MainSettingsFragment extends BasePreferenceFragment {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
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) {
final Preference debug = findPreference(getString(R.string.debug_pref_screen_key));
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

@@ -25,6 +25,7 @@ import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.res.ColorStateList;
import android.support.annotation.ColorInt;
import android.support.annotation.FloatRange;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.Log;
@@ -363,4 +364,24 @@ public class AnimationUtils {
}).start();
}
}
public static void slideUp(final View view,
long duration,
long delay,
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels *
(translationPercent));
view.animate().setListener(null).cancel();
view.setAlpha(0f);
view.setTranslationY(translationY);
view.setVisibility(View.VISIBLE);
view.animate()
.alpha(1f)
.translationY(0)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(new FastOutSlowInInterpolator())
.start();
}
}

View File

@@ -29,10 +29,12 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
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.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
@@ -46,6 +48,7 @@ import org.schabi.newpipe.report.UserAction;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.List;
import io.reactivex.Maybe;
@@ -60,7 +63,7 @@ public final class ExtractorHelper {
}
private static void checkServiceId(int serviceId) {
if(serviceId == Constants.NO_SERVICE_ID) {
if (serviceId == Constants.NO_SERVICE_ID) {
throw new IllegalArgumentException("serviceId is NO_SERVICE_ID");
}
}
@@ -95,17 +98,20 @@ public final class ExtractorHelper {
public static Single<List<String>> suggestionsFor(final int serviceId,
final String query) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
NewPipe.getService(serviceId)
.getSuggestionExtractor()
.suggestionList(query));
return Single.fromCallable(() -> {
SuggestionExtractor extractor = NewPipe.getService(serviceId)
.getSuggestionExtractor();
return extractor != null
? extractor.suggestionList(query)
: Collections.emptyList();
});
}
public static Single<StreamInfo> getStreamInfo(final int serviceId,
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM, Single.fromCallable(() ->
StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
}
@@ -113,29 +119,45 @@ public final class ExtractorHelper {
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.CHANNEL, Single.fromCallable(() ->
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage> getMoreChannelItems(final int serviceId,
final String url,
final String nextStreamsUrl) {
final String url,
final String nextStreamsUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
}
public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.COMMENT, Single.fromCallable(() ->
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage> getMoreCommentItems(final int serviceId,
final CommentsInfo info,
final String nextPageUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPageUrl));
}
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId,
final String url,
boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
PlaylistInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage> getMorePlaylistItems(final int serviceId,
final String url,
final String nextStreamsUrl) {
final String url,
final String nextStreamsUrl) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
@@ -144,7 +166,7 @@ public final class ExtractorHelper {
public static Single<KioskInfo> getKioskInfo(final int serviceId,
final String url,
boolean forceLoad) {
return checkCache(forceLoad, serviceId, url, Single.fromCallable(() ->
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.PLAYLIST, Single.fromCallable(() ->
KioskInfo.getInfo(NewPipe.getService(serviceId), url)));
}
@@ -168,16 +190,17 @@ public final class ExtractorHelper {
private static <I extends Info> Single<I> checkCache(boolean forceLoad,
int serviceId,
String url,
InfoItem.InfoType infoType,
Single<I> loadFromNetwork) {
checkServiceId(serviceId);
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info));
loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info, infoType));
Single<I> load;
if (forceLoad) {
cache.removeInfo(serviceId, url);
cache.removeInfo(serviceId, url, infoType);
load = loadFromNetwork;
} else {
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url),
load = Maybe.concat(ExtractorHelper.loadFromCache(serviceId, url, infoType),
loadFromNetwork.toMaybe())
.firstElement() //Take the first valid
.toSingle();
@@ -189,20 +212,20 @@ public final class ExtractorHelper {
/**
* Default implementation uses the {@link InfoCache} to get cached results
*/
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url) {
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url, InfoItem.InfoType infoType) {
checkServiceId(serviceId);
return Maybe.defer(() -> {
//noinspection unchecked
I info = (I) cache.getFromKey(serviceId, url);
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
//noinspection unchecked
I info = (I) cache.getFromKey(serviceId, url, infoType);
if (MainActivity.DEBUG) Log.d(TAG, "loadFromCache() called, info > " + info);
// Only return info if it's not null (it is cached)
if (info != null) {
return Maybe.just(info);
}
// Only return info if it's not null (it is cached)
if (info != null) {
return Maybe.just(info);
}
return Maybe.empty();
});
return Maybe.empty();
});
}
/**

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

@@ -26,6 +26,7 @@ import android.util.Log;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import java.util.Map;
@@ -52,27 +53,27 @@ public final class InfoCache {
}
@Nullable
public Info getFromKey(int serviceId, @NonNull String url) {
public Info getFromKey(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "getFromKey() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
synchronized (lruCache) {
return getInfo(keyOf(serviceId, url));
return getInfo(keyOf(serviceId, url, infoType));
}
}
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info) {
public void putInfo(int serviceId, @NonNull String url, @NonNull Info info, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
synchronized (lruCache) {
final CacheData data = new CacheData(info, expirationMillis);
lruCache.put(keyOf(serviceId, url), data);
lruCache.put(keyOf(serviceId, url, infoType), data);
}
}
public void removeInfo(int serviceId, @NonNull String url) {
public void removeInfo(int serviceId, @NonNull String url, @NonNull InfoItem.InfoType infoType) {
if (DEBUG) Log.d(TAG, "removeInfo() called with: serviceId = [" + serviceId + "], url = [" + url + "]");
synchronized (lruCache) {
lruCache.remove(keyOf(serviceId, url));
lruCache.remove(keyOf(serviceId, url, infoType));
}
}
@@ -98,8 +99,8 @@ public final class InfoCache {
}
@NonNull
private static String keyOf(final int serviceId, @NonNull final String url) {
return serviceId + url;
private static String keyOf(final int serviceId, @NonNull final String url, @NonNull InfoItem.InfoType infoType) {
return serviceId + url + infoType.toString();
}
private static void removeStaleCache() {

View File

@@ -31,6 +31,8 @@ public class KioskTranslator {
return c.getString(R.string.top_50);
case "New & hot":
return c.getString(R.string.new_and_hot);
case "conferences":
return c.getString(R.string.conferences);
default:
return kioskId;
}
@@ -44,6 +46,8 @@ public class KioskTranslator {
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
case "New & hot":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
case "conferences":
return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_hot);
default:
return 0;
}

View File

@@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
import org.schabi.newpipe.local.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
@@ -309,6 +310,18 @@ public class NavigationHelper {
.commit();
}
public static void openCommentsFragment(
FragmentManager fragmentManager,
int serviceId,
String url,
String name) {
if (name == null) name = "";
fragmentManager.beginTransaction().setCustomAnimations(R.anim.switch_service_in, R.anim.switch_service_out)
.replace(R.id.fragment_holder, CommentsFragment.getInstance(serviceId, url, name))
.addToBackStack(null)
.commit();
}
public static void openPlaylistFragment(FragmentManager fragmentManager,
int serviceId,
String url,

View File

@@ -0,0 +1,41 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RelatedStreamInfo extends ListInfo<InfoItem> {
private StreamInfoItem nextStream;
public RelatedStreamInfo(int serviceId, ListLinkHandler listUrlIdHandler, String name) {
super(serviceId, listUrlIdHandler, name);
}
public static RelatedStreamInfo getInfo(StreamInfo info) {
ListLinkHandler handler = new ListLinkHandler(info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null);
RelatedStreamInfo relatedStreamInfo = new RelatedStreamInfo(info.getServiceId(), handler, info.getName());
List<InfoItem> streams = new ArrayList<>();
if(info.getNextVideo() != null){
streams.add(info.getNextVideo());
}
streams.addAll(info.getRelatedStreams());
relatedStreamInfo.setRelatedItems(streams);
relatedStreamInfo.setNextStream(info.getNextVideo());
return relatedStreamInfo;
}
public StreamInfoItem getNextStream() {
return nextStream;
}
public void setNextStream(StreamInfoItem nextStream) {
this.nextStream = nextStream;
}
}

View File

@@ -36,7 +36,6 @@ public class SecondaryStreamHelper<T extends Stream> {
* @return selected audio stream or null if a candidate was not found
*/
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()) {
case WEBM:
case MPEG_4:

View File

@@ -24,9 +24,11 @@ public class ServiceHelper {
case 0:
return R.drawable.place_holder_youtube;
case 1:
return R.drawable.place_holder_circle;
return R.drawable.place_holder_cloud;
case 2:
return R.drawable.place_holder_gadse;
default:
return R.drawable.service;
return R.drawable.place_holder_circle;
}
}
@@ -38,6 +40,8 @@ public class ServiceHelper {
case "playlists": return c.getString(R.string.playlists);
case "tracks": return c.getString(R.string.tracks);
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;
}
}

View File

@@ -28,7 +28,8 @@ import io.reactivex.schedulers.Schedulers;
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 {
private final Context context;
@@ -110,7 +111,10 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
}
}
} 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) {
qualityString = ((SubtitlesStream) stream).getDisplayLanguageName();
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 String unknownSize;
public StreamSizeWrapper(List<T> streamsList, Context context) {
this.streamsList = streamsList;
public StreamSizeWrapper(List<T> sL, Context context) {
this.streamsList = sL != null
? sL
: Collections.emptyList();
this.streamSizes = new long[streamsList.size()];
this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content);

View File

@@ -137,7 +137,9 @@ public class ThemeHelper {
else if (selectedTheme.equals(darkTheme)) themeName = "DarkTheme";
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) {
return resourceId;