diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 873c1780f..f61e320c9 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -12,57 +12,53 @@ add a comment to it. You'll see exactly what is sent, the system is 100% transpa
## Issue reporting/feature requests
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
-hasn't been reported/requested before
-* Check whether your issue/feature is already fixed/implemented
-* Check if the issue still exists in the latest release/beta version
-* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome!
+hasn't been reported/requested before.
+* Check whether your issue/feature is already fixed/implemented.
+* Check if the issue still exists in the latest release/beta version.
+* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome!
* We use English for development. Issues in other languages will be closed and ignored.
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
-* When reporting a bug please give us a context, and a description how to reproduce it.
-* Issues that only contain a generated bug report, but no description might be closed.
+* Follow the template! Issues or feature requests not matching the template might be closed.
## Bug Fixing
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to
-tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request,
-register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information.
+tnp@newpipe.schabi.org to let us know that you intend to help. We'll send you further instructions. You may, on request,
+register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information).
## Translation
-* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
+* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
with your GitHub account.
+* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
## Code contribution
-* Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :))
-* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google
+* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project.
+* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google
libraries.
-* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
-* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You
- may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might
- not be considered, GitHub is the primary platform. (This only affects you if you are a member of TeamNewPipe)
+* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
+* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You
+ may then send your changes as a pull request (PR) on GitHub.
* When submitting changes, you confirm that your code is licensed under the terms of the
[GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR
description. Untested code will **not** be merged!
* Try to figure out yourself why builds on our CI fail.
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job,
- but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the
+ but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the
maintainers' jobs way easier.
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for
the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again
about submission, or clearly state that in the description of your PR.
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
-* Check if your contributions align with the [fdroid inclusion guidelines](https://f-droid.org/en/docs/Inclusion_Policy/).
-* Check if your submission can be build with the current fdroid build server setup.
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
independent solutions.
## Communication
-* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers:
[#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)!
* If you want to get in touch with the core team or one of our other contributors you can send an email to
- tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
+ tnp@newpipe.schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
tracker described above!
-* Feel free to post suggestions, changes, ideas etc. on GitHub, IRC or the mailing list!
+* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC!
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index 8073503ad..000000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,3 +0,0 @@
-- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
-- [ ] I checked if the issue/feature exists in the latest version.
-- [ ] I did use the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/) to paste bug reports.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..dbc1c05a5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,46 @@
+---
+name: Bug report
+about: Create a bug report to help us improve
+labels: bug
+assignees: ''
+
+---
+
+
+
+
+
+### Version
+
+-
+
+### Steps to reproduce the bug
+
+
+
+
+### Expected behavior
+
+
+### Actual behaviour
+
+
+### Screenshots/Screen recordings
+
+
+### Logs
+
+
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..90134a204
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,39 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+labels: enhancement
+assignees: ''
+
+---
+
+
+
+
+#### Describe the feature you want
+
+
+
+
+#### Is your feature request related to a problem? Please describe it
+
+
+
+
+#### Additional context
+
+
+
+
+#### How will you/everyone benefit from this feature?
+
+
+
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index d0e58680a..f12eb2fe8 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1 +1,28 @@
+
+
+#### What is it?
+- [ ] Bug fix (user facing)
+- [ ] Feature (user facing)
+- [ ] Code base improvement (dev facing)
+- [ ] Meta improvement to the project (dev facing)
+
+#### Description of the changes in your PR
+
+- record videos
+- create clones
+- take over the world
+
+#### Fixes the following issue(s)
+
+-
+
+#### Relies on the following changes
+
+-
+
+#### Testing apk
+
+debug.zip
+
+#### Agreement
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
diff --git a/.gitignore b/.gitignore
index f4f47c5ee..5c6962be1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,11 @@
*~
.weblate
*.class
+
+# vscode / eclipse files
+*.classpath
+*.project
+*.settings
+bin/
+.vscode/
+*.code-workspace
diff --git a/.travis.yml b/.travis.yml
index d6f97ab55..1714c70d5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,13 +5,13 @@ android:
components:
# The BuildTools version used by NewPipe
- tools
- - build-tools-28.0.3
+ - build-tools-29.0.3
# The SDK version used to compile NewPipe
- - android-28
+ - android-29
before_install:
- - yes | sdkmanager "platforms;android-28"
+ - yes | sdkmanager "platforms;android-29"
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
licenses:
diff --git a/README.md b/README.md
index f78725338..50eb40594 100644
--- a/README.md
+++ b/README.md
@@ -4,16 +4,16 @@
+ * It includes a workaround to fix the menu visibility when the adapter is restored.
+ *
+ *
+ * When restoring the state of this adapter, all the fragments' menu visibility were set to false,
+ * effectively disabling the menu from the user until he switched pages or another event
+ * that triggered the menu to be visible again happened.
+ *
+ *
+ * Check out the changes in:
+ *
+ *
+ *
{@link #saveState()}
+ *
{@link #restoreState(Parcelable, ClassLoader)}
+ *
+ */
+@SuppressWarnings("deprecation")
+public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
+ private static final String TAG = "FragmentStatePagerAdapt";
+ private static final boolean DEBUG = false;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
+ private @interface Behavior { }
+
+ /**
+ * Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current
+ * fragment changes.
+ *
+ * @deprecated This behavior relies on the deprecated
+ * {@link Fragment#setUserVisibleHint(boolean)} API. Use
+ * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement,
+ * {@link FragmentTransaction#setMaxLifecycle}.
+ * @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)
+ */
+ @Deprecated
+ public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
+
+ /**
+ * Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}
+ * state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.
+ *
+ * @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)
+ */
+ public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;
+
+ private final FragmentManager mFragmentManager;
+ private final int mBehavior;
+ private FragmentTransaction mCurTransaction = null;
+
+ private ArrayList mSavedState = new ArrayList();
+ private ArrayList mFragments = new ArrayList();
+ private Fragment mCurrentPrimaryItem = null;
+
+ /**
+ * Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
+ * that sets the fragment manager for the adapter. This is the equivalent of calling
+ * {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} and passing in
+ * {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.
+ *
+ *
Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the
+ * current Fragment changes.
+ *
+ * @param fm fragment manager that will interact with this adapter
+ * @deprecated use {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} with
+ * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}
+ */
+ @Deprecated
+ public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm) {
+ this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
+ }
+
+ /**
+ * Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}.
+ *
+ * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current
+ * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are
+ * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is
+ * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be
+ * callbacks to {@link Fragment#setUserVisibleHint(boolean)}.
+ *
+ * @param fm fragment manager that will interact with this adapter
+ * @param behavior determines if only current fragments are in a resumed state
+ */
+ public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm,
+ @Behavior final int behavior) {
+ mFragmentManager = fm;
+ mBehavior = behavior;
+ }
+
+ /**
+ * @param position the position of the item you want
+ * @return the {@link Fragment} associated with a specified position
+ */
+ @NonNull
+ public abstract Fragment getItem(int position);
+
+ @Override
+ public void startUpdate(@NonNull final ViewGroup container) {
+ if (container.getId() == View.NO_ID) {
+ throw new IllegalStateException("ViewPager with adapter " + this
+ + " requires a view id");
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @NonNull
+ @Override
+ public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
+ // If we already have this item instantiated, there is nothing
+ // to do. This can happen when we are restoring the entire pager
+ // from its saved state, where the fragment manager has already
+ // taken care of restoring the fragments we previously had instantiated.
+ if (mFragments.size() > position) {
+ Fragment f = mFragments.get(position);
+ if (f != null) {
+ return f;
+ }
+ }
+
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+
+ Fragment fragment = getItem(position);
+ if (DEBUG) {
+ Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
+ }
+ if (mSavedState.size() > position) {
+ Fragment.SavedState fss = mSavedState.get(position);
+ if (fss != null) {
+ fragment.setInitialSavedState(fss);
+ }
+ }
+ while (mFragments.size() <= position) {
+ mFragments.add(null);
+ }
+ fragment.setMenuVisibility(false);
+ if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {
+ fragment.setUserVisibleHint(false);
+ }
+
+ mFragments.set(position, fragment);
+ mCurTransaction.add(container.getId(), fragment);
+
+ if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
+ mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
+ }
+
+ return fragment;
+ }
+
+ @Override
+ public void destroyItem(@NonNull final ViewGroup container, final int position,
+ @NonNull final Object object) {
+ Fragment fragment = (Fragment) object;
+
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+ if (DEBUG) {
+ Log.v(TAG, "Removing item #" + position + ": f=" + object
+ + " v=" + ((Fragment) object).getView());
+ }
+ while (mSavedState.size() <= position) {
+ mSavedState.add(null);
+ }
+ mSavedState.set(position, fragment.isAdded()
+ ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
+ mFragments.set(position, null);
+
+ mCurTransaction.remove(fragment);
+ if (fragment == mCurrentPrimaryItem) {
+ mCurrentPrimaryItem = null;
+ }
+ }
+
+ @Override
+ @SuppressWarnings({"ReferenceEquality", "deprecation"})
+ public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
+ @NonNull final Object object) {
+ Fragment fragment = (Fragment) object;
+ if (fragment != mCurrentPrimaryItem) {
+ if (mCurrentPrimaryItem != null) {
+ mCurrentPrimaryItem.setMenuVisibility(false);
+ if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+ mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
+ } else {
+ mCurrentPrimaryItem.setUserVisibleHint(false);
+ }
+ }
+ fragment.setMenuVisibility(true);
+ if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+ mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
+ } else {
+ fragment.setUserVisibleHint(true);
+ }
+
+ mCurrentPrimaryItem = fragment;
+ }
+ }
+
+ @Override
+ public void finishUpdate(@NonNull final ViewGroup container) {
+ if (mCurTransaction != null) {
+ mCurTransaction.commitNowAllowingStateLoss();
+ mCurTransaction = null;
+ }
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
+ return ((Fragment) object).getView() == view;
+ }
+
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ private final String selectedFragment = "selected_fragment";
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+ @Override
+ @Nullable
+ public Parcelable saveState() {
+ Bundle state = null;
+ if (mSavedState.size() > 0) {
+ state = new Bundle();
+ Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
+ mSavedState.toArray(fss);
+ state.putParcelableArray("states", fss);
+ }
+ for (int i = 0; i < mFragments.size(); i++) {
+ Fragment f = mFragments.get(i);
+ if (f != null && f.isAdded()) {
+ if (state == null) {
+ state = new Bundle();
+ }
+ String key = "f" + i;
+ mFragmentManager.putFragment(state, key, f);
+
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ // Check if it's the same fragment instance
+ if (f == mCurrentPrimaryItem) {
+ state.putString(selectedFragment, key);
+ }
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ }
+ }
+ return state;
+ }
+
+ @Override
+ public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
+ if (state != null) {
+ Bundle bundle = (Bundle) state;
+ bundle.setClassLoader(loader);
+ Parcelable[] fss = bundle.getParcelableArray("states");
+ mSavedState.clear();
+ mFragments.clear();
+ if (fss != null) {
+ for (int i = 0; i < fss.length; i++) {
+ mSavedState.add((Fragment.SavedState) fss[i]);
+ }
+ }
+ Iterable keys = bundle.keySet();
+ for (String key: keys) {
+ if (key.startsWith("f")) {
+ int index = Integer.parseInt(key.substring(1));
+ Fragment f = mFragmentManager.getFragment(bundle, key);
+ if (f != null) {
+ while (mFragments.size() <= index) {
+ mFragments.add(null);
+ }
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ final boolean wasSelected = bundle.getString(selectedFragment, "")
+ .equals(key);
+ f.setMenuVisibility(wasSelected);
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ mFragments.set(index, f);
+ } else {
+ Log.w(TAG, "Bad fragment at key " + key);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
index 95137414e..2e8a0103f 100644
--- a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
+++ b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
@@ -8,6 +8,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.OverScroller;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import org.jetbrains.annotations.NotNull;
@@ -15,10 +16,11 @@ import org.schabi.newpipe.R;
import java.lang.reflect.Field;
-// check this https://stackoverflow.com/questions/56849221/recyclerview-fling-causes-laggy-while-appbarlayout-is-scrolling/57997489#57997489
+// See https://stackoverflow.com/questions/56849221#57997489
public final class FlingBehavior extends AppBarLayout.Behavior {
+ private final Rect focusScrollRect = new Rect();
- public FlingBehavior(Context context, AttributeSet attrs) {
+ public FlingBehavior(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@@ -26,7 +28,40 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
private final Rect globalRect = new Rect();
@Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
+ public boolean onRequestChildRectangleOnScreen(
+ @NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
+ @NonNull final Rect rectangle, final boolean immediate) {
+
+ focusScrollRect.set(rectangle);
+
+ coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
+
+ int height = coordinatorLayout.getHeight();
+
+ if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
+ // the child is too big to fit inside ourselves completely, ignore request
+ return false;
+ }
+
+ int dy;
+
+ if (focusScrollRect.bottom > height) {
+ dy = focusScrollRect.top;
+ } else if (focusScrollRect.top < 0) {
+ // scrolling up
+ dy = -(height - focusScrollRect.bottom);
+ } else {
+ // nothing to do
+ return false;
+ }
+
+ int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
+
+ return consumed == dy;
+ }
+
+ public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
+ final MotionEvent ev) {
final ViewGroup playQueue = child.findViewById(R.id.playQueuePanel);
if (playQueue != null) {
final boolean visible = playQueue.getGlobalVisibleRect(globalRect);
@@ -36,30 +71,36 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
}
}
allowScroll = true;
-
- if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
- // remove reference to old nested scrolling child
- resetNestedScrollingChild();
- // Stop fling when your finger touches the screen
- stopAppBarLayoutFling();
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ // remove reference to old nested scrolling child
+ resetNestedScrollingChild();
+ // Stop fling when your finger touches the screen
+ stopAppBarLayoutFling();
+ break;
+ default:
+ break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
- public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
+ public boolean onStartNestedScroll(@NotNull final CoordinatorLayout parent, @NotNull final AppBarLayout child,
+ @NotNull final View directTargetChild, final View target, final int nestedScrollAxes, final int type) {
return allowScroll && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}
@Override
- public boolean onNestedFling(@NotNull CoordinatorLayout coordinatorLayout, @NotNull AppBarLayout child, @NotNull View target, float velocityX, float velocityY, boolean consumed) {
+ public boolean onNestedFling(@NotNull final CoordinatorLayout coordinatorLayout, @NotNull final AppBarLayout child,
+ @NotNull final View target, final float velocityX, final float velocityY, final boolean consumed) {
return allowScroll && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
@Nullable
private OverScroller getScrollerField() {
try {
- Class> headerBehaviorType = this.getClass().getSuperclass().getSuperclass().getSuperclass();
+ Class> headerBehaviorType = this.getClass()
+ .getSuperclass().getSuperclass().getSuperclass();
if (headerBehaviorType != null) {
Field field = headerBehaviorType.getDeclaredField("scroller");
field.setAccessible(true);
@@ -86,12 +127,14 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
return null;
}
- private void resetNestedScrollingChild(){
+ private void resetNestedScrollingChild() {
Field field = getLastNestedScrollingChildRefField();
- if(field != null){
+ if (field != null) {
try {
Object value = field.get(this);
- if(value != null) field.set(this, null);
+ if (value != null) {
+ field.set(this, null);
+ }
} catch (IllegalAccessException e) {
// ?
}
@@ -100,7 +143,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
private void stopAppBarLayoutFling() {
OverScroller scroller = getScrollerField();
- if (scroller != null) scroller.forceFinished(true);
+ if (scroller != null) {
+ scroller.forceFinished(true);
+ }
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java
index da601a42f..9321b3071 100644
--- a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java
+++ b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java
@@ -23,17 +23,25 @@ package org.schabi.newpipe;
/**
* Singleton:
* Used to send data between certain Activity/Services within the same process.
- * This can be considered as an ugly hack inside the Android universe. **/
+ * This can be considered as an ugly hack inside the Android universe.
+ **/
public class ActivityCommunicator {
private static ActivityCommunicator activityCommunicator;
+ private volatile Class returnActivity;
public static ActivityCommunicator getCommunicator() {
- if(activityCommunicator == null) {
+ if (activityCommunicator == null) {
activityCommunicator = new ActivityCommunicator();
}
return activityCommunicator;
}
- public volatile Class returnActivity;
+ public Class getReturnActivity() {
+ return returnActivity;
+ }
+
+ public void setReturnActivity(final Class returnActivity) {
+ this.returnActivity = returnActivity;
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java
index 7f050e6c7..531cb5a38 100644
--- a/app/src/main/java/org/schabi/newpipe/App.java
+++ b/app/src/main/java/org/schabi/newpipe/App.java
@@ -5,21 +5,20 @@ import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
+import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
-import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
-import com.squareup.leakcanary.LeakCanary;
-import com.squareup.leakcanary.RefWatcher;
import org.acra.ACRA;
-import org.acra.config.ACRAConfiguration;
import org.acra.config.ACRAConfigurationException;
-import org.acra.config.ConfigurationBuilder;
+import org.acra.config.CoreConfiguration;
+import org.acra.config.CoreConfigurationBuilder;
import org.acra.sender.ReportSenderFactory;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
@@ -27,7 +26,7 @@ import org.schabi.newpipe.report.AcraReportSenderFactory;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
-import org.schabi.newpipe.util.ExtractorHelper;
+import org.schabi.newpipe.util.ExceptionUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
@@ -66,15 +65,17 @@ import io.reactivex.plugins.RxJavaPlugins;
public class App extends Application {
protected static final String TAG = App.class.toString();
- private RefWatcher refWatcher;
- private static App app;
-
@SuppressWarnings("unchecked")
private static final Class extends ReportSenderFactory>[]
- reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
+ REPORT_SENDER_FACTORY_CLASSES = new Class[]{AcraReportSenderFactory.class};
+ private static App app;
+
+ public static App getApp() {
+ return app;
+ }
@Override
- protected void attachBaseContext(Context base) {
+ protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
initACRA();
@@ -84,13 +85,6 @@ public class App extends Application {
public void onCreate() {
super.onCreate();
- if (LeakCanary.isInAnalyzerProcess(this)) {
- // This process is dedicated to LeakCanary for heap analysis.
- // You should not init your app in this process.
- return;
- }
- refWatcher = installLeakCanary();
-
app = this;
// Initialize settings first because others inits can use its values
@@ -99,7 +93,7 @@ public class App extends Application {
NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this));
- Localization.init();
+ Localization.init(getApplicationContext());
StateSaver.init(this);
initNotificationChannel();
@@ -116,31 +110,47 @@ public class App extends Application {
}
protected Downloader getDownloader() {
- return DownloaderImpl.init(null);
+ DownloaderImpl downloader = DownloaderImpl.init(null);
+ setCookiesToDownloader(downloader);
+ return downloader;
+ }
+
+ protected void setCookiesToDownloader(final DownloaderImpl downloader) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
+ getApplicationContext());
+ final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
+ downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, ""));
+ downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
}
private void configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(new Consumer() {
@Override
- public void accept(@NonNull Throwable throwable) {
- Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " +
- "throwable = [" + throwable.getClass().getName() + "]");
+ public void accept(@NonNull final Throwable throwable) {
+ Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
+ + "throwable = [" + throwable.getClass().getName() + "]");
+ final Throwable actualThrowable;
if (throwable instanceof UndeliverableException) {
- // As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
- throwable = throwable.getCause();
+ // As UndeliverableException is a wrapper,
+ // get the cause of it to get the "real" exception
+ actualThrowable = throwable.getCause();
+ } else {
+ actualThrowable = throwable;
}
final List errors;
- if (throwable instanceof CompositeException) {
- errors = ((CompositeException) throwable).getExceptions();
+ if (actualThrowable instanceof CompositeException) {
+ errors = ((CompositeException) actualThrowable).getExceptions();
} else {
- errors = Collections.singletonList(throwable);
+ errors = Collections.singletonList(actualThrowable);
}
for (final Throwable error : errors) {
- if (isThrowableIgnored(error)) return;
+ if (isThrowableIgnored(error)) {
+ return;
+ }
if (isThrowableCritical(error)) {
reportException(error);
return;
@@ -150,22 +160,24 @@ public class App extends Application {
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
// When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
- reportException(throwable);
+ reportException(actualThrowable);
} else {
- Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", throwable);
+ Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
}
}
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
// Don't crash the application over a simple network problem
- return ExtractorHelper.hasAssignableCauseThrowable(throwable,
- IOException.class, SocketException.class, // network api cancellation
- InterruptedException.class, InterruptedIOException.class); // blocking code disposed
+ return ExceptionUtils.hasAssignableCause(throwable,
+ // network api cancellation
+ IOException.class, SocketException.class,
+ // blocking code disposed
+ InterruptedException.class, InterruptedIOException.class);
}
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
// Though these exceptions cannot be ignored
- return ExtractorHelper.hasAssignableCauseThrowable(throwable,
+ return ExceptionUtils.hasAssignableCause(throwable,
NullPointerException.class, IllegalArgumentException.class, // bug in app
OnErrorNotImplementedException.class, MissingBackpressureException.class,
IllegalStateException.class); // bug in operator
@@ -190,8 +202,8 @@ public class App extends Application {
private void initACRA() {
try {
- final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
- .setReportSenderFactoryClasses(reportSenderFactoryClasses)
+ final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
+ .setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES)
.setBuildConfigClass(BuildConfig.class)
.build();
ACRA.init(this, acraConfig);
@@ -202,7 +214,7 @@ public class App extends Application {
null,
null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
- "Could not initialize ACRA crash report", R.string.app_ui_crash));
+ "Could not initialize ACRA crash report", R.string.app_ui_crash));
}
}
@@ -230,11 +242,11 @@ public class App extends Application {
/**
* Set up notification channel for app update.
+ *
* @param importance
*/
@TargetApi(Build.VERSION_CODES.O)
- private void setUpUpdateNotificationChannel(int importance) {
-
+ private void setUpUpdateNotificationChannel(final int importance) {
final String appUpdateId
= getString(R.string.app_update_notification_channel_id);
final CharSequence appUpdateName
@@ -251,21 +263,7 @@ public class App extends Application {
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
}
- @Nullable
- public static RefWatcher getRefWatcher(Context context) {
- final App application = (App) context.getApplicationContext();
- return application.refWatcher;
- }
-
- protected RefWatcher installLeakCanary() {
- return RefWatcher.DISABLED;
- }
-
protected boolean isDisposedRxExceptionsReported() {
return false;
}
-
- public static App getApp() {
- return app;
- }
}
diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java
index d4795cde2..54513a0af 100644
--- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java
@@ -2,32 +2,31 @@ package org.schabi.newpipe;
import android.content.Context;
import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
import com.nostra13.universalimageloader.core.ImageLoader;
-import com.squareup.leakcanary.RefWatcher;
import icepick.Icepick;
import icepick.State;
+import leakcanary.AppWatcher;
public abstract class BaseFragment extends Fragment {
+ public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
protected final boolean DEBUG = MainActivity.DEBUG;
-
protected AppCompatActivity activity;
- public static final ImageLoader imageLoader = ImageLoader.getInstance();
-
- //These values are used for controlling framgents when they are part of the frontpage
+ //These values are used for controlling fragments when they are part of the frontpage
@State
protected boolean useAsFrontPage = false;
- protected boolean mIsVisibleToUser = false;
+ private boolean mIsVisibleToUser = false;
- public void useAsFrontPage(boolean value) {
+ public void useAsFrontPage(final boolean value) {
useAsFrontPage = value;
}
@@ -36,7 +35,7 @@ public abstract class BaseFragment extends Fragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
- public void onAttach(Context context) {
+ public void onAttach(final Context context) {
super.onAttach(context);
activity = (AppCompatActivity) context;
}
@@ -48,43 +47,49 @@ public abstract class BaseFragment extends Fragment {
}
@Override
- public void onCreate(Bundle savedInstanceState) {
- if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
+ public void onCreate(final Bundle savedInstanceState) {
+ if (DEBUG) {
+ Log.d(TAG, "onCreate() called with: "
+ + "savedInstanceState = [" + savedInstanceState + "]");
+ }
super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
- if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState != null) {
+ onRestoreInstanceState(savedInstanceState);
+ }
}
@Override
- public void onViewCreated(View rootView, Bundle savedInstanceState) {
+ public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
if (DEBUG) {
- Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
+ Log.d(TAG, "onViewCreated() called with: "
+ + "rootView = [" + rootView + "], "
+ + "savedInstanceState = [" + savedInstanceState + "]");
}
initViews(rootView, savedInstanceState);
initListeners();
}
@Override
- public void onSaveInstanceState(Bundle outState) {
+ public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
- protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
}
@Override
public void onDestroy() {
super.onDestroy();
- RefWatcher refWatcher = App.getRefWatcher(getActivity());
- if (refWatcher != null) refWatcher.watch(this);
+ AppWatcher.INSTANCE.getObjectWatcher().watch(this);
}
@Override
- public void setUserVisibleHint(boolean isVisibleToUser) {
+ public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
}
@@ -93,7 +98,7 @@ public abstract class BaseFragment extends Fragment {
// Init
//////////////////////////////////////////////////////////////////////////*/
- protected void initViews(View rootView, Bundle savedInstanceState) {
+ protected void initViews(final View rootView, final Bundle savedInstanceState) {
}
protected void initListeners() {
@@ -103,10 +108,12 @@ public abstract class BaseFragment extends Fragment {
// Utils
//////////////////////////////////////////////////////////////////////////*/
- public void setTitle(String title) {
- if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
- if((!useAsFrontPage || mIsVisibleToUser)
- && (activity != null && activity.getSupportActionBar() != null)) {
+ public void setTitle(final String title) {
+ if (DEBUG) {
+ Log.d(TAG, "setTitle() called with: title = [" + title + "]");
+ }
+ if ((!useAsFrontPage || mIsVisibleToUser)
+ && (activity != null && activity.getSupportActionBar() != null)) {
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
activity.getSupportActionBar().setTitle(title);
}
diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java
index 22f7bc558..625f514e9 100644
--- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java
+++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java
@@ -12,12 +12,16 @@ import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
import android.util.Log;
-import org.json.JSONException;
-import org.json.JSONObject;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+import com.grack.nanojson.JsonObject;
+import com.grack.nanojson.JsonParser;
+import com.grack.nanojson.JsonParserException;
+
+import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
@@ -30,11 +34,6 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
-import java.util.concurrent.TimeUnit;
-
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
/**
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
@@ -42,149 +41,44 @@ import okhttp3.Response;
* the notification, the user will be directed to the download link.
*/
public class CheckForNewAppVersionTask extends AsyncTask {
-
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
- private static final Application app = App.getApp();
- private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
- private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
- private static final int timeoutPeriod = 30;
- private SharedPreferences mPrefs;
- private OkHttpClient client;
-
- @Override
- protected void onPreExecute() {
-
- mPrefs = PreferenceManager.getDefaultSharedPreferences(app);
-
- // Check if user has enabled/ disabled update checking
- // and if the current apk is a github one or not.
- if (!mPrefs.getBoolean(app.getString(R.string.update_app_key), true)
- || !isGithubApk()) {
- this.cancel(true);
- }
- }
-
- @Override
- protected String doInBackground(Void... voids) {
-
- if(isCancelled() || !isConnected()) return null;
-
- // Make a network request to get latest NewPipe data.
- if (client == null) {
-
- client = new OkHttpClient
- .Builder()
- .readTimeout(timeoutPeriod, TimeUnit.SECONDS)
- .build();
- }
-
- Request request = new Request.Builder()
- .url(newPipeApiUrl)
- .build();
-
- try {
- Response response = client.newCall(request).execute();
- return response.body().string();
- } catch (IOException ex) {
- // connectivity problems, do not alarm user and fail silently
- if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
- }
-
- return null;
- }
-
- @Override
- protected void onPostExecute(String response) {
-
- // Parse the json from the response.
- if (response != null) {
-
- try {
- JSONObject mainObject = new JSONObject(response);
- JSONObject flavoursObject = mainObject.getJSONObject("flavors");
- JSONObject githubObject = flavoursObject.getJSONObject("github");
- JSONObject githubStableObject = githubObject.getJSONObject("stable");
-
- String versionName = githubStableObject.getString("version");
- String versionCode = githubStableObject.getString("version_code");
- String apkLocationUrl = githubStableObject.getString("apk");
-
- compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
-
- } catch (JSONException ex) {
- // connectivity problems, do not alarm user and fail silently
- if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
- }
- }
- }
+ private static final Application APP = App.getApp();
+ private static final String GITHUB_APK_SHA1
+ = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
+ private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
/**
- * Method to compare the current and latest available app version.
- * If a newer version is available, we show the update notification.
- * @param versionName
- * @param apkLocationUrl
- */
- private void compareAppVersionAndShowNotification(String versionName,
- String apkLocationUrl,
- String versionCode) {
-
- int NOTIFICATION_ID = 2000;
-
- if (BuildConfig.VERSION_CODE < Integer.valueOf(versionCode)) {
-
- // A pending intent to open the apk location url in the browser.
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
- PendingIntent pendingIntent
- = PendingIntent.getActivity(app, 0, intent, 0);
-
- NotificationCompat.Builder notificationBuilder = new NotificationCompat
- .Builder(app, app.getString(R.string.app_update_notification_channel_id))
- .setSmallIcon(R.drawable.ic_newpipe_update)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setContentIntent(pendingIntent)
- .setAutoCancel(true)
- .setContentTitle(app.getString(R.string.app_update_notification_content_title))
- .setContentText(app.getString(R.string.app_update_notification_content_text)
- + " " + versionName);
-
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app);
- notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
- }
- }
-
- /**
- * Method to get the apk's SHA1 key.
- * https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133
+ * Method to get the apk's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
+ *
+ * @return String with the apk's SHA1 fingeprint in hexadecimal
*/
private static String getCertificateSHA1Fingerprint() {
-
- PackageManager pm = app.getPackageManager();
- String packageName = app.getPackageName();
- int flags = PackageManager.GET_SIGNATURES;
+ final PackageManager pm = APP.getPackageManager();
+ final String packageName = APP.getPackageName();
+ final int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
try {
packageInfo = pm.getPackageInfo(packageName, flags);
- } catch (PackageManager.NameNotFoundException ex) {
- ErrorActivity.reportError(app, ex, null, null,
+ } catch (PackageManager.NameNotFoundException e) {
+ ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not find package info", R.string.app_ui_crash));
}
- Signature[] signatures = packageInfo.signatures;
- byte[] cert = signatures[0].toByteArray();
- InputStream input = new ByteArrayInputStream(cert);
+ final Signature[] signatures = packageInfo.signatures;
+ final byte[] cert = signatures[0].toByteArray();
+ final InputStream input = new ByteArrayInputStream(cert);
- CertificateFactory cf = null;
X509Certificate c = null;
try {
- cf = CertificateFactory.getInstance("X509");
+ final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input);
- } catch (CertificateException ex) {
- ErrorActivity.reportError(app, ex, null, null,
+ } catch (CertificateException e) {
+ ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Certificate error", R.string.app_ui_crash));
}
@@ -193,14 +87,10 @@ public class CheckForNewAppVersionTask extends AsyncTask {
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
- byte[] publicKey = md.digest(c.getEncoded());
+ final byte[] publicKey = md.digest(c.getEncoded());
hexString = byte2HexFormatted(publicKey);
- } catch (NoSuchAlgorithmException ex1) {
- ErrorActivity.reportError(app, ex1, null, null,
- ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
- "Could not retrieve SHA1 key", R.string.app_ui_crash));
- } catch (CertificateEncodingException ex2) {
- ErrorActivity.reportError(app, ex2, null, null,
+ } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
+ ErrorActivity.reportError(APP, e, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not retrieve SHA1 key", R.string.app_ui_crash));
}
@@ -208,31 +98,124 @@ public class CheckForNewAppVersionTask extends AsyncTask {
return hexString;
}
- private static String byte2HexFormatted(byte[] arr) {
-
- StringBuilder str = new StringBuilder(arr.length * 2);
+ private static String byte2HexFormatted(final byte[] arr) {
+ final StringBuilder str = new StringBuilder(arr.length * 2);
for (int i = 0; i < arr.length; i++) {
String h = Integer.toHexString(arr[i]);
- int l = h.length();
- if (l == 1) h = "0" + h;
- if (l > 2) h = h.substring(l - 2, l);
+ final int l = h.length();
+ if (l == 1) {
+ h = "0" + h;
+ }
+ if (l > 2) {
+ h = h.substring(l - 2, l);
+ }
str.append(h.toUpperCase());
- if (i < (arr.length - 1)) str.append(':');
+ if (i < (arr.length - 1)) {
+ str.append(':');
+ }
}
return str.toString();
}
public static boolean isGithubApk() {
-
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
}
-
+
+ @Override
+ protected void onPreExecute() {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(APP);
+
+ // Check if user has enabled/disabled update checking
+ // and if the current apk is a github one or not.
+ if (!prefs.getBoolean(APP.getString(R.string.update_app_key), true) || !isGithubApk()) {
+ this.cancel(true);
+ }
+ }
+
+ @Override
+ protected String doInBackground(final Void... voids) {
+ if (isCancelled() || !isConnected()) {
+ return null;
+ }
+
+ // Make a network request to get latest NewPipe data.
+ try {
+ return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
+ } catch (IOException | ReCaptchaException e) {
+ // connectivity problems, do not alarm user and fail silently
+ if (DEBUG) {
+ Log.w(TAG, Log.getStackTraceString(e));
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(final String response) {
+ // Parse the json from the response.
+ if (response != null) {
+
+ try {
+ final JsonObject githubStableObject = JsonParser.object().from(response)
+ .getObject("flavors").getObject("github").getObject("stable");
+
+ final String versionName = githubStableObject.getString("version");
+ final int versionCode = githubStableObject.getInt("version_code");
+ final String apkLocationUrl = githubStableObject.getString("apk");
+
+ compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
+
+ } catch (JsonParserException e) {
+ // connectivity problems, do not alarm user and fail silently
+ if (DEBUG) {
+ Log.w(TAG, Log.getStackTraceString(e));
+ }
+ }
+ }
+ }
+
+ /**
+ * Method to compare the current and latest available app version.
+ * If a newer version is available, we show the update notification.
+ *
+ * @param versionName Name of new version
+ * @param apkLocationUrl Url with the new apk
+ * @param versionCode Code of new version
+ */
+ private void compareAppVersionAndShowNotification(final String versionName,
+ final String apkLocationUrl,
+ final int versionCode) {
+ int notificationId = 2000;
+
+ if (BuildConfig.VERSION_CODE < versionCode) {
+
+ // A pending intent to open the apk location url in the browser.
+ final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
+ final PendingIntent pendingIntent
+ = PendingIntent.getActivity(APP, 0, intent, 0);
+
+ final NotificationCompat.Builder notificationBuilder = new NotificationCompat
+ .Builder(APP, APP.getString(R.string.app_update_notification_channel_id))
+ .setSmallIcon(R.drawable.ic_newpipe_update)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ .setContentTitle(APP.getString(R.string.app_update_notification_content_title))
+ .setContentText(APP.getString(R.string.app_update_notification_content_text)
+ + " " + versionName);
+
+ final NotificationManagerCompat notificationManager
+ = NotificationManagerCompat.from(APP);
+ notificationManager.notify(notificationId, notificationBuilder.build());
+ }
+ }
+
private boolean isConnected() {
-
- ConnectivityManager cm =
- (ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
+ final ConnectivityManager cm =
+ (ConnectivityManager) APP.getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo() != null
- && cm.getActiveNetworkInfo().isConnected();
+ && cm.getActiveNetworkInfo().isConnected();
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
index 7e4ac304e..95d3c2b7c 100644
--- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
+++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
@@ -1,12 +1,18 @@
package org.schabi.newpipe;
+import android.content.Context;
import android.os.Build;
-import android.text.TextUtils;
+import android.preference.PreferenceManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
+import org.schabi.newpipe.util.CookieUtils;
+import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import java.io.IOException;
@@ -17,6 +23,7 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -26,9 +33,6 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
@@ -37,42 +41,139 @@ import okhttp3.ResponseBody;
import static org.schabi.newpipe.MainActivity.DEBUG;
-public class DownloaderImpl extends Downloader {
- public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
+public final class DownloaderImpl extends Downloader {
+ public static final String USER_AGENT
+ = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0";
+ public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
+ = "youtube_restricted_mode_key";
+ public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
+ public static final String YOUTUBE_DOMAIN = "youtube.com";
private static DownloaderImpl instance;
- private String mCookies;
+ private Map mCookies;
private OkHttpClient client;
- private DownloaderImpl(OkHttpClient.Builder builder) {
+ private DownloaderImpl(final OkHttpClient.Builder builder) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
enableModernTLS(builder);
}
this.client = builder
.readTimeout(30, TimeUnit.SECONDS)
- //.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
+// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
+// 16 * 1024 * 1024))
.build();
+ this.mCookies = new HashMap<>();
}
/**
* It's recommended to call exactly once in the entire lifetime of the application.
*
* @param builder if null, default builder will be used
+ * @return a new instance of {@link DownloaderImpl}
*/
- public static DownloaderImpl init(@Nullable OkHttpClient.Builder builder) {
- return instance = new DownloaderImpl(builder != null ? builder : new OkHttpClient.Builder());
+ public static DownloaderImpl init(@Nullable final OkHttpClient.Builder builder) {
+ instance = new DownloaderImpl(
+ builder != null ? builder : new OkHttpClient.Builder());
+ return instance;
}
public static DownloaderImpl getInstance() {
return instance;
}
- public String getCookies() {
- return mCookies;
+ /**
+ * Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken
+ * from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_).
+ *
+ * If there is an error, the function will safely fall back to doing nothing
+ * and printing the error to the console.
+ *
+ *
+ * @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
+ */
+ private static void enableModernTLS(final OkHttpClient.Builder builder) {
+ try {
+ // get the default TrustManager
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init((KeyStore) null);
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new IllegalStateException("Unexpected default trust managers:"
+ + Arrays.toString(trustManagers));
+ }
+ X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
+
+ // insert our own TLSSocketFactory
+ SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
+
+ builder.sslSocketFactory(sslSocketFactory, trustManager);
+
+ // This will try to enable all modern CipherSuites(+2 more)
+ // that are supported on the device.
+ // Necessary because some servers (e.g. Framatube.org)
+ // don't support the old cipher suites.
+ // https://github.com/square/okhttp/issues/4053#issuecomment-402579554
+ List cipherSuites = new ArrayList<>();
+ cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
+ cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
+ cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
+ ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
+ .build();
+
+ builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
+ } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ }
}
- public void setCookies(String cookies) {
- mCookies = cookies;
+ public String getCookies(final String url) {
+ List resultCookies = new ArrayList<>();
+ if (url.contains(YOUTUBE_DOMAIN)) {
+ String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
+ if (youtubeCookie != null) {
+ resultCookies.add(youtubeCookie);
+ }
+ }
+ // Recaptcha cookie is always added TODO: not sure if this is necessary
+ String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
+ if (recaptchaCookie != null) {
+ resultCookies.add(recaptchaCookie);
+ }
+ return CookieUtils.concatCookies(resultCookies);
+ }
+
+ public String getCookie(final String key) {
+ return mCookies.get(key);
+ }
+
+ public void setCookie(final String key, final String cookie) {
+ mCookies.put(key, cookie);
+ }
+
+ public void removeCookie(final String key) {
+ mCookies.remove(key);
+ }
+
+ public void updateYoutubeRestrictedModeCookies(final Context context) {
+ String restrictedModeEnabledKey =
+ context.getString(R.string.youtube_restricted_mode_enabled);
+ boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(restrictedModeEnabledKey, false);
+ updateYoutubeRestrictedModeCookies(restrictedModeEnabled);
+ }
+
+ public void updateYoutubeRestrictedModeCookies(final boolean youtubeRestrictedModeEnabled) {
+ if (youtubeRestrictedModeEnabled) {
+ setCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY,
+ YOUTUBE_RESTRICTED_MODE_COOKIE);
+ } else {
+ removeCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
+ }
+ InfoCache.getInstance().clearCache();
}
/**
@@ -81,7 +182,7 @@ public class DownloaderImpl extends Downloader {
* @param url an url pointing to the content
* @return the size of the content, in bytes
*/
- public long getContentLength(String url) throws IOException {
+ public long getContentLength(final String url) throws IOException {
try {
final Response response = head(url);
return Long.parseLong(response.getHeader("Content-Length"));
@@ -92,14 +193,15 @@ public class DownloaderImpl extends Downloader {
}
}
- public InputStream stream(String siteUrl) throws IOException {
+ public InputStream stream(final String siteUrl) throws IOException {
try {
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
.method("GET", null).url(siteUrl)
.addHeader("User-Agent", USER_AGENT);
- if (!TextUtils.isEmpty(mCookies)) {
- requestBuilder.addHeader("Cookie", mCookies);
+ String cookies = getCookies(siteUrl);
+ if (!cookies.isEmpty()) {
+ requestBuilder.addHeader("Cookie", cookies);
}
final okhttp3.Request request = requestBuilder.build();
@@ -122,7 +224,8 @@ public class DownloaderImpl extends Downloader {
}
@Override
- public Response execute(@NonNull Request request) throws IOException, ReCaptchaException {
+ public Response execute(@NonNull final Request request)
+ throws IOException, ReCaptchaException {
final String httpMethod = request.httpMethod();
final String url = request.url();
final Map> headers = request.headers();
@@ -137,8 +240,9 @@ public class DownloaderImpl extends Downloader {
.method(httpMethod, requestBody).url(url)
.addHeader("User-Agent", USER_AGENT);
- if (!TextUtils.isEmpty(mCookies)) {
- requestBuilder.addHeader("Cookie", mCookies);
+ String cookies = getCookies(url);
+ if (!cookies.isEmpty()) {
+ requestBuilder.addHeader("Cookie", cookies);
}
for (Map.Entry> pair : headers.entrySet()) {
@@ -171,49 +275,8 @@ public class DownloaderImpl extends Downloader {
responseBodyToReturn = body.string();
}
- return new Response(response.code(), response.message(), response.headers().toMultimap(), responseBodyToReturn);
- }
-
- /**
- * Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken from the documentation of
- * OkHttpClient.Builder.sslSocketFactory(_,_)
- *
- * If there is an error, the function will safely fall back to doing nothing and printing the error to the console.
- *
- * @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
- */
- private static void enableModernTLS(OkHttpClient.Builder builder) {
- try {
- // get the default TrustManager
- TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
- TrustManagerFactory.getDefaultAlgorithm());
- trustManagerFactory.init((KeyStore) null);
- TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
- if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
- throw new IllegalStateException("Unexpected default trust managers:"
- + Arrays.toString(trustManagers));
- }
- X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
-
- // insert our own TLSSocketFactory
- SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
-
- builder.sslSocketFactory(sslSocketFactory, trustManager);
-
- // This will try to enable all modern CipherSuites(+2 more) that are supported on the device.
- // Necessary because some servers (e.g. Framatube.org) don't support the old cipher suites.
- // https://github.com/square/okhttp/issues/4053#issuecomment-402579554
- List cipherSuites = new ArrayList<>();
- cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites());
- cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
- cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
- ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
- .cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
- .build();
-
- builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
- } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
- if (DEBUG) e.printStackTrace();
- }
+ final String latestUrl = response.request().url().toString();
+ return new Response(response.code(), response.message(), response.headers().toMultimap(),
+ responseBodyToReturn, latestUrl);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/ExitActivity.java b/app/src/main/java/org/schabi/newpipe/ExitActivity.java
index 1ea3abe34..94eff9560 100644
--- a/app/src/main/java/org/schabi/newpipe/ExitActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/ExitActivity.java
@@ -1,4 +1,3 @@
-
package org.schabi.newpipe;
import android.annotation.SuppressLint;
@@ -27,9 +26,20 @@ import android.os.Bundle;
public class ExitActivity extends Activity {
+ public static void exitAndRemoveFromRecentApps(final Activity activity) {
+ Intent intent = new Intent(activity, ExitActivity.class);
+
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+ | Intent.FLAG_ACTIVITY_CLEAR_TASK
+ | Intent.FLAG_ACTIVITY_NO_ANIMATION);
+
+ activity.startActivity(intent);
+ }
+
@SuppressLint("NewApi")
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= 21) {
@@ -40,15 +50,4 @@ public class ExitActivity extends Activity {
System.exit(0);
}
-
- public static void exitAndRemoveFromRecentApps(Activity activity) {
- Intent intent = new Intent(activity, ExitActivity.class);
-
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
- | Intent.FLAG_ACTIVITY_CLEAR_TASK
- | Intent.FLAG_ACTIVITY_NO_ANIMATION);
-
- activity.startActivity(intent);
- }
}
diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java
index dfb7d3276..ca61c9655 100644
--- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java
+++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java
@@ -18,7 +18,7 @@ public class ImageDownloader extends BaseImageDownloader {
private final SharedPreferences preferences;
private final String downloadThumbnailKey;
- public ImageDownloader(Context context) {
+ public ImageDownloader(final Context context) {
super(context);
this.resources = context.getResources();
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
@@ -31,7 +31,7 @@ public class ImageDownloader extends BaseImageDownloader {
@SuppressLint("ResourceType")
@Override
- public InputStream getStream(String imageUri, Object extra) throws IOException {
+ public InputStream getStream(final String imageUri, final Object extra) throws IOException {
if (isDownloadingThumbnail()) {
return super.getStream(imageUri, extra);
} else {
@@ -39,7 +39,8 @@ public class ImageDownloader extends BaseImageDownloader {
}
}
- protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
+ protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
+ throws IOException {
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
return downloader.stream(imageUri);
}
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 2fb5d616a..7aa9cd9ff 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -53,31 +53,36 @@ import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.player.VideoPlayer;
+import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.*;
+import org.schabi.newpipe.views.FocusOverlayView;
import java.util.ArrayList;
import java.util.List;
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
- private ActionBarDrawerToggle toggle = null;
- private DrawerLayout drawer = null;
- private NavigationView drawerItems = null;
- private TextView headerServiceView = null;
- private Button toggleServiceButton = null;
+ private ActionBarDrawerToggle toggle;
+ private DrawerLayout drawer;
+ private NavigationView drawerItems;
+ private ImageView headerServiceIcon;
+ private TextView headerServiceView;
+ private Button toggleServiceButton;
private boolean servicesShown = false;
private ImageView serviceArrow;
- private static final int ITEM_ID_SUBSCRIPTIONS = - 1;
- private static final int ITEM_ID_FEED = - 2;
- private static final int ITEM_ID_BOOKMARKS = - 3;
- private static final int ITEM_ID_DOWNLOADS = - 4;
- private static final int ITEM_ID_HISTORY = - 5;
+ private static final int ITEM_ID_SUBSCRIPTIONS = -1;
+ private static final int ITEM_ID_FEED = -2;
+ private static final int ITEM_ID_BOOKMARKS = -3;
+ private static final int ITEM_ID_DOWNLOADS = -4;
+ private static final int ITEM_ID_HISTORY = -5;
private static final int ITEM_ID_SETTINGS = 0;
private static final int ITEM_ID_ABOUT = 1;
@@ -88,25 +93,24 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
@Override
- protected void onCreate(Bundle savedInstanceState) {
- if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
+ protected void onCreate(final Bundle savedInstanceState) {
+ if (DEBUG) {
+ Log.d(TAG, "onCreate() called with: "
+ + "savedInstanceState = [" + savedInstanceState + "]");
+ }
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
TLSSocketFactoryCompat.setAsDefault();
}
-
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
+ assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- Window w = getWindow();
- w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- }
-
- if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) {
+ if (getSupportFragmentManager() != null
+ && getSupportFragmentManager().getBackStackEntryCount() == 0) {
initFragments();
}
@@ -116,6 +120,10 @@ public class MainActivity extends AppCompatActivity {
} catch (Exception e) {
ErrorActivity.reportUiError(this, e);
}
+
+ if (AndroidTvUtils.isTv(this)) {
+ FocusOverlayView.setupFocusObserver(this);
+ }
}
private void setupDrawer() throws Exception {
@@ -131,49 +139,52 @@ public class MainActivity extends AppCompatActivity {
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerItems.getMenu()
- .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator.getTranslatedKioskName(ks, this))
- .setIcon(KioskTranslator.getKioskIcons(ks, this));
- kioskId ++;
+ .add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
+ .getTranslatedKioskName(ks, this))
+ .setIcon(KioskTranslator.getKioskIcon(ks, this));
+ kioskId++;
}
drawerItems.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
+ .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
+ R.string.tab_subscriptions)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
drawerItems.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
+ .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
//Settings and About
drawerItems.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
drawerItems.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
- toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
+ toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open,
+ R.string.drawer_close);
toggle.syncState();
drawer.addDrawerListener(toggle);
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
private int lastService;
@Override
- public void onDrawerOpened(View drawerView) {
+ public void onDrawerOpened(final View drawerView) {
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
}
@Override
- public void onDrawerClosed(View drawerView) {
- if(servicesShown) {
+ public void onDrawerClosed(final View drawerView) {
+ if (servicesShown) {
toggleServices();
}
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
@@ -186,7 +197,7 @@ public class MainActivity extends AppCompatActivity {
setupDrawerHeader();
}
- private boolean drawerItemSelected(MenuItem item) {
+ private boolean drawerItemSelected(final MenuItem item) {
switch (item.getGroupId()) {
case R.id.menu_services_group:
changeService(item);
@@ -209,19 +220,21 @@ public class MainActivity extends AppCompatActivity {
return true;
}
- private void changeService(MenuItem item) {
- drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
+ private void changeService(final MenuItem item) {
+ drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
+ .setChecked(false);
ServiceHelper.setSelectedServiceId(this, item.getItemId());
- drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
+ drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
+ .setChecked(true);
}
- private void tabSelected(MenuItem item) throws ExtractionException {
- switch(item.getItemId()) {
+ private void tabSelected(final MenuItem item) throws ExtractionException {
+ switch (item.getItemId()) {
case ITEM_ID_SUBSCRIPTIONS:
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
break;
case ITEM_ID_FEED:
- NavigationHelper.openWhatsNewFragment(getSupportFragmentManager());
+ NavigationHelper.openFeedFragment(getSupportFragmentManager());
break;
case ITEM_ID_BOOKMARKS:
NavigationHelper.openBookmarksFragment(getSupportFragmentManager());
@@ -239,19 +252,20 @@ public class MainActivity extends AppCompatActivity {
int kioskId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) {
- if(kioskId == item.getItemId()) {
+ if (kioskId == item.getItemId()) {
serviceName = ks;
}
- kioskId ++;
+ kioskId++;
}
- NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, serviceName);
+ NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
+ serviceName);
break;
}
}
- private void optionsAboutSelected(MenuItem item) {
- switch(item.getItemId()) {
+ private void optionsAboutSelected(final MenuItem item) {
+ switch (item.getItemId()) {
case ITEM_ID_SETTINGS:
NavigationHelper.openSettings(this);
break;
@@ -263,14 +277,27 @@ public class MainActivity extends AppCompatActivity {
private void setupDrawerHeader() {
NavigationView navigationView = findViewById(R.id.navigation);
- View hView = navigationView.getHeaderView(0);
+ View hView = navigationView.getHeaderView(0);
serviceArrow = hView.findViewById(R.id.drawer_arrow);
+ headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon);
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
- toggleServiceButton.setOnClickListener(view -> {
- toggleServices();
- });
+ toggleServiceButton.setOnClickListener(view -> toggleServices());
+
+ // If the current app name is bigger than the default "NewPipe" (7 chars),
+ // let the text view grow a little more as well.
+ if (getString(R.string.app_name).length() > "NewPipe".length()) {
+ final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title);
+ final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams();
+ layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+ headerTitle.setLayoutParams(layoutParams);
+ headerTitle.setMaxLines(2);
+ headerTitle.setMinWidth(getResources()
+ .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
+ headerTitle.setMaxWidth(getResources()
+ .getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
+ }
}
private void toggleServices() {
@@ -280,8 +307,7 @@ public class MainActivity extends AppCompatActivity {
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
-
- if(servicesShown) {
+ if (servicesShown) {
showServices();
} else {
try {
@@ -293,57 +319,64 @@ public class MainActivity extends AppCompatActivity {
}
private void showServices() {
- serviceArrow.setImageResource(R.drawable.ic_arrow_up_white);
+ serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
- for(StreamingService s : NewPipe.getServices()) {
- final String title = s.getServiceInfo().getName() +
- (ServiceHelper.isBeta(s) ? " (beta)" : "");
+ for (StreamingService s : NewPipe.getServices()) {
+ final String title = s.getServiceInfo().getName()
+ + (ServiceHelper.isBeta(s) ? " (beta)" : "");
MenuItem menuItem = drawerItems.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
// peertube specifics
- if(s.getServiceId() == 3){
+ if (s.getServiceId() == 3) {
enhancePeertubeMenu(s, menuItem);
}
}
- drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
+ drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
+ .setChecked(true);
}
- private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) {
+ private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
- Spinner spinner = (Spinner) LayoutInflater.from(this).inflate(R.layout.instance_spinner_layout, null);
+ Spinner spinner = (Spinner) LayoutInflater.from(this)
+ .inflate(R.layout.instance_spinner_layout, null);
List instances = PeertubeHelper.getInstanceList(this);
List items = new ArrayList<>();
int defaultSelect = 0;
- for(PeertubeInstance instance: instances){
+ for (PeertubeInstance instance : instances) {
items.add(instance.getName());
- if(instance.getUrl().equals(currentInstace.getUrl())){
- defaultSelect = items.size()-1;
+ if (instance.getUrl().equals(currentInstace.getUrl())) {
+ defaultSelect = items.size() - 1;
}
}
- ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items);
+ ArrayAdapter adapter = new ArrayAdapter<>(this,
+ R.layout.instance_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.setSelection(defaultSelect, false);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
- public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ public void onItemSelected(final AdapterView> parent, final View view,
+ final int position, final long id) {
PeertubeInstance newInstance = instances.get(position);
- if(newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) return;
+ if (newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) {
+ return;
+ }
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
changeService(menuItem);
drawer.closeDrawers();
new Handler(Looper.getMainLooper()).postDelayed(() -> {
- getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ getSupportFragmentManager().popBackStack(null,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
recreate();
}, 300);
}
@Override
- public void onNothingSelected(AdapterView> parent) {
+ public void onNothingSelected(final AdapterView> parent) {
}
});
@@ -351,7 +384,7 @@ public class MainActivity extends AppCompatActivity {
}
private void showTabs() throws ExtractionException {
- serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
+ serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
//Tabs
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
@@ -361,34 +394,35 @@ public class MainActivity extends AppCompatActivity {
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerItems.getMenu()
- .add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this))
- .setIcon(KioskTranslator.getKioskIcons(ks, this));
- kioskId ++;
+ .add(R.id.menu_tabs_group, kioskId, ORDER,
+ KioskTranslator.getTranslatedKioskName(ks, this))
+ .setIcon(KioskTranslator.getKioskIcon(ks, this));
+ kioskId++;
}
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
drawerItems.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
+ .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
drawerItems.getMenu()
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
//Settings and About
drawerItems.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
drawerItems.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
- .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
+ .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
}
@Override
@@ -401,15 +435,21 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onResume() {
+ assureCorrectAppLanguage(this);
+ // Change the date format to match the selected language on resume
+ Localization.init(getApplicationContext());
super.onResume();
- // close drawer on return, and don't show animation, so its looks like the drawer isn't open
- // when the user returns to MainActivity
+ // Close drawer on return, and don't show animation,
+ // so it looks like the drawer isn't open when the user returns to MainActivity
drawer.closeDrawer(GravityCompat.START, false);
try {
- String selectedServiceName = NewPipe.getService(
- ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
+ final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
+ final String selectedServiceName = NewPipe.getService(selectedServiceId)
+ .getServiceInfo().getName();
headerServiceView.setText(selectedServiceName);
+ headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId));
+
headerServiceView.post(() -> headerServiceView.setSelected(true));
toggleServiceButton.setContentDescription(
getString(R.string.drawer_header_description) + selectedServiceName);
@@ -419,28 +459,42 @@ public class MainActivity extends AppCompatActivity {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
- if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
+ if (DEBUG) {
+ Log.d(TAG, "Theme has changed, recreating activity...");
+ }
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
- // https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed
- // Briefly, let the activity resume properly posting the recreate call to end of the message queue
+ // https://stackoverflow.com/questions/10844112/
+ // Briefly, let the activity resume
+ // properly posting the recreate call to end of the message queue
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
}
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
- if (DEBUG) Log.d(TAG, "main page has changed, recreating main fragment...");
+ if (DEBUG) {
+ Log.d(TAG, "main page has changed, recreating main fragment...");
+ }
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
NavigationHelper.openMainActivity(this);
}
+
+ final boolean isHistoryEnabled = sharedPreferences.getBoolean(
+ getString(R.string.enable_watch_history_key), true);
+ drawerItems.getMenu().findItem(ITEM_ID_HISTORY).setVisible(isHistoryEnabled);
}
@Override
- protected void onNewIntent(Intent intent) {
- if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
+ protected void onNewIntent(final Intent intent) {
+ if (DEBUG) {
+ Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
+ }
if (intent != null) {
// Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
// to not destroy the already created backstack
String action = intent.getAction();
- if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) return;
+ if ((action != null && action.equals(Intent.ACTION_MAIN))
+ && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
+ return;
+ }
}
super.onNewIntent(intent);
@@ -448,17 +502,33 @@ public class MainActivity extends AppCompatActivity {
handleIntent(intent);
}
+ @Override
+ public boolean onKeyDown(final int keyCode, final KeyEvent event) {
+ final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder);
+ if (fragment instanceof OnKeyDownListener && !bottomSheetHiddenOrCollapsed()) {
+ // Provide keyDown event to fragment which then sends this event to the main player service
+ return ((OnKeyDownListener) fragment).onKeyDown(keyCode) || super.onKeyDown(keyCode, event);
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
@Override
public void onBackPressed() {
- if (DEBUG) Log.d(TAG, "onBackPressed() called");
+ if (DEBUG) {
+ Log.d(TAG, "onBackPressed() called");
+ }
- final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
- final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
+ if (AndroidTvUtils.isTv(this)) {
+ View drawerPanel = findViewById(R.id.navigation);
+ if (drawer.isDrawerOpen(drawerPanel)) {
+ drawer.closeDrawers();
+ return;
+ }
+ }
- final int sheetState = bottomSheetBehavior.getState();
- // In case bottomSheet is not visible on the screen or collapsed we can assume that the user interacts with a fragment
- // inside fragment_holder so all back presses should be handled by it
- if (sheetState == BottomSheetBehavior.STATE_HIDDEN || sheetState == BottomSheetBehavior.STATE_COLLAPSED) {
+ // In case bottomSheet is not visible on the screen or collapsed we can assume that the user
+ // interacts with a fragment inside fragment_holder so all back presses should be handled by it
+ if (bottomSheetHiddenOrCollapsed()) {
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
if (fragment instanceof BackPressable) {
@@ -469,21 +539,27 @@ public class MainActivity extends AppCompatActivity {
final Fragment fragmentPlayer = getSupportFragmentManager().findFragmentById(R.id.fragment_player_holder);
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
if (fragmentPlayer instanceof BackPressable) {
- if (!((BackPressable) fragmentPlayer).onBackPressed())
- bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ if (!((BackPressable) fragmentPlayer).onBackPressed()) {
+ final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
+ BottomSheetBehavior.from(bottomSheetLayout).setState(BottomSheetBehavior.STATE_COLLAPSED);
+ }
return;
}
}
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
finish();
- } else super.onBackPressed();
+ } else {
+ super.onBackPressed();
+ }
}
@Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- for (int i: grantResults){
- if (i == PackageManager.PERMISSION_DENIED){
+ public void onRequestPermissionsResult(final int requestCode,
+ @NonNull final String[] permissions,
+ @NonNull final int[] grantResults) {
+ for (int i : grantResults) {
+ if (i == PackageManager.PERMISSION_DENIED) {
return;
}
}
@@ -537,16 +613,16 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
@Override
- public boolean onCreateOptionsMenu(Menu menu) {
- if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ if (DEBUG) {
+ Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
+ }
super.onCreateOptionsMenu(menu);
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (!(fragment instanceof SearchFragment)) {
- findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container).setVisibility(View.GONE);
-
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.main_menu, menu);
+ findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
+ .setVisibility(View.GONE);
}
ActionBar actionBar = getSupportActionBar();
@@ -560,22 +636,16 @@ public class MainActivity extends AppCompatActivity {
}
@Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (DEBUG) {
+ Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
+ }
int id = item.getItemId();
switch (id) {
case android.R.id.home:
onHomeButtonPressed();
return true;
- case R.id.action_show_downloads:
- return NavigationHelper.openDownloads(this);
- case R.id.action_history:
- NavigationHelper.openStatisticFragment(getSupportFragmentManager());
- return true;
- case R.id.action_settings:
- NavigationHelper.openSettings(this);
- return true;
default:
return super.onOptionsItemSelected(item);
}
@@ -586,7 +656,9 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
private void initFragments() {
- if (DEBUG) Log.d(TAG, "initFragments() called");
+ if (DEBUG) {
+ Log.d(TAG, "initFragments() called");
+ }
StateSaver.clearStateFiles();
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
// When user watch a video inside popup and then tries to open the video in main player while the app is closed
@@ -595,7 +667,9 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.openMainFragment(getSupportFragmentManager());
handleIntent(getIntent());
- } else NavigationHelper.gotoMainFragment(getSupportFragmentManager());
+ } else {
+ NavigationHelper.gotoMainFragment(getSupportFragmentManager());
+ }
}
/*//////////////////////////////////////////////////////////////////////////
@@ -603,12 +677,14 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
private void updateDrawerNavigation() {
- if (getSupportActionBar() == null) return;
+ if (getSupportActionBar() == null) {
+ return;
+ }
final Toolbar toolbar = findViewById(R.id.toolbar);
- final DrawerLayout drawer = findViewById(R.id.drawer_layout);
- final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
+ final Fragment fragment = getSupportFragmentManager()
+ .findFragmentById(R.id.fragment_holder);
if (fragment instanceof MainFragment) {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
if (toggle != null) {
@@ -623,23 +699,18 @@ public class MainActivity extends AppCompatActivity {
}
}
- private void updateDrawerHeaderString(String content) {
- NavigationView navigationView = findViewById(R.id.navigation);
- View hView = navigationView.getHeaderView(0);
- Button action = hView.findViewById(R.id.drawer_header_action_button);
-
- action.setContentDescription(content);
- }
-
- private void handleIntent(Intent intent) {
+ private void handleIntent(final Intent intent) {
try {
- if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
+ if (DEBUG) {
+ Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
+ }
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
String url = intent.getStringExtra(Constants.KEY_URL);
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
String title = intent.getStringExtra(Constants.KEY_TITLE);
- switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
+ switch (((StreamingService.LinkType) intent
+ .getSerializableExtra(Constants.KEY_LINK_TYPE))) {
case STREAM:
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
final String intentCacheKey = intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY);
@@ -661,7 +732,9 @@ public class MainActivity extends AppCompatActivity {
}
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
- if (searchString == null) searchString = "";
+ if (searchString == null) {
+ searchString = "";
+ }
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
NavigationHelper.openSearchFragment(
getSupportFragmentManager(),
@@ -675,4 +748,15 @@ public class MainActivity extends AppCompatActivity {
ErrorActivity.reportUiError(this, e);
}
}
+ /*
+ * Utils
+ * */
+
+ private boolean bottomSheetHiddenOrCollapsed() {
+ final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
+ final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
+
+ final int sheetState = bottomSheetBehavior.getState();
+ return sheetState == BottomSheetBehavior.STATE_HIDDEN || sheetState == BottomSheetBehavior.STATE_COLLAPSED;
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
index f3356d6e8..c59c48367 100644
--- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
+++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
@@ -1,42 +1,54 @@
package org.schabi.newpipe;
-import androidx.room.Room;
import android.content.Context;
+import android.database.Cursor;
+
import androidx.annotation.NonNull;
+import androidx.room.Room;
import org.schabi.newpipe.database.AppDatabase;
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
-import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12;
+import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
+import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
public final class NewPipeDatabase {
-
private static volatile AppDatabase databaseInstance;
private NewPipeDatabase() {
//no instance
}
- private static AppDatabase getDatabase(Context context) {
+ private static AppDatabase getDatabase(final Context context) {
return Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
- .addMigrations(MIGRATION_11_12)
- .fallbackToDestructiveMigration()
+ .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build();
}
@NonNull
- public static AppDatabase getInstance(@NonNull Context context) {
+ public static AppDatabase getInstance(@NonNull final Context context) {
AppDatabase result = databaseInstance;
if (result == null) {
synchronized (NewPipeDatabase.class) {
result = databaseInstance;
if (result == null) {
- databaseInstance = (result = getDatabase(context));
+ databaseInstance = getDatabase(context);
+ result = databaseInstance;
}
}
}
return result;
}
+
+ public static void checkpoint() {
+ if (databaseInstance == null) {
+ throw new IllegalStateException("database is not initialized");
+ }
+ Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
+ if (c.moveToFirst() && c.getInt(0) == 1) {
+ throw new RuntimeException("Checkpoint was blocked from completing");
+ }
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java
index 4118070d5..2e1abd598 100644
--- a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java
@@ -1,4 +1,3 @@
-
package org.schabi.newpipe;
import android.annotation.SuppressLint;
@@ -26,17 +25,18 @@ import android.os.Bundle;
*/
public class PanicResponderActivity extends Activity {
-
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
@SuppressLint("NewApi")
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
- // TODO explicitly clear the search results once they are restored when the app restarts
- // or if the app reloads the current video after being killed, that should be cleared also
+ // TODO: Explicitly clear the search results
+ // once they are restored when the app restarts
+ // or if the app reloads the current video after being killed,
+ // that should be cleared also
ExitActivity.exitAndRemoveFromRecentApps(this);
}
diff --git a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java
index 0a2d51b53..40ea4fd58 100644
--- a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java
@@ -1,20 +1,32 @@
package org.schabi.newpipe;
-import android.app.Activity;
import android.content.Intent;
-import android.graphics.Bitmap;
+import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
-import androidx.core.app.NavUtils;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.Toolbar;
+import android.util.Log;
+import android.view.Menu;
import android.view.MenuItem;
import android.webkit.CookieManager;
+import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.app.NavUtils;
+import androidx.preference.PreferenceManager;
+
+import org.schabi.newpipe.util.ThemeHelper;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
/*
* Created by beneth on 06.12.16.
*
@@ -37,126 +49,200 @@ import android.webkit.WebViewClient;
public class ReCaptchaActivity extends AppCompatActivity {
public static final int RECAPTCHA_REQUEST = 10;
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
-
public static final String TAG = ReCaptchaActivity.class.toString();
public static final String YT_URL = "https://www.youtube.com";
+ public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
- private String url;
+ private WebView webView;
+ private String foundCookies = "";
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate(final Bundle savedInstanceState) {
+ ThemeHelper.setTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recaptcha);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
- url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
+ String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
if (url == null || url.isEmpty()) {
url = YT_URL;
}
-
- // Set return to Cancel by default
+ // set return to Cancel by default
setResult(RESULT_CANCELED);
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
- ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(true);
- actionBar.setTitle(R.string.reCaptcha_title);
- actionBar.setDisplayShowTitleEnabled(true);
- }
+ webView = findViewById(R.id.reCaptchaWebView);
- WebView myWebView = findViewById(R.id.reCaptchaWebView);
-
- // Enable Javascript
- WebSettings webSettings = myWebView.getSettings();
+ // enable Javascript
+ WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
- ReCaptchaWebViewClient webClient = new ReCaptchaWebViewClient(this);
- myWebView.setWebViewClient(webClient);
+ webView.setWebViewClient(new WebViewClient() {
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public boolean shouldOverrideUrlLoading(final WebView view,
+ final WebResourceRequest request) {
+ String url = request.getUrl().toString();
+ if (MainActivity.DEBUG) {
+ Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
+ }
- // Cleaning cache, history and cookies from webView
- myWebView.clearCache(true);
- myWebView.clearHistory();
+ handleCookiesFromUrl(url);
+ return false;
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
+ if (MainActivity.DEBUG) {
+ Log.d(TAG, "shouldOverrideUrlLoading: url=" + url);
+ }
+
+ handleCookiesFromUrl(url);
+ return false;
+ }
+
+ @Override
+ public void onPageFinished(final WebView view, final String url) {
+ super.onPageFinished(view, url);
+ handleCookiesFromUrl(url);
+ }
+ });
+
+ // cleaning cache, history and cookies from webView
+ webView.clearCache(true);
+ webView.clearHistory();
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- cookieManager.removeAllCookies(aBoolean -> {});
+ cookieManager.removeAllCookies(aBoolean -> {
+ });
} else {
cookieManager.removeAllCookie();
}
- myWebView.loadUrl(url);
- }
-
- private class ReCaptchaWebViewClient extends WebViewClient {
- private final Activity context;
- private String mCookies;
-
- ReCaptchaWebViewClient(Activity ctx) {
- context = ctx;
- }
-
- @Override
- public void onPageStarted(WebView view, String url, Bitmap favicon) {
- // TODO: Start Loader
- super.onPageStarted(view, url, favicon);
- }
-
- @Override
- public void onPageFinished(WebView view, String url) {
- String cookies = CookieManager.getInstance().getCookie(url);
-
- // TODO: Stop Loader
-
- // find cookies : s_gl & goojf and Add cookies to Downloader
- if (find_access_cookies(cookies)) {
- // Give cookies to Downloader class
- DownloaderImpl.getInstance().setCookies(mCookies);
-
- // Closing activity and return to parent
- setResult(RESULT_OK);
- finish();
- }
- }
-
- private boolean find_access_cookies(String cookies) {
- boolean ret = false;
- String c_s_gl = "";
- String c_goojf = "";
-
- String[] parts = cookies.split("; ");
- for (String part : parts) {
- if (part.trim().startsWith("s_gl")) {
- c_s_gl = part.trim();
- }
- if (part.trim().startsWith("goojf")) {
- c_goojf = part.trim();
- }
- }
- if (c_s_gl.length() > 0 && c_goojf.length() > 0) {
- ret = true;
- //mCookies = c_s_gl + "; " + c_goojf;
- // Youtube seems to also need the other cookies:
- mCookies = cookies;
- }
-
- return ret;
- }
+ webView.loadUrl(url);
}
@Override
- public boolean onOptionsItemSelected(MenuItem item) {
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setTitle(R.string.title_activity_recaptcha);
+ actionBar.setSubtitle(R.string.subtitle_activity_recaptcha);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onBackPressed() {
+ saveCookiesAndFinish();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
switch (id) {
- case android.R.id.home: {
- Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- NavUtils.navigateUpTo(this, intent);
+ case R.id.menu_item_done:
+ saveCookiesAndFinish();
return true;
- }
default:
return false;
}
}
+
+ private void saveCookiesAndFinish() {
+ handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page
+ if (MainActivity.DEBUG) {
+ Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
+ }
+
+ if (!foundCookies.isEmpty()) {
+ // save cookies to preferences
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
+ getApplicationContext());
+ final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
+ prefs.edit().putString(key, foundCookies).apply();
+
+ // give cookies to Downloader class
+ DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies);
+ setResult(RESULT_OK);
+ }
+
+ Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ NavUtils.navigateUpTo(this, intent);
+ }
+
+
+ private void handleCookiesFromUrl(@Nullable final String url) {
+ if (MainActivity.DEBUG) {
+ Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url));
+ }
+
+ if (url == null) {
+ return;
+ }
+
+ String cookies = CookieManager.getInstance().getCookie(url);
+ handleCookies(cookies);
+
+ // sometimes cookies are inside the url
+ int abuseStart = url.indexOf("google_abuse=");
+ if (abuseStart != -1) {
+ int abuseEnd = url.indexOf("+path");
+
+ try {
+ String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
+ abuseCookie = URLDecoder.decode(abuseCookie, "UTF-8");
+ handleCookies(abuseCookie);
+ } catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) {
+ if (MainActivity.DEBUG) {
+ e.printStackTrace();
+ Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
+ + abuseStart + " and ending at " + abuseEnd + " for url " + url);
+ }
+ }
+ }
+ }
+
+ private void handleCookies(@Nullable final String cookies) {
+ if (MainActivity.DEBUG) {
+ Log.d(TAG, "handleCookies: cookies=" + (cookies == null ? "null" : cookies));
+ }
+
+ if (cookies == null) {
+ return;
+ }
+
+ addYoutubeCookies(cookies);
+ // add here methods to extract cookies for other services
+ }
+
+ private void addYoutubeCookies(@NonNull final String cookies) {
+ if (cookies.contains("s_gl=") || cookies.contains("goojf=")
+ || cookies.contains("VISITOR_INFO1_LIVE=")
+ || cookies.contains("GOOGLE_ABUSE_EXEMPTION=")) {
+ // youtube seems to also need the other cookies:
+ addCookie(cookies);
+ }
+ }
+
+ private void addCookie(final String cookie) {
+ if (foundCookies.contains(cookie)) {
+ return;
+ }
+
+ if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
+ foundCookies += cookie;
+ } else if (foundCookies.endsWith(";")) {
+ foundCookies += " " + cookie;
+ } else {
+ foundCookies += "; " + cookie;
+ }
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
index 84e4a13b7..1ce35d6fe 100644
--- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
@@ -9,12 +9,6 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -26,6 +20,13 @@ import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Toast;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.app.NotificationCompat;
import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.download.DownloadDialog;
@@ -44,12 +45,15 @@ 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.AndroidTvUtils;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper;
+import org.schabi.newpipe.util.urlfinder.UrlFinder;
+import org.schabi.newpipe.views.FocusOverlayView;
import java.io.Serializable;
import java.util.ArrayList;
@@ -73,29 +77,31 @@ import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCap
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
/**
- * Get the url from the intent and open it in the chosen preferred player
+ * Get the url from the intent and open it in the chosen preferred player.
*/
public class RouterActivity extends AppCompatActivity {
-
+ public static final String INTERNAL_ROUTE_KEY = "internalRoute";
+ /**
+ * Removes invisible separators (\p{Z}) and punctuation characters including
+ * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
+ * more details.
+ */
+ private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
+ protected final CompositeDisposable disposables = new CompositeDisposable();
@State
protected int currentServiceId = -1;
- private StreamingService currentService;
@State
protected LinkType currentLinkType;
@State
protected int selectedRadioPosition = -1;
protected int selectedPreviously = -1;
-
protected String currentUrl;
protected boolean internalRoute = false;
- protected final CompositeDisposable disposables = new CompositeDisposable();
-
+ private StreamingService currentService;
private boolean selectionIsDownload = false;
- public static final String internalRouteKey = "internalRoute";
-
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
@@ -108,14 +114,14 @@ public class RouterActivity extends AppCompatActivity {
}
}
- internalRoute = getIntent().getBooleanExtra(internalRouteKey, false);
+ internalRoute = getIntent().getBooleanExtra(INTERNAL_ROUTE_KEY, false);
setTheme(ThemeHelper.isLightThemeSelected(this)
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
}
@Override
- protected void onSaveInstanceState(Bundle outState) {
+ protected void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
@@ -134,7 +140,7 @@ public class RouterActivity extends AppCompatActivity {
disposables.clear();
}
- private void handleUrl(String url) {
+ private void handleUrl(final String url) {
disposables.add(Observable
.fromCallable(() -> {
if (currentServiceId == -1) {
@@ -159,13 +165,14 @@ public class RouterActivity extends AppCompatActivity {
}, this::handleError));
}
- private void handleError(Throwable error) {
+ private void handleError(final Throwable error) {
error.printStackTrace();
if (error instanceof ExtractionException) {
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
} else {
- ExtractorHelper.handleGeneralException(this, -1, null, error, UserAction.SOMETHING_ELSE, null);
+ ExtractorHelper.handleGeneralException(this, -1, null, error,
+ UserAction.SOMETHING_ELSE, null);
}
finish();
@@ -177,8 +184,11 @@ public class RouterActivity extends AppCompatActivity {
}
protected void onSuccess() {
- final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
- final String selectedChoiceKey = preferences.getString(getString(R.string.preferred_open_action_key), getString(R.string.preferred_open_action_default));
+ final SharedPreferences preferences = PreferenceManager
+ .getDefaultSharedPreferences(this);
+ final String selectedChoiceKey = preferences
+ .getString(getString(R.string.preferred_open_action_key),
+ getString(R.string.preferred_open_action_default));
final String showInfoKey = getString(R.string.show_info_key);
final String videoPlayerKey = getString(R.string.video_player_key);
@@ -188,7 +198,8 @@ public class RouterActivity extends AppCompatActivity {
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
if (selectedChoiceKey.equals(alwaysAskKey)) {
- final List choices = getChoicesForService(currentService, currentLinkType);
+ final List choices
+ = getChoicesForService(currentService, currentLinkType);
switch (choices.size()) {
case 1:
@@ -206,20 +217,26 @@ public class RouterActivity extends AppCompatActivity {
} else if (selectedChoiceKey.equals(downloadKey)) {
handleChoice(downloadKey);
} else {
- final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
- final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
- final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey) || selectedChoiceKey.equals(popupPlayerKey);
+ final boolean isExtVideoEnabled = preferences.getBoolean(
+ getString(R.string.use_external_video_player_key), false);
+ final boolean isExtAudioEnabled = preferences.getBoolean(
+ getString(R.string.use_external_audio_player_key), false);
+ final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey)
+ || selectedChoiceKey.equals(popupPlayerKey);
final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey);
if (currentLinkType != LinkType.STREAM) {
- if (isExtAudioEnabled && isAudioPlayerSelected || isExtVideoEnabled && isVideoPlayerSelected) {
- Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
+ if (isExtAudioEnabled && isAudioPlayerSelected
+ || isExtVideoEnabled && isVideoPlayerSelected) {
+ Toast.makeText(this, R.string.external_player_unsupported_link_type,
+ Toast.LENGTH_LONG).show();
handleChoice(showInfoKey);
return;
}
}
- final List capabilities = currentService.getServiceInfo().getMediaCapabilities();
+ final List capabilities
+ = currentService.getServiceInfo().getMediaCapabilities();
boolean serviceSupportsChoice = false;
if (isVideoPlayerSelected) {
@@ -241,7 +258,8 @@ public class RouterActivity extends AppCompatActivity {
final Context themeWrapperContext = getThemeWrapperContext();
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
- final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
+ final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
+ R.layout.preferred_player_dialog_view, null, false);
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
@@ -252,7 +270,9 @@ public class RouterActivity extends AppCompatActivity {
handleChoice(choice.key);
if (which == DialogInterface.BUTTON_POSITIVE) {
- preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply();
+ preferences.edit()
+ .putString(getString(R.string.preferred_open_action_key), choice.key)
+ .apply();
}
};
@@ -263,7 +283,9 @@ public class RouterActivity extends AppCompatActivity {
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
.setPositiveButton(R.string.always, dialogButtonsClickListener)
.setOnDismissListener((dialog) -> {
- if (!selectionIsDownload) finish();
+ if (!selectionIsDownload) {
+ finish();
+ }
})
.create();
@@ -272,10 +294,13 @@ public class RouterActivity extends AppCompatActivity {
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
});
- radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true));
+ radioGroup.setOnCheckedChangeListener((group, checkedId) ->
+ setDialogButtonsState(alertDialog, true));
final View.OnClickListener radioButtonsClickListener = v -> {
final int indexOfChild = radioGroup.indexOfChild(v);
- if (indexOfChild == -1) return;
+ if (indexOfChild == -1) {
+ return;
+ }
selectedPreviously = selectedRadioPosition;
selectedRadioPosition = indexOfChild;
@@ -287,18 +312,23 @@ public class RouterActivity extends AppCompatActivity {
int id = 12345;
for (AdapterChoiceItem item : choices) {
- final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
+ final RadioButton radioButton
+ = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
radioButton.setText(item.description);
- radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0);
+ radioButton.setCompoundDrawablesWithIntrinsicBounds(
+ AppCompatResources.getDrawable(getApplicationContext(), item.icon),
+ null, null, null);
radioButton.setChecked(false);
radioButton.setId(id++);
- radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ radioButton.setLayoutParams(new RadioGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
radioButton.setOnClickListener(radioButtonsClickListener);
radioGroup.addView(radioButton);
}
if (selectedRadioPosition == -1) {
- final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null);
+ final String lastSelectedPlayer = preferences.getString(
+ getString(R.string.preferred_open_action_last_selected_key), null);
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
for (int i = 0; i < choices.size(); i++) {
AdapterChoiceItem c = choices.get(i);
@@ -317,48 +347,64 @@ public class RouterActivity extends AppCompatActivity {
selectedPreviously = selectedRadioPosition;
alertDialog.show();
+
+ if (AndroidTvUtils.isTv(this)) {
+ FocusOverlayView.setupFocusObserver(alertDialog);
+ }
}
- private List getChoicesForService(StreamingService service, LinkType linkType) {
+ private List getChoicesForService(final StreamingService service,
+ final LinkType linkType) {
final Context context = getThemeWrapperContext();
final List returnList = new ArrayList<>();
- final List capabilities = service.getServiceInfo().getMediaCapabilities();
+ final List capabilities
+ = service.getServiceInfo().getMediaCapabilities();
- final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
- boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
- boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
+ final SharedPreferences preferences = PreferenceManager
+ .getDefaultSharedPreferences(this);
+ boolean isExtVideoEnabled = preferences.getBoolean(
+ getString(R.string.use_external_video_player_key), false);
+ boolean isExtAudioEnabled = preferences.getBoolean(
+ getString(R.string.use_external_audio_player_key), false);
- returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
- resolveResourceIdFromAttr(context, R.attr.info)));
+ returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key),
+ getString(R.string.show_info),
+ resolveResourceIdFromAttr(context, R.attr.ic_info_outline)));
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
- returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
- resolveResourceIdFromAttr(context, R.attr.play)));
- returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
- resolveResourceIdFromAttr(context, R.attr.popup)));
+ returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key),
+ getString(R.string.video_player),
+ resolveResourceIdFromAttr(context, R.attr.ic_play_arrow)));
+ returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key),
+ getString(R.string.popup_player),
+ resolveResourceIdFromAttr(context, R.attr.ic_popup)));
}
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
- returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
- resolveResourceIdFromAttr(context, R.attr.audio)));
+ returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key),
+ getString(R.string.background_player),
+ resolveResourceIdFromAttr(context, R.attr.ic_headset)));
}
- returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download),
- resolveResourceIdFromAttr(context, R.attr.download)));
+ returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
+ getString(R.string.download),
+ resolveResourceIdFromAttr(context, R.attr.ic_file_download)));
return returnList;
}
private Context getThemeWrapperContext() {
- return new ContextThemeWrapper(this,
- ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
+ return new ContextThemeWrapper(this, ThemeHelper.isLightThemeSelected(this)
+ ? R.style.LightTheme : R.style.DarkTheme);
}
- private void setDialogButtonsState(AlertDialog dialog, boolean state) {
+ private void setDialogButtonsState(final AlertDialog dialog, final boolean state) {
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
- if (negativeButton == null || positiveButton == null) return;
+ if (negativeButton == null || positiveButton == null) {
+ return;
+ }
negativeButton.setEnabled(state);
positiveButton.setEnabled(state);
@@ -374,21 +420,25 @@ public class RouterActivity extends AppCompatActivity {
}
private void handleChoice(final String selectedChoiceKey) {
- final List validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
+ final List validChoicesList = Arrays.asList(getResources()
+ .getStringArray(R.array.preferred_open_action_values_list));
if (validChoicesList.contains(selectedChoiceKey)) {
PreferenceManager.getDefaultSharedPreferences(this).edit()
- .putString(getString(R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
+ .putString(getString(
+ R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
.apply();
}
- if (selectedChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) {
+ if (selectedChoiceKey.equals(getString(R.string.popup_player_key))
+ && !PermissionHelper.isPopupEnabled(this)) {
PermissionHelper.showPopupEnablementToast(this);
finish();
return;
}
if (selectedChoiceKey.equals(getString(R.string.download_key))) {
- if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
+ if (PermissionHelper.checkStoragePermissions(this,
+ PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
selectionIsDownload = true;
openDownloadDialog();
}
@@ -416,7 +466,8 @@ public class RouterActivity extends AppCompatActivity {
}
final Intent intent = new Intent(this, FetcherService.class);
- final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, currentUrl, selectedChoiceKey);
+ final Choice choice = new Choice(currentService.getServiceId(), currentLinkType,
+ currentUrl, selectedChoiceKey);
intent.putExtra(FetcherService.KEY_CHOICE, choice);
startService(intent);
@@ -429,12 +480,11 @@ public class RouterActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@NonNull StreamInfo result) -> {
- List sortedVideoStreams = ListHelper.getSortedStreamVideosList(this,
- result.getVideoStreams(),
- result.getVideoOnlyStreams(),
- false);
- int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this,
- sortedVideoStreams);
+ List sortedVideoStreams = ListHelper
+ .getSortedStreamVideosList(this, result.getVideoStreams(),
+ result.getVideoOnlyStreams(), false);
+ int selectedVideoStreamIndex = ListHelper
+ .getDefaultResolutionIndex(this, sortedVideoStreams);
FragmentManager fm = getSupportFragmentManager();
DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
@@ -452,7 +502,9 @@ public class RouterActivity extends AppCompatActivity {
}
@Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ public void onRequestPermissionsResult(final int requestCode,
+ @NonNull final String[] permissions,
+ @NonNull final int[] grantResults) {
for (int i : grantResults) {
if (i == PackageManager.PERMISSION_DENIED) {
finish();
@@ -464,196 +516,10 @@ public class RouterActivity extends AppCompatActivity {
}
}
- private static class AdapterChoiceItem {
- final String description, key;
- @DrawableRes
- final int icon;
-
- AdapterChoiceItem(String key, String description, int icon) {
- this.description = description;
- this.key = key;
- this.icon = icon;
- }
- }
-
- private static class Choice implements Serializable {
- final int serviceId;
- final String url, playerChoice;
- final LinkType linkType;
-
- Choice(int serviceId, LinkType linkType, String url, String playerChoice) {
- this.serviceId = serviceId;
- this.linkType = linkType;
- this.url = url;
- this.playerChoice = playerChoice;
- }
-
- @Override
- public String toString() {
- return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
- }
- }
-
/*//////////////////////////////////////////////////////////////////////////
// Service Fetcher
//////////////////////////////////////////////////////////////////////////*/
- public static class FetcherService extends IntentService {
-
- private static final int ID = 456;
- public static final String KEY_CHOICE = "key_choice";
- private Disposable fetcher;
-
- public FetcherService() {
- super(FetcherService.class.getSimpleName());
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- startForeground(ID, createNotification().build());
- }
-
- @Override
- protected void onHandleIntent(@Nullable Intent intent) {
- if (intent == null) return;
-
- final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
- if (!(serializable instanceof Choice)) return;
- Choice playerChoice = (Choice) serializable;
- handleChoice(playerChoice);
- }
-
- public void handleChoice(Choice choice) {
- Single extends Info> single = null;
- UserAction userAction = UserAction.SOMETHING_ELSE;
-
- switch (choice.linkType) {
- case STREAM:
- single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
- userAction = UserAction.REQUESTED_STREAM;
- break;
- case CHANNEL:
- single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
- userAction = UserAction.REQUESTED_CHANNEL;
- break;
- case PLAYLIST:
- single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
- userAction = UserAction.REQUESTED_PLAYLIST;
- break;
- }
-
-
- if (single != null) {
- final UserAction finalUserAction = userAction;
- final Consumer resultHandler = getResultHandler(choice);
- fetcher = single
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(info -> {
- resultHandler.accept(info);
- if (fetcher != null) fetcher.dispose();
- }, throwable -> ExtractorHelper.handleGeneralException(this,
- choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice));
- }
- }
-
- public Consumer getResultHandler(Choice choice) {
- return info -> {
- final String videoPlayerKey = getString(R.string.video_player_key);
- final String backgroundPlayerKey = getString(R.string.background_player_key);
- final String popupPlayerKey = getString(R.string.popup_player_key);
-
- final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
- boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
- boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
- ;
-
- PlayQueue playQueue;
- String playerChoice = choice.playerChoice;
-
- if (info instanceof StreamInfo) {
- if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
- NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
-
- } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
- NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
-
- } else {
- playQueue = new SinglePlayQueue((StreamInfo) info);
-
- if (playerChoice.equals(videoPlayerKey)) {
- openMainPlayer(playQueue, choice);
- } else if (playerChoice.equals(backgroundPlayerKey)) {
- NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
- } else if (playerChoice.equals(popupPlayerKey)) {
- NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
- }
- }
- }
-
- if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
- playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
-
- if (playerChoice.equals(videoPlayerKey)) {
- openMainPlayer(playQueue, choice);
- } else if (playerChoice.equals(backgroundPlayerKey)) {
- NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
- } else if (playerChoice.equals(popupPlayerKey)) {
- NavigationHelper.playOnPopupPlayer(this, playQueue, true);
- }
- }
- };
- }
-
- private void openMainPlayer(PlayQueue playQueue, Choice choice) {
- NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType,
- choice.url, "", true, true);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- stopForeground(true);
- if (fetcher != null) fetcher.dispose();
- }
-
- private NotificationCompat.Builder createNotification() {
- return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
- .setOngoing(true)
- .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setContentTitle(getString(R.string.preferred_player_fetcher_notification_title))
- .setContentText(getString(R.string.preferred_player_fetcher_notification_message));
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- /**
- * Removes invisible separators (\p{Z}) and punctuation characters including
- * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
- * more details.
- */
- private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
-
- private String getUrl(Intent intent) {
- // first gather data and find service
- String videoUrl = null;
- if (intent.getData() != null) {
- // this means the video was called though another app
- videoUrl = intent.getData().toString();
- } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
- //this means that vidoe was called through share menu
- String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
- final String[] uris = getUris(extraText);
- videoUrl = uris.length > 0 ? uris[0] : null;
- }
-
- return videoUrl;
- }
-
private String removeHeadingGibberish(final String input) {
int start = 0;
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
@@ -662,9 +528,13 @@ public class RouterActivity extends AppCompatActivity {
break;
}
}
- return input.substring(start, input.length());
+ return input.substring(start);
}
+ /*//////////////////////////////////////////////////////////////////////////
+ // Utils
+ //////////////////////////////////////////////////////////////////////////*/
+
private String trim(final String input) {
if (input == null || input.length() < 1) {
return input;
@@ -674,7 +544,7 @@ public class RouterActivity extends AppCompatActivity {
output = output.substring(1);
}
while (output.length() > 0
- && output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
+ && output.substring(output.length() - 1).matches(REGEX_REMOVE_FROM_URL)) {
output = output.substring(0, output.length() - 1);
}
return output;
@@ -705,4 +575,200 @@ public class RouterActivity extends AppCompatActivity {
}
return result.toArray(new String[result.size()]);
}
+
+ private static class AdapterChoiceItem {
+ final String description;
+ final String key;
+ @DrawableRes
+ final int icon;
+
+ AdapterChoiceItem(final String key, final String description, final int icon) {
+ this.description = description;
+ this.key = key;
+ this.icon = icon;
+ }
+ }
+
+ private static class Choice implements Serializable {
+ final int serviceId;
+ final String url;
+ final String playerChoice;
+ final LinkType linkType;
+
+ Choice(final int serviceId, final LinkType linkType,
+ final String url, final String playerChoice) {
+ this.serviceId = serviceId;
+ this.linkType = linkType;
+ this.url = url;
+ this.playerChoice = playerChoice;
+ }
+
+ @Override
+ public String toString() {
+ return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
+ }
+ }
+
+ public static class FetcherService extends IntentService {
+
+ public static final String KEY_CHOICE = "key_choice";
+ private static final int ID = 456;
+ private Disposable fetcher;
+
+ public FetcherService() {
+ super(FetcherService.class.getSimpleName());
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ startForeground(ID, createNotification().build());
+ }
+
+ @Override
+ protected void onHandleIntent(@Nullable final Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
+ if (!(serializable instanceof Choice)) {
+ return;
+ }
+ Choice playerChoice = (Choice) serializable;
+ handleChoice(playerChoice);
+ }
+
+ public void handleChoice(final Choice choice) {
+ Single extends Info> single = null;
+ UserAction userAction = UserAction.SOMETHING_ELSE;
+
+ switch (choice.linkType) {
+ case STREAM:
+ single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
+ userAction = UserAction.REQUESTED_STREAM;
+ break;
+ case CHANNEL:
+ single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
+ userAction = UserAction.REQUESTED_CHANNEL;
+ break;
+ case PLAYLIST:
+ single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
+ userAction = UserAction.REQUESTED_PLAYLIST;
+ break;
+ }
+
+
+ if (single != null) {
+ final UserAction finalUserAction = userAction;
+ final Consumer resultHandler = getResultHandler(choice);
+ fetcher = single
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(info -> {
+ resultHandler.accept(info);
+ if (fetcher != null) {
+ fetcher.dispose();
+ }
+ }, throwable -> ExtractorHelper.handleGeneralException(this,
+ choice.serviceId, choice.url, throwable, finalUserAction,
+ ", opened with " + choice.playerChoice));
+ }
+ }
+
+ public Consumer getResultHandler(final Choice choice) {
+ return info -> {
+ final String videoPlayerKey = getString(R.string.video_player_key);
+ final String backgroundPlayerKey = getString(R.string.background_player_key);
+ final String popupPlayerKey = getString(R.string.popup_player_key);
+
+ final SharedPreferences preferences = PreferenceManager
+ .getDefaultSharedPreferences(this);
+ boolean isExtVideoEnabled = preferences.getBoolean(
+ getString(R.string.use_external_video_player_key), false);
+ boolean isExtAudioEnabled = preferences.getBoolean(
+ getString(R.string.use_external_audio_player_key), false);
+
+ PlayQueue playQueue;
+ String playerChoice = choice.playerChoice;
+
+ if (info instanceof StreamInfo) {
+ if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
+ NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
+
+ } else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
+ NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
+
+ } else {
+ playQueue = new SinglePlayQueue((StreamInfo) info);
+
+ if (playerChoice.equals(videoPlayerKey)) {
+ openMainPlayer(playQueue, choice);
+ } else if (playerChoice.equals(backgroundPlayerKey)) {
+ NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
+ } else if (playerChoice.equals(popupPlayerKey)) {
+ NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
+ }
+ }
+ }
+
+ if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
+ playQueue = info instanceof ChannelInfo
+ ? new ChannelPlayQueue((ChannelInfo) info)
+ : new PlaylistPlayQueue((PlaylistInfo) info);
+
+ if (playerChoice.equals(videoPlayerKey)) {
+ openMainPlayer(playQueue, choice);
+ } else if (playerChoice.equals(backgroundPlayerKey)) {
+ NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
+ } else if (playerChoice.equals(popupPlayerKey)) {
+ NavigationHelper.playOnPopupPlayer(this, playQueue, true);
+ }
+ }
+ };
+ }
+
+ private void openMainPlayer(PlayQueue playQueue, Choice choice) {
+ NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType,
+ choice.url, "", true, true);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ stopForeground(true);
+ if (fetcher != null) {
+ fetcher.dispose();
+ }
+ }
+
+ private NotificationCompat.Builder createNotification() {
+ return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
+ .setOngoing(true)
+ .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setContentTitle(
+ getString(R.string.preferred_player_fetcher_notification_title))
+ .setContentText(
+ getString(R.string.preferred_player_fetcher_notification_message));
+ }
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Utils
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @Nullable
+ private String getUrl(final Intent intent) {
+ String foundUrl = null;
+ if (intent.getData() != null) {
+ // Called from another app
+ foundUrl = intent.getData().toString();
+ } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
+ // Called from the share menu
+ final String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
+ foundUrl = UrlFinder.firstUrlFromInput(extraText);
+ }
+
+ return foundUrl;
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java
index 2326e795e..b5be2dde6 100644
--- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java
@@ -1,48 +1,67 @@
package org.schabi.newpipe.about;
import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
import android.os.Bundle;
-import com.google.android.material.tabs.TabLayout;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.Toolbar;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
+
+import com.google.android.material.tabs.TabLayout;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
-public class AboutActivity extends AppCompatActivity {
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser;
+public class AboutActivity extends AppCompatActivity {
/**
- * List of all software components
+ * List of all software components.
*/
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
- new SoftwareComponent("Giga Get", "2014", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
- new SoftwareComponent("NewPipe Extractor", "2017", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
- new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", "https://github.com/jhy/jsoup", StandardLicenses.MIT),
- new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
- new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2),
- new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2),
- new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
- new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
- new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
- new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
- new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
- new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2)
+ new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
+ "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
+ new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger",
+ "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
+ new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley",
+ "https://github.com/jhy/jsoup", StandardLicenses.MIT),
+ new SoftwareComponent("Rhino", "2015", "Mozilla",
+ "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
+ new SoftwareComponent("ACRA", "2013", "Kevin Gaudin",
+ "http://www.acra.ch", StandardLicenses.APACHE2),
+ new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
+ "https://github.com/nostra13/Android-Universal-Image-Loader",
+ StandardLicenses.APACHE2),
+ new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof",
+ "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
+ new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam",
+ "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
+ new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc",
+ "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
+ new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors",
+ "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
+ new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors",
+ "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
+ new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton",
+ "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2),
+ new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
+ "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2),
+ new SoftwareComponent("Markwon", "2017 - 2020", "Noties",
+ "https://github.com/noties/Markwon", StandardLicenses.APACHE2),
+ new SoftwareComponent("Groupie", "2016", "Lisa Wray",
+ "https://github.com/lisawray/groupie", StandardLicenses.MIT)
};
/**
@@ -61,9 +80,11 @@ public class AboutActivity extends AppCompatActivity {
private ViewPager mViewPager;
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ protected void onCreate(final Bundle savedInstanceState) {
+ assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this);
+ this.setTitle(getString(R.string.title_activity_about));
setContentView(R.layout.activity_about);
@@ -82,28 +103,14 @@ public class AboutActivity extends AppCompatActivity {
tabLayout.setupWithViewPager(mViewPager);
}
-
@Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.menu_about, menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
-
+ public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
switch (id) {
case android.R.id.home:
finish();
return true;
- case R.id.action_settings:
- NavigationHelper.openSettings(this);
- return true;
- case R.id.action_show_downloads:
- return NavigationHelper.openDownloads(this);
}
return super.onOptionsItemSelected(item);
@@ -113,21 +120,20 @@ public class AboutActivity extends AppCompatActivity {
* A placeholder fragment containing a simple view.
*/
public static class AboutFragment extends Fragment {
-
- public AboutFragment() {
- }
+ public AboutFragment() { }
/**
- * Returns a new instance of this fragment for the given section
- * number.
+ * Created a new instance of this fragment for the given section number.
+ *
+ * @return New instance of {@link AboutFragment}
*/
public static AboutFragment newInstance() {
return new AboutFragment();
}
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
Context context = this.getContext();
@@ -135,40 +141,37 @@ public class AboutActivity extends AppCompatActivity {
version.setText(BuildConfig.VERSION_NAME);
View githubLink = rootView.findViewById(R.id.github_link);
- githubLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.github_url), context));
+ githubLink.setOnClickListener(nv ->
+ openUrlInBrowser(context, context.getString(R.string.github_url)));
View donationLink = rootView.findViewById(R.id.donation_link);
- donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context));
+ donationLink.setOnClickListener(v ->
+ openUrlInBrowser(context, context.getString(R.string.donation_url)));
View websiteLink = rootView.findViewById(R.id.website_link);
- websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context));
+ websiteLink.setOnClickListener(nv ->
+ openUrlInBrowser(context, context.getString(R.string.website_url)));
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
- privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context));
+ privacyPolicyLink.setOnClickListener(v ->
+ openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
return rootView;
}
- private void openWebsite(String url, Context context) {
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- context.startActivity(intent);
- }
-
}
-
/**
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
-
- public SectionsPagerAdapter(FragmentManager fm) {
+ public SectionsPagerAdapter(final FragmentManager fm) {
super(fm);
}
@Override
- public Fragment getItem(int position) {
+ public Fragment getItem(final int position) {
switch (position) {
case 0:
return AboutFragment.newInstance();
@@ -185,7 +188,7 @@ public class AboutActivity extends AppCompatActivity {
}
@Override
- public CharSequence getPageTitle(int position) {
+ public CharSequence getPageTitle(final int position) {
switch (position) {
case 0:
return getString(R.string.tab_about);
diff --git a/app/src/main/java/org/schabi/newpipe/about/License.java b/app/src/main/java/org/schabi/newpipe/about/License.java
index e51e1d0f1..370009860 100644
--- a/app/src/main/java/org/schabi/newpipe/about/License.java
+++ b/app/src/main/java/org/schabi/newpipe/about/License.java
@@ -5,18 +5,17 @@ import android.os.Parcel;
import android.os.Parcelable;
/**
- * A software license
+ * Class for storing information about a software license.
*/
public class License implements Parcelable {
-
public static final Creator CREATOR = new Creator() {
@Override
- public License createFromParcel(Parcel source) {
+ public License createFromParcel(final Parcel source) {
return new License(source);
}
@Override
- public License[] newArray(int size) {
+ public License[] newArray(final int size) {
return new License[size];
}
};
@@ -24,16 +23,22 @@ public class License implements Parcelable {
private final String name;
private String filename;
- public License(String name, String abbreviation, String filename) {
- if(name == null) throw new NullPointerException("name is null");
- if(abbreviation == null) throw new NullPointerException("abbreviation is null");
- if(filename == null) throw new NullPointerException("filename is null");
+ public License(final String name, final String abbreviation, final String filename) {
+ if (name == null) {
+ throw new NullPointerException("name is null");
+ }
+ if (abbreviation == null) {
+ throw new NullPointerException("abbreviation is null");
+ }
+ if (filename == null) {
+ throw new NullPointerException("filename is null");
+ }
this.name = name;
this.filename = filename;
this.abbreviation = abbreviation;
}
- protected License(Parcel in) {
+ protected License(final Parcel in) {
this.filename = in.readString();
this.abbreviation = in.readString();
this.name = in.readString();
@@ -50,7 +55,7 @@ public class License implements Parcelable {
public String getAbbreviation() {
return abbreviation;
}
-
+
public String getFilename() {
return filename;
}
@@ -61,7 +66,7 @@ public class License implements Parcelable {
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(final Parcel dest, final int flags) {
dest.writeString(this.filename);
dest.writeString(this.abbreviation);
dest.writeString(this.name);
diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java
index fe78ff9f1..bc6310601 100644
--- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java
@@ -2,29 +2,33 @@ package org.schabi.newpipe.about;
import android.app.Activity;
import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
-import android.view.*;
-import android.widget.TextView;
+
import org.schabi.newpipe.R;
+import org.schabi.newpipe.util.ShareUtils;
import java.util.Arrays;
-import java.util.Comparator;
/**
- * Fragment containing the software licenses
+ * Fragment containing the software licenses.
*/
public class LicenseFragment extends Fragment {
-
private static final String ARG_COMPONENTS = "components";
private SoftwareComponent[] softwareComponents;
- private SoftwareComponent mComponentForContextMenu;
+ private SoftwareComponent componentForContextMenu;
- public static LicenseFragment newInstance(SoftwareComponent[] softwareComponents) {
- if(softwareComponents == null) {
+ public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
+ if (softwareComponents == null) {
throw new NullPointerException("softwareComponents is null");
}
LicenseFragment fragment = new LicenseFragment();
@@ -35,57 +39,50 @@ public class LicenseFragment extends Fragment {
}
/**
- * Shows a popup containing the license
+ * Shows a popup containing the license.
+ *
* @param context the context to use
* @param license the license to show
*/
- public static void showLicense(Context context, License license) {
+ private static void showLicense(final Context context, final License license) {
new LicenseFragmentHelper((Activity) context).execute(license);
}
@Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
+ public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- softwareComponents = (SoftwareComponent[]) getArguments().getParcelableArray(ARG_COMPONENTS);
+ softwareComponents = (SoftwareComponent[]) getArguments()
+ .getParcelableArray(ARG_COMPONENTS);
// Sort components by name
- Arrays.sort(softwareComponents, new Comparator() {
- @Override
- public int compare(SoftwareComponent o1, SoftwareComponent o2) {
- return o1.getName().compareTo(o2.getName());
- }
- });
+ Arrays.sort(softwareComponents, (o1, o2) -> o1.getName().compareTo(o2.getName()));
}
@Nullable
@Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
- ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
+ public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
+ @Nullable final Bundle savedInstanceState) {
+ final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
+ final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
- View licenseLink = rootView.findViewById(R.id.app_read_license);
- licenseLink.setOnClickListener(new OnReadFullLicenseClickListener());
+ final View licenseLink = rootView.findViewById(R.id.app_read_license);
+ licenseLink.setOnClickListener(v ->
+ showLicense(getActivity(), StandardLicenses.GPL3));
for (final SoftwareComponent component : softwareComponents) {
- View componentView = inflater.inflate(R.layout.item_software_component, container, false);
- TextView softwareName = componentView.findViewById(R.id.name);
- TextView copyright = componentView.findViewById(R.id.copyright);
+ final View componentView = inflater
+ .inflate(R.layout.item_software_component, container, false);
+ final TextView softwareName = componentView.findViewById(R.id.name);
+ final TextView copyright = componentView.findViewById(R.id.copyright);
softwareName.setText(component.getName());
- copyright.setText(getContext().getString(R.string.copyright,
+ copyright.setText(getString(R.string.copyright,
component.getYears(),
component.getCopyrightOwner(),
component.getLicense().getAbbreviation()));
componentView.setTag(component);
- componentView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Context context = v.getContext();
- if (context != null) {
- showLicense(context, component.getLicense());
- }
- }
- });
+ componentView.setOnClickListener(v ->
+ showLicense(getActivity(), component.getLicense()));
softwareComponentsView.addView(componentView);
registerForContextMenu(componentView);
}
@@ -93,41 +90,30 @@ public class LicenseFragment extends Fragment {
}
@Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- MenuInflater inflater = getActivity().getMenuInflater();
- SoftwareComponent component = (SoftwareComponent) v.getTag();
+ public void onCreateContextMenu(final ContextMenu menu, final View v,
+ final ContextMenu.ContextMenuInfo menuInfo) {
+ final MenuInflater inflater = getActivity().getMenuInflater();
+ final SoftwareComponent component = (SoftwareComponent) v.getTag();
menu.setHeaderTitle(component.getName());
inflater.inflate(R.menu.software_component, menu);
super.onCreateContextMenu(menu, v, menuInfo);
- mComponentForContextMenu = (SoftwareComponent) v.getTag();
+ componentForContextMenu = (SoftwareComponent) v.getTag();
}
@Override
- public boolean onContextItemSelected(MenuItem item) {
+ public boolean onContextItemSelected(final MenuItem item) {
// item.getMenuInfo() is null so we use the tag of the view
- final SoftwareComponent component = mComponentForContextMenu;
+ final SoftwareComponent component = componentForContextMenu;
if (component == null) {
return false;
}
switch (item.getItemId()) {
case R.id.action_website:
- openWebsite(component.getLink());
+ ShareUtils.openUrlInBrowser(getActivity(), component.getLink());
return true;
case R.id.action_show_license:
- showLicense(getContext(), component.getLicense());
+ showLicense(getActivity(), component.getLicense());
}
return false;
}
-
- private void openWebsite(String componentLink) {
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(componentLink));
- startActivity(browserIntent);
- }
-
- private static class OnReadFullLicenseClickListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- LicenseFragment.showLicense(v.getContext(), StandardLicenses.GPL3);
- }
- }
}
diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java
index eeafc1f57..1c425567f 100644
--- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java
@@ -2,30 +2,95 @@ package org.schabi.newpipe.about;
import android.app.Activity;
import android.content.Context;
-import android.content.DialogInterface;
import android.os.AsyncTask;
+import android.util.Base64;
+import android.webkit.WebView;
+
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
-import android.webkit.WebView;
+
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.BufferedReader;
+import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
+import java.nio.charset.StandardCharsets;
+
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class LicenseFragmentHelper extends AsyncTask