diff --git a/README.md b/README.md index 0d615f43c..6c1aa3d4b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@

WebsiteBlogPress


-WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS. +WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY. + +PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS. ## Screenshots diff --git a/app/build.gradle b/app/build.gradle index 75f966a9b..da735302c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 19 targetSdkVersion 28 - versionCode 71 - versionName "0.15.1" + versionCode 740 + versionName "0.16.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -44,10 +44,10 @@ android { ext { supportLibVersion = '28.0.0' - exoPlayerLibVersion = '2.8.4' //2.9.0 + exoPlayerLibVersion = '2.9.6' roomDbLibVersion = '1.1.1' leakCanaryLibVersion = '1.5.4' //1.6.1 - okHttpLibVersion = '3.11.0' + okHttpLibVersion = '3.12.1' icepickLibVersion = '3.2.0' stethoLibVersion = '1.5.0' } @@ -57,7 +57,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:79b0a19d1af' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:2ac713e' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' diff --git a/app/src/main/java/android/support/design/widget/FlingBehavior.java b/app/src/main/java/android/support/design/widget/FlingBehavior.java new file mode 100644 index 000000000..59eb08294 --- /dev/null +++ b/app/src/main/java/android/support/design/widget/FlingBehavior.java @@ -0,0 +1,116 @@ +package android.support.design.widget; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.design.animation.AnimationUtils; +import android.util.AttributeSet; +import android.view.View; + +// check this https://github.com/ToDou/appbarlayout-spring-behavior/blob/master/appbarspring/src/main/java/android/support/design/widget/AppBarFlingFixBehavior.java +public final class FlingBehavior extends AppBarLayout.Behavior { + + private ValueAnimator mOffsetAnimator; + private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms + + public FlingBehavior() { + } + + public FlingBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) { + if (dy != 0) { + int val = child.getBottom(); + if (val != 0) { + int min, max; + if (dy < 0) { + // We're scrolling down + } else { + // We're scrolling up + if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) { + mOffsetAnimator.cancel(); + } + min = -child.getUpNestedPreScrollRange(); + max = 0; + consumed[1] = scroll(coordinatorLayout, child, dy, min, max); + } + } + } + } + + @Override + public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull AppBarLayout child, @NonNull View target, float velocityX, float velocityY) { + + if (velocityY != 0) { + if (velocityY < 0) { + // We're flinging down + int val = child.getBottom(); + if (val != 0) { + final int targetScroll = + +child.getDownNestedPreScrollRange(); + animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY); + } + + } else { + // We're flinging up + int val = child.getBottom(); + if (val != 0) { + final int targetScroll = -child.getUpNestedPreScrollRange(); + if (getTopBottomOffsetForScrollingSibling() > targetScroll) { + animateOffsetTo(coordinatorLayout, child, targetScroll, velocityY); + } + } + } + } + + return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); + } + + private void animateOffsetTo(final CoordinatorLayout coordinatorLayout, + final AppBarLayout child, final int offset, float velocity) { + final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset); + + final int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 3 * Math.round(1000 * (distance / velocity)); + } else { + final float distanceRatio = (float) distance / child.getHeight(); + duration = (int) ((distanceRatio + 1) * 150); + } + + animateOffsetWithDuration(coordinatorLayout, child, offset, duration); + } + + private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout, + final AppBarLayout child, final int offset, final int duration) { + final int currentOffset = getTopBottomOffsetForScrollingSibling(); + if (currentOffset == offset) { + if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) { + mOffsetAnimator.cancel(); + } + return; + } + + if (mOffsetAnimator == null) { + mOffsetAnimator = new ValueAnimator(); + mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR); + mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + setHeaderTopBottomOffset(coordinatorLayout, child, + (Integer) animator.getAnimatedValue()); + } + }); + } else { + mOffsetAnimator.cancel(); + } + + mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION)); + mOffsetAnimator.setIntValues(currentOffset, offset); + mOffsetAnimator.start(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index af9b88ac1..6a6d1b9c2 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -2,11 +2,13 @@ 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; @@ -68,6 +70,8 @@ public class CheckForNewAppVersionTask extends AsyncTask { @Override protected String doInBackground(Void... voids) { + + if(isCancelled() || !isConnected()) return null; // Make a network request to get latest NewPipe data. if (client == null) { @@ -227,4 +231,12 @@ public class CheckForNewAppVersionTask extends AsyncTask { return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1); } + + private boolean isConnected() { + + ConnectivityManager cm = + (ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE); + return cm.getActiveNetworkInfo() != null + && cm.getActiveNetworkInfo().isConnected(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index 32e8bd414..ff274a91a 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -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 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 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> requestHeaders = request.getRequestHeaders(); + // set custom headers in request + for (Map.Entry> 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> 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> 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()); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index b8941670f..f040dc8b4 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -36,12 +36,12 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -81,10 +81,13 @@ public class RouterActivity extends AppCompatActivity { protected int selectedPreviously = -1; protected String currentUrl; + protected boolean internalRoute = false; protected final CompositeDisposable disposables = new CompositeDisposable(); private boolean selectionIsDownload = false; + public static final String internalRouteKey = "internalRoute"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -94,11 +97,13 @@ public class RouterActivity extends AppCompatActivity { currentUrl = getUrl(getIntent()); if (TextUtils.isEmpty(currentUrl)) { - Toast.makeText(this, R.string.invalid_url_toast, Toast.LENGTH_LONG).show(); + handleText(); finish(); } } + internalRoute = getIntent().getBooleanExtra(internalRouteKey, false); + setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); } @@ -112,7 +117,7 @@ public class RouterActivity extends AppCompatActivity { @Override protected void onStart() { super.onStart(); - + handleUrl(currentUrl); } @@ -353,6 +358,15 @@ public class RouterActivity extends AppCompatActivity { positiveButton.setEnabled(state); } + private void handleText(){ + String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT); + int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0); + Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + NavigationHelper.openSearch(getThemeWrapperContext(),serviceId,searchString); + } + private void handleChoice(final String selectedChoiceKey) { final List validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list)); if (validChoicesList.contains(selectedChoiceKey)) { @@ -383,8 +397,10 @@ public class RouterActivity extends AppCompatActivity { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(intent -> { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + if(!internalRoute){ + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + } startActivity(intent); finish(); diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 251e4c730..7ee686a66 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -47,7 +47,7 @@ public class DownloadActivity extends AppCompatActivity { @Override public void onGlobalLayout() { updateFragments(); - getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this); + getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index acee1f111..4546483d2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -230,21 +230,4 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId)); } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - protected void openUrlInBrowser(String url) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title))); - } - - protected void shareUrl(String subject, String url) { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_SUBJECT, subject); - intent.putExtra(Intent.EXTRA_TEXT, url); - startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); - } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java new file mode 100644 index 000000000..0666667d6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/EmptyFragment.java @@ -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); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java new file mode 100644 index 000000000..8314f9539 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java @@ -0,0 +1,86 @@ +package org.schabi.newpipe.fragments.detail; + +import android.support.annotation.Nullable; +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 mFragmentList = new ArrayList<>(); + private final List 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 int getItemPositionByTitle(String title) { + return mFragmentTitleList.indexOf(title); + } + + @Nullable + public String getItemTitle(int position) { + if (position < 0 || position >= mFragmentTitleList.size()) { + return null; + } + return mFragmentTitleList.get(position); + } + + public void notifyDataSetUpdate(){ + notifyDataSetChanged(); + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss(); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index c346e1329..bbd1a315d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -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; @@ -57,6 +54,7 @@ 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; @@ -64,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; @@ -86,11 +85,10 @@ 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.ShareUtils; 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; @@ -105,6 +103,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 @@ -115,27 +114,28 @@ 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; + private String selectedTabTag; - @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 sortedVideoStreams; private int selectedVideoStreamIndex = -1; @@ -148,7 +148,6 @@ public class VideoDetailFragment private Spinner spinnerToolbar; - private ParallaxScrollView parallaxScrollRootView; private LinearLayout contentRootLayoutHiding; private View thumbnailBackgroundButton; @@ -157,7 +156,6 @@ public class VideoDetailFragment private View videoTitleRoot; private TextView videoTitleTextView; - @Nullable private ImageView videoTitleToggleArrow; private TextView videoCountView; @@ -182,10 +180,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; /*////////////////////////////////////////////////////////////////////////*/ @@ -201,12 +204,20 @@ 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); + + selectedTabTag = PreferenceManager.getDefaultSharedPreferences(activity) + .getString(getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG); + PreferenceManager.getDefaultSharedPreferences(activity) .registerOnSharedPreferenceChangeListener(this); } @@ -220,6 +231,10 @@ public class VideoDetailFragment public void onPause() { super.onPause(); if (currentWorker != null) currentWorker.dispose(); + PreferenceManager.getDefaultSharedPreferences(getContext()) + .edit() + .putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem())) + .apply(); } @Override @@ -228,14 +243,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; } @@ -292,6 +309,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; } } @@ -301,7 +321,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) { @@ -310,10 +329,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); @@ -326,12 +341,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); @@ -339,6 +353,7 @@ public class VideoDetailFragment //noinspection unchecked stack.addAll((Collection) serializable); } + } /*////////////////////////////////////////////////////////////////////////// @@ -394,9 +409,6 @@ public class VideoDetailFragment case R.id.detail_title_root_layout: toggleTitleAndDescription(); break; - case R.id.detail_related_streams_expand: - toggleExpandRelatedVideos(currentInfo); - break; } } @@ -420,44 +432,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; - } - - 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 //////////////////////////////////////////////////////////////////////////*/ @@ -467,8 +452,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); @@ -504,32 +487,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() { - @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); @@ -539,7 +513,6 @@ public class VideoDetailFragment detailControlsAddToPlaylist.setOnClickListener(this); detailControlsDownload.setOnClickListener(this); detailControlsDownload.setOnLongClickListener(this); - relatedStreamExpandButton.setOnClickListener(this); detailControlsBackground.setLongClickable(true); detailControlsPopup.setLongClickable(true); @@ -575,7 +548,7 @@ public class VideoDetailFragment } break; case 3: - shareUrl(item.getName(), item.getUrl()); + ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl()); break; default: break; @@ -622,44 +595,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 @@ -693,7 +628,7 @@ public class VideoDetailFragment @Override public boolean onOptionsItemSelected(MenuItem item) { - if(isLoading.get()) { + if (isLoading.get()) { // if is still loading block menu return true; } @@ -702,13 +637,13 @@ public class VideoDetailFragment switch (id) { case R.id.menu_item_share: { if (currentInfo != null) { - shareUrl(currentInfo.getName(), currentInfo.getOriginalUrl()); + ShareUtils.shareUrl(this.getContext(), currentInfo.getName(), currentInfo.getOriginalUrl()); } return true; } case R.id.menu_item_openInBrowser: { if (currentInfo != null) { - openUrlInBrowser(currentInfo.getOriginalUrl()); + ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl()); } return true; } @@ -717,7 +652,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; @@ -731,7 +666,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(); } @@ -850,23 +786,16 @@ public class VideoDetailFragment 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); } @@ -875,6 +804,7 @@ public class VideoDetailFragment public void startLoading(boolean forceLoad) { super.startLoading(forceLoad); + initTabs(); currentInfo = null; if (currentWorker != null) currentWorker.dispose(); @@ -884,12 +814,54 @@ 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() { + if (pageAdapter.getCount() != 0) { + selectedTabTag = pageAdapter.getItemTitle(viewPager.getCurrentItem()); + } + 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{ + int position = pageAdapter.getItemPositionByTitle(selectedTabTag); + if(position != -1) viewPager.setCurrentItem(position); + tabLayout.setVisibility(View.VISIBLE); + } + } + + private boolean shouldShowComments() { + try { + return showComments && NewPipe.getService(serviceId) + .getServiceInfo() + .getMediaCapabilities() + .contains(COMMENTS); + } catch (ExtractionException e) { + return false; + } } /*////////////////////////////////////////////////////////////////////////// @@ -1009,24 +981,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; @@ -1038,50 +992,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) { @@ -1116,7 +1028,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); @@ -1126,17 +1038,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); @@ -1149,6 +1061,19 @@ public class VideoDetailFragment super.handleResult(info); setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); + + 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); @@ -1213,14 +1138,10 @@ public class VideoDetailFragment } 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())); } @@ -1229,11 +1150,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()); @@ -1268,11 +1184,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); - } } @@ -1339,13 +1250,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); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 0816334ea..dbc3dd8a2 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -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; @@ -33,6 +34,7 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; +import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StateSaver; import java.util.Collections; @@ -220,6 +222,13 @@ public abstract class BaseListFragment extends BaseStateFragment implem } }); + infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture() { + @Override + public void selected(CommentsInfoItem selectedItem) { + onItemSelected(selectedItem); + } + }); + itemsList.clearOnScrollListeners(); itemsList.addOnScrollListener(new OnScrollBelowItemsListener() { @Override @@ -247,6 +256,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem if (context == null || context.getResources() == null || getActivity() == null) return; final String[] commands = new String[]{ + context.getResources().getString(R.string.direct_on_background), context.getResources().getString(R.string.enqueue_on_background), context.getResources().getString(R.string.enqueue_on_popup), context.getResources().getString(R.string.append_playlist), @@ -256,19 +266,22 @@ public abstract class BaseListFragment extends BaseStateFragment implem final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item)); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); break; case 2: + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + break; + case 3: if (getFragmentManager() != null) { PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item)) .show(getFragmentManager(), TAG); } break; - case 3: - shareUrl(item.getName(), item.getUrl()); + case 4: + ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl()); break; default: break; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index b9489ffa7..71865b04d 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -46,6 +46,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ShareUtils; import java.util.ArrayList; import java.util.Collections; @@ -190,7 +191,7 @@ public class ChannelFragment extends BaseListInfoFragment { } break; case 6: - shareUrl(item.getName(), item.getUrl()); + ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl()); break; default: break; @@ -233,10 +234,10 @@ public class ChannelFragment extends BaseListInfoFragment { openRssFeed(); break; case R.id.menu_item_openInBrowser: - openUrlInBrowser(currentInfo.getOriginalUrl()); + ShareUtils.openUrlInBrowser(this.getContext(), currentInfo.getOriginalUrl()); break; case R.id.menu_item_share: - shareUrl(name, currentInfo.getOriginalUrl()); + ShareUtils.shareUrl(this.getContext(), name, currentInfo.getOriginalUrl()); break; default: return super.onOptionsItemSelected(item); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java new file mode 100644 index 000000000..956e6c1c8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java @@ -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 { + + 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 loadMoreItemsLogic() { + return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl); + } + + @Override + protected Single 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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 0019a3819..2a775fe8f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -40,6 +40,7 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; @@ -168,7 +169,7 @@ public class PlaylistFragment extends BaseListInfoFragment { NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); break; case 5: - shareUrl(item.getName(), item.getUrl()); + ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl()); break; default: break; @@ -230,10 +231,10 @@ public class PlaylistFragment extends BaseListInfoFragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_openInBrowser: - openUrlInBrowser(url); + ShareUtils.openUrlInBrowser(this.getContext(), url); break; case R.id.menu_item_share: - shareUrl(name, url); + ShareUtils.shareUrl(this.getContext(), name, url); break; case R.id.menu_item_bookmark: onBookmarkClicked(); @@ -305,6 +306,16 @@ public class PlaylistFragment extends BaseListInfoFragment { NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); headerBackgroundButton.setOnClickListener(view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + + headerPopupButton.setOnLongClickListener(view -> { + NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue()); + return true; + }); + + headerBackgroundButton.setOnLongClickListener(view -> { + NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue()); + return true; + }); } private PlayQueue getPlayQueue() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index e20d6bad3..a3b01f251 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -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; @@ -45,10 +46,9 @@ 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; @@ -73,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 @@ -298,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); @@ -901,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); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index 7b5f72c53..3f4e9af0b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -75,7 +75,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter 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 loadMoreItemsLogic() { + return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage()); + } + + @Override + protected Single 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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index f473e5d08..0e9fd3277 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -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 onStreamSelectedListener; private OnClickGesture onChannelSelectedListener; private OnClickGesture onPlaylistSelectedListener; + private OnClickGesture 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 getOnCommentsSelectedListener() { + return onCommentsSelectedListener; + } + + public void setOnCommentsSelectedListener(OnClickGesture onCommentsSelectedListener) { + this.onCommentsSelectedListener = onCommentsSelectedListener; + } + } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 15fdcad05..5e7095c7d 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -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 infoItemList; @@ -98,6 +103,10 @@ public class InfoListAdapter extends RecyclerView.Adapter listener) { + infoItemBuilder.setOnCommentsSelectedListener(listener); + } + public void useMiniItemVariants(boolean useMiniVariant) { this.useMiniVariant = useMiniVariant; } @@ -223,6 +232,8 @@ public class InfoListAdapter extends RecyclerView.Adapter + * 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 . + */ + +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()); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java new file mode 100644 index 000000000..ce8412b20 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -0,0 +1,150 @@ +package org.schabi.newpipe.info_list.holder; + +import android.support.v7.app.AppCompatActivity; +import android.text.util.Linkify; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.jsoup.helper.StringUtil; +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.CommentTextOnTouchListener; +import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.NavigationHelper; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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; + + private String commentText; + private String streamUrl; + + private static final Pattern pattern = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)"); + + private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() { + @Override + public String transformUrl(Matcher match, String url) { + int timestamp = 0; + String hours = match.group(1); + String minutes = match.group(2); + String seconds = match.group(3); + if(hours != null) timestamp += (Integer.parseInt(hours.replace(":", ""))*3600); + if(minutes != null) timestamp += (Integer.parseInt(minutes.replace(":", ""))*60); + if(seconds != null) timestamp += (Integer.parseInt(seconds)); + return streamUrl + url.replace(match.group(0), "#timestamp=" + String.valueOf(timestamp)); + } + }; + + 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) { + if(StringUtil.isBlank(item.getAuthorEndpoint())) return; + 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); + } + } + }); + + streamUrl = item.getUrl(); + + itemContentView.setLines(commentDefaultLines); + commentText = item.getCommentText(); + itemContentView.setText(commentText); + itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); + + if (itemContentView.getLineCount() == 0) { + itemContentView.post(() -> ellipsize()); + } else { + ellipsize(); + } + + if (null != item.getLikeCount()) { + itemLikesCountView.setText(String.valueOf(item.getLikeCount())); + } + itemPublishedTime.setText(item.getPublishedTime()); + + itemView.setOnClickListener(view -> { + toggleEllipsize(); + if (itemBuilder.getOnCommentsSelectedListener() != null) { + itemBuilder.getOnCommentsSelectedListener().selected(item); + } + }); + } + + private void ellipsize() { + if (itemContentView.getLineCount() > commentDefaultLines){ + int endOfLastLine = itemContentView.getLayout().getLineEnd(commentDefaultLines - 1); + int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine -2); + if(end == -1) end = Math.max(endOfLastLine -2, 0); + String newVal = itemContentView.getText().subSequence(0, end) + " …"; + itemContentView.setText(newVal); + } + linkify(); + } + + private void toggleEllipsize() { + if (itemContentView.getText().toString().equals(commentText)) { + if (itemContentView.getLineCount() > commentDefaultLines) ellipsize(); + } else { + expand(); + } + } + + private void expand() { + itemContentView.setMaxLines(commentExpandedLines); + itemContentView.setText(commentText); + linkify(); + } + + private void linkify(){ + Linkify.addLinks(itemContentView, Linkify.WEB_URLS); + Linkify.addLinks(itemContentView, pattern, null, null, timestampLink); + itemContentView.setMovementMethod(null); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 32083fd42..5a62a3969 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -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,13 +25,16 @@ 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.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; @@ -104,6 +111,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 +168,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 /////////////////////////////////////////////////////////////////////////// @@ -335,7 +395,7 @@ public class StatisticsPlaylistFragment deleteEntry(index); break; case 6: - shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl()); + ShareUtils.shareUrl(this.getContext(), item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl()); break; default: break; diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index f400061e1..dc101fade 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -34,6 +34,7 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; +import org.schabi.newpipe.util.ShareUtils; import java.util.ArrayList; import java.util.Collections; @@ -555,7 +556,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment= 26 /*Oreo*/) updateNotificationThumbnail(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) updateNotificationThumbnail(); if (bigNotRemoteView != null) { + if(cachedDuration != duration) { + cachedDuration = duration; + cachedDurationString = getTimeString(duration); + } bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); - bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration)); + bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + cachedDurationString); } if (notRemoteView != null) { notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index d1b06c9c5..3db4be4a6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -207,8 +207,7 @@ public abstract class BasePlayer implements final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); - final TrackSelection.Factory trackSelectionFactory = - PlayerHelper.getQualitySelector(context, bandwidthMeter); + final TrackSelection.Factory trackSelectionFactory = PlayerHelper.getQualitySelector(context); this.trackSelector = new CustomTrackSelector(trackSelectionFactory); this.loadControl = new LoadController(context); @@ -225,7 +224,7 @@ public abstract class BasePlayer implements public void initPlayer(final boolean playOnReady) { if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); - simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl); + simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderFactory, trackSelector, loadControl); simpleExoPlayer.addListener(this); simpleExoPlayer.setPlayWhenReady(playOnReady); simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); @@ -270,6 +269,18 @@ public abstract class BasePlayer implements final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE, getPlaybackSkipSilence()); + // seek to timestamp if stream is already playing + if (simpleExoPlayer != null + && queue.size() == 1 + && playQueue != null + && playQueue.getItem() != null + && queue.getItem().getUrl().equals(playQueue.getItem().getUrl()) + && queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET + ) { + simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition()); + return; + } + // Good to go... initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, /*playOnInit=*/true); diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index f4fea5165..0bb9c7b2b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -46,6 +46,7 @@ import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; +import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.PopupMenu; @@ -75,6 +76,7 @@ import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; @@ -241,6 +243,11 @@ public final class MainVideoPlayer extends AppCompatActivity isBackPressed = false; } + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); + } + /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ @@ -277,14 +284,9 @@ public final class MainVideoPlayer extends AppCompatActivity if (DEBUG) Log.d(TAG, "showSystemUi() called"); if (playerImpl != null && playerImpl.queueVisible) return; - final int visibility; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - } else { - visibility = View.STATUS_BAR_VISIBLE; - } + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @ColorInt final int systenUiColor = @@ -353,11 +355,7 @@ public final class MainVideoPlayer extends AppCompatActivity protected void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) { final int shuffleAlpha = shuffled ? 255 : 77; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - shuffleButton.setImageAlpha(shuffleAlpha); - } else { - shuffleButton.setAlpha(shuffleAlpha); - } + shuffleButton.setImageAlpha(shuffleAlpha); } private boolean isInMultiWindow() { @@ -397,6 +395,7 @@ public final class MainVideoPlayer extends AppCompatActivity private ImageButton playPauseButton; private ImageButton playPreviousButton; private ImageButton playNextButton; + private Button closeButton; private RelativeLayout queueLayout; private ImageButton itemsListCloseButton; @@ -406,6 +405,7 @@ public final class MainVideoPlayer extends AppCompatActivity private boolean queueVisible; private ImageButton moreOptionsButton; + private ImageButton shareButton; private ImageButton toggleOrientationButton; private ImageButton switchPopupButton; private ImageButton switchBackgroundButton; @@ -437,9 +437,11 @@ public final class MainVideoPlayer extends AppCompatActivity this.playPauseButton = rootView.findViewById(R.id.playPauseButton); this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton); this.playNextButton = rootView.findViewById(R.id.playNextButton); + this.closeButton = rootView.findViewById(R.id.closeButton); this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton); this.secondaryControls = rootView.findViewById(R.id.secondaryControls); + this.shareButton = rootView.findViewById(R.id.share); this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation); this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground); this.switchPopupButton = rootView.findViewById(R.id.switchPopup); @@ -483,8 +485,10 @@ public final class MainVideoPlayer extends AppCompatActivity playPauseButton.setOnClickListener(this); playPreviousButton.setOnClickListener(this); playNextButton.setOnClickListener(this); + closeButton.setOnClickListener(this); moreOptionsButton.setOnClickListener(this); + shareButton.setOnClickListener(this); toggleOrientationButton.setOnClickListener(this); switchBackgroundButton.setOnClickListener(this); switchPopupButton.setOnClickListener(this); @@ -635,6 +639,9 @@ public final class MainVideoPlayer extends AppCompatActivity } else if (v.getId() == moreOptionsButton.getId()) { onMoreOptionsClicked(); + } else if (v.getId() == shareButton.getId()) { + onShareClicked(); + } else if (v.getId() == toggleOrientationButton.getId()) { onScreenRotationClicked(); @@ -644,6 +651,9 @@ public final class MainVideoPlayer extends AppCompatActivity } else if (v.getId() == switchBackgroundButton.getId()) { onPlayBackgroundButtonClicked(); + } else if (v.getId() == closeButton.getId()) { + onPlaybackShutdown(); + return; } if (getCurrentState() != STATE_COMPLETED) { @@ -688,6 +698,13 @@ public final class MainVideoPlayer extends AppCompatActivity showControls(DEFAULT_CONTROLS_DURATION); } + private void onShareClicked() { + // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) + ShareUtils.shareUrl(MainVideoPlayer.this, + playerImpl.getVideoTitle(), + playerImpl.getVideoUrl() + "&t=" + String.valueOf(playerImpl.getPlaybackSeekBar().getProgress()/1000)); + } + private void onScreenRotationClicked() { if (DEBUG) Log.d(TAG, "onScreenRotationClicked() called"); toggleOrientation(); @@ -770,6 +787,7 @@ public final class MainVideoPlayer extends AppCompatActivity super.onBlocked(); playPauseButton.setImageResource(R.drawable.ic_pause_white); animatePlayButtons(false, 100); + animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); getRootView().setKeepScreenOn(true); } @@ -785,6 +803,7 @@ public final class MainVideoPlayer extends AppCompatActivity animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { playPauseButton.setImageResource(R.drawable.ic_pause_white); animatePlayButtons(true, 200); + animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); }); getRootView().setKeepScreenOn(true); @@ -796,6 +815,7 @@ public final class MainVideoPlayer extends AppCompatActivity animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); animatePlayButtons(true, 200); + animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); }); showSystemUi(); @@ -815,8 +835,8 @@ public final class MainVideoPlayer extends AppCompatActivity animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { playPauseButton.setImageResource(R.drawable.ic_replay_white); animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); + animateView(closeButton, true, DEFAULT_CONTROLS_DURATION); }); - getRootView().setKeepScreenOn(false); super.onCompleted(); } @@ -851,8 +871,8 @@ public final class MainVideoPlayer extends AppCompatActivity if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); getControlsVisibilityHandler().removeCallbacksAndMessages(null); getControlsVisibilityHandler().postDelayed(() -> - animateView(getControlsRoot(), false, duration, 0, - MainVideoPlayer.this::hideSystemUi), + animateView(getControlsRoot(), false, duration, 0, + MainVideoPlayer.this::hideSystemUi), /*delayMillis=*/delay ); } @@ -1056,9 +1076,9 @@ public final class MainVideoPlayer extends AppCompatActivity final int resId = currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp - : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp - : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp - : R.drawable.ic_volume_up_white_72dp; + : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp + : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp + : R.drawable.ic_volume_up_white_72dp; playerImpl.getVolumeImageView().setImageDrawable( AppCompatResources.getDrawable(getApplicationContext(), resId) @@ -1082,8 +1102,8 @@ public final class MainVideoPlayer extends AppCompatActivity final int resId = currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp - : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp - : R.drawable.ic_brightness_high_white_72dp; + : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp + : R.drawable.ic_brightness_high_white_72dp; playerImpl.getBrightnessImageView().setImageDrawable( AppCompatResources.getDrawable(getApplicationContext(), resId) diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 8ea3d509c..7578c444c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -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; diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 2ec4275fc..3e04f1e3a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -3,7 +3,6 @@ package org.schabi.newpipe.player; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; -import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.provider.Settings; @@ -653,11 +652,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } final int shuffleAlpha = shuffled ? 255 : 77; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - shuffleButton.setImageAlpha(shuffleAlpha); - } else { - shuffleButton.setAlpha(shuffleAlpha); - } + shuffleButton.setImageAlpha(shuffleAlpha); } private void onPlaybackParameterChanged(final PlaybackParameters parameters) { diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index d30d9b8be..4dbbc571d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -212,7 +212,6 @@ public abstract class VideoPlayer extends BasePlayer @Override public void initListeners() { - super.initListeners(); playbackSeekBar.setOnSeekBarChangeListener(this); playbackSpeedTextView.setOnClickListener(this); qualityTextView.setOnClickListener(this); @@ -298,7 +297,6 @@ public abstract class VideoPlayer extends BasePlayer return true; }); - // Add all available captions for (int i = 0; i < availableLanguages.size(); i++) { final String captionLanguage = availableLanguages.get(i); MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, @@ -316,7 +314,6 @@ public abstract class VideoPlayer extends BasePlayer captionPopupMenu.setOnDismissListener(this); } - private void updateStreamRelatedViews() { if (getCurrentMetadata() == null) return; @@ -509,12 +506,12 @@ public abstract class VideoPlayer extends BasePlayer } // Normalize mismatching language strings - final String preferredLanguage = trackSelector.getPreferredTextLanguage(); - + final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; // Build UI buildCaptionMenu(availableLanguages); if (trackSelector.getParameters().getRendererDisabled(textRenderer) || - preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) { + preferredLanguage == null || (!availableLanguages.contains(preferredLanguage) + && !containsCaseInsensitive(availableLanguages, preferredLanguage))) { captionTextView.setText(R.string.caption_none); } else { captionTextView.setText(preferredLanguage); @@ -522,6 +519,15 @@ public abstract class VideoPlayer extends BasePlayer captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); } + // workaround to match normalized captions like english to English or deutsch to Deutsch + private static boolean containsCaseInsensitive(List list, String toFind) { + for(String i : list){ + if(i.equalsIgnoreCase(toFind)) + return true; + } + return false; + } + /*////////////////////////////////////////////////////////////////////////// // General Player //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index f148aed27..24d1ee1ca 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -12,13 +12,11 @@ import android.os.Build; import android.support.annotation.NonNull; import android.util.Log; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.analytics.AnalyticsListener; public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, - AudioRendererEventListener { + AnalyticsListener { private static final String TAG = "AudioFocusReactor"; @@ -42,7 +40,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, this.player = player; this.context = context; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - player.addAudioDebugListener(this); + player.addAnalyticsListener(this); if (SHOULD_BUILD_FOCUS_REQUEST) { request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) @@ -57,7 +55,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, public void dispose() { abandonAudioFocus(); - player.removeAudioDebugListener(this); + player.removeAnalyticsListener(this); } /*////////////////////////////////////////////////////////////////////////// @@ -164,29 +162,12 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, //////////////////////////////////////////////////////////////////////////*/ @Override - public void onAudioSessionId(int i) { + public void onAudioSessionId(EventTime eventTime, int audioSessionId) { if (!PlayerHelper.isUsingDSP(context)) return; final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); - intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, i); + intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); context.sendBroadcast(intent); } - - @Override - public void onAudioEnabled(DecoderCounters decoderCounters) {} - - @Override - public void onAudioDecoderInitialized(String s, long l, long l1) {} - - @Override - public void onAudioInputFormatChanged(Format format) {} - - @Override - public void onAudioSinkUnderrun(int bufferSize, - long bufferSizeMs, - long elapsedSinceLastFeedMs) {} - - @Override - public void onAudioDisabled(DecoderCounters decoderCounters) {} } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index b8d8dc12f..091efc942 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -33,14 +33,14 @@ import java.io.File; public CacheFactory(@NonNull final Context context, @NonNull final String userAgent, - @NonNull final TransferListener transferListener) { + @NonNull final TransferListener transferListener) { this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(context)); } private CacheFactory(@NonNull final Context context, @NonNull final String userAgent, - @NonNull final TransferListener transferListener, + @NonNull final TransferListener transferListener, final long maxCacheSize, final long maxFileSize) { this.maxFileSize = maxFileSize; diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index 7670deb98..4239dd62f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -2,17 +2,12 @@ package org.schabi.newpipe.player.helper; import android.content.Context; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DefaultAllocator; - -import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS; -import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES; public class LoadController implements LoadControl { @@ -36,15 +31,10 @@ public class LoadController implements LoadControl { final int optimalPlaybackBufferMs) { this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; - final DefaultAllocator allocator = new DefaultAllocator(true, - C.DEFAULT_BUFFER_SEGMENT_SIZE); - - internalLoadControl = new DefaultLoadControl(allocator, - /*minBufferMs=*/minimumPlaybackbufferMs, - /*maxBufferMs=*/optimalPlaybackBufferMs, - /*bufferForPlaybackMs=*/initialPlaybackBufferMs, - /*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs, - DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); + DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder(); + builder.setBufferDurationsMs(minimumPlaybackbufferMs, optimalPlaybackBufferMs, + initialPlaybackBufferMs, initialPlaybackBufferMs); + internalLoadControl = builder.createDefaultLoadControl(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index 133121269..5743891c2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -12,6 +12,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; public class PlayerDataSource { @@ -24,7 +25,7 @@ public class PlayerDataSource { public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent, - @NonNull final TransferListener transferListener) { + @NonNull final TransferListener transferListener) { cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); } @@ -32,21 +33,21 @@ public class PlayerDataSource { public SsMediaSource.Factory getLiveSsMediaSourceFactory() { return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( cachelessDataSourceFactory), cachelessDataSourceFactory) - .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY) + .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); } public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { return new HlsMediaSource.Factory(cachelessDataSourceFactory) .setAllowChunklessPreparation(true) - .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY); + .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); } public DashMediaSource.Factory getLiveDashMediaSourceFactory() { return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( cachelessDataSourceFactory), cachelessDataSourceFactory) - .setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY) - .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); + .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) + .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true); } public SsMediaSource.Factory getSsMediaSourceFactory() { @@ -65,7 +66,7 @@ public class PlayerDataSource { public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() { return new ExtractorMediaSource.Factory(cacheDataSourceFactory) - .setMinLoadableRetryCount(EXTRACTOR_MINIMUM_RETRY); + .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); } public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 19b728b3a..7248857b5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -14,7 +14,6 @@ import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; @@ -45,7 +44,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() {} @@ -68,10 +69,10 @@ public class PlayerHelper { //////////////////////////////////////////////////////////////////////////// public static String getTimeString(int milliSeconds) { - long seconds = (milliSeconds % 60000L) / 1000L; - long minutes = (milliSeconds % 3600000L) / 60000L; - long hours = (milliSeconds % 86400000L) / 3600000L; - long days = (milliSeconds % (86400000L * 7L)) / 86400000L; + int seconds = (milliSeconds % 60000) / 1000; + int minutes = (milliSeconds % 3600000) / 60000; + int hours = (milliSeconds % 86400000) / 3600000; + int days = (milliSeconds % (86400000 * 7)) / 86400000; stringBuilder.setLength(0); return days > 0 ? stringFormatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString() @@ -238,9 +239,8 @@ public class PlayerHelper { return 60000; } - public static TrackSelection.Factory getQualitySelector(@NonNull final Context context, - @NonNull final BandwidthMeter meter) { - return new AdaptiveTrackSelection.Factory(meter, + public static TrackSelection.Factory getQualitySelector(@NonNull final Context context) { + return new AdaptiveTrackSelection.Factory( /*bufferDurationRequiredForQualityIncrease=*/1000, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java index 2f233c464..cc9cd36bc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.player.mediasource; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import org.schabi.newpipe.player.playqueue.PlayQueueItem; @@ -79,7 +80,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { return null; } @@ -88,7 +89,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo @Override - protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { Log.e(TAG, "Loading failed source: ", error); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java index c39b0a03d..d36a3e305 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/LoadedMediaSource.java @@ -2,12 +2,13 @@ package org.schabi.newpipe.player.mediasource; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import org.schabi.newpipe.player.playqueue.PlayQueueItem; @@ -36,9 +37,8 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, - SourceInfoRefreshListener listener) { - source.prepareSource(player, isTopLevelSource, listener); + public void prepareSource(SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener) { + source.prepareSource(listener, mediaTransferListener); } @Override @@ -47,8 +47,8 @@ public class LoadedMediaSource implements ManagedMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return source.createPeriod(id, allocator); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return source.createPeriod(id, allocator, startPositionUs); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java index 5fe107657..fe29707fc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/ManagedMediaSourcePlaylist.java @@ -1,5 +1,5 @@ package org.schabi.newpipe.player.mediasource; - +import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -86,21 +86,22 @@ public class ManagedMediaSourcePlaylist { /** * Invalidates the {@link ManagedMediaSource} at the given index by replacing it * with a {@link PlaceholderMediaSource}. - * @see #update(int, ManagedMediaSource, Runnable) + * @see #update(int, ManagedMediaSource, Handler, Runnable) * */ public synchronized void invalidate(final int index, + @Nullable final Handler handler, @Nullable final Runnable finalizingAction) { if (get(index) instanceof PlaceholderMediaSource) return; - update(index, new PlaceholderMediaSource(), finalizingAction); + update(index, new PlaceholderMediaSource(), handler, finalizingAction); } /** * Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource} * at the given index with a given {@link ManagedMediaSource}. - * @see #update(int, ManagedMediaSource, Runnable) + * @see #update(int, ManagedMediaSource, Handler, Runnable) * */ public synchronized void update(final int index, @NonNull final ManagedMediaSource source) { - update(index, source, /*doNothing=*/null); + update(index, source, null, /*doNothing=*/null); } /** @@ -108,9 +109,10 @@ public class ManagedMediaSourcePlaylist { * at the given index with a given {@link ManagedMediaSource}. If the index is out of bound, * then the replacement is ignored. * @see ConcatenatingMediaSource#addMediaSource - * @see ConcatenatingMediaSource#removeMediaSource(int, Runnable) + * @see ConcatenatingMediaSource#removeMediaSource(int, Handler, Runnable) * */ public synchronized void update(final int index, @NonNull final ManagedMediaSource source, + @Nullable final Handler handler, @Nullable final Runnable finalizingAction) { if (index < 0 || index >= internalSource.getSize()) return; @@ -126,6 +128,6 @@ public class ManagedMediaSourcePlaylist { // Because of the above race condition, it is thus only safe to synchronize the player // in the finalizing action AFTER the removal is complete and the timeline has changed. - internalSource.removeMediaSource(index, finalizingAction); + internalSource.removeMediaSource(index, handler, finalizingAction); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java index bfd734393..377ca55a3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/PlaceholderMediaSource.java @@ -1,20 +1,21 @@ package org.schabi.newpipe.player.mediasource; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; import org.schabi.newpipe.player.playqueue.PlayQueueItem; public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource { // Do nothing, so this will stall the playback @Override public void maybeThrowSourceInfoRefreshError() {} - @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; } + @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { return null; } @Override public void releasePeriod(MediaPeriod mediaPeriod) {} - @Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {} + @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {} @Override protected void releaseSourceInternal() {} @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java index efe6f3a58..063d6b93e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.playback; import android.support.annotation.NonNull; import android.text.TextUtils; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -17,7 +18,7 @@ import com.google.android.exoplayer2.util.Assertions; * is mostly a copy-paste from {@link DefaultTrackSelector}. * * This is a hack and should be removed once ExoPlayer fixes language normalization to accept - * a broader set of languages. + * a broader set of languages. * */ public class CustomTrackSelector extends DefaultTrackSelector { private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; @@ -52,8 +53,8 @@ public class CustomTrackSelector extends DefaultTrackSelector { /** @see DefaultTrackSelector#selectTextTrack(TrackGroupArray, int[][], Parameters) */ @Override - protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params) { + protected Pair selectTextTrack(TrackGroupArray groups, int[][] formatSupport, + Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -106,7 +107,9 @@ public class CustomTrackSelector extends DefaultTrackSelector { } } } - return selectedGroup == null ? null - : new FixedTrackSelection(selectedGroup, selectedTrackIndex); + return selectedGroup == null + ? null + : Pair.create( + new FixedTrackSelection(selectedGroup, selectedTrackIndex), selectedTrackScore); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 3c5642d51..db8cc797e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -1,5 +1,5 @@ package org.schabi.newpipe.player.playback; - +import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.ArraySet; @@ -103,6 +103,8 @@ public class MediaSourceManager { @NonNull private ManagedMediaSourcePlaylist playlist; + private Handler removeMediaSourceHandler = new Handler(); + public MediaSourceManager(@NonNull final PlaybackListener listener, @NonNull final PlayQueue playQueue) { this(listener, playQueue, /*loadDebounceMillis=*/400L, @@ -395,7 +397,7 @@ public class MediaSourceManager { if (isCorrectionNeeded(item)) { if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " + "title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]"); - playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer); + playlist.update(itemIndex, mediaSource, removeMediaSourceHandler, this::maybeSynchronizePlayer); } } @@ -441,7 +443,7 @@ public class MediaSourceManager { if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " + "index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]"); - playlist.invalidate(currentIndex, this::loadImmediate); + playlist.invalidate(currentIndex, removeMediaSourceHandler, this::loadImmediate); } private void maybeClearLoaders() { diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java index 5993481e2..40d1a11e7 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java @@ -16,6 +16,11 @@ public final class SinglePlayQueue extends PlayQueue { super(0, Collections.singletonList(new PlayQueueItem(info))); } + public SinglePlayQueue(final StreamInfo info, final long startPosition) { + super(0, Collections.singletonList(new PlayQueueItem(info))); + getItem().setRecoveryPosition(startPosition); + } + public SinglePlayQueue(final List items, final int index) { super(index, playQueueItemsOf(items)); } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index c62dc1088..7abebc49e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -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; diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java index 00a25ed8d..2b2369ad3 100644 --- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java @@ -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"); diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index 3c5f16929..6a398a8a2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -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(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java new file mode 100644 index 000000000..e1ecc662d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -0,0 +1,131 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.SinglePlayQueue; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +public class CommentTextOnTouchListener implements View.OnTouchListener { + + public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener(); + + private static final Pattern timestampPattern = Pattern.compile("(.*)#timestamp=(\\d+)"); + + @Override + public boolean onTouch(View v, MotionEvent event) { + if(!(v instanceof TextView)){ + return false; + } + TextView widget = (TextView) v; + Object text = widget.getText(); + if (text instanceof Spanned) { + Spannable buffer = (Spannable) text; + + int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] link = buffer.getSpans(off, off, + ClickableSpan.class); + + if (link.length != 0) { + if (action == MotionEvent.ACTION_UP) { + boolean handled = false; + if(link[0] instanceof URLSpan){ + handled = handleUrl(v.getContext(), (URLSpan) link[0]); + } + if(!handled) link[0].onClick(widget); + } else if (action == MotionEvent.ACTION_DOWN) { + Selection.setSelection(buffer, + buffer.getSpanStart(link[0]), + buffer.getSpanEnd(link[0])); + } + return true; + } + } + + } + + return false; + } + + private boolean handleUrl(Context context, URLSpan urlSpan) { + String url = urlSpan.getURL(); + int seconds = -1; + Matcher matcher = timestampPattern.matcher(url); + if(matcher.matches()){ + url = matcher.group(1); + seconds = Integer.parseInt(matcher.group(2)); + } + StreamingService service; + StreamingService.LinkType linkType; + try { + service = NewPipe.getServiceByUrl(url); + linkType = service.getLinkTypeByUrl(url); + } catch (ExtractionException e) { + return false; + } + if(linkType == StreamingService.LinkType.NONE){ + return false; + } + if(linkType == StreamingService.LinkType.STREAM && seconds != -1){ + return playOnPopup(context, url, service, seconds); + }else{ + NavigationHelper.openRouterActivity(context, url); + return true; + } + } + + private boolean playOnPopup(Context context, String url, StreamingService service, int seconds) { + LinkHandlerFactory factory = service.getStreamLHFactory(); + String cleanUrl = null; + try { + cleanUrl = factory.getUrl(factory.getId(url)); + } catch (ParsingException e) { + return false; + } + Single single = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); + single.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000); + NavigationHelper.playOnPopupPlayer(context, playQueue); + }); + return true; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 041f4933f..0f1c39473 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -29,11 +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; @@ -62,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"); } } @@ -110,7 +111,7 @@ 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.STREAM, Single.fromCallable(() -> StreamInfo.getInfo(NewPipe.getService(serviceId), url))); } @@ -118,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 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 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 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 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 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)); @@ -149,7 +166,7 @@ public final class ExtractorHelper { public static Single 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))); } @@ -173,16 +190,17 @@ public final class ExtractorHelper { private static Single checkCache(boolean forceLoad, int serviceId, String url, + InfoItem.InfoType infoType, Single loadFromNetwork) { checkServiceId(serviceId); - loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info)); + loadFromNetwork = loadFromNetwork.doOnSuccess(info -> cache.putInfo(serviceId, url, info, infoType)); Single 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(); @@ -194,20 +212,20 @@ public final class ExtractorHelper { /** * Default implementation uses the {@link InfoCache} to get cached results */ - public static Maybe loadFromCache(final int serviceId, final String url) { + public static Maybe 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(); + }); } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java index 318db37a1..23b134281 100644 --- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -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() { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 5df2e8be4..98ae3a88a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -21,6 +21,7 @@ import com.nostra13.universalimageloader.core.ImageLoader; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.about.AboutActivity; import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.extractor.NewPipe; @@ -33,11 +34,12 @@ 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.local.bookmark.BookmarkFragment; -import org.schabi.newpipe.local.feed.FeedFragment; +import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.local.bookmark.BookmarkFragment; +import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; @@ -309,6 +311,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, @@ -409,6 +423,13 @@ public class NavigationHelper { context.startActivity(mIntent); } + public static void openRouterActivity(Context context, String url) { + Intent mIntent = new Intent(context, RouterActivity.class); + mIntent.setData(Uri.parse(url)); + mIntent.putExtra(RouterActivity.internalRouteKey, true); + context.startActivity(mIntent); + } + public static void openAbout(Context context) { Intent intent = new Intent(context, AboutActivity.class); context.startActivity(intent); diff --git a/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java new file mode 100644 index 000000000..6de663c13 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java @@ -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 { + + 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 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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java new file mode 100644 index 000000000..c5c78a726 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -0,0 +1,22 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import org.schabi.newpipe.R; + +public class ShareUtils { + public static void openUrlInBrowser(Context context, String url) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title))); + } + + public static void shareUrl(Context context, String subject, String url) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + intent.putExtra(Intent.EXTRA_TEXT, url); + context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_dialog_title))); + } +} diff --git a/app/src/main/res/drawable/default_dot.xml b/app/src/main/res/drawable/default_dot.xml new file mode 100644 index 000000000..3380dca3b --- /dev/null +++ b/app/src/main/res/drawable/default_dot.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selected_dot.xml b/app/src/main/res/drawable/selected_dot.xml new file mode 100644 index 000000000..017e99d43 --- /dev/null +++ b/app/src/main/res/drawable/selected_dot.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_selector.xml b/app/src/main/res/drawable/tab_selector.xml new file mode 100644 index 000000000..b7307674b --- /dev/null +++ b/app/src/main/res/drawable/tab_selector.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index 5f484267b..8428d489a 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -249,7 +249,7 @@ android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_marginLeft="2dp" - android:padding="5dp" + android:padding="5dp" android:clickable="true" android:focusable="true" android:scaleType="fitXY" @@ -305,7 +305,7 @@ tools:text="English" /> + + +