mirror of https://github.com/TeamNewPipe/NewPipe
Autoconvert app/src/main to Kotlin
This commit is contained in:
parent
974167fcb8
commit
6b4f856a27
|
@ -1,344 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.fragment.app;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.os.BundleCompat;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
|
||||
// TODO: Replace this deprecated class with its ViewPager2 counterpart
|
||||
|
||||
/**
|
||||
* This is a copy from {@link androidx.fragment.app.FragmentStatePagerAdapter}.
|
||||
* <p>
|
||||
* It includes a workaround to fix the menu visibility when the adapter is restored.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* <b>Check out the changes in:</b>
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #saveState()}</li>
|
||||
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use
|
||||
* {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
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 final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<>();
|
||||
private Fragment mCurrentPrimaryItem = null;
|
||||
private boolean mExecutingFinishUpdate;
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
* <p>Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the
|
||||
* current Fragment changes.</p>
|
||||
*
|
||||
* @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) {
|
||||
final Fragment f = mFragments.get(position);
|
||||
if (f != null) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
if (mCurTransaction == null) {
|
||||
mCurTransaction = mFragmentManager.beginTransaction();
|
||||
}
|
||||
|
||||
final Fragment fragment = getItem(position);
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
|
||||
}
|
||||
if (mSavedState.size() > position) {
|
||||
final 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) {
|
||||
final 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.equals(mCurrentPrimaryItem)) {
|
||||
mCurrentPrimaryItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"ReferenceEquality", "deprecation"})
|
||||
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
||||
@NonNull final Object object) {
|
||||
final 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) {
|
||||
// We drop any transactions that attempt to be committed
|
||||
// from a re-entrant call to finishUpdate(). We need to
|
||||
// do this as a workaround for Robolectric running measure/layout
|
||||
// calls inline rather than allowing them to be posted
|
||||
// as they would on a real device.
|
||||
if (!mExecutingFinishUpdate) {
|
||||
try {
|
||||
mExecutingFinishUpdate = true;
|
||||
mCurTransaction.commitNowAllowingStateLoss();
|
||||
} finally {
|
||||
mExecutingFinishUpdate = false;
|
||||
}
|
||||
}
|
||||
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.isEmpty()) {
|
||||
state = new Bundle();
|
||||
state.putParcelableArrayList("states", mSavedState);
|
||||
}
|
||||
for (int i = 0; i < mFragments.size(); i++) {
|
||||
final Fragment f = mFragments.get(i);
|
||||
if (f != null && f.isAdded()) {
|
||||
if (state == null) {
|
||||
state = new Bundle();
|
||||
}
|
||||
final 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) {
|
||||
final Bundle bundle = (Bundle) state;
|
||||
bundle.setClassLoader(loader);
|
||||
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
||||
Fragment.SavedState.class);
|
||||
mSavedState.clear();
|
||||
mFragments.clear();
|
||||
if (states != null) {
|
||||
mSavedState.addAll(states);
|
||||
}
|
||||
final Iterable<String> keys = bundle.keySet();
|
||||
for (final String key : keys) {
|
||||
if (key.startsWith("f")) {
|
||||
final int index = Integer.parseInt(key.substring(1));
|
||||
final 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.fragment.app
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager.widget.PagerAdapter
|
||||
|
||||
// TODO: Replace this deprecated class with its ViewPager2 counterpart
|
||||
/**
|
||||
* This is a copy from [androidx.fragment.app.FragmentStatePagerAdapter].
|
||||
*
|
||||
*
|
||||
* 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:**
|
||||
*
|
||||
*
|
||||
* * [.saveState]
|
||||
* * [.restoreState]
|
||||
*
|
||||
*
|
||||
*/
|
||||
@Suppress("deprecation")
|
||||
@Deprecated("""Switch to {@link androidx.viewpager2.widget.ViewPager2} and use
|
||||
{@link androidx.viewpager2.adapter.FragmentStateAdapter} instead.""")
|
||||
abstract class FragmentStatePagerAdapterMenuWorkaround
|
||||
/**
|
||||
* Constructor for [FragmentStatePagerAdapterMenuWorkaround].
|
||||
*
|
||||
* If [.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT] is passed in, then only the current
|
||||
* Fragment is in the [Lifecycle.State.RESUMED] state, while all other fragments are
|
||||
* capped at [Lifecycle.State.STARTED]. If [.BEHAVIOR_SET_USER_VISIBLE_HINT] is
|
||||
* passed, all fragments are in the [Lifecycle.State.RESUMED] state and there will be
|
||||
* callbacks to [Fragment.setUserVisibleHint].
|
||||
*
|
||||
* @param fm fragment manager that will interact with this adapter
|
||||
* @param behavior determines if only current fragments are in a resumed state
|
||||
*/(private val mFragmentManager: FragmentManager,
|
||||
@param:Behavior private val mBehavior: Int) : PagerAdapter() {
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@IntDef([BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT])
|
||||
private annotation class Behavior
|
||||
|
||||
private var mCurTransaction: FragmentTransaction? = null
|
||||
private val mSavedState = ArrayList<Fragment.SavedState?>()
|
||||
private val mFragments = ArrayList<Fragment>()
|
||||
private var mCurrentPrimaryItem: Fragment? = null
|
||||
private var mExecutingFinishUpdate = false
|
||||
|
||||
/**
|
||||
* Constructor for [FragmentStatePagerAdapterMenuWorkaround]
|
||||
* that sets the fragment manager for the adapter. This is the equivalent of calling
|
||||
* [.FragmentStatePagerAdapterMenuWorkaround] and passing in
|
||||
* [.BEHAVIOR_SET_USER_VISIBLE_HINT].
|
||||
*
|
||||
*
|
||||
* Fragments will have [Fragment.setUserVisibleHint] 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}""")
|
||||
constructor(fm: FragmentManager) : this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT)
|
||||
|
||||
/**
|
||||
* @param position the position of the item you want
|
||||
* @return the [Fragment] associated with a specified position
|
||||
*/
|
||||
abstract fun getItem(position: Int): Fragment
|
||||
override fun startUpdate(container: ViewGroup) {
|
||||
check(container.id != View.NO_ID) {
|
||||
("ViewPager with adapter " + this
|
||||
+ " requires a view id")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("deprecation")
|
||||
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
||||
// 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) {
|
||||
val f = mFragments[position]
|
||||
if (f != null) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
if (mCurTransaction == null) {
|
||||
mCurTransaction = mFragmentManager.beginTransaction()
|
||||
}
|
||||
val fragment = getItem(position)
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Adding item #$position: f=$fragment")
|
||||
}
|
||||
if (mSavedState.size > position) {
|
||||
val fss = mSavedState[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[position] = fragment
|
||||
mCurTransaction!!.add(container.id, fragment)
|
||||
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
mCurTransaction!!.setMaxLifecycle(fragment, Lifecycle.State.STARTED)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
override fun destroyItem(container: ViewGroup, position: Int,
|
||||
`object`: Any) {
|
||||
val fragment = `object` as Fragment
|
||||
if (mCurTransaction == null) {
|
||||
mCurTransaction = mFragmentManager.beginTransaction()
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Removing item #" + position + ": f=" + `object`
|
||||
+ " v=" + `object`.view)
|
||||
}
|
||||
while (mSavedState.size <= position) {
|
||||
mSavedState.add(null)
|
||||
}
|
||||
mSavedState[position] = if (fragment.isAdded) mFragmentManager.saveFragmentInstanceState(fragment) else null
|
||||
mFragments.set(position, null)
|
||||
mCurTransaction!!.remove(fragment)
|
||||
if (fragment == mCurrentPrimaryItem) {
|
||||
mCurrentPrimaryItem = null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("deprecation")
|
||||
override fun setPrimaryItem(container: ViewGroup, position: Int,
|
||||
`object`: Any) {
|
||||
val fragment = `object` as Fragment
|
||||
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 fun finishUpdate(container: ViewGroup) {
|
||||
if (mCurTransaction != null) {
|
||||
// We drop any transactions that attempt to be committed
|
||||
// from a re-entrant call to finishUpdate(). We need to
|
||||
// do this as a workaround for Robolectric running measure/layout
|
||||
// calls inline rather than allowing them to be posted
|
||||
// as they would on a real device.
|
||||
if (!mExecutingFinishUpdate) {
|
||||
try {
|
||||
mExecutingFinishUpdate = true
|
||||
mCurTransaction!!.commitNowAllowingStateLoss()
|
||||
} finally {
|
||||
mExecutingFinishUpdate = false
|
||||
}
|
||||
}
|
||||
mCurTransaction = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun isViewFromObject(view: View, `object`: Any): Boolean {
|
||||
return (`object` as Fragment).view === view
|
||||
}
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
private val selectedFragment = "selected_fragment"
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
override fun saveState(): Parcelable? {
|
||||
var state: Bundle? = null
|
||||
if (!mSavedState.isEmpty()) {
|
||||
state = Bundle()
|
||||
state.putParcelableArrayList("states", mSavedState)
|
||||
}
|
||||
for (i in mFragments.indices) {
|
||||
val f = mFragments[i]
|
||||
if (f != null && f.isAdded) {
|
||||
if (state == null) {
|
||||
state = Bundle()
|
||||
}
|
||||
val 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 fun restoreState(state: Parcelable?, loader: ClassLoader?) {
|
||||
if (state != null) {
|
||||
val bundle = state as Bundle
|
||||
bundle.classLoader = loader
|
||||
val states = BundleCompat.getParcelableArrayList(bundle, "states",
|
||||
Fragment.SavedState::class.java)
|
||||
mSavedState.clear()
|
||||
mFragments.clear()
|
||||
if (states != null) {
|
||||
mSavedState.addAll(states)
|
||||
}
|
||||
val keys: Iterable<String> = bundle.keySet()
|
||||
for (key in keys) {
|
||||
if (key.startsWith("f")) {
|
||||
val index = key.substring(1).toInt()
|
||||
val f = mFragmentManager.getFragment(bundle, key)
|
||||
if (f != null) {
|
||||
while (mFragments.size <= index) {
|
||||
mFragments.add(null)
|
||||
}
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
val wasSelected = (bundle.getString(selectedFragment, "")
|
||||
== key)
|
||||
f.setMenuVisibility(wasSelected)
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
mFragments[index] = f
|
||||
} else {
|
||||
Log.w(TAG, "Bad fragment at key $key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FragmentStatePagerAdapt"
|
||||
private const val DEBUG = false
|
||||
|
||||
/**
|
||||
* Indicates that [Fragment.setUserVisibleHint] will be called when the current
|
||||
* fragment changes.
|
||||
*
|
||||
* @see .FragmentStatePagerAdapterMenuWorkaround
|
||||
*/
|
||||
@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}.
|
||||
""")
|
||||
val BEHAVIOR_SET_USER_VISIBLE_HINT = 0
|
||||
|
||||
/**
|
||||
* Indicates that only the current fragment will be in the [Lifecycle.State.RESUMED]
|
||||
* state. All other Fragments are capped at [Lifecycle.State.STARTED].
|
||||
*
|
||||
* @see .FragmentStatePagerAdapterMenuWorkaround
|
||||
*/
|
||||
const val BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1
|
||||
}
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
package com.google.android.material.appbar;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.OverScroller;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
// See https://stackoverflow.com/questions/56849221#57997489
|
||||
public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
private final Rect focusScrollRect = new Rect();
|
||||
|
||||
public FlingBehavior(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private boolean allowScroll = true;
|
||||
private final Rect globalRect = new Rect();
|
||||
private final List<Integer> skipInterceptionOfElements = List.of(
|
||||
R.id.itemsListPanel, R.id.playbackSeekBar,
|
||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||
|
||||
@Override
|
||||
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);
|
||||
|
||||
final 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;
|
||||
}
|
||||
|
||||
final 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;
|
||||
}
|
||||
|
||||
final int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
|
||||
|
||||
return consumed == dy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull final MotionEvent ev) {
|
||||
for (final int element : skipInterceptionOfElements) {
|
||||
final View view = child.findViewById(element);
|
||||
if (view != null) {
|
||||
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
||||
if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
|
||||
allowScroll = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
allowScroll = true;
|
||||
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(@NonNull final CoordinatorLayout parent,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull 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(@NonNull final CoordinatorLayout coordinatorLayout,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull 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 {
|
||||
final Class<?> headerBehaviorType = this.getClass()
|
||||
.getSuperclass().getSuperclass().getSuperclass();
|
||||
if (headerBehaviorType != null) {
|
||||
final Field field = headerBehaviorType.getDeclaredField("scroller");
|
||||
field.setAccessible(true);
|
||||
return ((OverScroller) field.get(this));
|
||||
}
|
||||
} catch (final NoSuchFieldException | IllegalAccessException e) {
|
||||
// ?
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Field getLastNestedScrollingChildRefField() {
|
||||
try {
|
||||
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||
if (headerBehaviorType != null) {
|
||||
final Field field =
|
||||
headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
} catch (final NoSuchFieldException e) {
|
||||
// ?
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void resetNestedScrollingChild() {
|
||||
final Field field = getLastNestedScrollingChildRefField();
|
||||
if (field != null) {
|
||||
try {
|
||||
final Object value = field.get(this);
|
||||
if (value != null) {
|
||||
field.set(this, null);
|
||||
}
|
||||
} catch (final IllegalAccessException e) {
|
||||
// ?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAppBarLayoutFling() {
|
||||
final OverScroller scroller = getScrollerField();
|
||||
if (scroller != null) {
|
||||
scroller.forceFinished(true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package com.google.android.material.appbar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.OverScroller
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import org.schabi.newpipe.R
|
||||
import java.lang.reflect.Field
|
||||
import java.util.List
|
||||
|
||||
// See https://stackoverflow.com/questions/56849221#57997489
|
||||
class FlingBehavior(context: Context?, attrs: AttributeSet?) : AppBarLayout.Behavior(context, attrs) {
|
||||
private val focusScrollRect = Rect()
|
||||
private var allowScroll = true
|
||||
private val globalRect = Rect()
|
||||
private val skipInterceptionOfElements = List.of(
|
||||
R.id.itemsListPanel, R.id.playbackSeekBar,
|
||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton)
|
||||
|
||||
override fun onRequestChildRectangleOnScreen(
|
||||
coordinatorLayout: CoordinatorLayout, child: AppBarLayout,
|
||||
rectangle: Rect, immediate: Boolean): Boolean {
|
||||
focusScrollRect.set(rectangle)
|
||||
coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect)
|
||||
val height = coordinatorLayout.height
|
||||
if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
|
||||
// the child is too big to fit inside ourselves completely, ignore request
|
||||
return false
|
||||
}
|
||||
val dy: Int
|
||||
dy = if (focusScrollRect.bottom > height) {
|
||||
focusScrollRect.top
|
||||
} else if (focusScrollRect.top < 0) {
|
||||
// scrolling up
|
||||
-(height - focusScrollRect.bottom)
|
||||
} else {
|
||||
// nothing to do
|
||||
return false
|
||||
}
|
||||
val consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0)
|
||||
return consumed == dy
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(parent: CoordinatorLayout,
|
||||
child: AppBarLayout,
|
||||
ev: MotionEvent): Boolean {
|
||||
for (element in skipInterceptionOfElements) {
|
||||
val view = child.findViewById<View>(element)
|
||||
if (view != null) {
|
||||
val visible = view.getGlobalVisibleRect(globalRect)
|
||||
if (visible && globalRect.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
|
||||
allowScroll = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
allowScroll = true
|
||||
when (ev.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// remove reference to old nested scrolling child
|
||||
resetNestedScrollingChild()
|
||||
// Stop fling when your finger touches the screen
|
||||
stopAppBarLayoutFling()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
return super.onInterceptTouchEvent(parent, child, ev)
|
||||
}
|
||||
|
||||
override fun onStartNestedScroll(parent: CoordinatorLayout,
|
||||
child: AppBarLayout,
|
||||
directTargetChild: View,
|
||||
target: View,
|
||||
nestedScrollAxes: Int,
|
||||
type: Int): Boolean {
|
||||
return allowScroll && super.onStartNestedScroll(
|
||||
parent, child, directTargetChild, target, nestedScrollAxes, type)
|
||||
}
|
||||
|
||||
override fun onNestedFling(coordinatorLayout: CoordinatorLayout,
|
||||
child: AppBarLayout,
|
||||
target: View, velocityX: Float,
|
||||
velocityY: Float, consumed: Boolean): Boolean {
|
||||
return allowScroll && super.onNestedFling(
|
||||
coordinatorLayout, child, target, velocityX, velocityY, consumed)
|
||||
}
|
||||
|
||||
private val scrollerField: OverScroller?
|
||||
private get() {
|
||||
try {
|
||||
val headerBehaviorType: Class<*>? = this.javaClass
|
||||
.superclass.superclass.superclass
|
||||
if (headerBehaviorType != null) {
|
||||
val field = headerBehaviorType.getDeclaredField("scroller")
|
||||
field.isAccessible = true
|
||||
return field[this] as OverScroller
|
||||
}
|
||||
} catch (e: NoSuchFieldException) {
|
||||
// ?
|
||||
} catch (e: IllegalAccessException) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
private val lastNestedScrollingChildRefField: Field?
|
||||
private get() {
|
||||
try {
|
||||
val headerBehaviorType: Class<*>? = this.javaClass.superclass.superclass
|
||||
if (headerBehaviorType != null) {
|
||||
val field = headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef")
|
||||
field.isAccessible = true
|
||||
return field
|
||||
}
|
||||
} catch (e: NoSuchFieldException) {
|
||||
// ?
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun resetNestedScrollingChild() {
|
||||
val field = lastNestedScrollingChildRefField
|
||||
if (field != null) {
|
||||
try {
|
||||
val value = field[this]
|
||||
if (value != null) {
|
||||
field[this] = null
|
||||
}
|
||||
} catch (e: IllegalAccessException) {
|
||||
// ?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopAppBarLayoutFling() {
|
||||
val scroller = scrollerField
|
||||
scroller?.forceFinished(true)
|
||||
}
|
||||
}
|
|
@ -14,51 +14,55 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.commons.text.similarity;
|
||||
package org.apache.commons.text.similarity
|
||||
|
||||
import java.util.Locale;
|
||||
import org.apache.commons.text.similarity.FuzzyScore
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* A matching algorithm that is similar to the searching algorithms implemented in editors such
|
||||
* as Sublime Text, TextMate, Atom and others.
|
||||
*
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* One point is given for every matched character. Subsequent matches yield two bonus points.
|
||||
* A higher score indicates a higher similarity.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
*
|
||||
* This code has been adapted from Apache Commons Lang 3.3.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @since 1.0
|
||||
*
|
||||
* Note: This class was forked from
|
||||
* <a href="https://git.io/JyYJg">
|
||||
* apache/commons-text (8cfdafc) FuzzyScore.java
|
||||
* </a>
|
||||
* [
|
||||
* apache/commons-text (8cfdafc) FuzzyScore.java
|
||||
](https://git.io/JyYJg) *
|
||||
*/
|
||||
public class FuzzyScore {
|
||||
|
||||
class FuzzyScore(locale: Locale?) {
|
||||
/**
|
||||
* Gets the locale.
|
||||
*
|
||||
* @return The locale
|
||||
*/
|
||||
/**
|
||||
* Locale used to change the case of text.
|
||||
*/
|
||||
private final Locale locale;
|
||||
|
||||
val locale: Locale
|
||||
|
||||
/**
|
||||
* This returns a {@link Locale}-specific {@link FuzzyScore}.
|
||||
* This returns a [Locale]-specific [FuzzyScore].
|
||||
*
|
||||
* @param locale The string matching logic is case insensitive.
|
||||
A {@link Locale} is necessary to normalize both Strings to lower case.
|
||||
* A [Locale] is necessary to normalize both Strings to lower case.
|
||||
* @throws IllegalArgumentException
|
||||
* This is thrown if the {@link Locale} parameter is {@code null}.
|
||||
* This is thrown if the [Locale] parameter is `null`.
|
||||
*/
|
||||
public FuzzyScore(final Locale locale) {
|
||||
if (locale == null) {
|
||||
throw new IllegalArgumentException("Locale must not be null");
|
||||
}
|
||||
this.locale = locale;
|
||||
init {
|
||||
requireNotNull(locale) { "Locale must not be null" }
|
||||
this.locale = locale
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,73 +80,57 @@ public class FuzzyScore {
|
|||
* score.fuzzyScore("Workshop", "ws") = 2
|
||||
* score.fuzzyScore("Workshop", "wo") = 4
|
||||
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
|
||||
* </pre>
|
||||
</pre> *
|
||||
*
|
||||
* @param term a full term that should be matched against, must not be null
|
||||
* @param query the query that will be matched against a term, must not be
|
||||
* null
|
||||
* null
|
||||
* @return result score
|
||||
* @throws IllegalArgumentException if the term or query is {@code null}
|
||||
* @throws IllegalArgumentException if the term or query is `null`
|
||||
*/
|
||||
public Integer fuzzyScore(final CharSequence term, final CharSequence query) {
|
||||
if (term == null || query == null) {
|
||||
throw new IllegalArgumentException("CharSequences must not be null");
|
||||
}
|
||||
fun fuzzyScore(term: CharSequence?, query: CharSequence?): Int {
|
||||
require(!(term == null || query == null)) { "CharSequences must not be null" }
|
||||
|
||||
// fuzzy logic is case insensitive. We normalize the Strings to lower
|
||||
// case right from the start. Turning characters to lower case
|
||||
// via Character.toLowerCase(char) is unfortunately insufficient
|
||||
// as it does not accept a locale.
|
||||
final String termLowerCase = term.toString().toLowerCase(locale);
|
||||
final String queryLowerCase = query.toString().toLowerCase(locale);
|
||||
val termLowerCase = term.toString().lowercase(locale)
|
||||
val queryLowerCase = query.toString().lowercase(locale)
|
||||
|
||||
// the resulting score
|
||||
int score = 0;
|
||||
var score = 0
|
||||
|
||||
// the position in the term which will be scanned next for potential
|
||||
// query character matches
|
||||
int termIndex = 0;
|
||||
var termIndex = 0
|
||||
|
||||
// index of the previously matched character in the term
|
||||
int previousMatchingCharacterIndex = Integer.MIN_VALUE;
|
||||
|
||||
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
|
||||
final char queryChar = queryLowerCase.charAt(queryIndex);
|
||||
|
||||
boolean termCharacterMatchFound = false;
|
||||
for (; termIndex < termLowerCase.length()
|
||||
&& !termCharacterMatchFound; termIndex++) {
|
||||
final char termChar = termLowerCase.charAt(termIndex);
|
||||
|
||||
var previousMatchingCharacterIndex = Int.MIN_VALUE
|
||||
for (queryIndex in 0 until queryLowerCase.length) {
|
||||
val queryChar = queryLowerCase[queryIndex]
|
||||
var termCharacterMatchFound = false
|
||||
while (termIndex < termLowerCase.length
|
||||
&& !termCharacterMatchFound) {
|
||||
val termChar = termLowerCase[termIndex]
|
||||
if (queryChar == termChar) {
|
||||
// simple character matches result in one point
|
||||
score++;
|
||||
score++
|
||||
|
||||
// subsequent character matches further improve
|
||||
// the score.
|
||||
if (previousMatchingCharacterIndex + 1 == termIndex) {
|
||||
score += 2;
|
||||
score += 2
|
||||
}
|
||||
|
||||
previousMatchingCharacterIndex = termIndex;
|
||||
previousMatchingCharacterIndex = termIndex
|
||||
|
||||
// we can leave the nested loop. Every character in the
|
||||
// query can match at most one character in the term.
|
||||
termCharacterMatchFound = true;
|
||||
termCharacterMatchFound = true
|
||||
}
|
||||
termIndex++
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
return score
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the locale.
|
||||
*
|
||||
* @return The locale
|
||||
*/
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,269 +0,0 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.CoreConfigurationBuilder;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* App.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class App extends Application {
|
||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||
private static final String TAG = App.class.toString();
|
||||
|
||||
private boolean isFirstRun = false;
|
||||
private static App app;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(base);
|
||||
initACRA();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
app = this;
|
||||
|
||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||
Log.i(TAG, "This is a phoenix process! "
|
||||
+ "Aborting initialization of App[onCreate]");
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the last used preference version is set
|
||||
// to determine whether this is the first app run
|
||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getInt(getString(R.string.last_used_preferences_version), -1);
|
||||
isFirstRun = lastUsedPrefVersion == -1;
|
||||
|
||||
// Initialize settings first because other initializations can use its values
|
||||
NewPipeSettings.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this));
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannels();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
// Initialize image loader
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
PicassoHelper.init(this);
|
||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
||||
prefs.getString(getString(R.string.image_quality_key),
|
||||
getString(R.string.image_quality_default))));
|
||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
PicassoHelper.terminate();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
final 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, null));
|
||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
||||
}
|
||||
|
||||
private void configureRxJavaErrorHandler() {
|
||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||
@Override
|
||||
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
|
||||
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
||||
} else {
|
||||
actualThrowable = throwable;
|
||||
}
|
||||
|
||||
final List<Throwable> errors;
|
||||
if (actualThrowable instanceof CompositeException) {
|
||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
||||
} else {
|
||||
errors = List.of(actualThrowable);
|
||||
}
|
||||
|
||||
for (final Throwable error : errors) {
|
||||
if (isThrowableIgnored(error)) {
|
||||
return;
|
||||
}
|
||||
if (isThrowableCritical(error)) {
|
||||
reportException(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||
// When exception is not reported, log it
|
||||
if (isDisposedRxExceptionsReported()) {
|
||||
reportException(actualThrowable);
|
||||
} else {
|
||||
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 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 ExceptionUtils.hasAssignableCause(throwable,
|
||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
||||
IllegalStateException.class); // bug in operator
|
||||
}
|
||||
|
||||
private void reportException(@NonNull final Throwable throwable) {
|
||||
// Throw uncaught exception that will trigger the report system
|
||||
Thread.currentThread().getUncaughtExceptionHandler()
|
||||
.uncaughtException(Thread.currentThread(), throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
*/
|
||||
protected void initACRA() {
|
||||
if (ACRA.isACRASenderServiceProcess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
||||
.withBuildConfigClass(BuildConfig.class);
|
||||
ACRA.init(this, acraConfig);
|
||||
}
|
||||
|
||||
private void initNotificationChannels() {
|
||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||
// the main and update channels
|
||||
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
||||
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat
|
||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.app_update_notification_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat
|
||||
.Builder(getString(R.string.streams_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(getString(R.string.streams_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.streams_notification_channel_description))
|
||||
.build()
|
||||
);
|
||||
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isFirstRun() {
|
||||
return isFirstRun;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
package org.schabi.newpipe
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException
|
||||
import io.reactivex.rxjava3.functions.Consumer
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import org.acra.ACRA.init
|
||||
import org.acra.ACRA.isACRASenderServiceProcess
|
||||
import org.acra.config.CoreConfigurationBuilder
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||
import org.schabi.newpipe.ktx.hasAssignableCause
|
||||
import org.schabi.newpipe.settings.NewPipeSettings
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.StateSaver
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import org.schabi.newpipe.util.image.PicassoHelper
|
||||
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||
import java.io.IOException
|
||||
import java.io.InterruptedIOException
|
||||
import java.net.SocketException
|
||||
import java.util.Objects
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* App.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
open class App() : Application() {
|
||||
private var isFirstRun: Boolean = false
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
initACRA()
|
||||
}
|
||||
|
||||
public override fun onCreate() {
|
||||
super.onCreate()
|
||||
app = this
|
||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||
Log.i(TAG, ("This is a phoenix process! "
|
||||
+ "Aborting initialization of App[onCreate]"))
|
||||
return
|
||||
}
|
||||
|
||||
// check if the last used preference version is set
|
||||
// to determine whether this is the first app run
|
||||
val lastUsedPrefVersion: Int = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getInt(getString(R.string.last_used_preferences_version), -1)
|
||||
isFirstRun = lastUsedPrefVersion == -1
|
||||
|
||||
// Initialize settings first because other initializations can use its values
|
||||
NewPipeSettings.initSettings(this)
|
||||
NewPipe.init(getDownloader(),
|
||||
Localization.getPreferredLocalization(this),
|
||||
Localization.getPreferredContentCountry(this))
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()))
|
||||
StateSaver.init(this)
|
||||
initNotificationChannels()
|
||||
ServiceHelper.initServices(this)
|
||||
|
||||
// Initialize image loader
|
||||
val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
PicassoHelper.init(this)
|
||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.Companion.fromPreferenceKey(this,
|
||||
prefs.getString(getString(R.string.image_quality_key),
|
||||
getString(R.string.image_quality_default))))
|
||||
PicassoHelper.setIndicatorsEnabled((MainActivity.Companion.DEBUG
|
||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false)))
|
||||
configureRxJavaErrorHandler()
|
||||
}
|
||||
|
||||
public override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
PicassoHelper.terminate()
|
||||
}
|
||||
|
||||
protected open fun getDownloader(): Downloader? {
|
||||
val downloader: DownloaderImpl? = DownloaderImpl.Companion.init(null)
|
||||
setCookiesToDownloader(downloader)
|
||||
return downloader
|
||||
}
|
||||
|
||||
protected fun setCookiesToDownloader(downloader: DownloaderImpl?) {
|
||||
val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(
|
||||
getApplicationContext())
|
||||
val key: String = getApplicationContext().getString(R.string.recaptcha_cookies_key)
|
||||
downloader!!.setCookie(ReCaptchaActivity.Companion.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
|
||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext())
|
||||
}
|
||||
|
||||
private fun configureRxJavaErrorHandler() {
|
||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||
RxJavaPlugins.setErrorHandler(object : Consumer<Throwable> {
|
||||
public override fun accept(throwable: Throwable) {
|
||||
Log.e(TAG, ("RxJavaPlugins.ErrorHandler called with -> : "
|
||||
+ "throwable = [" + throwable.javaClass.getName() + "]"))
|
||||
val actualThrowable: Throwable
|
||||
if (throwable is UndeliverableException) {
|
||||
// As UndeliverableException is a wrapper,
|
||||
// get the cause of it to get the "real" exception
|
||||
actualThrowable = Objects.requireNonNull(throwable.cause)
|
||||
} else {
|
||||
actualThrowable = throwable
|
||||
}
|
||||
val errors: List<Throwable>
|
||||
if (actualThrowable is CompositeException) {
|
||||
errors = actualThrowable.getExceptions()
|
||||
} else {
|
||||
errors = java.util.List.of(actualThrowable)
|
||||
}
|
||||
for (error: Throwable in errors) {
|
||||
if (isThrowableIgnored(error)) {
|
||||
return
|
||||
}
|
||||
if (isThrowableCritical(error)) {
|
||||
reportException(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||
// When exception is not reported, log it
|
||||
if (isDisposedRxExceptionsReported()) {
|
||||
reportException(actualThrowable)
|
||||
} else {
|
||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isThrowableIgnored(throwable: Throwable): Boolean {
|
||||
// Don't crash the application over a simple network problem
|
||||
return throwable.hasAssignableCause( // network api cancellation
|
||||
IOException::class.java, SocketException::class.java, // blocking code disposed
|
||||
InterruptedException::class.java, InterruptedIOException::class.java)
|
||||
}
|
||||
|
||||
private fun isThrowableCritical(throwable: Throwable): Boolean {
|
||||
// Though these exceptions cannot be ignored
|
||||
return throwable.hasAssignableCause(NullPointerException::class.java, IllegalArgumentException::class.java, // bug in app
|
||||
OnErrorNotImplementedException::class.java, MissingBackpressureException::class.java, IllegalStateException::class.java) // bug in operator
|
||||
}
|
||||
|
||||
private fun reportException(throwable: Throwable) {
|
||||
// Throw uncaught exception that will trigger the report system
|
||||
Thread.currentThread().getUncaughtExceptionHandler()
|
||||
.uncaughtException(Thread.currentThread(), throwable)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in [.attachBaseContext] after calling the `super` method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
*/
|
||||
protected fun initACRA() {
|
||||
if (isACRASenderServiceProcess()) {
|
||||
return
|
||||
}
|
||||
val acraConfig: CoreConfigurationBuilder = CoreConfigurationBuilder()
|
||||
.withBuildConfigClass(BuildConfig::class.java)
|
||||
init(this, acraConfig)
|
||||
}
|
||||
|
||||
private fun initNotificationChannels() {
|
||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||
// the main and update channels
|
||||
val notificationChannelCompats: List<NotificationChannelCompat> = java.util.List.of(
|
||||
NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build(),
|
||||
NotificationChannelCompat.Builder(getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.app_update_notification_channel_description))
|
||||
.build(),
|
||||
NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build(),
|
||||
NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build(),
|
||||
NotificationChannelCompat.Builder(getString(R.string.streams_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(getString(R.string.streams_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.streams_notification_channel_description))
|
||||
.build()
|
||||
)
|
||||
val notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(this)
|
||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats)
|
||||
}
|
||||
|
||||
protected open fun isDisposedRxExceptionsReported(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
fun isFirstRun(): Boolean {
|
||||
return isFirstRun
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
|
||||
private val TAG: String = App::class.java.toString()
|
||||
private var app: App? = null
|
||||
fun getApp(): App {
|
||||
return (app)!!
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
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 icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||
protected AppCompatActivity activity;
|
||||
//These values are used for controlling fragments when they are part of the frontpage
|
||||
@State
|
||||
protected boolean useAsFrontPage = false;
|
||||
|
||||
public void useAsFrontPage(final boolean value) {
|
||||
useAsFrontPage = value;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull final Context context) {
|
||||
super.onAttach(context);
|
||||
activity = (AppCompatActivity) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
activity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onViewCreated() called with: "
|
||||
+ "rootView = [" + rootView + "], "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
initViews(rootView, savedInstanceState);
|
||||
initListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* This method is called in {@link #onViewCreated(View, Bundle)} to initialize the views.
|
||||
*
|
||||
* <p>
|
||||
* {@link #initListeners()} is called after this method to initialize the corresponding
|
||||
* listeners.
|
||||
* </p>
|
||||
* @param rootView The inflated view for this fragment
|
||||
* (provided by {@link #onViewCreated(View, Bundle)})
|
||||
* @param savedInstanceState The saved state of this fragment
|
||||
* (provided by {@link #onViewCreated(View, Bundle)})
|
||||
*/
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the listeners for this fragment.
|
||||
*
|
||||
* <p>
|
||||
* This method is called after {@link #initViews(View, Bundle)}
|
||||
* in {@link #onViewCreated(View, Bundle)}.
|
||||
* </p>
|
||||
*/
|
||||
protected void initListeners() {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void setTitle(final String title) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
}
|
||||
if (!useAsFrontPage && activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the root fragment by looping through all of the parent fragments. The root fragment
|
||||
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
|
||||
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
|
||||
* sheet. This function therefore returns the fragment manager of said fragment.
|
||||
*
|
||||
* @return the fragment manager of the root fragment, i.e.
|
||||
* {@link org.schabi.newpipe.fragments.MainFragment}
|
||||
*/
|
||||
protected FragmentManager getFM() {
|
||||
Fragment current = this;
|
||||
while (current.getParentFragment() != null) {
|
||||
current = current.getParentFragment();
|
||||
}
|
||||
return current.getFragmentManager();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import icepick.Icepick
|
||||
import icepick.State
|
||||
|
||||
abstract class BaseFragment() : Fragment() {
|
||||
protected val TAG: String = javaClass.getSimpleName() + "@" + Integer.toHexString(hashCode())
|
||||
protected var activity: AppCompatActivity? = null
|
||||
|
||||
//These values are used for controlling fragments when they are part of the frontpage
|
||||
@State
|
||||
protected var useAsFrontPage: Boolean = false
|
||||
fun useAsFrontPage(value: Boolean) {
|
||||
useAsFrontPage = value
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's Lifecycle
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
public override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
activity = context as AppCompatActivity?
|
||||
}
|
||||
|
||||
public override fun onDetach() {
|
||||
super.onDetach()
|
||||
activity = null
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, ("onCreate() called with: "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]"))
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
Icepick.restoreInstanceState(this, savedInstanceState)
|
||||
if (savedInstanceState != null) {
|
||||
onRestoreInstanceState(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, ("onViewCreated() called with: "
|
||||
+ "rootView = [" + rootView + "], "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]"))
|
||||
}
|
||||
initViews(rootView, savedInstanceState)
|
||||
initListeners()
|
||||
}
|
||||
|
||||
public override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
protected open fun onRestoreInstanceState(savedInstanceState: Bundle) {}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
/**
|
||||
* This method is called in [.onViewCreated] to initialize the views.
|
||||
*
|
||||
*
|
||||
*
|
||||
* [.initListeners] is called after this method to initialize the corresponding
|
||||
* listeners.
|
||||
*
|
||||
* @param rootView The inflated view for this fragment
|
||||
* (provided by [.onViewCreated])
|
||||
* @param savedInstanceState The saved state of this fragment
|
||||
* (provided by [.onViewCreated])
|
||||
*/
|
||||
protected open fun initViews(rootView: View, savedInstanceState: Bundle?) {}
|
||||
|
||||
/**
|
||||
* Initialize the listeners for this fragment.
|
||||
*
|
||||
*
|
||||
*
|
||||
* This method is called after [.initViews]
|
||||
* in [.onViewCreated].
|
||||
*
|
||||
*/
|
||||
protected open fun initListeners() {}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
open fun setTitle(title: String?) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setTitle() called with: title = [" + title + "]")
|
||||
}
|
||||
if (!useAsFrontPage && (activity != null) && (activity!!.getSupportActionBar() != null)) {
|
||||
activity!!.getSupportActionBar()!!.setDisplayShowTitleEnabled(true)
|
||||
activity!!.getSupportActionBar()!!.setTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
protected val fM: FragmentManager?
|
||||
/**
|
||||
* Finds the root fragment by looping through all of the parent fragments. The root fragment
|
||||
* is supposed to be [org.schabi.newpipe.fragments.MainFragment], and is the fragment that
|
||||
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
|
||||
* sheet. This function therefore returns the fragment manager of said fragment.
|
||||
*
|
||||
* @return the fragment manager of the root fragment, i.e.
|
||||
* [org.schabi.newpipe.fragments.MainFragment]
|
||||
*/
|
||||
protected get() {
|
||||
var current: Fragment? = this
|
||||
while (current!!.getParentFragment() != null) {
|
||||
current = current.getParentFragment()
|
||||
}
|
||||
return current.getFragmentManager()
|
||||
}
|
||||
|
||||
companion object {
|
||||
protected val DEBUG: Boolean = MainActivity.Companion.DEBUG
|
||||
}
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
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.InfoCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public final class DownloaderImpl extends Downloader {
|
||||
public static final String USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.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 final Map<String, String> mCookies;
|
||||
private final OkHttpClient client;
|
||||
|
||||
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||
this.client = builder
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
// .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 final OkHttpClient.Builder builder) {
|
||||
instance = new DownloaderImpl(
|
||||
builder != null ? builder : new OkHttpClient.Builder());
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static DownloaderImpl getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getCookies(final String url) {
|
||||
final String youtubeCookie = url.contains(YOUTUBE_DOMAIN)
|
||||
? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null;
|
||||
|
||||
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||
return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY))
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(cookies -> Arrays.stream(cookies.split("; *")))
|
||||
.distinct()
|
||||
.collect(Collectors.joining("; "));
|
||||
}
|
||||
|
||||
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) {
|
||||
final String restrictedModeEnabledKey =
|
||||
context.getString(R.string.youtube_restricted_mode_enabled);
|
||||
final 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||
*
|
||||
* @param url an url pointing to the content
|
||||
* @return the size of the content, in bytes
|
||||
*/
|
||||
public long getContentLength(final String url) throws IOException {
|
||||
try {
|
||||
final Response response = head(url);
|
||||
return Long.parseLong(response.getHeader("Content-Length"));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} catch (final ReCaptchaException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(@NonNull final Request request)
|
||||
throws IOException, ReCaptchaException {
|
||||
final String httpMethod = request.httpMethod();
|
||||
final String url = request.url();
|
||||
final Map<String, List<String>> headers = request.headers();
|
||||
final byte[] dataToSend = request.dataToSend();
|
||||
|
||||
RequestBody requestBody = null;
|
||||
if (dataToSend != null) {
|
||||
requestBody = RequestBody.create(dataToSend);
|
||||
}
|
||||
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
final String cookies = getCookies(url);
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||
final String headerName = pair.getKey();
|
||||
final List<String> headerValueList = pair.getValue();
|
||||
|
||||
if (headerValueList.size() > 1) {
|
||||
requestBuilder.removeHeader(headerName);
|
||||
for (final String headerValue : headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue);
|
||||
}
|
||||
} else if (headerValueList.size() == 1) {
|
||||
requestBuilder.header(headerName, headerValueList.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
|
||||
|
||||
if (response.code() == 429) {
|
||||
response.close();
|
||||
|
||||
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||
}
|
||||
|
||||
final ResponseBody body = response.body();
|
||||
String responseBodyToReturn = null;
|
||||
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string();
|
||||
}
|
||||
|
||||
final String latestUrl = response.request().url().toString();
|
||||
return new Response(response.code(), response.message(), response.headers().toMultimap(),
|
||||
responseBodyToReturn, latestUrl);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.room.RoomDatabase.Builder.build
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.OkHttpClient.Builder.build
|
||||
import okhttp3.OkHttpClient.Builder.readTimeout
|
||||
import okhttp3.Request.Builder.addHeader
|
||||
import okhttp3.Request.Builder.build
|
||||
import okhttp3.Request.Builder.header
|
||||
import okhttp3.Request.Builder.method
|
||||
import okhttp3.Request.Builder.removeHeader
|
||||
import okhttp3.Request.Builder.url
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.ResponseBody
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.url
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||
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.InfoCache
|
||||
import java.io.IOException
|
||||
import java.util.Arrays
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Function
|
||||
import java.util.function.Predicate
|
||||
import java.util.stream.Collectors
|
||||
import java.util.stream.Stream
|
||||
|
||||
class DownloaderImpl private constructor(builder: Builder) : Downloader() {
|
||||
private val mCookies: MutableMap<String, String?>
|
||||
private val client: OkHttpClient
|
||||
|
||||
init {
|
||||
client = builder
|
||||
.readTimeout(30, TimeUnit.SECONDS) // .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
||||
// 16 * 1024 * 1024))
|
||||
.build()
|
||||
mCookies = HashMap()
|
||||
}
|
||||
|
||||
fun getCookies(url: String): String {
|
||||
val youtubeCookie: String? = if (url.contains(YOUTUBE_DOMAIN)) getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) else null
|
||||
|
||||
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||
return Stream.of<String?>(youtubeCookie, getCookie(ReCaptchaActivity.Companion.RECAPTCHA_COOKIES_KEY))
|
||||
.filter(Predicate<String?>({ obj: String? -> Objects.nonNull(obj) }))
|
||||
.flatMap<String?>(Function<String?, Stream<out String?>>({ cookies: String? -> Arrays.stream<String?>(cookies!!.split("; *".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()) }))
|
||||
.distinct()
|
||||
.collect(Collectors.joining("; "))
|
||||
}
|
||||
|
||||
fun getCookie(key: String): String? {
|
||||
return mCookies.get(key)
|
||||
}
|
||||
|
||||
fun setCookie(key: String, cookie: String?) {
|
||||
mCookies.put(key, cookie)
|
||||
}
|
||||
|
||||
fun removeCookie(key: String) {
|
||||
mCookies.remove(key)
|
||||
}
|
||||
|
||||
fun updateYoutubeRestrictedModeCookies(context: Context) {
|
||||
val restrictedModeEnabledKey: String = context.getString(R.string.youtube_restricted_mode_enabled)
|
||||
val restrictedModeEnabled: Boolean = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(restrictedModeEnabledKey, false)
|
||||
updateYoutubeRestrictedModeCookies(restrictedModeEnabled)
|
||||
}
|
||||
|
||||
fun updateYoutubeRestrictedModeCookies(youtubeRestrictedModeEnabled: Boolean) {
|
||||
if (youtubeRestrictedModeEnabled) {
|
||||
setCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY,
|
||||
YOUTUBE_RESTRICTED_MODE_COOKIE)
|
||||
} else {
|
||||
removeCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY)
|
||||
}
|
||||
InfoCache.Companion.getInstance().clearCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||
*
|
||||
* @param url an url pointing to the content
|
||||
* @return the size of the content, in bytes
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getContentLength(url: String?): Long {
|
||||
try {
|
||||
val response: Response = head(url)
|
||||
return response.getHeader("Content-Length")!!.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
throw IOException("Invalid content length", e)
|
||||
} catch (e: ReCaptchaException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ReCaptchaException::class)
|
||||
public override fun execute(request: Request): Response {
|
||||
val httpMethod: String = request.httpMethod()
|
||||
val url: String = request.url()
|
||||
val headers: Map<String, List<String>> = request.headers()
|
||||
val dataToSend: ByteArray? = request.dataToSend()
|
||||
var requestBody: RequestBody? = null
|
||||
if (dataToSend != null) {
|
||||
requestBody = RequestBody.create(dataToSend)
|
||||
}
|
||||
val requestBuilder: Builder = Builder()
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
val cookies: String = getCookies(url)
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies)
|
||||
}
|
||||
for (pair: Map.Entry<String, List<String>> in headers.entries) {
|
||||
val headerName: String = pair.key
|
||||
val headerValueList: List<String> = pair.value
|
||||
if (headerValueList.size > 1) {
|
||||
requestBuilder.removeHeader(headerName)
|
||||
for (headerValue: String? in headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue)
|
||||
}
|
||||
} else if (headerValueList.size == 1) {
|
||||
requestBuilder.header(headerName, headerValueList.get(0))
|
||||
}
|
||||
}
|
||||
val response: okhttp3.Response = client.newCall(requestBuilder.build()).execute()
|
||||
if (response.code() == 429) {
|
||||
response.close()
|
||||
throw ReCaptchaException("reCaptcha Challenge requested", url)
|
||||
}
|
||||
val body: ResponseBody? = response.body()
|
||||
var responseBodyToReturn: String? = null
|
||||
if (body != null) {
|
||||
responseBodyToReturn = body.string()
|
||||
}
|
||||
val latestUrl: String = response.request().url().toString()
|
||||
return Response(response.code(), response.message(), response.headers().toMultimap(),
|
||||
responseBodyToReturn, latestUrl)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val USER_AGENT: String = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
||||
val YOUTUBE_RESTRICTED_MODE_COOKIE_KEY: String = "youtube_restricted_mode_key"
|
||||
val YOUTUBE_RESTRICTED_MODE_COOKIE: String = "PREF=f2=8000000"
|
||||
val YOUTUBE_DOMAIN: String = "youtube.com"
|
||||
private var instance: DownloaderImpl? = null
|
||||
|
||||
/**
|
||||
* 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 [DownloaderImpl]
|
||||
*/
|
||||
fun init(builder: Builder?): DownloaderImpl? {
|
||||
instance = DownloaderImpl(
|
||||
if (builder != null) builder else Builder())
|
||||
return instance
|
||||
}
|
||||
|
||||
fun getInstance(): DownloaderImpl? {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* ExitActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ExitActivity extends Activity {
|
||||
|
||||
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
||||
final 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(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
finishAndRemoveTask();
|
||||
|
||||
NavigationHelper.restartApp(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package org.schabi.newpipe
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* ExitActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class ExitActivity() : Activity() {
|
||||
@SuppressLint("NewApi")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
finishAndRemoveTask()
|
||||
NavigationHelper.restartApp(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun exitAndRemoveFromRecentApps(activity: Activity) {
|
||||
val intent: Intent = Intent(activity, ExitActivity::class.java)
|
||||
intent.addFlags((Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||
or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
or Intent.FLAG_ACTIVITY_NO_ANIMATION))
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,927 +0,0 @@
|
|||
/*
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* DownloadActivity.java is part of NewPipe.
|
||||
* <p>
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentContainerView;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
|
||||
import org.schabi.newpipe.databinding.ActivityMainBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.DrawerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
|
||||
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.settings.UpdateSettingsFragment;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PeertubeHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
|
||||
private ActivityMainBinding mainBinding;
|
||||
private DrawerHeaderBinding drawerHeaderBinding;
|
||||
private DrawerLayoutBinding drawerLayoutBinding;
|
||||
private ToolbarLayoutBinding toolbarLayoutBinding;
|
||||
|
||||
private ActionBarDrawerToggle toggle;
|
||||
|
||||
private boolean servicesShown = false;
|
||||
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
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;
|
||||
|
||||
private static final int ORDER = 0;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreate() called with: "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
|
||||
ThemeHelper.setDayNightMode(this);
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
drawerLayoutBinding = mainBinding.drawerLayout;
|
||||
drawerHeaderBinding = DrawerHeaderBinding.bind(drawerLayoutBinding.navigation
|
||||
.getHeaderView(0));
|
||||
toolbarLayoutBinding = mainBinding.toolbarLayout;
|
||||
setContentView(mainBinding.getRoot());
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
setSupportActionBar(toolbarLayoutBinding.toolbar);
|
||||
try {
|
||||
setupDrawer();
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Setting up drawer", e);
|
||||
}
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
|
||||
if (PermissionHelper.checkPostNotificationsPermission(this,
|
||||
PermissionHelper.POST_NOTIFICATIONS_REQUEST_CODE)) {
|
||||
// Schedule worker for checking for new streams and creating corresponding notifications
|
||||
// if this is enabled by the user.
|
||||
NotificationWorker.initialize(this);
|
||||
}
|
||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||
&& !App.getApp().isFirstRun()
|
||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
final App app = App.getApp();
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
if (prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false)) {
|
||||
// Start the worker which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
NewVersionWorker.enqueueNewVersionCheckingWork(app, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrawer() throws ExtractionException {
|
||||
addDrawerMenuForCurrentService();
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
|
||||
toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
mainBinding.getRoot().addDrawerListener(toggle);
|
||||
mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(final View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(final View drawerView) {
|
||||
if (servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the drawer menu for the current service.
|
||||
*
|
||||
* @throws ExtractionException if the service didn't provide available kiosks
|
||||
*/
|
||||
private void addDrawerMenuForCurrentService() throws ExtractionException {
|
||||
//Tabs
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskMenuItemId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
|
||||
.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks));
|
||||
kioskMenuItemId++;
|
||||
}
|
||||
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
|
||||
R.string.tab_subscriptions)
|
||||
.setIcon(R.drawable.ic_tv);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(R.drawable.ic_subscriptions);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(R.drawable.ic_bookmark);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(R.drawable.ic_file_download);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(R.drawable.ic_history);
|
||||
|
||||
//Settings and About
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(R.drawable.ic_settings);
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline);
|
||||
}
|
||||
|
||||
private boolean drawerItemSelected(final MenuItem item) {
|
||||
switch (item.getGroupId()) {
|
||||
case R.id.menu_services_group:
|
||||
changeService(item);
|
||||
break;
|
||||
case R.id.menu_tabs_group:
|
||||
try {
|
||||
tabSelected(item);
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Selecting main page tab", e);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_options_about_group:
|
||||
optionsAboutSelected(item);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeService(final MenuItem item) {
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
private void tabSelected(final MenuItem item) throws ExtractionException {
|
||||
switch (item.getItemId()) {
|
||||
case ITEM_ID_SUBSCRIPTIONS:
|
||||
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
|
||||
break;
|
||||
case ITEM_ID_FEED:
|
||||
NavigationHelper.openFeedFragment(getSupportFragmentManager());
|
||||
break;
|
||||
case ITEM_ID_BOOKMARKS:
|
||||
NavigationHelper.openBookmarksFragment(getSupportFragmentManager());
|
||||
break;
|
||||
case ITEM_ID_DOWNLOADS:
|
||||
NavigationHelper.openDownloads(this);
|
||||
break;
|
||||
case ITEM_ID_HISTORY:
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
break;
|
||||
default:
|
||||
final StreamingService currentService = ServiceHelper.getSelectedService(this);
|
||||
int kioskMenuItemId = 0;
|
||||
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
|
||||
if (kioskMenuItemId == item.getItemId()) {
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
|
||||
currentService.getServiceId(), kioskId);
|
||||
break;
|
||||
}
|
||||
kioskMenuItemId++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void optionsAboutSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case ITEM_ID_SETTINGS:
|
||||
NavigationHelper.openSettings(this);
|
||||
break;
|
||||
case ITEM_ID_ABOUT:
|
||||
NavigationHelper.openAbout(this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
drawerHeaderBinding.drawerHeaderActionButton.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 ViewGroup.LayoutParams layoutParams =
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.getLayoutParams();
|
||||
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams);
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxLines(2);
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMinWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
|
||||
drawerHeaderBinding.drawerHeaderNewpipeTitle.setMaxWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleServices() {
|
||||
servicesShown = !servicesShown;
|
||||
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_services_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
// Show up or down arrow
|
||||
drawerHeaderBinding.drawerArrow.setImageResource(
|
||||
servicesShown ? R.drawable.ic_arrow_drop_up : R.drawable.ic_arrow_drop_down);
|
||||
|
||||
if (servicesShown) {
|
||||
showServices();
|
||||
} else {
|
||||
try {
|
||||
addDrawerMenuForCurrentService();
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Showing main page tabs", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName();
|
||||
|
||||
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
|
||||
// peertube specifics
|
||||
if (s.getServiceId() == 3) {
|
||||
enhancePeertubeMenu(menuItem);
|
||||
}
|
||||
}
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
private void enhancePeertubeMenu(final MenuItem menuItem) {
|
||||
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstance.getName());
|
||||
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
|
||||
.getRoot();
|
||||
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
final List<String> items = new ArrayList<>();
|
||||
int defaultSelect = 0;
|
||||
for (final PeertubeInstance instance : instances) {
|
||||
items.add(instance.getName());
|
||||
if (instance.getUrl().equals(currentInstance.getUrl())) {
|
||||
defaultSelect = items.size() - 1;
|
||||
}
|
||||
}
|
||||
final ArrayAdapter<String> 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(final AdapterView<?> parent, final View view,
|
||||
final int position, final long id) {
|
||||
final PeertubeInstance newInstance = instances.get(position);
|
||||
if (newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) {
|
||||
return;
|
||||
}
|
||||
PeertubeHelper.selectInstance(newInstance, getApplicationContext());
|
||||
changeService(menuItem);
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
getSupportFragmentManager().popBackStack(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
ActivityCompat.recreate(MainActivity.this);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(final AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
menuItem.setActionView(spinner);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (!isChangingConfigurations()) {
|
||||
StateSaver.clearStateFiles();
|
||||
}
|
||||
if (broadcastReceiver != null) {
|
||||
unregisterReceiver(broadcastReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
assureCorrectAppLanguage(this);
|
||||
// Change the date format to match the selected language on resume
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
|
||||
super.onResume();
|
||||
|
||||
// Close drawer on return, and don't show animation,
|
||||
// so it looks like the drawer isn't open when the user returns to MainActivity
|
||||
mainBinding.getRoot().closeDrawer(GravityCompat.START, false);
|
||||
try {
|
||||
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final String selectedServiceName = NewPipe.getService(selectedServiceId)
|
||||
.getServiceInfo().getName();
|
||||
drawerHeaderBinding.drawerHeaderServiceView.setText(selectedServiceName);
|
||||
drawerHeaderBinding.drawerHeaderServiceIcon.setImageResource(ServiceHelper
|
||||
.getIcon(selectedServiceId));
|
||||
|
||||
drawerHeaderBinding.drawerHeaderServiceView.post(() -> drawerHeaderBinding
|
||||
.drawerHeaderServiceView.setSelected(true));
|
||||
drawerHeaderBinding.drawerHeaderActionButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
|
||||
}
|
||||
|
||||
final SharedPreferences sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Theme has changed, recreating activity...");
|
||||
}
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
|
||||
ActivityCompat.recreate(this);
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
|
||||
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);
|
||||
drawerLayoutBinding.navigation.getMenu().findItem(ITEM_ID_HISTORY)
|
||||
.setVisible(isHistoryEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
final String action = intent.getAction();
|
||||
if ((action != null && action.equals(Intent.ACTION_MAIN))
|
||||
&& intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
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 (DeviceUtils.isTv(this)) {
|
||||
if (mainBinding.getRoot().isDrawerOpen(drawerLayoutBinding.navigation)) {
|
||||
mainBinding.getRoot().closeDrawers();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 FragmentManager fm = getSupportFragmentManager();
|
||||
final Fragment fragment = fm.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) {
|
||||
if (((BackPressable) fragment).onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
} else if (fragment instanceof CommentRepliesFragment) {
|
||||
// expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// to show the top level comments again
|
||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// and no other CommentRepliesFragments are on top of the back stack
|
||||
// to show the top level comments again.
|
||||
openDetailFragmentFromCommentReplies(fm, false);
|
||||
}
|
||||
|
||||
} else {
|
||||
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.from(mainBinding.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
finish();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
for (final int i : grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (requestCode) {
|
||||
case PermissionHelper.DOWNLOADS_REQUEST_CODE:
|
||||
NavigationHelper.openDownloads(this);
|
||||
break;
|
||||
case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (fragment instanceof VideoDetailFragment) {
|
||||
((VideoDetailFragment) fragment).openDownloadDialog();
|
||||
}
|
||||
break;
|
||||
case PermissionHelper.POST_NOTIFICATIONS_REQUEST_CODE:
|
||||
NotificationWorker.initialize(this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the following diagram behavior for the up button:
|
||||
* <pre>
|
||||
* +---------------+
|
||||
* | Main Screen +----+
|
||||
* +-------+-------+ |
|
||||
* | |
|
||||
* ▲ Up | Search Button
|
||||
* | |
|
||||
* +----+-----+ |
|
||||
* +------------+ Search |◄-----+
|
||||
* | +----+-----+
|
||||
* | Open |
|
||||
* | something ▲ Up
|
||||
* | |
|
||||
* | +------------+-------------+
|
||||
* | | |
|
||||
* | | Video <-> Channel |
|
||||
* +---►| Channel <-> Playlist |
|
||||
* | Video <-> .... |
|
||||
* | |
|
||||
* +--------------------------+
|
||||
* </pre>
|
||||
*/
|
||||
private void onHomeButtonPressed() {
|
||||
final FragmentManager fm = getSupportFragmentManager();
|
||||
final Fragment fragment = fm.findFragmentById(R.id.fragment_holder);
|
||||
|
||||
if (fragment instanceof CommentRepliesFragment) {
|
||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// and no other CommentRepliesFragments are on top of the back stack
|
||||
// to show the top level comments again.
|
||||
openDetailFragmentFromCommentReplies(fm, true);
|
||||
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||
// If search fragment wasn't found in the backstack go to the main fragment
|
||||
NavigationHelper.gotoMainFragment(fm);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
|
||||
}
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
final Fragment fragment =
|
||||
getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof SearchFragment)) {
|
||||
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
|
||||
updateDrawerNavigation();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
}
|
||||
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initFragments() {
|
||||
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 he will see a blank fragment on place of kiosk.
|
||||
// Let's open it first
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
NavigationHelper.openMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
handleIntent(getIntent());
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void updateDrawerNavigation() {
|
||||
if (getSupportActionBar() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof MainFragment) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
if (toggle != null) {
|
||||
toggle.syncState();
|
||||
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> mainBinding.getRoot()
|
||||
.open());
|
||||
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
|
||||
}
|
||||
} else {
|
||||
mainBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbarLayoutBinding.toolbar.setNavigationOnClickListener(v -> onHomeButtonPressed());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleIntent(final Intent intent) {
|
||||
try {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
}
|
||||
|
||||
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
final String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
String title = intent.getStringExtra(Constants.KEY_TITLE);
|
||||
if (title == null) {
|
||||
title = "";
|
||||
}
|
||||
|
||||
final StreamingService.LinkType linkType = ((StreamingService.LinkType) intent
|
||||
.getSerializableExtra(Constants.KEY_LINK_TYPE));
|
||||
assert linkType != null;
|
||||
switch (linkType) {
|
||||
case STREAM:
|
||||
final String intentCacheKey = intent.getStringExtra(
|
||||
Player.PLAY_QUEUE_KEY);
|
||||
final PlayQueue playQueue = intentCacheKey != null
|
||||
? SerializedCache.getInstance()
|
||||
.take(intentCacheKey, PlayQueue.class)
|
||||
: null;
|
||||
|
||||
final boolean switchingPlayers = intent.getBooleanExtra(
|
||||
VideoDetailFragment.KEY_SWITCHING_PLAYERS, false);
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
getApplicationContext(), getSupportFragmentManager(),
|
||||
serviceId, url, title, playQueue, switchingPlayers);
|
||||
break;
|
||||
case CHANNEL:
|
||||
NavigationHelper.openChannelFragment(getSupportFragmentManager(),
|
||||
serviceId, url, title);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
|
||||
serviceId, url, title);
|
||||
break;
|
||||
}
|
||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||
String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
|
||||
if (searchString == null) {
|
||||
searchString = "";
|
||||
}
|
||||
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
NavigationHelper.openSearchFragment(
|
||||
getSupportFragmentManager(),
|
||||
serviceId,
|
||||
searchString);
|
||||
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Handling intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void openMiniPlayerIfMissing() {
|
||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (fragmentPlayer == null) {
|
||||
// We still don't have a fragment attached to the activity. It can happen when a user
|
||||
// started popup or background players without opening a stream inside the fragment.
|
||||
// Adding it in a collapsed state (only mini player will be visible).
|
||||
NavigationHelper.showMiniPlayer(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
private void openMiniPlayerUponPlayerStarted() {
|
||||
if (getIntent().getSerializableExtra(Constants.KEY_LINK_TYPE)
|
||||
== StreamingService.LinkType.STREAM) {
|
||||
// handleIntent() already takes care of opening video detail fragment
|
||||
// due to an intent containing a STREAM link
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
||||
// if the player is already open, no need for a broadcast receiver
|
||||
openMiniPlayerIfMissing();
|
||||
} else {
|
||||
// listen for player start intent being sent around
|
||||
broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (Objects.equals(intent.getAction(),
|
||||
VideoDetailFragment.ACTION_PLAYER_STARTED)) {
|
||||
openMiniPlayerIfMissing();
|
||||
// At this point the player is added 100%, we can unregister. Other actions
|
||||
// are useless since the fragment will not be removed after that.
|
||||
unregisterReceiver(broadcastReceiver);
|
||||
broadcastReceiver = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
|
||||
registerReceiver(broadcastReceiver, intentFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private void openDetailFragmentFromCommentReplies(
|
||||
@NonNull final FragmentManager fm,
|
||||
final boolean popBackStack
|
||||
) {
|
||||
// obtain the name of the fragment under the replies fragment that's going to be popped
|
||||
@Nullable final String fragmentUnderEntryName;
|
||||
if (fm.getBackStackEntryCount() < 2) {
|
||||
fragmentUnderEntryName = null;
|
||||
} else {
|
||||
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
||||
.getName();
|
||||
}
|
||||
|
||||
// the root comment is the comment for which the user opened the replies page
|
||||
@Nullable final CommentRepliesFragment repliesFragment =
|
||||
(CommentRepliesFragment) fm.findFragmentByTag(CommentRepliesFragment.TAG);
|
||||
@Nullable final CommentsInfoItem rootComment =
|
||||
repliesFragment == null ? null : repliesFragment.getCommentsInfoItem();
|
||||
|
||||
// sometimes this function pops the backstack, other times it's handled by the system
|
||||
if (popBackStack) {
|
||||
fm.popBackStackImmediate();
|
||||
}
|
||||
|
||||
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
||||
// stacked under the one that is currently being popped
|
||||
if (CommentRepliesFragment.TAG.equals(fragmentUnderEntryName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final BottomSheetBehavior<FragmentContainerView> behavior = BottomSheetBehavior
|
||||
.from(mainBinding.fragmentPlayerHolder);
|
||||
// do not return to the comment if the details fragment was closed
|
||||
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
// scroll to the root comment once the bottom sheet expansion animation is finished
|
||||
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull final View bottomSheet,
|
||||
final int newState) {
|
||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
final Fragment detailFragment = fm.findFragmentById(
|
||||
R.id.fragment_player_holder);
|
||||
if (detailFragment instanceof VideoDetailFragment && rootComment != null) {
|
||||
// should always be the case
|
||||
((VideoDetailFragment) detailFragment).scrollToComment(rootComment);
|
||||
}
|
||||
behavior.removeBottomSheetCallback(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
||||
// not needed, listener is removed once the sheet is expanded
|
||||
}
|
||||
});
|
||||
|
||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
|
||||
private boolean bottomSheetHiddenOrCollapsed() {
|
||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||
BottomSheetBehavior.from(mainBinding.fragmentPlayerHolder);
|
||||
|
||||
final int sheetState = bottomSheetBehavior.getState();
|
||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,820 @@
|
|||
/*
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* DownloadActivity.java is part of NewPipe.
|
||||
* <p>
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <p>
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.schabi.newpipe
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Spinner
|
||||
import androidx.appcompat.app.ActionBar
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.drawerlayout.widget.DrawerLayout.SimpleDrawerListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import org.schabi.newpipe.NewVersionWorker.Companion.enqueueNewVersionCheckingWork
|
||||
import org.schabi.newpipe.databinding.ActivityMainBinding
|
||||
import org.schabi.newpipe.databinding.DrawerHeaderBinding
|
||||
import org.schabi.newpipe.databinding.DrawerLayoutBinding
|
||||
import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding
|
||||
import org.schabi.newpipe.databinding.ToolbarLayoutBinding
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.StreamingService
|
||||
import org.schabi.newpipe.extractor.StreamingService.LinkType
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
|
||||
import org.schabi.newpipe.fragments.BackPressable
|
||||
import org.schabi.newpipe.fragments.MainFragment
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment
|
||||
import org.schabi.newpipe.fragments.list.comments.CommentRepliesFragment
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment
|
||||
import org.schabi.newpipe.local.feed.notifications.NotificationWorker.Companion.initialize
|
||||
import org.schabi.newpipe.player.Player
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue
|
||||
import org.schabi.newpipe.settings.UpdateSettingsFragment
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.KioskTranslator
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.PeertubeHelper
|
||||
import org.schabi.newpipe.util.PermissionHelper
|
||||
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
|
||||
import org.schabi.newpipe.util.SerializedCache
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.StateSaver
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.views.FocusOverlayView
|
||||
import java.util.Objects
|
||||
|
||||
class MainActivity() : AppCompatActivity() {
|
||||
private var mainBinding: ActivityMainBinding? = null
|
||||
private var drawerHeaderBinding: DrawerHeaderBinding? = null
|
||||
private var drawerLayoutBinding: DrawerLayoutBinding? = null
|
||||
private var toolbarLayoutBinding: ToolbarLayoutBinding? = null
|
||||
private var toggle: ActionBarDrawerToggle? = null
|
||||
private var servicesShown: Boolean = false
|
||||
private var broadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity's LifeCycle
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, ("onCreate() called with: "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]"))
|
||||
}
|
||||
ThemeHelper.setDayNightMode(this)
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this))
|
||||
Localization.assureCorrectAppLanguage(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
mainBinding = ActivityMainBinding.inflate(getLayoutInflater())
|
||||
drawerLayoutBinding = mainBinding!!.drawerLayout
|
||||
drawerHeaderBinding = DrawerHeaderBinding.bind(drawerLayoutBinding!!.navigation
|
||||
.getHeaderView(0))
|
||||
toolbarLayoutBinding = mainBinding!!.toolbarLayout
|
||||
setContentView(mainBinding!!.getRoot())
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments()
|
||||
}
|
||||
setSupportActionBar(toolbarLayoutBinding!!.toolbar)
|
||||
try {
|
||||
setupDrawer()
|
||||
} catch (e: Exception) {
|
||||
showUiErrorSnackbar(this, "Setting up drawer", e)
|
||||
}
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.Companion.setupFocusObserver(this)
|
||||
}
|
||||
openMiniPlayerUponPlayerStarted()
|
||||
if (PermissionHelper.checkPostNotificationsPermission(this,
|
||||
PermissionHelper.POST_NOTIFICATIONS_REQUEST_CODE)) {
|
||||
// Schedule worker for checking for new streams and creating corresponding notifications
|
||||
// if this is enabled by the user.
|
||||
initialize(this)
|
||||
}
|
||||
if ((!UpdateSettingsFragment.Companion.wasUserAskedForConsent(this)
|
||||
&& !App.Companion.getApp().isFirstRun()
|
||||
&& isReleaseApk)) {
|
||||
UpdateSettingsFragment.Companion.askForConsentToUpdateChecks(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
val app: App = App.Companion.getApp()
|
||||
val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(app)
|
||||
if ((prefs.getBoolean(app.getString(R.string.update_app_key), false)
|
||||
&& prefs.getBoolean(app.getString(R.string.update_check_consent_key), false))) {
|
||||
// Start the worker which is checking all conditions
|
||||
// and eventually searching for a new version.
|
||||
enqueueNewVersionCheckingWork(app, false)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ExtractionException::class)
|
||||
private fun setupDrawer() {
|
||||
addDrawerMenuForCurrentService()
|
||||
toggle = ActionBarDrawerToggle(this, mainBinding!!.getRoot(),
|
||||
toolbarLayoutBinding!!.toolbar, R.string.drawer_open, R.string.drawer_close)
|
||||
toggle!!.syncState()
|
||||
mainBinding!!.getRoot().addDrawerListener(toggle!!)
|
||||
mainBinding!!.getRoot().addDrawerListener(object : SimpleDrawerListener() {
|
||||
private var lastService: Int = 0
|
||||
public override fun onDrawerOpened(drawerView: View) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(this@MainActivity)
|
||||
}
|
||||
|
||||
public override fun onDrawerClosed(drawerView: View) {
|
||||
if (servicesShown) {
|
||||
toggleServices()
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(this@MainActivity)) {
|
||||
ActivityCompat.recreate(this@MainActivity)
|
||||
}
|
||||
}
|
||||
})
|
||||
drawerLayoutBinding!!.navigation.setNavigationItemSelectedListener(NavigationView.OnNavigationItemSelectedListener({ item: MenuItem -> drawerItemSelected(item) }))
|
||||
setupDrawerHeader()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the drawer menu for the current service.
|
||||
*
|
||||
* @throws ExtractionException if the service didn't provide available kiosks
|
||||
*/
|
||||
@Throws(ExtractionException::class)
|
||||
private fun addDrawerMenuForCurrentService() {
|
||||
//Tabs
|
||||
val currentServiceId: Int = ServiceHelper.getSelectedServiceId(this)
|
||||
val service: StreamingService = NewPipe.getService(currentServiceId)
|
||||
var kioskMenuItemId: Int = 0
|
||||
for (ks: String in service.getKioskList().getAvailableKiosks()) {
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks))
|
||||
kioskMenuItemId++
|
||||
}
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
|
||||
R.string.tab_subscriptions)
|
||||
.setIcon(R.drawable.ic_tv)
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(R.drawable.ic_subscriptions)
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(R.drawable.ic_bookmark)
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(R.drawable.ic_file_download)
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(R.drawable.ic_history)
|
||||
|
||||
//Settings and About
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(R.drawable.ic_settings)
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(R.drawable.ic_info_outline)
|
||||
}
|
||||
|
||||
private fun drawerItemSelected(item: MenuItem): Boolean {
|
||||
when (item.getGroupId()) {
|
||||
R.id.menu_services_group -> changeService(item)
|
||||
R.id.menu_tabs_group -> try {
|
||||
tabSelected(item)
|
||||
} catch (e: Exception) {
|
||||
showUiErrorSnackbar(this, "Selecting main page tab", e)
|
||||
}
|
||||
|
||||
R.id.menu_options_about_group -> optionsAboutSelected(item)
|
||||
else -> return false
|
||||
}
|
||||
mainBinding!!.getRoot().closeDrawers()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun changeService(item: MenuItem) {
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(false)
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId())
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true)
|
||||
}
|
||||
|
||||
@Throws(ExtractionException::class)
|
||||
private fun tabSelected(item: MenuItem) {
|
||||
when (item.getItemId()) {
|
||||
ITEM_ID_SUBSCRIPTIONS -> NavigationHelper.openSubscriptionFragment(getSupportFragmentManager())
|
||||
ITEM_ID_FEED -> openFeedFragment(getSupportFragmentManager())
|
||||
ITEM_ID_BOOKMARKS -> NavigationHelper.openBookmarksFragment(getSupportFragmentManager())
|
||||
ITEM_ID_DOWNLOADS -> NavigationHelper.openDownloads(this)
|
||||
ITEM_ID_HISTORY -> NavigationHelper.openStatisticFragment(getSupportFragmentManager())
|
||||
else -> {
|
||||
val currentService: StreamingService? = ServiceHelper.getSelectedService(this)
|
||||
var kioskMenuItemId: Int = 0
|
||||
for (kioskId: String in currentService!!.getKioskList().getAvailableKiosks()) {
|
||||
if (kioskMenuItemId == item.getItemId()) {
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
|
||||
currentService.getServiceId(), kioskId)
|
||||
break
|
||||
}
|
||||
kioskMenuItemId++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun optionsAboutSelected(item: MenuItem) {
|
||||
when (item.getItemId()) {
|
||||
ITEM_ID_SETTINGS -> NavigationHelper.openSettings(this)
|
||||
ITEM_ID_ABOUT -> NavigationHelper.openAbout(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDrawerHeader() {
|
||||
drawerHeaderBinding!!.drawerHeaderActionButton.setOnClickListener(View.OnClickListener({ view: 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) {
|
||||
val layoutParams: ViewGroup.LayoutParams = drawerHeaderBinding!!.drawerHeaderNewpipeTitle.getLayoutParams()
|
||||
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
drawerHeaderBinding!!.drawerHeaderNewpipeTitle.setLayoutParams(layoutParams)
|
||||
drawerHeaderBinding!!.drawerHeaderNewpipeTitle.setMaxLines(2)
|
||||
drawerHeaderBinding!!.drawerHeaderNewpipeTitle.setMinWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width))
|
||||
drawerHeaderBinding!!.drawerHeaderNewpipeTitle.setMaxWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width))
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleServices() {
|
||||
servicesShown = !servicesShown
|
||||
drawerLayoutBinding!!.navigation.getMenu().removeGroup(R.id.menu_services_group)
|
||||
drawerLayoutBinding!!.navigation.getMenu().removeGroup(R.id.menu_tabs_group)
|
||||
drawerLayoutBinding!!.navigation.getMenu().removeGroup(R.id.menu_options_about_group)
|
||||
|
||||
// Show up or down arrow
|
||||
drawerHeaderBinding!!.drawerArrow.setImageResource(
|
||||
if (servicesShown) R.drawable.ic_arrow_drop_up else R.drawable.ic_arrow_drop_down)
|
||||
if (servicesShown) {
|
||||
showServices()
|
||||
} else {
|
||||
try {
|
||||
addDrawerMenuForCurrentService()
|
||||
} catch (e: Exception) {
|
||||
showUiErrorSnackbar(this, "Showing main page tabs", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showServices() {
|
||||
for (s: StreamingService in NewPipe.getServices()) {
|
||||
val title: String = s.getServiceInfo().getName()
|
||||
val menuItem: MenuItem = drawerLayoutBinding!!.navigation.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()))
|
||||
|
||||
// peertube specifics
|
||||
if (s.getServiceId() == 3) {
|
||||
enhancePeertubeMenu(menuItem)
|
||||
}
|
||||
}
|
||||
drawerLayoutBinding!!.navigation.getMenu()
|
||||
.getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true)
|
||||
}
|
||||
|
||||
private fun enhancePeertubeMenu(menuItem: MenuItem) {
|
||||
val currentInstance: PeertubeInstance? = PeertubeHelper.getCurrentInstance()
|
||||
menuItem.setTitle(currentInstance!!.getName())
|
||||
val spinner: Spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
|
||||
.getRoot()
|
||||
val instances: List<PeertubeInstance?>? = PeertubeHelper.getInstanceList(this)
|
||||
val items: MutableList<String> = ArrayList()
|
||||
var defaultSelect: Int = 0
|
||||
for (instance: PeertubeInstance? in instances!!) {
|
||||
items.add(instance!!.getName())
|
||||
if ((instance.getUrl() == currentInstance.getUrl())) {
|
||||
defaultSelect = items.size - 1
|
||||
}
|
||||
}
|
||||
val adapter: ArrayAdapter<String> = 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(object : AdapterView.OnItemSelectedListener {
|
||||
public override fun onItemSelected(parent: AdapterView<*>?, view: View,
|
||||
position: Int, id: Long) {
|
||||
val newInstance: PeertubeInstance? = instances.get(position)
|
||||
if ((newInstance!!.getUrl() == PeertubeHelper.getCurrentInstance().getUrl())) {
|
||||
return
|
||||
}
|
||||
PeertubeHelper.selectInstance(newInstance, getApplicationContext())
|
||||
changeService(menuItem)
|
||||
mainBinding!!.getRoot().closeDrawers()
|
||||
Handler(Looper.getMainLooper()).postDelayed(Runnable({
|
||||
getSupportFragmentManager().popBackStack(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
ActivityCompat.recreate(this@MainActivity)
|
||||
}), 300)
|
||||
}
|
||||
|
||||
public override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
})
|
||||
menuItem.setActionView(spinner)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!isChangingConfigurations()) {
|
||||
StateSaver.clearStateFiles()
|
||||
}
|
||||
if (broadcastReceiver != null) {
|
||||
unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
Localization.assureCorrectAppLanguage(this)
|
||||
// Change the date format to match the selected language on resume
|
||||
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()))
|
||||
super.onResume()
|
||||
|
||||
// Close drawer on return, and don't show animation,
|
||||
// so it looks like the drawer isn't open when the user returns to MainActivity
|
||||
mainBinding!!.getRoot().closeDrawer(GravityCompat.START, false)
|
||||
try {
|
||||
val selectedServiceId: Int = ServiceHelper.getSelectedServiceId(this)
|
||||
val selectedServiceName: String = NewPipe.getService(selectedServiceId)
|
||||
.getServiceInfo().getName()
|
||||
drawerHeaderBinding!!.drawerHeaderServiceView.setText(selectedServiceName)
|
||||
drawerHeaderBinding!!.drawerHeaderServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId))
|
||||
drawerHeaderBinding!!.drawerHeaderServiceView.post(Runnable({ drawerHeaderBinding!!.drawerHeaderServiceView.setSelected(true) }))
|
||||
drawerHeaderBinding!!.drawerHeaderActionButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName)
|
||||
} catch (e: Exception) {
|
||||
showUiErrorSnackbar(this, "Setting up service toggle", e)
|
||||
}
|
||||
val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if (sharedPreferences.getBoolean(KEY_THEME_CHANGE, false)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Theme has changed, recreating activity...")
|
||||
}
|
||||
sharedPreferences.edit().putBoolean(KEY_THEME_CHANGE, false).apply()
|
||||
ActivityCompat.recreate(this)
|
||||
}
|
||||
if (sharedPreferences.getBoolean(KEY_MAIN_PAGE_CHANGE, false)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "main page has changed, recreating main fragment...")
|
||||
}
|
||||
sharedPreferences.edit().putBoolean(KEY_MAIN_PAGE_CHANGE, false).apply()
|
||||
NavigationHelper.openMainActivity(this)
|
||||
}
|
||||
val isHistoryEnabled: Boolean = sharedPreferences.getBoolean(
|
||||
getString(R.string.enable_watch_history_key), true)
|
||||
drawerLayoutBinding!!.navigation.getMenu().findItem(ITEM_ID_HISTORY)
|
||||
.setVisible(isHistoryEnabled)
|
||||
}
|
||||
|
||||
override fun onNewIntent(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
|
||||
val action: String? = intent.getAction()
|
||||
if (((action != null && (action == Intent.ACTION_MAIN))
|
||||
&& intent.hasCategory(Intent.CATEGORY_LAUNCHER))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
public override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
val fragment: Fragment? = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder)
|
||||
if ((fragment is OnKeyDownListener
|
||||
&& !bottomSheetHiddenOrCollapsed())) {
|
||||
// Provide keyDown event to fragment which then sends this event
|
||||
// to the main player service
|
||||
return ((fragment as OnKeyDownListener).onKeyDown(keyCode)
|
||||
|| super.onKeyDown(keyCode, event))
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
public override fun onBackPressed() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBackPressed() called")
|
||||
}
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
if (mainBinding!!.getRoot().isDrawerOpen(drawerLayoutBinding!!.navigation)) {
|
||||
mainBinding!!.getRoot().closeDrawers()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
val fm: FragmentManager = getSupportFragmentManager()
|
||||
val fragment: Fragment? = fm.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 is BackPressable) {
|
||||
if ((fragment as BackPressable).onBackPressed()) {
|
||||
return
|
||||
}
|
||||
} else if (fragment is CommentRepliesFragment) {
|
||||
// expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// to show the top level comments again
|
||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// and no other CommentRepliesFragments are on top of the back stack
|
||||
// to show the top level comments again.
|
||||
openDetailFragmentFromCommentReplies(fm, false)
|
||||
}
|
||||
} else {
|
||||
val fragmentPlayer: Fragment? = 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 is BackPressable) {
|
||||
if (!(fragmentPlayer as BackPressable).onBackPressed()) {
|
||||
BottomSheetBehavior.from(mainBinding!!.fragmentPlayerHolder)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
finish()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onRequestPermissionsResult(requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
for (i: Int in grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
return
|
||||
}
|
||||
}
|
||||
when (requestCode) {
|
||||
PermissionHelper.DOWNLOADS_REQUEST_CODE -> NavigationHelper.openDownloads(this)
|
||||
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE -> {
|
||||
val fragment: Fragment? = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder)
|
||||
if (fragment is VideoDetailFragment) {
|
||||
fragment.openDownloadDialog()
|
||||
}
|
||||
}
|
||||
|
||||
PermissionHelper.POST_NOTIFICATIONS_REQUEST_CODE -> initialize(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the following diagram behavior for the up button:
|
||||
* <pre>
|
||||
* +---------------+
|
||||
* | Main Screen +----+
|
||||
* +-------+-------+ |
|
||||
* | |
|
||||
* ▲ Up | Search Button
|
||||
* | |
|
||||
* +----+-----+ |
|
||||
* +------------+ Search |◄-----+
|
||||
* | +----+-----+
|
||||
* | Open |
|
||||
* | something ▲ Up
|
||||
* | |
|
||||
* | +------------+-------------+
|
||||
* | | |
|
||||
* | | Video <-> Channel |
|
||||
* +---►| Channel <-> Playlist |
|
||||
* | Video <-> .... |
|
||||
* | |
|
||||
* +--------------------------+
|
||||
</pre> *
|
||||
*/
|
||||
private fun onHomeButtonPressed() {
|
||||
val fm: FragmentManager = getSupportFragmentManager()
|
||||
val fragment: Fragment? = fm.findFragmentById(R.id.fragment_holder)
|
||||
if (fragment is CommentRepliesFragment) {
|
||||
// Expand DetailsFragment if CommentRepliesFragment was opened
|
||||
// and no other CommentRepliesFragments are on top of the back stack
|
||||
// to show the top level comments again.
|
||||
openDetailFragmentFromCommentReplies(fm, true)
|
||||
} else if (!NavigationHelper.tryGotoSearchFragment(fm)) {
|
||||
// If search fragment wasn't found in the backstack go to the main fragment
|
||||
NavigationHelper.gotoMainFragment(fm)
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
public override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]")
|
||||
}
|
||||
super.onCreateOptionsMenu(menu)
|
||||
val fragment: Fragment? = getSupportFragmentManager().findFragmentById(R.id.fragment_holder)
|
||||
if (!(fragment is SearchFragment)) {
|
||||
toolbarLayoutBinding!!.toolbarSearchContainer.getRoot().setVisibility(View.GONE)
|
||||
}
|
||||
val actionBar: ActionBar? = getSupportActionBar()
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false)
|
||||
}
|
||||
updateDrawerNavigation()
|
||||
return true
|
||||
}
|
||||
|
||||
public override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]")
|
||||
}
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onHomeButtonPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
private fun initFragments() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initFragments() called")
|
||||
}
|
||||
StateSaver.clearStateFiles()
|
||||
if (getIntent() != null && getIntent().hasExtra(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 he will see a blank fragment on place of kiosk.
|
||||
// Let's open it first
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
NavigationHelper.openMainFragment(getSupportFragmentManager())
|
||||
}
|
||||
handleIntent(getIntent())
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager())
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
private fun updateDrawerNavigation() {
|
||||
if (getSupportActionBar() == null) {
|
||||
return
|
||||
}
|
||||
val fragment: Fragment? = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder)
|
||||
if (fragment is MainFragment) {
|
||||
getSupportActionBar()!!.setDisplayHomeAsUpEnabled(false)
|
||||
if (toggle != null) {
|
||||
toggle!!.syncState()
|
||||
toolbarLayoutBinding!!.toolbar.setNavigationOnClickListener(View.OnClickListener({ v: View? ->
|
||||
mainBinding!!.getRoot()
|
||||
.open()
|
||||
}))
|
||||
mainBinding!!.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED)
|
||||
}
|
||||
} else {
|
||||
mainBinding!!.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
getSupportActionBar()!!.setDisplayHomeAsUpEnabled(true)
|
||||
toolbarLayoutBinding!!.toolbar.setNavigationOnClickListener(View.OnClickListener({ v: View? -> onHomeButtonPressed() }))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
try {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]")
|
||||
}
|
||||
if (intent.hasExtra(KEY_LINK_TYPE)) {
|
||||
val url: String? = intent.getStringExtra(KEY_URL)
|
||||
val serviceId: Int = intent.getIntExtra(KEY_SERVICE_ID, 0)
|
||||
var title: String? = intent.getStringExtra(KEY_TITLE)
|
||||
if (title == null) {
|
||||
title = ""
|
||||
}
|
||||
val linkType: LinkType? = (intent
|
||||
.getSerializableExtra(KEY_LINK_TYPE) as LinkType?)
|
||||
assert(linkType != null)
|
||||
when (linkType) {
|
||||
LinkType.STREAM -> {
|
||||
val intentCacheKey: String? = intent.getStringExtra(
|
||||
Player.Companion.PLAY_QUEUE_KEY)
|
||||
val playQueue: PlayQueue? = if (intentCacheKey != null) SerializedCache.Companion.getInstance()
|
||||
.take<PlayQueue>(intentCacheKey, PlayQueue::class.java) else null
|
||||
val switchingPlayers: Boolean = intent.getBooleanExtra(
|
||||
VideoDetailFragment.Companion.KEY_SWITCHING_PLAYERS, false)
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
getApplicationContext(), getSupportFragmentManager(),
|
||||
serviceId, url, title, playQueue, switchingPlayers)
|
||||
}
|
||||
|
||||
LinkType.CHANNEL -> NavigationHelper.openChannelFragment(getSupportFragmentManager(),
|
||||
serviceId, url, title)
|
||||
|
||||
LinkType.PLAYLIST -> NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
|
||||
serviceId, url, title)
|
||||
}
|
||||
} else if (intent.hasExtra(KEY_OPEN_SEARCH)) {
|
||||
var searchString: String? = intent.getStringExtra(KEY_SEARCH_STRING)
|
||||
if (searchString == null) {
|
||||
searchString = ""
|
||||
}
|
||||
val serviceId: Int = intent.getIntExtra(KEY_SERVICE_ID, 0)
|
||||
NavigationHelper.openSearchFragment(
|
||||
getSupportFragmentManager(),
|
||||
serviceId,
|
||||
searchString)
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showUiErrorSnackbar(this, "Handling intent", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openMiniPlayerIfMissing() {
|
||||
val fragmentPlayer: Fragment? = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder)
|
||||
if (fragmentPlayer == null) {
|
||||
// We still don't have a fragment attached to the activity. It can happen when a user
|
||||
// started popup or background players without opening a stream inside the fragment.
|
||||
// Adding it in a collapsed state (only mini player will be visible).
|
||||
NavigationHelper.showMiniPlayer(getSupportFragmentManager())
|
||||
}
|
||||
}
|
||||
|
||||
private fun openMiniPlayerUponPlayerStarted() {
|
||||
if ((getIntent().getSerializableExtra(KEY_LINK_TYPE)
|
||||
=== LinkType.STREAM)) {
|
||||
// handleIntent() already takes care of opening video detail fragment
|
||||
// due to an intent containing a STREAM link
|
||||
return
|
||||
}
|
||||
if (PlayerHolder.Companion.getInstance().isPlayerOpen()) {
|
||||
// if the player is already open, no need for a broadcast receiver
|
||||
openMiniPlayerIfMissing()
|
||||
} else {
|
||||
// listen for player start intent being sent around
|
||||
broadcastReceiver = object : BroadcastReceiver() {
|
||||
public override fun onReceive(context: Context, intent: Intent) {
|
||||
if (Objects.equals(intent.getAction(),
|
||||
VideoDetailFragment.Companion.ACTION_PLAYER_STARTED)) {
|
||||
openMiniPlayerIfMissing()
|
||||
// At this point the player is added 100%, we can unregister. Other actions
|
||||
// are useless since the fragment will not be removed after that.
|
||||
unregisterReceiver(broadcastReceiver)
|
||||
broadcastReceiver = null
|
||||
}
|
||||
}
|
||||
}
|
||||
val intentFilter: IntentFilter = IntentFilter()
|
||||
intentFilter.addAction(VideoDetailFragment.Companion.ACTION_PLAYER_STARTED)
|
||||
registerReceiver(broadcastReceiver, intentFilter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDetailFragmentFromCommentReplies(
|
||||
fm: FragmentManager,
|
||||
popBackStack: Boolean
|
||||
) {
|
||||
// obtain the name of the fragment under the replies fragment that's going to be popped
|
||||
val fragmentUnderEntryName: String?
|
||||
if (fm.getBackStackEntryCount() < 2) {
|
||||
fragmentUnderEntryName = null
|
||||
} else {
|
||||
fragmentUnderEntryName = fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 2)
|
||||
.getName()
|
||||
}
|
||||
|
||||
// the root comment is the comment for which the user opened the replies page
|
||||
val repliesFragment: CommentRepliesFragment? = fm.findFragmentByTag(CommentRepliesFragment.Companion.TAG) as CommentRepliesFragment?
|
||||
val rootComment: CommentsInfoItem? = if (repliesFragment == null) null else repliesFragment.getCommentsInfoItem()
|
||||
|
||||
// sometimes this function pops the backstack, other times it's handled by the system
|
||||
if (popBackStack) {
|
||||
fm.popBackStackImmediate()
|
||||
}
|
||||
|
||||
// only expand the bottom sheet back if there are no more nested comment replies fragments
|
||||
// stacked under the one that is currently being popped
|
||||
if ((CommentRepliesFragment.Companion.TAG == fragmentUnderEntryName)) {
|
||||
return
|
||||
}
|
||||
val behavior: BottomSheetBehavior<FragmentContainerView> = BottomSheetBehavior
|
||||
.from(mainBinding!!.fragmentPlayerHolder)
|
||||
// do not return to the comment if the details fragment was closed
|
||||
if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
return
|
||||
}
|
||||
|
||||
// scroll to the root comment once the bottom sheet expansion animation is finished
|
||||
behavior.addBottomSheetCallback(object : BottomSheetCallback() {
|
||||
public override fun onStateChanged(bottomSheet: View,
|
||||
newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
val detailFragment: Fragment? = fm.findFragmentById(
|
||||
R.id.fragment_player_holder)
|
||||
if (detailFragment is VideoDetailFragment && rootComment != null) {
|
||||
// should always be the case
|
||||
detailFragment.scrollToComment(rootComment)
|
||||
}
|
||||
behavior.removeBottomSheetCallback(this)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
// not needed, listener is removed once the sheet is expanded
|
||||
}
|
||||
})
|
||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED)
|
||||
}
|
||||
|
||||
private fun bottomSheetHiddenOrCollapsed(): Boolean {
|
||||
val bottomSheetBehavior: BottomSheetBehavior<FrameLayout> = BottomSheetBehavior.from(mainBinding!!.fragmentPlayerHolder)
|
||||
val sheetState: Int = bottomSheetBehavior.getState()
|
||||
return (sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = "MainActivity"
|
||||
val DEBUG: Boolean = !BuildConfig.BUILD_TYPE.equals("release")
|
||||
private val ITEM_ID_SUBSCRIPTIONS: Int = -1
|
||||
private val ITEM_ID_FEED: Int = -2
|
||||
private val ITEM_ID_BOOKMARKS: Int = -3
|
||||
private val ITEM_ID_DOWNLOADS: Int = -4
|
||||
private val ITEM_ID_HISTORY: Int = -5
|
||||
private val ITEM_ID_SETTINGS: Int = 0
|
||||
private val ITEM_ID_ABOUT: Int = 1
|
||||
private val ORDER: Int = 0
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Room;
|
||||
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
|
||||
public final class NewPipeDatabase {
|
||||
private static volatile AppDatabase databaseInstance;
|
||||
|
||||
private NewPipeDatabase() {
|
||||
//no instance
|
||||
}
|
||||
|
||||
private static AppDatabase getDatabase(final Context context) {
|
||||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static AppDatabase getInstance(@NonNull final Context context) {
|
||||
AppDatabase result = databaseInstance;
|
||||
if (result == null) {
|
||||
synchronized (NewPipeDatabase.class) {
|
||||
result = databaseInstance;
|
||||
if (result == null) {
|
||||
databaseInstance = getDatabase(context);
|
||||
result = databaseInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void checkpoint() {
|
||||
if (databaseInstance == null) {
|
||||
throw new IllegalStateException("database is not initialized");
|
||||
}
|
||||
final Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
|
||||
if (c.moveToFirst() && c.getInt(0) == 1) {
|
||||
throw new RuntimeException("Checkpoint was blocked from completing");
|
||||
}
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
if (databaseInstance != null) {
|
||||
synchronized (NewPipeDatabase.class) {
|
||||
if (databaseInstance != null) {
|
||||
databaseInstance.close();
|
||||
databaseInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.Migrations
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
object NewPipeDatabase {
|
||||
@Volatile
|
||||
private var databaseInstance: AppDatabase? = null
|
||||
private fun getDatabase(context: Context): AppDatabase {
|
||||
return databaseBuilder<AppDatabase>(context.getApplicationContext(), AppDatabase::class.java, AppDatabase.Companion.DATABASE_NAME)
|
||||
.addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4, Migrations.MIGRATION_4_5,
|
||||
Migrations.MIGRATION_5_6, Migrations.MIGRATION_6_7, Migrations.MIGRATION_7_8, Migrations.MIGRATION_8_9)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun getInstance(context: Context): AppDatabase {
|
||||
var result: AppDatabase? = databaseInstance
|
||||
if (result == null) {
|
||||
synchronized(NewPipeDatabase::class.java, {
|
||||
result = databaseInstance
|
||||
if (result == null) {
|
||||
databaseInstance = getDatabase(context)
|
||||
result = databaseInstance
|
||||
}
|
||||
})
|
||||
}
|
||||
return (result)!!
|
||||
}
|
||||
|
||||
fun checkpoint() {
|
||||
if (databaseInstance == null) {
|
||||
throw IllegalStateException("database is not initialized")
|
||||
}
|
||||
val c: Cursor = databaseInstance!!.query("pragma wal_checkpoint(full)", null)
|
||||
if (c.moveToFirst() && c.getInt(0) == 1) {
|
||||
throw RuntimeException("Checkpoint was blocked from completing")
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
if (databaseInstance != null) {
|
||||
synchronized(NewPipeDatabase::class.java, {
|
||||
if (databaseInstance != null) {
|
||||
databaseInstance!!.close()
|
||||
databaseInstance = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* PanicResponderActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class PanicResponderActivity extends Activity {
|
||||
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final 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
|
||||
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||
}
|
||||
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.schabi.newpipe
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
* PanicResponderActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class PanicResponderActivity() : Activity() {
|
||||
@SuppressLint("NewApi")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val intent: Intent? = getIntent()
|
||||
if (intent != null && (PANIC_TRIGGER_ACTION == 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
|
||||
ExitActivity.Companion.exitAndRemoveFromRecentApps(this)
|
||||
}
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PANIC_TRIGGER_ACTION: String = "info.guardianproject.panic.action.TRIGGER"
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
|
||||
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.SparseItemUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class QueueItemMenuUtil {
|
||||
private QueueItemMenuUtil() {
|
||||
}
|
||||
|
||||
public static void openPopupMenu(final PlayQueue playQueue,
|
||||
final PlayQueueItem item,
|
||||
final View view,
|
||||
final boolean hideDetails,
|
||||
final FragmentManager fragmentManager,
|
||||
final Context context) {
|
||||
final ContextThemeWrapper themeWrapper =
|
||||
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
|
||||
|
||||
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
|
||||
popupMenu.inflate(R.menu.menu_play_queue_item);
|
||||
|
||||
if (hideDetails) {
|
||||
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.menu_item_remove:
|
||||
final int index = playQueue.indexOf(item);
|
||||
playQueue.remove(index);
|
||||
return true;
|
||||
case R.id.menu_item_details:
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||
item.getUrl(), item.getTitle(), null,
|
||||
false);
|
||||
return true;
|
||||
case R.id.menu_item_append_playlist:
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
context,
|
||||
List.of(new StreamEntity(item)),
|
||||
dialog -> dialog.show(
|
||||
fragmentManager,
|
||||
"QueueItemMenuUtil@append_playlist"
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
case R.id.menu_item_channel_details:
|
||||
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
||||
item.getUrl(), item.getUploaderUrl(),
|
||||
// An intent must be used here.
|
||||
// Opening with FragmentManager transactions is not working,
|
||||
// as PlayQueueActivity doesn't use fragments.
|
||||
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||
));
|
||||
return true;
|
||||
case R.id.menu_item_share:
|
||||
shareText(context, item.getTitle(), item.getUrl(),
|
||||
item.getThumbnails());
|
||||
return true;
|
||||
case R.id.menu_item_download:
|
||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||
info -> {
|
||||
final DownloadDialog downloadDialog = new DownloadDialog(context,
|
||||
info);
|
||||
downloadDialog.show(fragmentManager, "downloadDialog");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
popupMenu.show();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package org.schabi.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.download.DownloadDialog
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.SparseItemUtil
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.util.List
|
||||
import java.util.function.Consumer
|
||||
|
||||
object QueueItemMenuUtil {
|
||||
fun openPopupMenu(playQueue: PlayQueue?,
|
||||
item: PlayQueueItem,
|
||||
view: View?,
|
||||
hideDetails: Boolean,
|
||||
fragmentManager: FragmentManager?,
|
||||
context: Context) {
|
||||
val themeWrapper: ContextThemeWrapper = ContextThemeWrapper(context, R.style.DarkPopupMenu)
|
||||
val popupMenu: PopupMenu = PopupMenu(themeWrapper, view)
|
||||
popupMenu.inflate(R.menu.menu_play_queue_item)
|
||||
if (hideDetails) {
|
||||
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false)
|
||||
}
|
||||
popupMenu.setOnMenuItemClickListener(PopupMenu.OnMenuItemClickListener({ menuItem: MenuItem ->
|
||||
when (menuItem.getItemId()) {
|
||||
R.id.menu_item_remove -> {
|
||||
val index: Int = playQueue!!.indexOf(item)
|
||||
playQueue.remove(index)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
||||
R.id.menu_item_details -> {
|
||||
// playQueue is null since we don't want any queue change
|
||||
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||
item.getUrl(), item.getTitle(), null,
|
||||
false)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
||||
R.id.menu_item_append_playlist -> {
|
||||
PlaylistDialog.Companion.createCorrespondingDialog(
|
||||
context,
|
||||
List.of<StreamEntity?>(StreamEntity(item)),
|
||||
Consumer<PlaylistDialog>({ dialog: PlaylistDialog ->
|
||||
dialog.show(
|
||||
(fragmentManager)!!,
|
||||
"QueueItemMenuUtil@append_playlist"
|
||||
)
|
||||
})
|
||||
)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
||||
R.id.menu_item_channel_details -> {
|
||||
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
||||
item.getUrl(), item.getUploaderUrl(), // An intent must be used here.
|
||||
// Opening with FragmentManager transactions is not working,
|
||||
// as PlayQueueActivity doesn't use fragments.
|
||||
Consumer({ uploaderUrl: String? ->
|
||||
NavigationHelper.openChannelFragmentUsingIntent(
|
||||
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||
)
|
||||
}))
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
||||
R.id.menu_item_share -> {
|
||||
ShareUtils.shareText(context, item.getTitle(), item.getUrl(),
|
||||
item.getThumbnails())
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
||||
R.id.menu_item_download -> {
|
||||
SparseItemUtil.fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||
Consumer({ info: StreamInfo ->
|
||||
val downloadDialog: DownloadDialog = DownloadDialog(context,
|
||||
info)
|
||||
downloadDialog.show((fragmentManager)!!, "downloadDialog")
|
||||
}))
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
}
|
||||
false
|
||||
}))
|
||||
popupMenu.show()
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,65 +0,0 @@
|
|||
package org.schabi.newpipe.database;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_9;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.RoomDatabase;
|
||||
import androidx.room.TypeConverters;
|
||||
|
||||
import org.schabi.newpipe.database.feed.dao.FeedDAO;
|
||||
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO;
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity;
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity;
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity;
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
|
||||
@TypeConverters({Converters.class})
|
||||
@Database(
|
||||
entities = {
|
||||
SubscriptionEntity.class, SearchHistoryEntry.class,
|
||||
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
||||
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
|
||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||
FeedLastUpdatedEntity.class
|
||||
},
|
||||
version = DB_VER_9
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
||||
public abstract SearchHistoryDAO searchHistoryDAO();
|
||||
|
||||
public abstract StreamDAO streamDAO();
|
||||
|
||||
public abstract StreamHistoryDAO streamHistoryDAO();
|
||||
|
||||
public abstract StreamStateDAO streamStateDAO();
|
||||
|
||||
public abstract PlaylistDAO playlistDAO();
|
||||
|
||||
public abstract PlaylistStreamDAO playlistStreamDAO();
|
||||
|
||||
public abstract PlaylistRemoteDAO playlistRemoteDAO();
|
||||
|
||||
public abstract FeedDAO feedDAO();
|
||||
|
||||
public abstract FeedGroupDAO feedGroupDAO();
|
||||
|
||||
public abstract SubscriptionDAO subscriptionDAO();
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package org.schabi.newpipe.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import org.schabi.newpipe.database.feed.dao.FeedDAO
|
||||
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO
|
||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||
import org.schabi.newpipe.database.stream.dao.StreamDAO
|
||||
import org.schabi.newpipe.database.stream.dao.StreamStateDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@TypeConverters([Converters::class])
|
||||
@Database(entities = [SubscriptionEntity::class, SearchHistoryEntry::class, StreamEntity::class, StreamHistoryEntity::class, StreamStateEntity::class, PlaylistEntity::class, PlaylistStreamEntity::class, PlaylistRemoteEntity::class, FeedEntity::class, FeedGroupEntity::class, FeedGroupSubscriptionEntity::class, FeedLastUpdatedEntity::class], version = Migrations.DB_VER_9)
|
||||
abstract class AppDatabase() : RoomDatabase() {
|
||||
abstract fun searchHistoryDAO(): SearchHistoryDAO?
|
||||
abstract fun streamDAO(): StreamDAO
|
||||
abstract fun streamHistoryDAO(): StreamHistoryDAO?
|
||||
abstract fun streamStateDAO(): StreamStateDAO?
|
||||
abstract fun playlistDAO(): PlaylistDAO?
|
||||
abstract fun playlistStreamDAO(): PlaylistStreamDAO?
|
||||
abstract fun playlistRemoteDAO(): PlaylistRemoteDAO?
|
||||
abstract fun feedDAO(): FeedDAO?
|
||||
abstract fun feedGroupDAO(): FeedGroupDAO?
|
||||
abstract fun subscriptionDAO(): SubscriptionDAO?
|
||||
|
||||
companion object {
|
||||
val DATABASE_NAME: String = "newpipe.db"
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package org.schabi.newpipe.database;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Update;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
@Dao
|
||||
public interface BasicDAO<Entity> {
|
||||
/* Inserts */
|
||||
@Insert
|
||||
long insert(Entity entity);
|
||||
|
||||
@Insert
|
||||
List<Long> insertAll(Collection<Entity> entities);
|
||||
|
||||
/* Searches */
|
||||
Flowable<List<Entity>> getAll();
|
||||
|
||||
Flowable<List<Entity>> listByService(int serviceId);
|
||||
|
||||
/* Deletes */
|
||||
@Delete
|
||||
void delete(Entity entity);
|
||||
|
||||
int deleteAll();
|
||||
|
||||
/* Updates */
|
||||
@Update
|
||||
int update(Entity entity);
|
||||
|
||||
@Update
|
||||
void update(Collection<Entity> entities);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package org.schabi.newpipe.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Update
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
|
||||
@Dao
|
||||
open interface BasicDAO<Entity> {
|
||||
/* Inserts */
|
||||
@Insert
|
||||
fun insert(entity: Entity): Long
|
||||
|
||||
@Insert
|
||||
fun insertAll(entities: Collection<Entity>?): List<Long?>?
|
||||
|
||||
/* Searches */
|
||||
fun getAll(): Flowable<List<Entity>?>?
|
||||
fun listByService(serviceId: Int): Flowable<List<Entity>?>?
|
||||
|
||||
/* Deletes */
|
||||
@Delete
|
||||
fun delete(entity: Entity)
|
||||
fun deleteAll(): Int
|
||||
|
||||
/* Updates */
|
||||
@Update
|
||||
fun update(entity: Entity): Int
|
||||
|
||||
@Update
|
||||
fun update(entities: Collection<Entity>?)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package org.schabi.newpipe.database;
|
||||
|
||||
public interface LocalItem {
|
||||
LocalItemType getLocalItemType();
|
||||
|
||||
enum LocalItemType {
|
||||
PLAYLIST_LOCAL_ITEM,
|
||||
PLAYLIST_REMOTE_ITEM,
|
||||
|
||||
PLAYLIST_STREAM_ITEM,
|
||||
STATISTIC_STREAM_ITEM,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.database
|
||||
|
||||
open interface LocalItem {
|
||||
fun getLocalItemType(): LocalItemType
|
||||
enum class LocalItemType {
|
||||
PLAYLIST_LOCAL_ITEM,
|
||||
PLAYLIST_REMOTE_ITEM,
|
||||
PLAYLIST_STREAM_ITEM,
|
||||
STATISTIC_STREAM_ITEM
|
||||
}
|
||||
}
|
|
@ -1,15 +1,11 @@
|
|||
package org.schabi.newpipe.database;
|
||||
package org.schabi.newpipe.database
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
||||
public final class Migrations {
|
||||
import android.util.Log
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import org.schabi.newpipe.MainActivity
|
||||
|
||||
object Migrations {
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Test new migrations manually by importing a database from daily usage //
|
||||
// and checking if the migration works (Use the Database Inspector //
|
||||
|
@ -17,25 +13,21 @@ public final class Migrations {
|
|||
// If you add a migration point it out in the pull request, so that //
|
||||
// others remember to test it themselves. //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static final int DB_VER_1 = 1;
|
||||
public static final int DB_VER_2 = 2;
|
||||
public static final int DB_VER_3 = 3;
|
||||
public static final int DB_VER_4 = 4;
|
||||
public static final int DB_VER_5 = 5;
|
||||
public static final int DB_VER_6 = 6;
|
||||
public static final int DB_VER_7 = 7;
|
||||
public static final int DB_VER_8 = 8;
|
||||
public static final int DB_VER_9 = 9;
|
||||
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
val DB_VER_1: Int = 1
|
||||
val DB_VER_2: Int = 2
|
||||
val DB_VER_3: Int = 3
|
||||
val DB_VER_4: Int = 4
|
||||
val DB_VER_5: Int = 5
|
||||
val DB_VER_6: Int = 6
|
||||
val DB_VER_7: Int = 7
|
||||
val DB_VER_8: Int = 8
|
||||
val DB_VER_9: Int = 9
|
||||
private val TAG: String = Migrations::class.java.getName()
|
||||
val DEBUG: Boolean = MainActivity.Companion.DEBUG
|
||||
val MIGRATION_1_2: Migration = object : Migration(DB_VER_1, DB_VER_2) {
|
||||
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Start migrating database");
|
||||
Log.d(TAG, "Start migrating database")
|
||||
}
|
||||
/*
|
||||
* Unfortunately these queries must be hardcoded due to the possibility of
|
||||
|
@ -45,170 +37,152 @@ public final class Migrations {
|
|||
|
||||
// Not much we can do about this, since room doesn't create tables before migration.
|
||||
// It's either this or blasting the entire database anew.
|
||||
database.execSQL("CREATE INDEX `index_search_history_search` "
|
||||
+ "ON `search_history` (`search`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` "
|
||||
database.execSQL(("CREATE INDEX `index_search_history_search` "
|
||||
+ "ON `search_history` (`search`)"))
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS `streams` "
|
||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, "
|
||||
+ "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, "
|
||||
+ "`thumbnail_url` TEXT)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` "
|
||||
+ "ON `streams` (`service_id`, `url`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` "
|
||||
+ "`thumbnail_url` TEXT)"))
|
||||
database.execSQL(("CREATE UNIQUE INDEX `index_streams_service_id_url` "
|
||||
+ "ON `streams` (`service_id`, `url`)"))
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS `stream_history` "
|
||||
+ "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, "
|
||||
+ "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), "
|
||||
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||
database.execSQL("CREATE INDEX `index_stream_history_stream_id` "
|
||||
+ "ON `stream_history` (`stream_id`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE )"))
|
||||
database.execSQL(("CREATE INDEX `index_stream_history_stream_id` "
|
||||
+ "ON `stream_history` (`stream_id`)"))
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS `stream_state` "
|
||||
+ "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) "
|
||||
+ "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` "
|
||||
+ "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"))
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS `playlists` "
|
||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "`name` TEXT, `thumbnail_url` TEXT)");
|
||||
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
|
||||
+ "`name` TEXT, `thumbnail_url` TEXT)"))
|
||||
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)")
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
|
||||
+ "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, "
|
||||
+ "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), "
|
||||
+ "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE UNIQUE INDEX "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
|
||||
database.execSQL(("CREATE UNIQUE INDEX "
|
||||
+ "`index_playlist_stream_join_playlist_id_join_index` "
|
||||
+ "ON `playlist_stream_join` (`playlist_id`, `join_index`)");
|
||||
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` "
|
||||
+ "ON `playlist_stream_join` (`stream_id`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` "
|
||||
+ "ON `playlist_stream_join` (`playlist_id`, `join_index`)"))
|
||||
database.execSQL(("CREATE INDEX `index_playlist_stream_join_stream_id` "
|
||||
+ "ON `playlist_stream_join` (`stream_id`)"))
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS `remote_playlists` "
|
||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||
+ "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
|
||||
database.execSQL("CREATE INDEX `index_remote_playlists_name` "
|
||||
+ "ON `remote_playlists` (`name`)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||
+ "ON `remote_playlists` (`service_id`, `url`)");
|
||||
+ "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"))
|
||||
database.execSQL(("CREATE INDEX `index_remote_playlists_name` "
|
||||
+ "ON `remote_playlists` (`name`)"))
|
||||
database.execSQL(("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||
+ "ON `remote_playlists` (`service_id`, `url`)"))
|
||||
|
||||
// Populate streams table with existing entries in watch history
|
||||
// Latest data first, thus ignoring older entries with the same indices
|
||||
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, "
|
||||
database.execSQL(("INSERT OR IGNORE INTO streams (service_id, url, title, "
|
||||
+ "stream_type, duration, uploader, thumbnail_url) "
|
||||
|
||||
+ "SELECT service_id, url, title, 'VIDEO_STREAM', duration, "
|
||||
+ "uploader, thumbnail_url "
|
||||
|
||||
+ "FROM watch_history "
|
||||
+ "ORDER BY creation_date DESC");
|
||||
+ "ORDER BY creation_date DESC"))
|
||||
|
||||
// Once the streams have PKs, join them with the normalized history table
|
||||
// and populate it with the remaining data from watch history
|
||||
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)"
|
||||
database.execSQL(("INSERT INTO stream_history (stream_id, access_date, repeat_count)"
|
||||
+ "SELECT uid, creation_date, 1 "
|
||||
+ "FROM watch_history INNER JOIN streams "
|
||||
+ "ON watch_history.service_id == streams.service_id "
|
||||
+ "AND watch_history.url == streams.url "
|
||||
+ "ORDER BY creation_date DESC");
|
||||
|
||||
database.execSQL("DROP TABLE IF EXISTS watch_history");
|
||||
|
||||
+ "ORDER BY creation_date DESC"))
|
||||
database.execSQL("DROP TABLE IF EXISTS watch_history")
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Stop migrating database");
|
||||
Log.d(TAG, "Stop migrating database")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
}
|
||||
val MIGRATION_2_3: Migration = object : Migration(DB_VER_2, DB_VER_3) {
|
||||
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// Add NOT NULLs and new fields
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS streams_new "
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS streams_new "
|
||||
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, "
|
||||
+ "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, "
|
||||
+ "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, "
|
||||
+ "textual_upload_date TEXT, upload_date INTEGER, "
|
||||
+ "is_upload_date_approximation INTEGER)");
|
||||
|
||||
database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
|
||||
+ "is_upload_date_approximation INTEGER)"))
|
||||
database.execSQL(("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
|
||||
+ "duration, uploader, thumbnail_url, view_count, textual_upload_date, "
|
||||
+ "upload_date, is_upload_date_approximation) "
|
||||
|
||||
+ "SELECT uid, service_id, url, ifnull(title, ''), "
|
||||
+ "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), "
|
||||
+ "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL "
|
||||
|
||||
+ "FROM streams WHERE url IS NOT NULL");
|
||||
|
||||
database.execSQL("DROP TABLE streams");
|
||||
database.execSQL("ALTER TABLE streams_new RENAME TO streams");
|
||||
database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url "
|
||||
+ "ON streams (service_id, url)");
|
||||
+ "FROM streams WHERE url IS NOT NULL"))
|
||||
database.execSQL("DROP TABLE streams")
|
||||
database.execSQL("ALTER TABLE streams_new RENAME TO streams")
|
||||
database.execSQL(("CREATE UNIQUE INDEX index_streams_service_id_url "
|
||||
+ "ON streams (service_id, url)"))
|
||||
|
||||
// Tables for feed feature
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed "
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS feed "
|
||||
+ "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY(stream_id, subscription_id), "
|
||||
+ "FOREIGN KEY(stream_id) REFERENCES streams(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
|
||||
database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)")
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS feed_group "
|
||||
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, "
|
||||
+ "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)");
|
||||
database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
|
||||
+ "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)"))
|
||||
database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)")
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
|
||||
+ "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY(group_id, subscription_id), "
|
||||
+ "FOREIGN KEY(group_id) REFERENCES feed_group(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id "
|
||||
+ "ON feed_group_subscription_join (subscription_id)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
|
||||
database.execSQL(("CREATE INDEX index_feed_group_subscription_join_subscription_id "
|
||||
+ "ON feed_group_subscription_join (subscription_id)"))
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS feed_last_updated "
|
||||
+ "(subscription_id INTEGER NOT NULL, last_updated INTEGER, "
|
||||
+ "PRIMARY KEY(subscription_id), "
|
||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
}
|
||||
val MIGRATION_3_4: Migration = object : Migration(DB_VER_3, DB_VER_4) {
|
||||
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_4_5 = new Migration(DB_VER_4, DB_VER_5) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
||||
+ "INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
val MIGRATION_4_5: Migration = object : Migration(DB_VER_4, DB_VER_5) {
|
||||
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
||||
+ "INTEGER NOT NULL DEFAULT 0"))
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_5_6 = new Migration(DB_VER_5, DB_VER_6) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` "
|
||||
+ "INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
val MIGRATION_5_6: Migration = object : Migration(DB_VER_5, DB_VER_6) {
|
||||
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(("ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` "
|
||||
+ "INTEGER NOT NULL DEFAULT 0"))
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_6_7 = new Migration(DB_VER_6, DB_VER_7) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
}
|
||||
val MIGRATION_6_7: Migration = object : Migration(DB_VER_6, DB_VER_7) {
|
||||
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// Create a new column thumbnail_stream_id
|
||||
database.execSQL("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
|
||||
+ "INTEGER NOT NULL DEFAULT -1");
|
||||
database.execSQL(("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
|
||||
+ "INTEGER NOT NULL DEFAULT -1"))
|
||||
|
||||
// Migrate the thumbnail_url to the thumbnail_stream_id
|
||||
database.execSQL("UPDATE playlists SET thumbnail_stream_id = ("
|
||||
database.execSQL(("UPDATE playlists SET thumbnail_stream_id = ("
|
||||
+ " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END"
|
||||
+ " FROM ("
|
||||
+ " SELECT p.uid AS playlist_uid, s.uid AS stream_uid"
|
||||
|
@ -216,92 +190,81 @@ public final class Migrations {
|
|||
+ " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id"
|
||||
+ " LEFT JOIN streams s ON s.uid = ps.stream_id"
|
||||
+ " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table"
|
||||
+ " WHERE playlist_uid = playlists.uid)");
|
||||
+ " WHERE playlist_uid = playlists.uid)"))
|
||||
|
||||
// Remove the thumbnail_url field in the playlist table
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists_new`"
|
||||
database.execSQL(("CREATE TABLE IF NOT EXISTS `playlists_new`"
|
||||
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "name TEXT, "
|
||||
+ "is_thumbnail_permanent INTEGER NOT NULL, "
|
||||
+ "thumbnail_stream_id INTEGER NOT NULL)");
|
||||
|
||||
database.execSQL("INSERT INTO playlists_new"
|
||||
+ "thumbnail_stream_id INTEGER NOT NULL)"))
|
||||
database.execSQL(("INSERT INTO playlists_new"
|
||||
+ " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id "
|
||||
+ " FROM playlists");
|
||||
|
||||
|
||||
database.execSQL("DROP TABLE playlists");
|
||||
database.execSQL("ALTER TABLE playlists_new RENAME TO playlists");
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS "
|
||||
+ "`index_playlists_name` ON `playlists` (`name`)");
|
||||
+ " FROM playlists"))
|
||||
database.execSQL("DROP TABLE playlists")
|
||||
database.execSQL("ALTER TABLE playlists_new RENAME TO playlists")
|
||||
database.execSQL(("CREATE INDEX IF NOT EXISTS "
|
||||
+ "`index_playlists_name` ON `playlists` (`name`)"))
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
||||
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)");
|
||||
database.execSQL("UPDATE search_history SET search = trim(search)");
|
||||
}
|
||||
val MIGRATION_7_8: Migration = object : Migration(DB_VER_7, DB_VER_8) {
|
||||
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
||||
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)"))
|
||||
database.execSQL("UPDATE search_history SET search = trim(search)")
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
}
|
||||
val MIGRATION_8_9: Migration = object : Migration(DB_VER_8, DB_VER_9) {
|
||||
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||
try {
|
||||
database.beginTransaction();
|
||||
database.beginTransaction()
|
||||
|
||||
// Update playlists.
|
||||
// Create a temp table to initialize display_index.
|
||||
database.execSQL("CREATE TABLE `playlists_tmp` "
|
||||
database.execSQL(("CREATE TABLE `playlists_tmp` "
|
||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
||||
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
||||
+ "`display_index` INTEGER NOT NULL)");
|
||||
database.execSQL("INSERT INTO `playlists_tmp` "
|
||||
+ "`display_index` INTEGER NOT NULL)"))
|
||||
database.execSQL(("INSERT INTO `playlists_tmp` "
|
||||
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||
+ "`display_index`) "
|
||||
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||
+ "-1 "
|
||||
+ "FROM `playlists`");
|
||||
+ "FROM `playlists`"))
|
||||
|
||||
// Replace the old table, note that this also removes the index on the name which
|
||||
// we don't need anymore.
|
||||
database.execSQL("DROP TABLE `playlists`");
|
||||
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
|
||||
database.execSQL("DROP TABLE `playlists`")
|
||||
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`")
|
||||
|
||||
|
||||
// Update remote_playlists.
|
||||
// Create a temp table to initialize display_index.
|
||||
database.execSQL("CREATE TABLE `remote_playlists_tmp` "
|
||||
database.execSQL(("CREATE TABLE `remote_playlists_tmp` "
|
||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
||||
+ "`display_index` INTEGER NOT NULL,"
|
||||
+ "`stream_count` INTEGER)");
|
||||
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
||||
+ "`stream_count` INTEGER)"))
|
||||
database.execSQL(("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
||||
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
||||
+ "`stream_count`)"
|
||||
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
|
||||
+ "-1, `stream_count` FROM `remote_playlists`");
|
||||
+ "-1, `stream_count` FROM `remote_playlists`"))
|
||||
|
||||
// Replace the old table, note that this also removes the index on the name which
|
||||
// we don't need anymore.
|
||||
database.execSQL("DROP TABLE `remote_playlists`");
|
||||
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
|
||||
database.execSQL("DROP TABLE `remote_playlists`")
|
||||
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`")
|
||||
|
||||
// Create index on the new table.
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||
+ "ON `remote_playlists` (`service_id`, `url`)");
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
database.execSQL(("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||
+ "ON `remote_playlists` (`service_id`, `url`)"))
|
||||
database.setTransactionSuccessful()
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
database.endTransaction()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private Migrations() {
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
|
||||
public interface HistoryDAO<T> extends BasicDAO<T> {
|
||||
T getLatestEntry();
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.schabi.newpipe.database.history.dao
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
|
||||
open interface HistoryDAO<T> : BasicDAO<T> {
|
||||
fun getLatestEntry(): T
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
|
||||
|
||||
@Dao
|
||||
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||
String ORDER_BY_MAX_CREATION_DATE = " ORDER BY MAX(" + CREATION_DATE + ") DESC";
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME
|
||||
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||
@Nullable
|
||||
SearchHistoryEntry getLatestEntry();
|
||||
|
||||
@Query("DELETE FROM " + TABLE_NAME)
|
||||
@Override
|
||||
int deleteAll();
|
||||
|
||||
@Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query")
|
||||
int deleteAllWhereQuery(String query);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> getAll();
|
||||
|
||||
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH
|
||||
+ ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
||||
Flowable<List<String>> getUniqueEntries(int limit);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME
|
||||
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
||||
+ " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit")
|
||||
Flowable<List<String>> getSimilarEntries(String query, int limit);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package org.schabi.newpipe.database.history.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||
|
||||
@Dao
|
||||
open interface SearchHistoryDAO : HistoryDAO<SearchHistoryEntry?> {
|
||||
@Query(("SELECT * FROM " + SearchHistoryEntry.TABLE_NAME
|
||||
+ " WHERE " + SearchHistoryEntry.ID + " = (SELECT MAX(" + SearchHistoryEntry.ID + ") FROM " + SearchHistoryEntry.TABLE_NAME + ")"))
|
||||
public override fun getLatestEntry(): SearchHistoryEntry?
|
||||
@Query("DELETE FROM " + SearchHistoryEntry.TABLE_NAME)
|
||||
public override fun deleteAll(): Int
|
||||
|
||||
@Query("DELETE FROM " + SearchHistoryEntry.TABLE_NAME + " WHERE " + SearchHistoryEntry.SEARCH + " = :query")
|
||||
fun deleteAllWhereQuery(query: String?): Int
|
||||
@Query("SELECT * FROM " + SearchHistoryEntry.TABLE_NAME + ORDER_BY_CREATION_DATE)
|
||||
public override fun getAll(): Flowable<MutableList<SearchHistoryEntry?>>?
|
||||
|
||||
@Query(("SELECT " + SearchHistoryEntry.SEARCH + " FROM " + SearchHistoryEntry.TABLE_NAME + " GROUP BY " + SearchHistoryEntry.SEARCH
|
||||
+ ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit"))
|
||||
fun getUniqueEntries(limit: Int): Flowable<List<String?>?>?
|
||||
|
||||
@Query(("SELECT * FROM " + SearchHistoryEntry.TABLE_NAME
|
||||
+ " WHERE " + SearchHistoryEntry.SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE))
|
||||
public override fun listByService(serviceId: Int): Flowable<MutableList<SearchHistoryEntry?>>?
|
||||
|
||||
@Query(("SELECT " + SearchHistoryEntry.SEARCH + " FROM " + SearchHistoryEntry.TABLE_NAME + " WHERE " + SearchHistoryEntry.SEARCH + " LIKE :query || '%'"
|
||||
+ " GROUP BY " + SearchHistoryEntry.SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit"))
|
||||
fun getSimilarEntries(query: String?, limit: Int): Flowable<List<String?>?>?
|
||||
|
||||
companion object {
|
||||
val ORDER_BY_CREATION_DATE: String = " ORDER BY " + SearchHistoryEntry.CREATION_DATE + " DESC"
|
||||
val ORDER_BY_MAX_CREATION_DATE: String = " ORDER BY MAX(" + SearchHistoryEntry.CREATION_DATE + ") DESC"
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns;
|
||||
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE
|
||||
+ " WHERE " + STREAM_ACCESS_DATE + " = "
|
||||
+ "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
|
||||
@Override
|
||||
@Nullable
|
||||
public abstract StreamHistoryEntity getLatestEntry();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE)
|
||||
public abstract Flowable<List<StreamHistoryEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE)
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<StreamHistoryEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_TABLE
|
||||
+ " INNER JOIN " + STREAM_HISTORY_TABLE
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_TABLE
|
||||
+ " INNER JOIN " + STREAM_HISTORY_TABLE
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " ORDER BY " + STREAM_ID + " ASC")
|
||||
public abstract Flowable<List<StreamHistoryEntry>> getHistorySortedById();
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID
|
||||
+ " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
||||
@Nullable
|
||||
public abstract StreamHistoryEntity getLatestEntry(long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract int deleteStreamHistory(long streamId);
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Query("SELECT * FROM " + STREAM_TABLE
|
||||
|
||||
// Select the latest entry and watch count for each stream id on history table
|
||||
+ " INNER JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + ", "
|
||||
+ " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", "
|
||||
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
|
||||
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
|
||||
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
|
||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package org.schabi.newpipe.database.history.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
|
||||
@Dao
|
||||
abstract class StreamHistoryDAO() : HistoryDAO<StreamHistoryEntity?> {
|
||||
@Query(("SELECT * FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
|
||||
+ " WHERE " + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + " = "
|
||||
+ "(SELECT MAX(" + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + ") FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE + ")"))
|
||||
abstract override fun getLatestEntry(): StreamHistoryEntity?
|
||||
@Query("SELECT * FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE)
|
||||
abstract override fun getAll(): Flowable<List<StreamHistoryEntity>?>?
|
||||
@Query("DELETE FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE)
|
||||
abstract override fun deleteAll(): Int
|
||||
public override fun listByService(serviceId: Int): Flowable<MutableList<StreamHistoryEntity?>>? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Query(("SELECT * FROM " + StreamEntity.STREAM_TABLE
|
||||
+ " INNER JOIN " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + StreamHistoryEntity.Companion.JOIN_STREAM_ID
|
||||
+ " ORDER BY " + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + " DESC"))
|
||||
abstract fun getHistory(): Flowable<List<StreamHistoryEntry?>?>?
|
||||
|
||||
@Query(("SELECT * FROM " + StreamEntity.STREAM_TABLE
|
||||
+ " INNER JOIN " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + StreamHistoryEntity.Companion.JOIN_STREAM_ID
|
||||
+ " ORDER BY " + StreamEntity.STREAM_ID + " ASC"))
|
||||
abstract fun getHistorySortedById(): Flowable<List<StreamHistoryEntry?>?>
|
||||
|
||||
@Query(("SELECT * FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE + " WHERE " + StreamHistoryEntity.Companion.JOIN_STREAM_ID
|
||||
+ " = :streamId ORDER BY " + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + " DESC LIMIT 1"))
|
||||
abstract fun getLatestEntry(streamId: Long): StreamHistoryEntity?
|
||||
@Query("DELETE FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE + " WHERE " + StreamHistoryEntity.Companion.JOIN_STREAM_ID + " = :streamId")
|
||||
abstract fun deleteStreamHistory(streamId: Long): Int
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Query(("SELECT * FROM " + StreamEntity.STREAM_TABLE // Select the latest entry and watch count for each stream id on history table
|
||||
+ " INNER JOIN "
|
||||
+ "(SELECT " + StreamHistoryEntity.Companion.JOIN_STREAM_ID + ", "
|
||||
+ " MAX(" + StreamHistoryEntity.Companion.STREAM_ACCESS_DATE + ") AS " + StreamStatisticsEntry.STREAM_LATEST_DATE + ", "
|
||||
+ " SUM(" + StreamHistoryEntity.Companion.STREAM_REPEAT_COUNT + ") AS " + StreamStatisticsEntry.STREAM_WATCH_COUNT
|
||||
+ " FROM " + StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE + " GROUP BY " + StreamHistoryEntity.Companion.JOIN_STREAM_ID + ")"
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + StreamHistoryEntity.Companion.JOIN_STREAM_ID
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + StreamHistoryEntity.Companion.JOIN_STREAM_ID + " AS " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS))
|
||||
abstract fun getStatistics(): Flowable<List<StreamStatisticsEntry?>?>
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Index;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
|
||||
@Entity(tableName = STREAM_HISTORY_TABLE,
|
||||
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
|
||||
// No need to index for timestamp as they will almost always be unique
|
||||
indices = {@Index(value = {JOIN_STREAM_ID})},
|
||||
foreignKeys = {
|
||||
@ForeignKey(entity = StreamEntity.class,
|
||||
parentColumns = StreamEntity.STREAM_ID,
|
||||
childColumns = JOIN_STREAM_ID,
|
||||
onDelete = CASCADE, onUpdate = CASCADE)
|
||||
})
|
||||
public class StreamHistoryEntity {
|
||||
public static final String STREAM_HISTORY_TABLE = "stream_history";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
public static final String STREAM_ACCESS_DATE = "access_date";
|
||||
public static final String STREAM_REPEAT_COUNT = "repeat_count";
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(name = STREAM_ACCESS_DATE)
|
||||
private OffsetDateTime accessDate;
|
||||
|
||||
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||
private long repeatCount;
|
||||
|
||||
/**
|
||||
* @param streamUid the stream id this history item will refer to
|
||||
* @param accessDate the last time the stream was accessed
|
||||
* @param repeatCount the total number of views this stream received
|
||||
*/
|
||||
public StreamHistoryEntity(final long streamUid,
|
||||
@NonNull final OffsetDateTime accessDate,
|
||||
final long repeatCount) {
|
||||
this.streamUid = streamUid;
|
||||
this.accessDate = accessDate;
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(final long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OffsetDateTime getAccessDate() {
|
||||
return accessDate;
|
||||
}
|
||||
|
||||
public void setAccessDate(@NonNull final OffsetDateTime accessDate) {
|
||||
this.accessDate = accessDate;
|
||||
}
|
||||
|
||||
public long getRepeatCount() {
|
||||
return repeatCount;
|
||||
}
|
||||
|
||||
public void setRepeatCount(final long repeatCount) {
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.schabi.newpipe.database.history.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(tableName = StreamHistoryEntity.STREAM_HISTORY_TABLE, primaryKeys = [StreamHistoryEntity.JOIN_STREAM_ID, StreamHistoryEntity.STREAM_ACCESS_DATE], indices = [Index(value = [StreamHistoryEntity.JOIN_STREAM_ID])], foreignKeys = [ForeignKey(entity = StreamEntity::class, parentColumns = StreamEntity.STREAM_ID, childColumns = StreamHistoryEntity.JOIN_STREAM_ID, onDelete = CASCADE, onUpdate = CASCADE)])
|
||||
class StreamHistoryEntity
|
||||
/**
|
||||
* @param streamUid the stream id this history item will refer to
|
||||
* @param accessDate the last time the stream was accessed
|
||||
* @param repeatCount the total number of views this stream received
|
||||
*/(@field:ColumnInfo(name = JOIN_STREAM_ID) private var streamUid: Long,
|
||||
@field:ColumnInfo(name = STREAM_ACCESS_DATE) private var accessDate: OffsetDateTime,
|
||||
@field:ColumnInfo(name = STREAM_REPEAT_COUNT) private var repeatCount: Long) {
|
||||
fun getStreamUid(): Long {
|
||||
return streamUid
|
||||
}
|
||||
|
||||
fun setStreamUid(streamUid: Long) {
|
||||
this.streamUid = streamUid
|
||||
}
|
||||
|
||||
fun getAccessDate(): OffsetDateTime {
|
||||
return accessDate
|
||||
}
|
||||
|
||||
fun setAccessDate(accessDate: OffsetDateTime) {
|
||||
this.accessDate = accessDate
|
||||
}
|
||||
|
||||
fun getRepeatCount(): Long {
|
||||
return repeatCount
|
||||
}
|
||||
|
||||
fun setRepeatCount(repeatCount: Long) {
|
||||
this.repeatCount = repeatCount
|
||||
}
|
||||
|
||||
companion object {
|
||||
val STREAM_HISTORY_TABLE: String = "stream_history"
|
||||
val JOIN_STREAM_ID: String = "stream_id"
|
||||
val STREAM_ACCESS_DATE: String = "access_date"
|
||||
val STREAM_REPEAT_COUNT: String = "repeat_count"
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
|
||||
/**
|
||||
* This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing
|
||||
* how many times a specific stream is already contained inside a local playlist. Used to be able
|
||||
* to grey out playlists which already contain the current stream in the playlist append dialog.
|
||||
* @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String)
|
||||
*/
|
||||
public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
|
||||
public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained";
|
||||
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
||||
public final long timesStreamIsContained;
|
||||
|
||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||
public PlaylistDuplicatesEntry(final long uid,
|
||||
final String name,
|
||||
final String thumbnailUrl,
|
||||
final boolean isThumbnailPermanent,
|
||||
final long thumbnailStreamId,
|
||||
final long displayIndex,
|
||||
final long streamCount,
|
||||
final long timesStreamIsContained) {
|
||||
super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
|
||||
streamCount);
|
||||
this.timesStreamIsContained = timesStreamIsContained;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package org.schabi.newpipe.database.playlist
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
|
||||
/**
|
||||
* This class adds a field to [PlaylistMetadataEntry] that contains an integer representing
|
||||
* how many times a specific stream is already contained inside a local playlist. Used to be able
|
||||
* to grey out playlists which already contain the current stream in the playlist append dialog.
|
||||
* @see org.schabi.newpipe.local.playlist.LocalPlaylistManager.getPlaylistDuplicates
|
||||
*/
|
||||
class PlaylistDuplicatesEntry(uid: Long,
|
||||
name: String,
|
||||
thumbnailUrl: String,
|
||||
isThumbnailPermanent: Boolean,
|
||||
thumbnailStreamId: Long,
|
||||
displayIndex: Long,
|
||||
streamCount: Long,
|
||||
@field:ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED) val timesStreamIsContained: Long) : PlaylistMetadataEntry(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex,
|
||||
streamCount) {
|
||||
companion object {
|
||||
val PLAYLIST_TIMES_STREAM_IS_CONTAINED: String = "timesStreamIsContained"
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
|
||||
public interface PlaylistLocalItem extends LocalItem {
|
||||
String getOrderingName();
|
||||
|
||||
long getDisplayIndex();
|
||||
|
||||
long getUid();
|
||||
|
||||
void setDisplayIndex(long displayIndex);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.schabi.newpipe.database.playlist
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
|
||||
open interface PlaylistLocalItem : LocalItem {
|
||||
fun getOrderingName(): String
|
||||
fun getDisplayIndex(): Long
|
||||
fun getUid(): Long
|
||||
fun setDisplayIndex(displayIndex: Long)
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||
|
||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_ID)
|
||||
private final long uid;
|
||||
@ColumnInfo(name = PLAYLIST_NAME)
|
||||
public final String name;
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||
private final boolean isThumbnailPermanent;
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||
private final long thumbnailStreamId;
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||
public final String thumbnailUrl;
|
||||
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||
private long displayIndex;
|
||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||
public final long streamCount;
|
||||
|
||||
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
||||
final boolean isThumbnailPermanent, final long thumbnailStreamId,
|
||||
final long displayIndex, final long streamCount) {
|
||||
this.uid = uid;
|
||||
this.name = name;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||
this.thumbnailStreamId = thumbnailStreamId;
|
||||
this.displayIndex = displayIndex;
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalItemType getLocalItemType() {
|
||||
return LocalItemType.PLAYLIST_LOCAL_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOrderingName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isThumbnailPermanent() {
|
||||
return isThumbnailPermanent;
|
||||
}
|
||||
|
||||
public long getThumbnailStreamId() {
|
||||
return thumbnailStreamId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDisplayIndex() {
|
||||
return displayIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisplayIndex(final long displayIndex) {
|
||||
this.displayIndex = displayIndex;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package org.schabi.newpipe.database.playlist
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import org.schabi.newpipe.database.LocalItem.LocalItemType
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||
|
||||
open class PlaylistMetadataEntry(@field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_ID) private val uid: Long, @JvmField @field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_NAME) val name: String, @field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_URL) val thumbnailUrl: String,
|
||||
@field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_PERMANENT) private val isThumbnailPermanent: Boolean, @field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID) private val thumbnailStreamId: Long,
|
||||
@field:ColumnInfo(name = PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX) private var displayIndex: Long, @field:ColumnInfo(name = PLAYLIST_STREAM_COUNT) val streamCount: Long) : PlaylistLocalItem {
|
||||
public override fun getLocalItemType(): LocalItemType {
|
||||
return LocalItemType.PLAYLIST_LOCAL_ITEM
|
||||
}
|
||||
|
||||
public override fun getOrderingName(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
fun isThumbnailPermanent(): Boolean {
|
||||
return isThumbnailPermanent
|
||||
}
|
||||
|
||||
fun getThumbnailStreamId(): Long {
|
||||
return thumbnailStreamId
|
||||
}
|
||||
|
||||
public override fun getDisplayIndex(): Long {
|
||||
return displayIndex
|
||||
}
|
||||
|
||||
public override fun getUid(): Long {
|
||||
return uid
|
||||
}
|
||||
|
||||
public override fun setDisplayIndex(displayIndex: Long) {
|
||||
this.displayIndex = displayIndex
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PLAYLIST_STREAM_COUNT: String = "streamCount"
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist.dao;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
|
||||
@Dao
|
||||
public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + PLAYLIST_TABLE)
|
||||
Flowable<List<PlaylistEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + PLAYLIST_TABLE)
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
default Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
int deletePlaylist(long playlistId);
|
||||
|
||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||
Flowable<Long> getCount();
|
||||
|
||||
@Transaction
|
||||
default long upsertPlaylist(final PlaylistEntity playlist) {
|
||||
final long playlistId = playlist.getUid();
|
||||
|
||||
if (playlistId == -1) {
|
||||
// This situation is probably impossible.
|
||||
return insert(playlist);
|
||||
} else {
|
||||
update(playlist);
|
||||
return playlistId;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package org.schabi.newpipe.database.playlist.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||
|
||||
@Dao
|
||||
open interface PlaylistDAO : BasicDAO<PlaylistEntity?> {
|
||||
@Query("SELECT * FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE)
|
||||
public override fun getAll(): Flowable<List<PlaylistEntity?>?>?
|
||||
@Query("DELETE FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE)
|
||||
public override fun deleteAll(): Int
|
||||
public override fun listByService(serviceId: Int): Flowable<List<PlaylistEntity?>?>? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE + " WHERE " + PlaylistEntity.Companion.PLAYLIST_ID + " = :playlistId")
|
||||
fun getPlaylist(playlistId: Long): Flowable<List<PlaylistEntity?>>
|
||||
|
||||
@Query("DELETE FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE + " WHERE " + PlaylistEntity.Companion.PLAYLIST_ID + " = :playlistId")
|
||||
fun deletePlaylist(playlistId: Long): Int
|
||||
|
||||
@Query("SELECT COUNT(*) FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE)
|
||||
fun getCount(): Flowable<Long?>
|
||||
|
||||
@Transaction
|
||||
fun upsertPlaylist(playlist: PlaylistEntity): Long {
|
||||
val playlistId: Long = playlist.getUid()
|
||||
if (playlistId == -1L) {
|
||||
// This situation is probably impossible.
|
||||
return insert(playlist)
|
||||
} else {
|
||||
update(playlist)
|
||||
return playlistId
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist.dao;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
||||
|
||||
@Dao
|
||||
public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
|
||||
Flowable<List<PlaylistRemoteEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long playlistId);
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||
Flowable<List<PlaylistRemoteEntity>> getPlaylists();
|
||||
|
||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
Long getPlaylistIdInternal(long serviceId, String url);
|
||||
|
||||
@Transaction
|
||||
default long upsert(final PlaylistRemoteEntity playlist) {
|
||||
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
||||
|
||||
if (playlistId == null) {
|
||||
return insert(playlist);
|
||||
} else {
|
||||
playlist.setUid(playlistId);
|
||||
update(playlist);
|
||||
return playlistId;
|
||||
}
|
||||
}
|
||||
|
||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||
int deletePlaylist(long playlistId);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package org.schabi.newpipe.database.playlist.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||
|
||||
@Dao
|
||||
open interface PlaylistRemoteDAO : BasicDAO<PlaylistRemoteEntity?> {
|
||||
@Query("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE)
|
||||
public override fun getAll(): Flowable<List<PlaylistRemoteEntity?>?>?
|
||||
@Query("DELETE FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE)
|
||||
public override fun deleteAll(): Int
|
||||
|
||||
@Query(("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId"))
|
||||
public override fun listByService(serviceId: Int): Flowable<List<PlaylistRemoteEntity?>?>?
|
||||
|
||||
@Query(("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_ID + " = :playlistId"))
|
||||
fun getPlaylist(playlistId: Long): Flowable<List<PlaylistRemoteEntity?>?>?
|
||||
|
||||
@Query(("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_URL + " = :url AND " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId"))
|
||||
fun getPlaylist(serviceId: Long, url: String?): Flowable<List<PlaylistRemoteEntity?>?>
|
||||
|
||||
@Query(("SELECT * FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
|
||||
+ " ORDER BY " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_DISPLAY_INDEX))
|
||||
fun getPlaylists(): Flowable<List<PlaylistRemoteEntity?>?>
|
||||
|
||||
@Query(("SELECT " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_ID + " FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_URL + " = :url "
|
||||
+ "AND " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId"))
|
||||
fun getPlaylistIdInternal(serviceId: Long, url: String?): Long
|
||||
|
||||
@Transaction
|
||||
fun upsert(playlist: PlaylistRemoteEntity): Long {
|
||||
val playlistId: Long = getPlaylistIdInternal(playlist.getServiceId().toLong(), playlist.getUrl())
|
||||
if (playlistId == null) {
|
||||
return insert(playlist)
|
||||
} else {
|
||||
playlist.setUid(playlistId)
|
||||
update(playlist)
|
||||
return playlistId
|
||||
}
|
||||
}
|
||||
|
||||
@Query(("DELETE FROM " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_ID + " = :playlistId"))
|
||||
fun deletePlaylist(playlistId: Long): Int
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist.dao;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns;
|
||||
import androidx.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||
Flowable<List<PlaylistStreamEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
default Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
void deleteBatch(long playlistId);
|
||||
|
||||
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
|
||||
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
Flowable<Integer> getMaximumIndexOf(long playlistId);
|
||||
|
||||
@Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_ID
|
||||
+ " ELSE " + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " END"
|
||||
+ " FROM " + STREAM_TABLE
|
||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId "
|
||||
+ " LIMIT 1"
|
||||
)
|
||||
Flowable<Long> getAutomaticThumbnailStreamId(long playlistId);
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Transaction
|
||||
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
|
||||
// get ids of streams of the given playlist
|
||||
+ "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
|
||||
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
|
||||
|
||||
// then merge with the stream metadata
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
|
||||
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
||||
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||
|
||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||
+ " ELSE (SELECT " + STREAM_THUMBNAIL_URL
|
||||
+ " FROM " + STREAM_TABLE
|
||||
+ " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
|
||||
+ " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||
|
||||
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
|
||||
+ " FROM " + PLAYLIST_TABLE
|
||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||
+ " GROUP BY " + PLAYLIST_ID
|
||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Transaction
|
||||
@Query("SELECT *, MIN(" + JOIN_INDEX + ")"
|
||||
+ " FROM " + STREAM_TABLE + " INNER JOIN"
|
||||
+ " (SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
|
||||
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
+ " GROUP BY " + STREAM_ID
|
||||
+ " ORDER BY MIN(" + JOIN_INDEX + ") ASC")
|
||||
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", "
|
||||
+ PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||
+ PLAYLIST_DISPLAY_INDEX + ", "
|
||||
|
||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||
+ " ELSE (SELECT " + STREAM_THUMBNAIL_URL
|
||||
+ " FROM " + STREAM_TABLE
|
||||
+ " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
|
||||
+ " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||
|
||||
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", "
|
||||
+ "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS "
|
||||
+ PLAYLIST_TIMES_STREAM_IS_CONTAINED
|
||||
|
||||
+ " FROM " + PLAYLIST_TABLE
|
||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||
|
||||
+ " LEFT JOIN " + STREAM_TABLE
|
||||
+ " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " AND :streamUrl = :streamUrl"
|
||||
|
||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
|
||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package org.schabi.newpipe.database.playlist.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
|
||||
@Dao
|
||||
open interface PlaylistStreamDAO : BasicDAO<PlaylistStreamEntity?> {
|
||||
@Query("SELECT * FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE)
|
||||
public override fun getAll(): Flowable<List<PlaylistStreamEntity?>?>?
|
||||
@Query("DELETE FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE)
|
||||
public override fun deleteAll(): Int
|
||||
public override fun listByService(serviceId: Int): Flowable<List<PlaylistStreamEntity?>?>? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Query(("DELETE FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId"))
|
||||
fun deleteBatch(playlistId: Long)
|
||||
|
||||
@Query(("SELECT COALESCE(MAX(" + PlaylistStreamEntity.Companion.JOIN_INDEX + "), -1)"
|
||||
+ " FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId"))
|
||||
fun getMaximumIndexOf(playlistId: Long): Flowable<Int?>
|
||||
|
||||
@Query(("SELECT CASE WHEN COUNT(*) != 0 then " + StreamEntity.STREAM_ID
|
||||
+ " ELSE " + PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID + " END"
|
||||
+ " FROM " + StreamEntity.STREAM_TABLE
|
||||
+ " LEFT JOIN " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID
|
||||
+ " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId "
|
||||
+ " LIMIT 1"))
|
||||
fun getAutomaticThumbnailStreamId(playlistId: Long): Flowable<Long>
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Transaction
|
||||
@Query(("SELECT * FROM " + StreamEntity.STREAM_TABLE + " INNER JOIN " // get ids of streams of the given playlist
|
||||
+ "(SELECT " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID + "," + PlaylistStreamEntity.Companion.JOIN_INDEX
|
||||
+ " FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId)" // then merge with the stream metadata
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID + " AS " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS
|
||||
+ " ORDER BY " + PlaylistStreamEntity.Companion.JOIN_INDEX + " ASC"))
|
||||
fun getOrderedStreamsOf(playlistId: Long): Flowable<List<PlaylistStreamEntry?>?>
|
||||
|
||||
@Transaction
|
||||
@Query(("SELECT " + PlaylistEntity.Companion.PLAYLIST_ID + ", " + PlaylistEntity.Companion.PLAYLIST_NAME + ", "
|
||||
+ PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_PERMANENT + ", " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||
+ PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX + ", "
|
||||
+ " CASE WHEN " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||
+ PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + PlaylistEntity.Companion.DEFAULT_THUMBNAIL + "'"
|
||||
+ " ELSE (SELECT " + StreamEntity.STREAM_THUMBNAIL_URL
|
||||
+ " FROM " + StreamEntity.STREAM_TABLE
|
||||
+ " WHERE " + StreamEntity.STREAM_TABLE + "." + StreamEntity.STREAM_ID + " = " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID
|
||||
+ " ) END AS " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_URL + ", "
|
||||
+ "COALESCE(COUNT(" + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + "), 0) AS " + PlaylistMetadataEntry.Companion.PLAYLIST_STREAM_COUNT
|
||||
+ " FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE
|
||||
+ " LEFT JOIN " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + PlaylistEntity.Companion.PLAYLIST_TABLE + "." + PlaylistEntity.Companion.PLAYLIST_ID + " = " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID
|
||||
+ " GROUP BY " + PlaylistEntity.Companion.PLAYLIST_ID
|
||||
+ " ORDER BY " + PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX))
|
||||
fun getPlaylistMetadata(): Flowable<List<PlaylistMetadataEntry?>?>
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Transaction
|
||||
@Query(("SELECT *, MIN(" + PlaylistStreamEntity.Companion.JOIN_INDEX + ")"
|
||||
+ " FROM " + StreamEntity.STREAM_TABLE + " INNER JOIN"
|
||||
+ " (SELECT " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID + "," + PlaylistStreamEntity.Companion.JOIN_INDEX
|
||||
+ " FROM " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + " = :playlistId)"
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID + " AS " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + StreamEntity.STREAM_ID + " = " + StreamStateEntity.Companion.JOIN_STREAM_ID_ALIAS
|
||||
+ " GROUP BY " + StreamEntity.STREAM_ID
|
||||
+ " ORDER BY MIN(" + PlaylistStreamEntity.Companion.JOIN_INDEX + ") ASC"))
|
||||
fun getStreamsWithoutDuplicates(playlistId: Long): Flowable<List<PlaylistStreamEntry?>?>
|
||||
|
||||
@Transaction
|
||||
@Query(("SELECT " + PlaylistEntity.Companion.PLAYLIST_TABLE + "." + PlaylistEntity.Companion.PLAYLIST_ID + ", " + PlaylistEntity.Companion.PLAYLIST_NAME + ", "
|
||||
+ PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_PERMANENT + ", " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID + ", "
|
||||
+ PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX + ", "
|
||||
+ " CASE WHEN " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||
+ PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + PlaylistEntity.Companion.DEFAULT_THUMBNAIL + "'"
|
||||
+ " ELSE (SELECT " + StreamEntity.STREAM_THUMBNAIL_URL
|
||||
+ " FROM " + StreamEntity.STREAM_TABLE
|
||||
+ " WHERE " + StreamEntity.STREAM_TABLE + "." + StreamEntity.STREAM_ID + " = " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_STREAM_ID
|
||||
+ " ) END AS " + PlaylistEntity.Companion.PLAYLIST_THUMBNAIL_URL + ", "
|
||||
+ "COALESCE(COUNT(" + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID + "), 0) AS " + PlaylistMetadataEntry.Companion.PLAYLIST_STREAM_COUNT + ", "
|
||||
+ "COALESCE(SUM(" + StreamEntity.STREAM_URL + " = :streamUrl), 0) AS "
|
||||
+ PlaylistDuplicatesEntry.Companion.PLAYLIST_TIMES_STREAM_IS_CONTAINED
|
||||
+ " FROM " + PlaylistEntity.Companion.PLAYLIST_TABLE
|
||||
+ " LEFT JOIN " + PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + PlaylistEntity.Companion.PLAYLIST_TABLE + "." + PlaylistEntity.Companion.PLAYLIST_ID + " = " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID
|
||||
+ " LEFT JOIN " + StreamEntity.STREAM_TABLE
|
||||
+ " ON " + StreamEntity.STREAM_TABLE + "." + StreamEntity.STREAM_ID + " = " + PlaylistStreamEntity.Companion.JOIN_STREAM_ID
|
||||
+ " AND :streamUrl = :streamUrl"
|
||||
+ " GROUP BY " + PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID
|
||||
+ " ORDER BY " + PlaylistEntity.Companion.PLAYLIST_DISPLAY_INDEX))
|
||||
fun getPlaylistDuplicatesMetadata(streamUrl: String?): Flowable<List<PlaylistDuplicatesEntry?>?>
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
|
||||
@Entity(tableName = PLAYLIST_TABLE)
|
||||
public class PlaylistEntity {
|
||||
|
||||
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
||||
+ R.drawable.placeholder_thumbnail_playlist;
|
||||
public static final long DEFAULT_THUMBNAIL_ID = -1;
|
||||
|
||||
public static final String PLAYLIST_TABLE = "playlists";
|
||||
public static final String PLAYLIST_ID = "uid";
|
||||
public static final String PLAYLIST_NAME = "name";
|
||||
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
public static final String PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
||||
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = PLAYLIST_ID)
|
||||
private long uid = 0;
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_NAME)
|
||||
private String name;
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||
private boolean isThumbnailPermanent;
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||
private long thumbnailStreamId;
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||
private long displayIndex;
|
||||
|
||||
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
||||
final long thumbnailStreamId, final long displayIndex) {
|
||||
this.name = name;
|
||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||
this.thumbnailStreamId = thumbnailStreamId;
|
||||
this.displayIndex = displayIndex;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public PlaylistEntity(final PlaylistMetadataEntry item) {
|
||||
this.uid = item.getUid();
|
||||
this.name = item.name;
|
||||
this.isThumbnailPermanent = item.isThumbnailPermanent();
|
||||
this.thumbnailStreamId = item.getThumbnailStreamId();
|
||||
this.displayIndex = item.getDisplayIndex();
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(final long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public long getThumbnailStreamId() {
|
||||
return thumbnailStreamId;
|
||||
}
|
||||
|
||||
public void setThumbnailStreamId(final long thumbnailStreamId) {
|
||||
this.thumbnailStreamId = thumbnailStreamId;
|
||||
}
|
||||
|
||||
public boolean getIsThumbnailPermanent() {
|
||||
return isThumbnailPermanent;
|
||||
}
|
||||
|
||||
public void setIsThumbnailPermanent(final boolean isThumbnailSet) {
|
||||
this.isThumbnailPermanent = isThumbnailSet;
|
||||
}
|
||||
|
||||
public long getDisplayIndex() {
|
||||
return displayIndex;
|
||||
}
|
||||
|
||||
public void setDisplayIndex(final long displayIndex) {
|
||||
this.displayIndex = displayIndex;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package org.schabi.newpipe.database.playlist.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||
|
||||
@Entity(tableName = PlaylistEntity.PLAYLIST_TABLE)
|
||||
class PlaylistEntity {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = PLAYLIST_ID)
|
||||
private var uid: Long = 0
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_NAME)
|
||||
private var name: String?
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||
private var isThumbnailPermanent: Boolean
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||
private var thumbnailStreamId: Long
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||
private var displayIndex: Long
|
||||
|
||||
constructor(name: String?, isThumbnailPermanent: Boolean,
|
||||
thumbnailStreamId: Long, displayIndex: Long) {
|
||||
this.name = name
|
||||
this.isThumbnailPermanent = isThumbnailPermanent
|
||||
this.thumbnailStreamId = thumbnailStreamId
|
||||
this.displayIndex = displayIndex
|
||||
}
|
||||
|
||||
@Ignore
|
||||
constructor(item: PlaylistMetadataEntry) {
|
||||
uid = item.getUid()
|
||||
name = item.name
|
||||
isThumbnailPermanent = item.isThumbnailPermanent()
|
||||
thumbnailStreamId = item.getThumbnailStreamId()
|
||||
displayIndex = item.getDisplayIndex()
|
||||
}
|
||||
|
||||
fun getUid(): Long {
|
||||
return uid
|
||||
}
|
||||
|
||||
fun setUid(uid: Long) {
|
||||
this.uid = uid
|
||||
}
|
||||
|
||||
fun getName(): String? {
|
||||
return name
|
||||
}
|
||||
|
||||
fun setName(name: String?) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun getThumbnailStreamId(): Long {
|
||||
return thumbnailStreamId
|
||||
}
|
||||
|
||||
fun setThumbnailStreamId(thumbnailStreamId: Long) {
|
||||
this.thumbnailStreamId = thumbnailStreamId
|
||||
}
|
||||
|
||||
fun getIsThumbnailPermanent(): Boolean {
|
||||
return isThumbnailPermanent
|
||||
}
|
||||
|
||||
fun setIsThumbnailPermanent(isThumbnailSet: Boolean) {
|
||||
isThumbnailPermanent = isThumbnailSet
|
||||
}
|
||||
|
||||
fun getDisplayIndex(): Long {
|
||||
return displayIndex
|
||||
}
|
||||
|
||||
fun setDisplayIndex(displayIndex: Long) {
|
||||
this.displayIndex = displayIndex
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DEFAULT_THUMBNAIL: String = ("drawable://"
|
||||
+ R.drawable.placeholder_thumbnail_playlist)
|
||||
val DEFAULT_THUMBNAIL_ID: Long = -1
|
||||
val PLAYLIST_TABLE: String = "playlists"
|
||||
val PLAYLIST_ID: String = "uid"
|
||||
val PLAYLIST_NAME: String = "name"
|
||||
val PLAYLIST_THUMBNAIL_URL: String = "thumbnail_url"
|
||||
val PLAYLIST_DISPLAY_INDEX: String = "display_index"
|
||||
val PLAYLIST_THUMBNAIL_PERMANENT: String = "is_thumbnail_permanent"
|
||||
val PLAYLIST_THUMBNAIL_STREAM_ID: String = "thumbnail_stream_id"
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
|
||||
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
||||
|
||||
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
||||
indices = {
|
||||
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||
})
|
||||
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
public static final String REMOTE_PLAYLIST_TABLE = "remote_playlists";
|
||||
public static final String REMOTE_PLAYLIST_ID = "uid";
|
||||
public static final String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
|
||||
public static final String REMOTE_PLAYLIST_NAME = "name";
|
||||
public static final String REMOTE_PLAYLIST_URL = "url";
|
||||
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||
public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index";
|
||||
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
|
||||
private long uid = 0;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID)
|
||||
private int serviceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_NAME)
|
||||
private String name;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_URL)
|
||||
private String url;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL)
|
||||
private String thumbnailUrl;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||
private String uploader;
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||
private long displayIndex = -1; // Make sure the new item is on the top
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||
private Long streamCount;
|
||||
|
||||
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
|
||||
final String thumbnailUrl, final String uploader,
|
||||
final Long streamCount) {
|
||||
this.serviceId = serviceId;
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.uploader = uploader;
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
|
||||
final String thumbnailUrl, final String uploader,
|
||||
final long displayIndex, final Long streamCount) {
|
||||
this.serviceId = serviceId;
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.uploader = uploader;
|
||||
this.displayIndex = displayIndex;
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||
// use uploader avatar when no thumbnail is available
|
||||
ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty()
|
||||
? info.getUploaderAvatars() : info.getThumbnails()),
|
||||
info.getUploaderName(), info.getStreamCount());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public boolean isIdenticalTo(final PlaylistInfo info) {
|
||||
/*
|
||||
* Returns boolean comparing the online playlist and the local copy.
|
||||
* (False if info changed such as playlist name or track count)
|
||||
*/
|
||||
return getServiceId() == info.getServiceId()
|
||||
&& getStreamCount() == info.getStreamCount()
|
||||
&& TextUtils.equals(getName(), info.getName())
|
||||
&& TextUtils.equals(getUrl(), info.getUrl())
|
||||
// we want to update the local playlist data even when either the remote thumbnail
|
||||
// URL changes, or the preferred image quality setting is changed by the user
|
||||
&& TextUtils.equals(getThumbnailUrl(),
|
||||
ImageStrategy.imageListToDbUrl(info.getThumbnails()))
|
||||
&& TextUtils.equals(getUploader(), info.getUploaderName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(final long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(final int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(final String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUploader() {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
public void setUploader(final String uploader) {
|
||||
this.uploader = uploader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDisplayIndex() {
|
||||
return displayIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisplayIndex(final long displayIndex) {
|
||||
this.displayIndex = displayIndex;
|
||||
}
|
||||
|
||||
public Long getStreamCount() {
|
||||
return streamCount;
|
||||
}
|
||||
|
||||
public void setStreamCount(final Long streamCount) {
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalItemType getLocalItemType() {
|
||||
return PLAYLIST_REMOTE_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOrderingName() {
|
||||
return name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package org.schabi.newpipe.database.playlist.model
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import org.schabi.newpipe.database.LocalItem.LocalItemType
|
||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
|
||||
@Entity(tableName = PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE, indices = [Index(value = [PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID, PlaylistRemoteEntity.REMOTE_PLAYLIST_URL], unique = true)])
|
||||
class PlaylistRemoteEntity : PlaylistLocalItem {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
|
||||
private var uid: Long = 0
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID)
|
||||
private var serviceId: Int = NO_SERVICE_ID
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_NAME)
|
||||
private var name: String
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_URL)
|
||||
private var url: String
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL)
|
||||
private var thumbnailUrl: String?
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||
private var uploader: String
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||
private var displayIndex: Long = -1 // Make sure the new item is on the top
|
||||
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||
private var streamCount: Long
|
||||
|
||||
constructor(serviceId: Int, name: String, url: String,
|
||||
thumbnailUrl: String?, uploader: String,
|
||||
streamCount: Long) {
|
||||
this.serviceId = serviceId
|
||||
this.name = name
|
||||
this.url = url
|
||||
this.thumbnailUrl = thumbnailUrl
|
||||
this.uploader = uploader
|
||||
this.streamCount = streamCount
|
||||
}
|
||||
|
||||
@Ignore
|
||||
constructor(serviceId: Int, name: String, url: String,
|
||||
thumbnailUrl: String?, uploader: String,
|
||||
displayIndex: Long, streamCount: Long) {
|
||||
this.serviceId = serviceId
|
||||
this.name = name
|
||||
this.url = url
|
||||
this.thumbnailUrl = thumbnailUrl
|
||||
this.uploader = uploader
|
||||
this.displayIndex = displayIndex
|
||||
this.streamCount = streamCount
|
||||
}
|
||||
|
||||
@Ignore
|
||||
constructor(info: PlaylistInfo) : this(info.getServiceId(), info.getName(), info.getUrl(), // use uploader avatar when no thumbnail is available
|
||||
ImageStrategy.imageListToDbUrl(if (info.getThumbnails().isEmpty()) info.getUploaderAvatars() else info.getThumbnails()),
|
||||
info.getUploaderName(), info.getStreamCount())
|
||||
|
||||
@Ignore
|
||||
fun isIdenticalTo(info: PlaylistInfo): Boolean {
|
||||
/*
|
||||
* Returns boolean comparing the online playlist and the local copy.
|
||||
* (False if info changed such as playlist name or track count)
|
||||
*/
|
||||
return ((getServiceId() == info.getServiceId()
|
||||
) && (getStreamCount() == info.getStreamCount()
|
||||
) && TextUtils.equals(getName(), info.getName())
|
||||
&& TextUtils.equals(getUrl(), info.getUrl()) // we want to update the local playlist data even when either the remote thumbnail
|
||||
// URL changes, or the preferred image quality setting is changed by the user
|
||||
&& TextUtils.equals(getThumbnailUrl(),
|
||||
ImageStrategy.imageListToDbUrl(info.getThumbnails()))
|
||||
&& TextUtils.equals(getUploader(), info.getUploaderName()))
|
||||
}
|
||||
|
||||
public override fun getUid(): Long {
|
||||
return uid
|
||||
}
|
||||
|
||||
fun setUid(uid: Long) {
|
||||
this.uid = uid
|
||||
}
|
||||
|
||||
fun getServiceId(): Int {
|
||||
return serviceId
|
||||
}
|
||||
|
||||
fun setServiceId(serviceId: Int) {
|
||||
this.serviceId = serviceId
|
||||
}
|
||||
|
||||
fun getName(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
fun setName(name: String) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun getThumbnailUrl(): String? {
|
||||
return thumbnailUrl
|
||||
}
|
||||
|
||||
fun setThumbnailUrl(thumbnailUrl: String?) {
|
||||
this.thumbnailUrl = thumbnailUrl
|
||||
}
|
||||
|
||||
fun getUrl(): String {
|
||||
return url
|
||||
}
|
||||
|
||||
fun setUrl(url: String) {
|
||||
this.url = url
|
||||
}
|
||||
|
||||
fun getUploader(): String {
|
||||
return uploader
|
||||
}
|
||||
|
||||
fun setUploader(uploader: String) {
|
||||
this.uploader = uploader
|
||||
}
|
||||
|
||||
public override fun getDisplayIndex(): Long {
|
||||
return displayIndex
|
||||
}
|
||||
|
||||
public override fun setDisplayIndex(displayIndex: Long) {
|
||||
this.displayIndex = displayIndex
|
||||
}
|
||||
|
||||
fun getStreamCount(): Long {
|
||||
return streamCount
|
||||
}
|
||||
|
||||
fun setStreamCount(streamCount: Long) {
|
||||
this.streamCount = streamCount
|
||||
}
|
||||
|
||||
public override fun getLocalItemType(): LocalItemType {
|
||||
return LocalItemType.PLAYLIST_REMOTE_ITEM
|
||||
}
|
||||
|
||||
public override fun getOrderingName(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
companion object {
|
||||
val REMOTE_PLAYLIST_TABLE: String = "remote_playlists"
|
||||
val REMOTE_PLAYLIST_ID: String = "uid"
|
||||
val REMOTE_PLAYLIST_SERVICE_ID: String = "service_id"
|
||||
val REMOTE_PLAYLIST_NAME: String = "name"
|
||||
val REMOTE_PLAYLIST_URL: String = "url"
|
||||
val REMOTE_PLAYLIST_THUMBNAIL_URL: String = "thumbnail_url"
|
||||
val REMOTE_PLAYLIST_UPLOADER_NAME: String = "uploader"
|
||||
val REMOTE_PLAYLIST_DISPLAY_INDEX: String = "display_index"
|
||||
val REMOTE_PLAYLIST_STREAM_COUNT: String = "stream_count"
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package org.schabi.newpipe.database.playlist.model;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Index;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||
|
||||
@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE,
|
||||
primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX},
|
||||
indices = {
|
||||
@Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true),
|
||||
@Index(value = {JOIN_STREAM_ID})
|
||||
},
|
||||
foreignKeys = {
|
||||
@ForeignKey(entity = PlaylistEntity.class,
|
||||
parentColumns = PlaylistEntity.PLAYLIST_ID,
|
||||
childColumns = JOIN_PLAYLIST_ID,
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
||||
@ForeignKey(entity = StreamEntity.class,
|
||||
parentColumns = StreamEntity.STREAM_ID,
|
||||
childColumns = JOIN_STREAM_ID,
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||
})
|
||||
public class PlaylistStreamEntity {
|
||||
public static final String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
|
||||
public static final String JOIN_PLAYLIST_ID = "playlist_id";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
public static final String JOIN_INDEX = "join_index";
|
||||
|
||||
@ColumnInfo(name = JOIN_PLAYLIST_ID)
|
||||
private long playlistUid;
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@ColumnInfo(name = JOIN_INDEX)
|
||||
private int index;
|
||||
|
||||
public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) {
|
||||
this.playlistUid = playlistUid;
|
||||
this.streamUid = streamUid;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public long getPlaylistUid() {
|
||||
return playlistUid;
|
||||
}
|
||||
|
||||
public void setPlaylistUid(final long playlistUid) {
|
||||
this.playlistUid = playlistUid;
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(final long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(final int index) {
|
||||
this.index = index;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.schabi.newpipe.database.playlist.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
|
||||
@Entity(tableName = PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE, primaryKeys = [PlaylistStreamEntity.JOIN_PLAYLIST_ID, PlaylistStreamEntity.JOIN_INDEX], indices = [Index(value = [PlaylistStreamEntity.JOIN_PLAYLIST_ID, PlaylistStreamEntity.JOIN_INDEX], unique = true), Index(value = [PlaylistStreamEntity.JOIN_STREAM_ID])], foreignKeys = [ForeignKey(entity = PlaylistEntity::class, parentColumns = PlaylistEntity.Companion.PLAYLIST_ID, childColumns = PlaylistStreamEntity.JOIN_PLAYLIST_ID, onDelete = CASCADE, onUpdate = CASCADE, deferred = true), ForeignKey(entity = StreamEntity::class, parentColumns = StreamEntity.STREAM_ID, childColumns = PlaylistStreamEntity.JOIN_STREAM_ID, onDelete = CASCADE, onUpdate = CASCADE, deferred = true)])
|
||||
class PlaylistStreamEntity(@field:ColumnInfo(name = JOIN_PLAYLIST_ID) private var playlistUid: Long, @field:ColumnInfo(name = JOIN_STREAM_ID) private var streamUid: Long, @field:ColumnInfo(name = JOIN_INDEX) private var index: Int) {
|
||||
fun getPlaylistUid(): Long {
|
||||
return playlistUid
|
||||
}
|
||||
|
||||
fun setPlaylistUid(playlistUid: Long) {
|
||||
this.playlistUid = playlistUid
|
||||
}
|
||||
|
||||
fun getStreamUid(): Long {
|
||||
return streamUid
|
||||
}
|
||||
|
||||
fun setStreamUid(streamUid: Long) {
|
||||
this.streamUid = streamUid
|
||||
}
|
||||
|
||||
fun getIndex(): Int {
|
||||
return index
|
||||
}
|
||||
|
||||
fun setIndex(index: Int) {
|
||||
this.index = index
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PLAYLIST_STREAM_JOIN_TABLE: String = "playlist_stream_join"
|
||||
val JOIN_PLAYLIST_ID: String = "playlist_id"
|
||||
val JOIN_STREAM_ID: String = "stream_id"
|
||||
val JOIN_INDEX: String = "join_index"
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package org.schabi.newpipe.database.stream.dao;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE)
|
||||
Flowable<List<StreamStateEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE)
|
||||
int deleteAll();
|
||||
|
||||
@Override
|
||||
default Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
Flowable<List<StreamStateEntity>> getState(long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
int deleteState(long streamId);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
void silentInsertInternal(StreamStateEntity streamState);
|
||||
|
||||
@Transaction
|
||||
default long upsert(final StreamStateEntity stream) {
|
||||
silentInsertInternal(stream);
|
||||
return update(stream);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package org.schabi.newpipe.database.stream.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
|
||||
@Dao
|
||||
open interface StreamStateDAO : BasicDAO<StreamStateEntity?> {
|
||||
@Query("SELECT * FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE)
|
||||
public override fun getAll(): Flowable<List<StreamStateEntity?>?>?
|
||||
@Query("DELETE FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE)
|
||||
public override fun deleteAll(): Int
|
||||
public override fun listByService(serviceId: Int): Flowable<List<StreamStateEntity?>?>? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.Companion.JOIN_STREAM_ID + " = :streamId")
|
||||
fun getState(streamId: Long): Flowable<List<StreamStateEntity?>?>
|
||||
|
||||
@Query("DELETE FROM " + StreamStateEntity.Companion.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.Companion.JOIN_STREAM_ID + " = :streamId")
|
||||
fun deleteState(streamId: Long): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun silentInsertInternal(streamState: StreamStateEntity?)
|
||||
|
||||
@Transaction
|
||||
fun upsert(stream: StreamStateEntity?): Long {
|
||||
silentInsertInternal(stream)
|
||||
return update(stream).toLong()
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package org.schabi.newpipe.database.stream.model;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Entity(tableName = STREAM_STATE_TABLE,
|
||||
primaryKeys = {JOIN_STREAM_ID},
|
||||
foreignKeys = {
|
||||
@ForeignKey(entity = StreamEntity.class,
|
||||
parentColumns = StreamEntity.STREAM_ID,
|
||||
childColumns = JOIN_STREAM_ID,
|
||||
onDelete = CASCADE, onUpdate = CASCADE)
|
||||
})
|
||||
public class StreamStateEntity {
|
||||
public static final String STREAM_STATE_TABLE = "stream_state";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
// This additional field is required for the SQL query because 'stream_id' is used
|
||||
// for some other joins already
|
||||
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
|
||||
public static final String STREAM_PROGRESS_MILLIS = "progress_time";
|
||||
|
||||
/**
|
||||
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
|
||||
*/
|
||||
public static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
|
||||
|
||||
/**
|
||||
* Stream will be considered finished if the playback time left exceeds this threshold
|
||||
* (60000ms = 60s).
|
||||
* @see #isFinished(long)
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
|
||||
*/
|
||||
public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000;
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@ColumnInfo(name = STREAM_PROGRESS_MILLIS)
|
||||
private long progressMillis;
|
||||
|
||||
public StreamStateEntity(final long streamUid, final long progressMillis) {
|
||||
this.streamUid = streamUid;
|
||||
this.progressMillis = progressMillis;
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(final long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public long getProgressMillis() {
|
||||
return progressMillis;
|
||||
}
|
||||
|
||||
public void setProgressMillis(final long progressMillis) {
|
||||
this.progressMillis = progressMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* The state will be considered valid, and thus be saved, if the progress is more than {@link
|
||||
* #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length.
|
||||
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||
* @return whether this stream state entity should be saved or not
|
||||
*/
|
||||
public boolean isValid(final long durationInSeconds) {
|
||||
return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||
|| progressMillis > durationInSeconds * 1000 / 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* The video will be considered as finished, if the time left is less than {@link
|
||||
* #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length.
|
||||
* The state will be saved anyway, so that it can be shown under stream info items, but the
|
||||
* player will not resume if a state is considered as finished. Finished streams are also the
|
||||
* ones that can be filtered out in the feed fragment.
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
|
||||
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||
* @return whether the stream is finished or not
|
||||
*/
|
||||
public boolean isFinished(final long durationInSeconds) {
|
||||
return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
&& progressMillis >= durationInSeconds * 1000 * 3 / 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable final Object obj) {
|
||||
if (obj instanceof StreamStateEntity) {
|
||||
return ((StreamStateEntity) obj).streamUid == streamUid
|
||||
&& ((StreamStateEntity) obj).progressMillis == progressMillis;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(streamUid, progressMillis);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package org.schabi.newpipe.database.stream.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import java.util.Objects
|
||||
|
||||
@Entity(tableName = StreamStateEntity.STREAM_STATE_TABLE, primaryKeys = [StreamStateEntity.JOIN_STREAM_ID], foreignKeys = [ForeignKey(entity = StreamEntity::class, parentColumns = StreamEntity.STREAM_ID, childColumns = StreamStateEntity.JOIN_STREAM_ID, onDelete = CASCADE, onUpdate = CASCADE)])
|
||||
class StreamStateEntity(@field:ColumnInfo(name = JOIN_STREAM_ID) private var streamUid: Long, @field:ColumnInfo(name = STREAM_PROGRESS_MILLIS) private var progressMillis: Long) {
|
||||
fun getStreamUid(): Long {
|
||||
return streamUid
|
||||
}
|
||||
|
||||
fun setStreamUid(streamUid: Long) {
|
||||
this.streamUid = streamUid
|
||||
}
|
||||
|
||||
fun getProgressMillis(): Long {
|
||||
return progressMillis
|
||||
}
|
||||
|
||||
fun setProgressMillis(progressMillis: Long) {
|
||||
this.progressMillis = progressMillis
|
||||
}
|
||||
|
||||
/**
|
||||
* The state will be considered valid, and thus be saved, if the progress is more than [ ][.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS] or at least 1/4 of the video length.
|
||||
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||
* @return whether this stream state entity should be saved or not
|
||||
*/
|
||||
fun isValid(durationInSeconds: Long): Boolean {
|
||||
return (progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||
|| progressMillis > durationInSeconds * 1000 / 4)
|
||||
}
|
||||
|
||||
/**
|
||||
* The video will be considered as finished, if the time left is less than [ ][.PLAYBACK_FINISHED_END_MILLISECONDS] and the progress is at least 3/4 of the video length.
|
||||
* The state will be saved anyway, so that it can be shown under stream info items, but the
|
||||
* player will not resume if a state is considered as finished. Finished streams are also the
|
||||
* ones that can be filtered out in the feed fragment.
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO.getLiveOrNotPlayedStreams
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO.getLiveOrNotPlayedStreamsForGroup
|
||||
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||
* @return whether the stream is finished or not
|
||||
*/
|
||||
fun isFinished(durationInSeconds: Long): Boolean {
|
||||
return (progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
&& progressMillis >= durationInSeconds * 1000 * 3 / 4)
|
||||
}
|
||||
|
||||
public override fun equals(obj: Any?): Boolean {
|
||||
if (obj is StreamStateEntity) {
|
||||
return (obj.streamUid == streamUid
|
||||
&& obj.progressMillis == progressMillis)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public override fun hashCode(): Int {
|
||||
return Objects.hash(streamUid, progressMillis)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val STREAM_STATE_TABLE: String = "stream_state"
|
||||
val JOIN_STREAM_ID: String = "stream_id"
|
||||
|
||||
// This additional field is required for the SQL query because 'stream_id' is used
|
||||
// for some other joins already
|
||||
val JOIN_STREAM_ID_ALIAS: String = "stream_id_alias"
|
||||
val STREAM_PROGRESS_MILLIS: String = "progress_time"
|
||||
|
||||
/**
|
||||
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
|
||||
*/
|
||||
val PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS: Long = 5000
|
||||
|
||||
/**
|
||||
* Stream will be considered finished if the playback time left exceeds this threshold
|
||||
* (60000ms = 60s).
|
||||
* @see .isFinished
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO.getLiveOrNotPlayedStreams
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO.getLiveOrNotPlayedStreamsForGroup
|
||||
*/
|
||||
val PLAYBACK_FINISHED_END_MILLISECONDS: Long = 60000
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package org.schabi.newpipe.database.subscription;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@IntDef({NotificationMode.DISABLED, NotificationMode.ENABLED})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface NotificationMode {
|
||||
|
||||
int DISABLED = 0;
|
||||
int ENABLED = 1;
|
||||
//other values reserved for the future
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.schabi.newpipe.database.subscription
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||
|
||||
@IntDef([NotificationMode.DISABLED, NotificationMode.ENABLED])
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class NotificationMode() {
|
||||
companion object {
|
||||
val DISABLED: Int = 0
|
||||
val ENABLED: Int = 1 //other values reserved for the future
|
||||
}
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
package org.schabi.newpipe.database.subscription;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
||||
|
||||
@Entity(tableName = SUBSCRIPTION_TABLE,
|
||||
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
||||
public class SubscriptionEntity {
|
||||
public static final String SUBSCRIPTION_UID = "uid";
|
||||
public static final String SUBSCRIPTION_TABLE = "subscriptions";
|
||||
public static final String SUBSCRIPTION_SERVICE_ID = "service_id";
|
||||
public static final String SUBSCRIPTION_URL = "url";
|
||||
public static final String SUBSCRIPTION_NAME = "name";
|
||||
public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url";
|
||||
public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
|
||||
public static final String SUBSCRIPTION_DESCRIPTION = "description";
|
||||
public static final String SUBSCRIPTION_NOTIFICATION_MODE = "notification_mode";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long uid = 0;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
|
||||
private int serviceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_URL)
|
||||
private String url;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_NAME)
|
||||
private String name;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_AVATAR_URL)
|
||||
private String avatarUrl;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
|
||||
private Long subscriberCount;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
||||
private String description;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE)
|
||||
private int notificationMode;
|
||||
|
||||
@Ignore
|
||||
public static SubscriptionEntity from(@NonNull final ChannelInfo info) {
|
||||
final SubscriptionEntity result = new SubscriptionEntity();
|
||||
result.setServiceId(info.getServiceId());
|
||||
result.setUrl(info.getUrl());
|
||||
result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||
info.getDescription(), info.getSubscriberCount());
|
||||
return result;
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(final long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(final int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(final String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Long getSubscriberCount() {
|
||||
return subscriberCount;
|
||||
}
|
||||
|
||||
public void setSubscriberCount(final Long subscriberCount) {
|
||||
this.subscriberCount = subscriberCount;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@NotificationMode
|
||||
public int getNotificationMode() {
|
||||
return notificationMode;
|
||||
}
|
||||
|
||||
public void setNotificationMode(@NotificationMode final int notificationMode) {
|
||||
this.notificationMode = notificationMode;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public void setData(final String n, final String au, final String d, final Long sc) {
|
||||
this.setName(n);
|
||||
this.setAvatarUrl(au);
|
||||
this.setDescription(d);
|
||||
this.setSubscriberCount(sc);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public ChannelInfoItem toChannelInfoItem() {
|
||||
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||
item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()));
|
||||
item.setSubscriberCount(getSubscriberCount());
|
||||
item.setDescription(getDescription());
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
// TODO: Remove these generated methods by migrating this class to a data class from Kotlin.
|
||||
@Override
|
||||
@SuppressWarnings("EqualsReplaceableByObjectsCall")
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final SubscriptionEntity that = (SubscriptionEntity) o;
|
||||
|
||||
if (uid != that.uid) {
|
||||
return false;
|
||||
}
|
||||
if (serviceId != that.serviceId) {
|
||||
return false;
|
||||
}
|
||||
if (!url.equals(that.url)) {
|
||||
return false;
|
||||
}
|
||||
if (name != null ? !name.equals(that.name) : that.name != null) {
|
||||
return false;
|
||||
}
|
||||
if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) {
|
||||
return false;
|
||||
}
|
||||
if (subscriberCount != null
|
||||
? !subscriberCount.equals(that.subscriberCount)
|
||||
: that.subscriberCount != null) {
|
||||
return false;
|
||||
}
|
||||
return description != null
|
||||
? description.equals(that.description)
|
||||
: that.description == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (int) (uid ^ (uid >>> 32));
|
||||
result = 31 * result + serviceId;
|
||||
result = 31 * result + url.hashCode();
|
||||
result = 31 * result + (name != null ? name.hashCode() : 0);
|
||||
result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0);
|
||||
result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0);
|
||||
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
package org.schabi.newpipe.database.subscription
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
|
||||
@Entity(tableName = SubscriptionEntity.SUBSCRIPTION_TABLE, indices = [Index(value = [SubscriptionEntity.SUBSCRIPTION_SERVICE_ID, SubscriptionEntity.SUBSCRIPTION_URL], unique = true)])
|
||||
class SubscriptionEntity() {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private var uid: Long = 0
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
|
||||
private var serviceId: Int = NO_SERVICE_ID
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_URL)
|
||||
private var url: String? = null
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_NAME)
|
||||
private var name: String? = null
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_AVATAR_URL)
|
||||
private var avatarUrl: String? = null
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
|
||||
private var subscriberCount: Long? = null
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
||||
private var description: String? = null
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE)
|
||||
private var notificationMode: Int = 0
|
||||
fun getUid(): Long {
|
||||
return uid
|
||||
}
|
||||
|
||||
fun setUid(uid: Long) {
|
||||
this.uid = uid
|
||||
}
|
||||
|
||||
fun getServiceId(): Int {
|
||||
return serviceId
|
||||
}
|
||||
|
||||
fun setServiceId(serviceId: Int) {
|
||||
this.serviceId = serviceId
|
||||
}
|
||||
|
||||
fun getUrl(): String? {
|
||||
return url
|
||||
}
|
||||
|
||||
fun setUrl(url: String?) {
|
||||
this.url = url
|
||||
}
|
||||
|
||||
fun getName(): String? {
|
||||
return name
|
||||
}
|
||||
|
||||
fun setName(name: String?) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun getAvatarUrl(): String? {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
fun setAvatarUrl(avatarUrl: String?) {
|
||||
this.avatarUrl = avatarUrl
|
||||
}
|
||||
|
||||
fun getSubscriberCount(): Long? {
|
||||
return subscriberCount
|
||||
}
|
||||
|
||||
fun setSubscriberCount(subscriberCount: Long?) {
|
||||
this.subscriberCount = subscriberCount
|
||||
}
|
||||
|
||||
fun getDescription(): String? {
|
||||
return description
|
||||
}
|
||||
|
||||
fun setDescription(description: String?) {
|
||||
this.description = description
|
||||
}
|
||||
|
||||
@NotificationMode
|
||||
fun getNotificationMode(): Int {
|
||||
return notificationMode
|
||||
}
|
||||
|
||||
fun setNotificationMode(@NotificationMode notificationMode: Int) {
|
||||
this.notificationMode = notificationMode
|
||||
}
|
||||
|
||||
@Ignore
|
||||
fun setData(n: String?, au: String?, d: String?, sc: Long?) {
|
||||
setName(n)
|
||||
setAvatarUrl(au)
|
||||
setDescription(d)
|
||||
setSubscriberCount(sc)
|
||||
}
|
||||
|
||||
@Ignore
|
||||
fun toChannelInfoItem(): ChannelInfoItem {
|
||||
val item: ChannelInfoItem = ChannelInfoItem(getServiceId(), getUrl(), getName())
|
||||
item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()))
|
||||
item.setSubscriberCount((getSubscriberCount())!!)
|
||||
item.setDescription(getDescription())
|
||||
return item
|
||||
}
|
||||
|
||||
// TODO: Remove these generated methods by migrating this class to a data class from Kotlin.
|
||||
public override fun equals(o: Any?): Boolean {
|
||||
if (this === o) {
|
||||
return true
|
||||
}
|
||||
if (o == null || javaClass != o.javaClass) {
|
||||
return false
|
||||
}
|
||||
val that: SubscriptionEntity = o as SubscriptionEntity
|
||||
if (uid != that.uid) {
|
||||
return false
|
||||
}
|
||||
if (serviceId != that.serviceId) {
|
||||
return false
|
||||
}
|
||||
if (!(url == that.url)) {
|
||||
return false
|
||||
}
|
||||
if (if (name != null) !(name == that.name) else that.name != null) {
|
||||
return false
|
||||
}
|
||||
if (if (avatarUrl != null) !(avatarUrl == that.avatarUrl) else that.avatarUrl != null) {
|
||||
return false
|
||||
}
|
||||
if (if (subscriberCount != null) !(subscriberCount == that.subscriberCount) else that.subscriberCount != null) {
|
||||
return false
|
||||
}
|
||||
return if (description != null) (description == that.description) else that.description == null
|
||||
}
|
||||
|
||||
public override fun hashCode(): Int {
|
||||
var result: Int = (uid xor (uid ushr 32)).toInt()
|
||||
result = 31 * result + serviceId
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + (if (name != null) name.hashCode() else 0)
|
||||
result = 31 * result + (if (avatarUrl != null) avatarUrl.hashCode() else 0)
|
||||
result = 31 * result + (if (subscriberCount != null) subscriberCount.hashCode() else 0)
|
||||
result = 31 * result + (if (description != null) description.hashCode() else 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
val SUBSCRIPTION_UID: String = "uid"
|
||||
val SUBSCRIPTION_TABLE: String = "subscriptions"
|
||||
val SUBSCRIPTION_SERVICE_ID: String = "service_id"
|
||||
val SUBSCRIPTION_URL: String = "url"
|
||||
val SUBSCRIPTION_NAME: String = "name"
|
||||
val SUBSCRIPTION_AVATAR_URL: String = "avatar_url"
|
||||
val SUBSCRIPTION_SUBSCRIBER_COUNT: String = "subscriber_count"
|
||||
val SUBSCRIPTION_DESCRIPTION: String = "description"
|
||||
val SUBSCRIPTION_NOTIFICATION_MODE: String = "notification_mode"
|
||||
@JvmStatic
|
||||
@Ignore
|
||||
fun from(info: ChannelInfo): SubscriptionEntity {
|
||||
val result: SubscriptionEntity = SubscriptionEntity()
|
||||
result.setServiceId(info.getServiceId())
|
||||
result.setUrl(info.getUrl())
|
||||
result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
|
||||
info.getDescription(), info.getSubscriberCount())
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityDownloaderBinding;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class DownloadActivity extends AppCompatActivity {
|
||||
|
||||
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
// Service
|
||||
final Intent i = new Intent();
|
||||
i.setClass(this, DownloadManagerService.class);
|
||||
startService(i);
|
||||
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final ActivityDownloaderBinding downloaderBinding =
|
||||
ActivityDownloaderBinding.inflate(getLayoutInflater());
|
||||
setContentView(downloaderBinding.getRoot());
|
||||
|
||||
setSupportActionBar(downloaderBinding.toolbarLayout.toolbar);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.downloads_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
getWindow().getDecorView().getViewTreeObserver()
|
||||
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFragments() {
|
||||
final MissionsFragment fragment = new MissionsFragment();
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
|
||||
inflater.inflate(R.menu.download_menu, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package org.schabi.newpipe.download
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
||||
import androidx.appcompat.app.ActionBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ActivityDownloaderBinding
|
||||
import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.views.FocusOverlayView
|
||||
import us.shandian.giga.service.DownloadManagerService
|
||||
import us.shandian.giga.ui.fragment.MissionsFragment
|
||||
|
||||
class DownloadActivity() : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Service
|
||||
val i: Intent = Intent()
|
||||
i.setClass(this, DownloadManagerService::class.java)
|
||||
startService(i)
|
||||
Localization.assureCorrectAppLanguage(this)
|
||||
ThemeHelper.setTheme(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
val downloaderBinding: ActivityDownloaderBinding = ActivityDownloaderBinding.inflate(getLayoutInflater())
|
||||
setContentView(downloaderBinding.getRoot())
|
||||
setSupportActionBar(downloaderBinding.toolbarLayout.toolbar)
|
||||
val actionBar: ActionBar? = getSupportActionBar()
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true)
|
||||
actionBar.setTitle(R.string.downloads_title)
|
||||
actionBar.setDisplayShowTitleEnabled(true)
|
||||
}
|
||||
getWindow().getDecorView().getViewTreeObserver()
|
||||
.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
|
||||
public override fun onGlobalLayout() {
|
||||
updateFragments()
|
||||
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
})
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.Companion.setupFocusObserver(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFragments() {
|
||||
val fragment: MissionsFragment = MissionsFragment()
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.commit()
|
||||
}
|
||||
|
||||
public override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
val inflater: MenuInflater = getMenuInflater()
|
||||
inflater.inflate(R.menu.download_menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
public override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.getItemId()) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val MISSIONS_FRAGMENT_TAG: String = "fragment_tag"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,87 +0,0 @@
|
|||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.DownloadLoadingDialogBinding;
|
||||
|
||||
/**
|
||||
* This class contains a dialog which shows a loading indicator and has a customizable title.
|
||||
*/
|
||||
public class LoadingDialog extends DialogFragment {
|
||||
private static final String TAG = "LoadingDialog";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private DownloadLoadingDialogBinding dialogLoadingBinding;
|
||||
private final @StringRes int title;
|
||||
|
||||
/**
|
||||
* Create a new LoadingDialog.
|
||||
*
|
||||
* <p>
|
||||
* The dialog contains a loading indicator and has a customizable title.
|
||||
* <br/>
|
||||
* Use {@code show()} to display the dialog to the user.
|
||||
* </p>
|
||||
*
|
||||
* @param title an informative title shown in the dialog's toolbar
|
||||
*/
|
||||
public LoadingDialog(final @StringRes int title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreate() called with: "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
this.setCancelable(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(
|
||||
@NonNull final LayoutInflater inflater,
|
||||
final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateView() called with: "
|
||||
+ "inflater = [" + inflater + "], container = [" + container + "], "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
return inflater.inflate(R.layout.download_loading_dialog, container);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
dialogLoadingBinding = DownloadLoadingDialogBinding.bind(view);
|
||||
initToolbar(dialogLoadingBinding.toolbarLayout.toolbar);
|
||||
}
|
||||
|
||||
private void initToolbar(final Toolbar toolbar) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||
}
|
||||
toolbar.setTitle(requireContext().getString(title));
|
||||
toolbar.setNavigationOnClickListener(v -> dismiss());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
dialogLoadingBinding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package org.schabi.newpipe.download
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.DownloadLoadingDialogBinding
|
||||
|
||||
/**
|
||||
* This class contains a dialog which shows a loading indicator and has a customizable title.
|
||||
*/
|
||||
class LoadingDialog
|
||||
/**
|
||||
* Create a new LoadingDialog.
|
||||
*
|
||||
*
|
||||
*
|
||||
* The dialog contains a loading indicator and has a customizable title.
|
||||
* <br></br>
|
||||
* Use `show()` to display the dialog to the user.
|
||||
*
|
||||
*
|
||||
* @param title an informative title shown in the dialog's toolbar
|
||||
*/(@field:StringRes @param:StringRes private val title: Int) : DialogFragment() {
|
||||
private var dialogLoadingBinding: DownloadLoadingDialogBinding? = null
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, ("onCreate() called with: "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]"))
|
||||
}
|
||||
setCancelable(false)
|
||||
}
|
||||
|
||||
public override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, ("onCreateView() called with: "
|
||||
+ "inflater = [" + inflater + "], container = [" + container + "], "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]"))
|
||||
}
|
||||
return inflater.inflate(R.layout.download_loading_dialog, container)
|
||||
}
|
||||
|
||||
public override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
dialogLoadingBinding = DownloadLoadingDialogBinding.bind(view)
|
||||
initToolbar(dialogLoadingBinding!!.toolbarLayout.toolbar)
|
||||
}
|
||||
|
||||
private fun initToolbar(toolbar: Toolbar) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]")
|
||||
}
|
||||
toolbar.setTitle(requireContext().getString(title))
|
||||
toolbar.setNavigationOnClickListener(View.OnClickListener({ v: View? -> dismiss() }))
|
||||
}
|
||||
|
||||
public override fun onDestroyView() {
|
||||
dialogLoadingBinding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = "LoadingDialog"
|
||||
private val DEBUG: Boolean = MainActivity.Companion.DEBUG
|
||||
}
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
package org.schabi.newpipe.error;
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.acra.ReportField;
|
||||
import org.acra.data.CrashReportData;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.schabi.newpipe.R;
|
||||
import android.content.Context
|
||||
import org.acra.ReportField
|
||||
import org.acra.data.CrashReportData
|
||||
import org.acra.sender.ReportSender
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.openActivity
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 13.09.16.
|
||||
|
@ -28,16 +26,12 @@ import org.schabi.newpipe.R;
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class AcraReportSender implements ReportSender {
|
||||
|
||||
@Override
|
||||
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
|
||||
ErrorUtil.openActivity(context, new ErrorInfo(
|
||||
new String[]{report.getString(ReportField.STACK_TRACE)},
|
||||
class AcraReportSender() : ReportSender {
|
||||
public override fun send(context: Context, report: CrashReportData) {
|
||||
openActivity(context, ErrorInfo(arrayOf<String?>(report.getString(ReportField.STACK_TRACE)),
|
||||
UserAction.UI_ERROR,
|
||||
ErrorInfo.SERVICE_NONE,
|
||||
"ACRA report",
|
||||
R.string.app_ui_crash));
|
||||
R.string.app_ui_crash))
|
||||
}
|
||||
}
|
|
@ -1,15 +1,11 @@
|
|||
package org.schabi.newpipe.error;
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
|
||||
import org.acra.config.CoreConfiguration;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.schabi.newpipe.App;
|
||||
import android.content.Context
|
||||
import com.google.auto.service.AutoService
|
||||
import org.acra.config.CoreConfiguration
|
||||
import org.acra.sender.ReportSender
|
||||
import org.acra.sender.ReportSenderFactory
|
||||
import org.schabi.newpipe.App
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 13.09.16.
|
||||
|
@ -30,15 +26,13 @@ import org.schabi.newpipe.App;
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used by ACRA in {@link App}.initAcra() as the factory for report senders.
|
||||
* Used by ACRA in [App].initAcra() as the factory for report senders.
|
||||
*/
|
||||
@AutoService(ReportSenderFactory.class)
|
||||
public class AcraReportSenderFactory implements ReportSenderFactory {
|
||||
@NonNull
|
||||
public ReportSender create(@NonNull final Context context,
|
||||
@NonNull final CoreConfiguration config) {
|
||||
return new AcraReportSender();
|
||||
@AutoService(ReportSenderFactory::class)
|
||||
class AcraReportSenderFactory() : ReportSenderFactory {
|
||||
public override fun create(context: Context,
|
||||
config: CoreConfiguration): ReportSender {
|
||||
return AcraReportSender()
|
||||
}
|
||||
}
|
|
@ -1,348 +0,0 @@
|
|||
package org.schabi.newpipe.error;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.IntentCompat;
|
||||
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ErrorActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This activity is used to show error details and allow reporting them in various ways. Use {@link
|
||||
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
|
||||
*/
|
||||
public class ErrorActivity extends AppCompatActivity {
|
||||
// LOG TAGS
|
||||
public static final String TAG = ErrorActivity.class.toString();
|
||||
// BUNDLE TAGS
|
||||
public static final String ERROR_INFO = "error_info";
|
||||
|
||||
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
|
||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
|
||||
|
||||
public static final String ERROR_GITHUB_ISSUE_URL =
|
||||
"https://github.com/TeamNewPipe/NewPipe/issues";
|
||||
|
||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
|
||||
private ErrorInfo errorInfo;
|
||||
private String currentTimeStamp;
|
||||
|
||||
private ActivityErrorBinding activityErrorBinding;
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Activity lifecycle
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
ThemeHelper.setDayNightMode(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
|
||||
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
|
||||
setContentView(activityErrorBinding.getRoot());
|
||||
|
||||
final Intent intent = getIntent();
|
||||
|
||||
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.error_report_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
|
||||
|
||||
// important add guru meditation
|
||||
addGuruMeditation();
|
||||
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now());
|
||||
|
||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
||||
|
||||
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
|
||||
ShareUtils.copyToClipboard(this, buildMarkdown()));
|
||||
|
||||
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
|
||||
openPrivacyPolicyDialog(this, "GITHUB"));
|
||||
|
||||
// normal bugreport
|
||||
buildInfo(errorInfo);
|
||||
activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId());
|
||||
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
|
||||
|
||||
// print stack trace once again for debugging:
|
||||
for (final String e : errorInfo.getStackTraces()) {
|
||||
Log.e(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.error_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
case R.id.menu_item_share_error:
|
||||
ShareUtils.shareText(getApplicationContext(),
|
||||
getString(R.string.error_report_title), buildJson());
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void openPrivacyPolicyDialog(final Context context, final String action) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setTitle(R.string.privacy_policy_title)
|
||||
.setMessage(R.string.start_accept_privacy_policy)
|
||||
.setCancelable(false)
|
||||
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
|
||||
ShareUtils.openUrlInApp(context,
|
||||
context.getString(R.string.privacy_policy_url)))
|
||||
.setPositiveButton(R.string.accept, (dialog, which) -> {
|
||||
if (action.equals("EMAIL")) { // send on email
|
||||
final Intent i = new Intent(Intent.ACTION_SENDTO)
|
||||
.setData(Uri.parse("mailto:")) // only email apps should handle this
|
||||
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
|
||||
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
|
||||
+ getString(R.string.app_name) + " "
|
||||
+ BuildConfig.VERSION_NAME)
|
||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||
ShareUtils.openIntentInApp(context, i);
|
||||
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
||||
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.decline, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private String formErrorText(final String[] el) {
|
||||
final String separator = "-------------------------------------";
|
||||
return Arrays.stream(el)
|
||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checked activity.
|
||||
*
|
||||
* @param returnActivity the activity to return to
|
||||
* @return the casted return activity or null
|
||||
*/
|
||||
@Nullable
|
||||
static Class<? extends Activity> getReturnActivity(final Class<?> returnActivity) {
|
||||
Class<? extends Activity> checkedReturnActivity = null;
|
||||
if (returnActivity != null) {
|
||||
if (Activity.class.isAssignableFrom(returnActivity)) {
|
||||
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
|
||||
} else {
|
||||
checkedReturnActivity = MainActivity.class;
|
||||
}
|
||||
}
|
||||
return checkedReturnActivity;
|
||||
}
|
||||
|
||||
private void buildInfo(final ErrorInfo info) {
|
||||
String text = "";
|
||||
|
||||
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
|
||||
.replace("\\n", "\n"));
|
||||
|
||||
text += getUserActionString(info.getUserAction()) + "\n"
|
||||
+ info.getRequest() + "\n"
|
||||
+ getContentLanguageString() + "\n"
|
||||
+ getContentCountryString() + "\n"
|
||||
+ getAppLanguage() + "\n"
|
||||
+ info.getServiceName() + "\n"
|
||||
+ currentTimeStamp + "\n"
|
||||
+ getPackageName() + "\n"
|
||||
+ BuildConfig.VERSION_NAME + "\n"
|
||||
+ getOsString();
|
||||
|
||||
activityErrorBinding.errorInfosView.setText(text);
|
||||
}
|
||||
|
||||
private String buildJson() {
|
||||
try {
|
||||
return JsonWriter.string()
|
||||
.object()
|
||||
.value("user_action", getUserActionString(errorInfo.getUserAction()))
|
||||
.value("request", errorInfo.getRequest())
|
||||
.value("content_language", getContentLanguageString())
|
||||
.value("content_country", getContentCountryString())
|
||||
.value("app_language", getAppLanguage())
|
||||
.value("service", errorInfo.getServiceName())
|
||||
.value("package", getPackageName())
|
||||
.value("version", BuildConfig.VERSION_NAME)
|
||||
.value("os", getOsString())
|
||||
.value("time", currentTimeStamp)
|
||||
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
|
||||
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
|
||||
.toString())
|
||||
.end()
|
||||
.done();
|
||||
} catch (final Throwable e) {
|
||||
Log.e(TAG, "Error while erroring: Could not build json");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private String buildMarkdown() {
|
||||
try {
|
||||
final StringBuilder htmlErrorReport = new StringBuilder();
|
||||
|
||||
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
|
||||
if (!userComment.isEmpty()) {
|
||||
htmlErrorReport.append(userComment).append("\n");
|
||||
}
|
||||
|
||||
// basic error info
|
||||
htmlErrorReport
|
||||
.append("## Exception")
|
||||
.append("\n* __User Action:__ ")
|
||||
.append(getUserActionString(errorInfo.getUserAction()))
|
||||
.append("\n* __Request:__ ").append(errorInfo.getRequest())
|
||||
.append("\n* __Content Country:__ ").append(getContentCountryString())
|
||||
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
||||
.append("\n* __App Language:__ ").append(getAppLanguage())
|
||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
||||
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
||||
|
||||
|
||||
// Collapse all logs to a single paragraph when there are more than one
|
||||
// to keep the GitHub issue clean.
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport
|
||||
.append("<details><summary><b>Exceptions (")
|
||||
.append(errorInfo.getStackTraces().length)
|
||||
.append(")</b></summary><p>\n");
|
||||
}
|
||||
|
||||
// add the logs
|
||||
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
|
||||
htmlErrorReport.append("<details><summary><b>Crash log ");
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport.append(i + 1);
|
||||
}
|
||||
htmlErrorReport.append("</b>")
|
||||
.append("</summary><p>\n")
|
||||
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
|
||||
.append("</details>\n");
|
||||
}
|
||||
|
||||
// make sure to close everything
|
||||
if (errorInfo.getStackTraces().length > 1) {
|
||||
htmlErrorReport.append("</p></details>\n");
|
||||
}
|
||||
htmlErrorReport.append("<hr>\n");
|
||||
return htmlErrorReport.toString();
|
||||
} catch (final Throwable e) {
|
||||
Log.e(TAG, "Error while erroring: Could not build markdown");
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String getUserActionString(final UserAction userAction) {
|
||||
if (userAction == null) {
|
||||
return "Your description is in another castle.";
|
||||
} else {
|
||||
return userAction.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentCountryString() {
|
||||
return Localization.getPreferredContentCountry(this).getCountryCode();
|
||||
}
|
||||
|
||||
private String getContentLanguageString() {
|
||||
return Localization.getPreferredLocalization(this).getLocalizationCode();
|
||||
}
|
||||
|
||||
private String getAppLanguage() {
|
||||
return Localization.getAppLocale(getApplicationContext()).toString();
|
||||
}
|
||||
|
||||
private String getOsString() {
|
||||
final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
? Build.VERSION.BASE_OS : "Android";
|
||||
return System.getProperty("os.name")
|
||||
+ " " + (osBase.isEmpty() ? "Android" : osBase)
|
||||
+ " " + Build.VERSION.RELEASE
|
||||
+ " - " + Build.VERSION.SDK_INT;
|
||||
}
|
||||
|
||||
private void addGuruMeditation() {
|
||||
//just an easter egg
|
||||
String text = activityErrorBinding.errorSorryView.getText().toString();
|
||||
text += "\n" + getString(R.string.guru_meditation);
|
||||
activityErrorBinding.errorSorryView.setText(text);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
package org.schabi.newpipe.error
|
||||
|
||||
import android.R
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.ActionBar
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.grack.nanojson.JsonWriter
|
||||
import org.schabi.newpipe.BuildConfig
|
||||
import org.schabi.newpipe.databinding.ActivityErrorBinding
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Arrays
|
||||
import java.util.stream.Collectors
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ErrorActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* <
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
* <
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* This activity is used to show error details and allow reporting them in various ways. Use [ ][ErrorUtil.openActivity] to correctly open this activity.
|
||||
*/
|
||||
class ErrorActivity() : AppCompatActivity() {
|
||||
private var errorInfo: ErrorInfo? = null
|
||||
private var currentTimeStamp: String? = null
|
||||
private var activityErrorBinding: ActivityErrorBinding? = null
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Activity lifecycle
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
protected override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Localization.assureCorrectAppLanguage(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeHelper.setDayNightMode(this)
|
||||
ThemeHelper.setTheme(this)
|
||||
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater())
|
||||
setContentView(activityErrorBinding!!.getRoot())
|
||||
val intent: Intent = getIntent()
|
||||
setSupportActionBar(activityErrorBinding!!.toolbarLayout.toolbar)
|
||||
val actionBar: ActionBar? = getSupportActionBar()
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true)
|
||||
actionBar.setTitle(R.string.error_report_title)
|
||||
actionBar.setDisplayShowTitleEnabled(true)
|
||||
}
|
||||
errorInfo = IntentCompat.getParcelableExtra<ErrorInfo>(intent, ERROR_INFO, ErrorInfo::class.java)
|
||||
|
||||
// important add guru meditation
|
||||
addGuruMeditation()
|
||||
currentTimeStamp = CURRENT_TIMESTAMP_FORMATTER.format(LocalDateTime.now())
|
||||
activityErrorBinding!!.errorReportEmailButton.setOnClickListener(View.OnClickListener({ v: View? -> openPrivacyPolicyDialog(this, "EMAIL") }))
|
||||
activityErrorBinding!!.errorReportCopyButton.setOnClickListener(View.OnClickListener({ v: View? -> ShareUtils.copyToClipboard(this, buildMarkdown()) }))
|
||||
activityErrorBinding!!.errorReportGitHubButton.setOnClickListener(View.OnClickListener({ v: View? -> openPrivacyPolicyDialog(this, "GITHUB") }))
|
||||
|
||||
// normal bugreport
|
||||
buildInfo(errorInfo)
|
||||
activityErrorBinding!!.errorMessageView.setText(errorInfo!!.messageStringId)
|
||||
activityErrorBinding!!.errorView.setText(formErrorText(errorInfo!!.stackTraces))
|
||||
|
||||
// print stack trace once again for debugging:
|
||||
for (e: String? in errorInfo!!.stackTraces) {
|
||||
Log.e(TAG, (e)!!)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater: MenuInflater = getMenuInflater()
|
||||
inflater.inflate(R.menu.error_menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
public override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.getItemId()) {
|
||||
R.id.home -> {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_item_share_error -> {
|
||||
shareText(getApplicationContext(),
|
||||
getString(R.string.error_report_title), buildJson())
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPrivacyPolicyDialog(context: Context, action: String) {
|
||||
AlertDialog.Builder(context)
|
||||
.setIcon(R.drawable.ic_dialog_alert)
|
||||
.setTitle(R.string.privacy_policy_title)
|
||||
.setMessage(R.string.start_accept_privacy_policy)
|
||||
.setCancelable(false)
|
||||
.setNeutralButton(R.string.read_privacy_policy, DialogInterface.OnClickListener({ dialog: DialogInterface?, which: Int ->
|
||||
ShareUtils.openUrlInApp(context,
|
||||
context.getString(R.string.privacy_policy_url))
|
||||
}))
|
||||
.setPositiveButton(R.string.accept, DialogInterface.OnClickListener({ dialog: DialogInterface?, which: Int ->
|
||||
if ((action == "EMAIL")) { // send on email
|
||||
val i: Intent = Intent(Intent.ACTION_SENDTO)
|
||||
.setData(Uri.parse("mailto:")) // only email apps should handle this
|
||||
.putExtra(Intent.EXTRA_EMAIL, arrayOf<String>(ERROR_EMAIL_ADDRESS))
|
||||
.putExtra(Intent.EXTRA_SUBJECT, (ERROR_EMAIL_SUBJECT
|
||||
+ getString(R.string.app_name) + " "
|
||||
+ BuildConfig.VERSION_NAME))
|
||||
.putExtra(Intent.EXTRA_TEXT, buildJson())
|
||||
ShareUtils.openIntentInApp(context, i)
|
||||
} else if ((action == "GITHUB")) { // open the NewPipe issue page on GitHub
|
||||
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
|
||||
}
|
||||
}))
|
||||
.setNegativeButton(R.string.decline, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun formErrorText(el: Array<String>): String {
|
||||
val separator: String = "-------------------------------------"
|
||||
return Arrays.stream(el)
|
||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator))
|
||||
}
|
||||
|
||||
private fun buildInfo(info: ErrorInfo?) {
|
||||
var text: String? = ""
|
||||
activityErrorBinding!!.errorInfoLabelsView.setText(getString(R.string.info_labels)
|
||||
.replace("\\n", "\n"))
|
||||
text += ((getUserActionString(info!!.userAction) + "\n"
|
||||
+ info.request + "\n"
|
||||
+ contentLanguageString + "\n"
|
||||
+ contentCountryString + "\n"
|
||||
+ appLanguage + "\n"
|
||||
+ info.serviceName + "\n"
|
||||
+ currentTimeStamp + "\n"
|
||||
+ getPackageName() + "\n"
|
||||
+ BuildConfig.VERSION_NAME).toString() + "\n"
|
||||
+ osString)
|
||||
activityErrorBinding!!.errorInfosView.setText(text)
|
||||
}
|
||||
|
||||
private fun buildJson(): String {
|
||||
try {
|
||||
return JsonWriter.string()
|
||||
.`object`()
|
||||
.value("user_action", getUserActionString(errorInfo!!.userAction))
|
||||
.value("request", errorInfo!!.request)
|
||||
.value("content_language", contentLanguageString)
|
||||
.value("content_country", contentCountryString)
|
||||
.value("app_language", appLanguage)
|
||||
.value("service", errorInfo!!.serviceName)
|
||||
.value("package", getPackageName())
|
||||
.value("version", BuildConfig.VERSION_NAME)
|
||||
.value("os", osString)
|
||||
.value("time", currentTimeStamp)
|
||||
.array("exceptions", Arrays.asList<String>(*errorInfo!!.stackTraces))
|
||||
.value("user_comment", activityErrorBinding!!.errorCommentBox.getText()
|
||||
.toString())
|
||||
.end()
|
||||
.done()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Error while erroring: Could not build json")
|
||||
e.printStackTrace()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun buildMarkdown(): String {
|
||||
try {
|
||||
val htmlErrorReport: StringBuilder = StringBuilder()
|
||||
val userComment: String = activityErrorBinding!!.errorCommentBox.getText().toString()
|
||||
if (!userComment.isEmpty()) {
|
||||
htmlErrorReport.append(userComment).append("\n")
|
||||
}
|
||||
|
||||
// basic error info
|
||||
htmlErrorReport
|
||||
.append("## Exception")
|
||||
.append("\n* __User Action:__ ")
|
||||
.append(getUserActionString(errorInfo!!.userAction))
|
||||
.append("\n* __Request:__ ").append(errorInfo!!.request)
|
||||
.append("\n* __Content Country:__ ").append(contentCountryString)
|
||||
.append("\n* __Content Language:__ ").append(contentLanguageString)
|
||||
.append("\n* __App Language:__ ").append(appLanguage)
|
||||
.append("\n* __Service:__ ").append(errorInfo!!.serviceName)
|
||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
||||
.append("\n* __OS:__ ").append(osString).append("\n")
|
||||
|
||||
|
||||
// Collapse all logs to a single paragraph when there are more than one
|
||||
// to keep the GitHub issue clean.
|
||||
if (errorInfo!!.stackTraces.size > 1) {
|
||||
htmlErrorReport
|
||||
.append("<details><summary><b>Exceptions (")
|
||||
.append(errorInfo!!.stackTraces.size)
|
||||
.append(")</b></summary><p>\n")
|
||||
}
|
||||
|
||||
// add the logs
|
||||
for (i in errorInfo!!.stackTraces.indices) {
|
||||
htmlErrorReport.append("<details><summary><b>Crash log ")
|
||||
if (errorInfo!!.stackTraces.size > 1) {
|
||||
htmlErrorReport.append(i + 1)
|
||||
}
|
||||
htmlErrorReport.append("</b>")
|
||||
.append("</summary><p>\n")
|
||||
.append("\n```\n").append(errorInfo!!.stackTraces.get(i)).append("\n```\n")
|
||||
.append("</details>\n")
|
||||
}
|
||||
|
||||
// make sure to close everything
|
||||
if (errorInfo!!.stackTraces.size > 1) {
|
||||
htmlErrorReport.append("</p></details>\n")
|
||||
}
|
||||
htmlErrorReport.append("<hr>\n")
|
||||
return htmlErrorReport.toString()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Error while erroring: Could not build markdown")
|
||||
e.printStackTrace()
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUserActionString(userAction: UserAction?): String? {
|
||||
if (userAction == null) {
|
||||
return "Your description is in another castle."
|
||||
} else {
|
||||
return userAction.getMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private val contentCountryString: String
|
||||
private get() {
|
||||
return Localization.getPreferredContentCountry(this).getCountryCode()
|
||||
}
|
||||
private val contentLanguageString: String
|
||||
private get() {
|
||||
return Localization.getPreferredLocalization(this).getLocalizationCode()
|
||||
}
|
||||
private val appLanguage: String
|
||||
private get() {
|
||||
return Localization.getAppLocale(getApplicationContext()).toString()
|
||||
}
|
||||
private val osString: String
|
||||
private get() {
|
||||
val osBase: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) Build.VERSION.BASE_OS else "Android"
|
||||
return (System.getProperty("os.name")
|
||||
+ " " + (if (osBase.isEmpty()) "Android" else osBase)
|
||||
+ " " + Build.VERSION.RELEASE
|
||||
+ " - " + Build.VERSION.SDK_INT)
|
||||
}
|
||||
|
||||
private fun addGuruMeditation() {
|
||||
//just an easter egg
|
||||
var text: String? = activityErrorBinding!!.errorSorryView.getText().toString()
|
||||
text += "\n" + getString(R.string.guru_meditation)
|
||||
activityErrorBinding!!.errorSorryView.setText(text)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// LOG TAGS
|
||||
val TAG: String = ErrorActivity::class.java.toString()
|
||||
|
||||
// BUNDLE TAGS
|
||||
val ERROR_INFO: String = "error_info"
|
||||
val ERROR_EMAIL_ADDRESS: String = "crashreport@newpipe.schabi.org"
|
||||
val ERROR_EMAIL_SUBJECT: String = "Exception in "
|
||||
val ERROR_GITHUB_ISSUE_URL: String = "https://github.com/TeamNewPipe/NewPipe/issues"
|
||||
val CURRENT_TIMESTAMP_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
||||
|
||||
/**
|
||||
* Get the checked activity.
|
||||
*
|
||||
* @param returnActivity the activity to return to
|
||||
* @return the casted return activity or null
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getReturnActivity(returnActivity: Class<*>?): Class<out Activity?>? {
|
||||
var checkedReturnActivity: Class<out Activity?>? = null
|
||||
if (returnActivity != null) {
|
||||
if (Activity::class.java.isAssignableFrom(returnActivity)) {
|
||||
checkedReturnActivity = returnActivity.asSubclass<Activity?>(Activity::class.java)
|
||||
} else {
|
||||
checkedReturnActivity = MainActivity::class.java
|
||||
}
|
||||
}
|
||||
return checkedReturnActivity
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
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.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/*
|
||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* ReCaptchaActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
public class 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";
|
||||
|
||||
public static String sanitizeRecaptchaUrl(@Nullable final String url) {
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
return YT_URL; // YouTube is the most likely service to have thrown a recaptcha
|
||||
} else {
|
||||
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
|
||||
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "");
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityRecaptchaBinding recaptchaBinding;
|
||||
private String foundCookies = "";
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater());
|
||||
setContentView(recaptchaBinding.getRoot());
|
||||
setSupportActionBar(recaptchaBinding.toolbar);
|
||||
|
||||
final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA));
|
||||
// set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
// enable Javascript
|
||||
final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
|
||||
|
||||
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||
final WebResourceRequest request) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString());
|
||||
}
|
||||
|
||||
handleCookiesFromUrl(request.getUrl().toString());
|
||||
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
|
||||
recaptchaBinding.reCaptchaWebView.clearCache(true);
|
||||
recaptchaBinding.reCaptchaWebView.clearHistory();
|
||||
CookieManager.getInstance().removeAllCookies(null);
|
||||
|
||||
recaptchaBinding.reCaptchaWebView.loadUrl(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
|
||||
|
||||
final 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) {
|
||||
if (item.getItemId() == R.id.menu_item_done) {
|
||||
saveCookiesAndFinish();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void saveCookiesAndFinish() {
|
||||
// try to get cookies of unclosed page
|
||||
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.getUrl());
|
||||
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);
|
||||
}
|
||||
|
||||
// Navigate to blank page (unloads youtube to prevent background playback)
|
||||
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank");
|
||||
|
||||
final 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;
|
||||
}
|
||||
|
||||
final String cookies = CookieManager.getInstance().getCookie(url);
|
||||
handleCookies(cookies);
|
||||
|
||||
// sometimes cookies are inside the url
|
||||
final int abuseStart = url.indexOf("google_abuse=");
|
||||
if (abuseStart != -1) {
|
||||
final int abuseEnd = url.indexOf("+path");
|
||||
|
||||
try {
|
||||
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
||||
abuseCookie = Utils.decodeUrlUtf8(abuseCookie);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package org.schabi.newpipe.error
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
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.appcompat.app.ActionBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.schabi.newpipe.DownloaderImpl
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding
|
||||
import org.schabi.newpipe.extractor.utils.Utils
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.io.UnsupportedEncodingException
|
||||
|
||||
/*
|
||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* ReCaptchaActivity.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class ReCaptchaActivity() : AppCompatActivity() {
|
||||
private var recaptchaBinding: ActivityRecaptchaBinding? = null
|
||||
private var foundCookies: String = ""
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeHelper.setTheme(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater())
|
||||
setContentView(recaptchaBinding!!.getRoot())
|
||||
setSupportActionBar(recaptchaBinding!!.toolbar)
|
||||
val url: String = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA))
|
||||
// set return to Cancel by default
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
// enable Javascript
|
||||
val webSettings: WebSettings = recaptchaBinding!!.reCaptchaWebView.getSettings()
|
||||
webSettings.setJavaScriptEnabled(true)
|
||||
webSettings.setUserAgentString(DownloaderImpl.Companion.USER_AGENT)
|
||||
recaptchaBinding!!.reCaptchaWebView.setWebViewClient(object : WebViewClient() {
|
||||
public override fun shouldOverrideUrlLoading(view: WebView,
|
||||
request: WebResourceRequest): Boolean {
|
||||
if (MainActivity.Companion.DEBUG) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString())
|
||||
}
|
||||
handleCookiesFromUrl(request.getUrl().toString())
|
||||
return false
|
||||
}
|
||||
|
||||
public override fun onPageFinished(view: WebView, url: String) {
|
||||
super.onPageFinished(view, url)
|
||||
handleCookiesFromUrl(url)
|
||||
}
|
||||
})
|
||||
|
||||
// cleaning cache, history and cookies from webView
|
||||
recaptchaBinding!!.reCaptchaWebView.clearCache(true)
|
||||
recaptchaBinding!!.reCaptchaWebView.clearHistory()
|
||||
CookieManager.getInstance().removeAllCookies(null)
|
||||
recaptchaBinding!!.reCaptchaWebView.loadUrl(url)
|
||||
}
|
||||
|
||||
public override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
getMenuInflater().inflate(R.menu.menu_recaptcha, menu)
|
||||
val 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
|
||||
}
|
||||
|
||||
public override fun onBackPressed() {
|
||||
saveCookiesAndFinish()
|
||||
}
|
||||
|
||||
public override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.getItemId() == R.id.menu_item_done) {
|
||||
saveCookiesAndFinish()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun saveCookiesAndFinish() {
|
||||
// try to get cookies of unclosed page
|
||||
handleCookiesFromUrl(recaptchaBinding!!.reCaptchaWebView.getUrl())
|
||||
if (MainActivity.Companion.DEBUG) {
|
||||
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies)
|
||||
}
|
||||
if (!foundCookies.isEmpty()) {
|
||||
// save cookies to preferences
|
||||
val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(
|
||||
getApplicationContext())
|
||||
val key: String = getApplicationContext().getString(R.string.recaptcha_cookies_key)
|
||||
prefs.edit().putString(key, foundCookies).apply()
|
||||
|
||||
// give cookies to Downloader class
|
||||
DownloaderImpl.Companion.getInstance()!!.setCookie(RECAPTCHA_COOKIES_KEY, foundCookies)
|
||||
setResult(RESULT_OK)
|
||||
}
|
||||
|
||||
// Navigate to blank page (unloads youtube to prevent background playback)
|
||||
recaptchaBinding!!.reCaptchaWebView.loadUrl("about:blank")
|
||||
val intent: Intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
NavUtils.navigateUpTo(this, intent)
|
||||
}
|
||||
|
||||
private fun handleCookiesFromUrl(url: String?) {
|
||||
if (MainActivity.Companion.DEBUG) {
|
||||
Log.d(TAG, "handleCookiesFromUrl: url=" + (if (url == null) "null" else url))
|
||||
}
|
||||
if (url == null) {
|
||||
return
|
||||
}
|
||||
val cookies: String = CookieManager.getInstance().getCookie(url)
|
||||
handleCookies(cookies)
|
||||
|
||||
// sometimes cookies are inside the url
|
||||
val abuseStart: Int = url.indexOf("google_abuse=")
|
||||
if (abuseStart != -1) {
|
||||
val abuseEnd: Int = url.indexOf("+path")
|
||||
try {
|
||||
var abuseCookie: String? = url.substring(abuseStart + 13, abuseEnd)
|
||||
abuseCookie = Utils.decodeUrlUtf8(abuseCookie)
|
||||
handleCookies(abuseCookie)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
if (MainActivity.Companion.DEBUG) {
|
||||
e.printStackTrace()
|
||||
Log.d(TAG, ("handleCookiesFromUrl: invalid google abuse starting at "
|
||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url))
|
||||
}
|
||||
} catch (e: StringIndexOutOfBoundsException) {
|
||||
if (MainActivity.Companion.DEBUG) {
|
||||
e.printStackTrace()
|
||||
Log.d(TAG, ("handleCookiesFromUrl: invalid google abuse starting at "
|
||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCookies(cookies: String?) {
|
||||
if (MainActivity.Companion.DEBUG) {
|
||||
Log.d(TAG, "handleCookies: cookies=" + (if (cookies == null) "null" else cookies))
|
||||
}
|
||||
if (cookies == null) {
|
||||
return
|
||||
}
|
||||
addYoutubeCookies(cookies)
|
||||
// add here methods to extract cookies for other services
|
||||
}
|
||||
|
||||
private fun addYoutubeCookies(cookies: String) {
|
||||
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 fun addCookie(cookie: String) {
|
||||
if (foundCookies.contains(cookie)) {
|
||||
return
|
||||
}
|
||||
if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
|
||||
foundCookies += cookie
|
||||
} else if (foundCookies.endsWith(";")) {
|
||||
foundCookies += " " + cookie
|
||||
} else {
|
||||
foundCookies += "; " + cookie
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val RECAPTCHA_REQUEST: Int = 10
|
||||
val RECAPTCHA_URL_EXTRA: String = "recaptcha_url_extra"
|
||||
val TAG: String = ReCaptchaActivity::class.java.toString()
|
||||
val YT_URL: String = "https://www.youtube.com"
|
||||
val RECAPTCHA_COOKIES_KEY: String = "recaptcha_cookies"
|
||||
fun sanitizeRecaptchaUrl(url: String?): String {
|
||||
if (url == null || url.trim({ it <= ' ' }).isEmpty()) {
|
||||
return YT_URL // YouTube is the most likely service to have thrown a recaptcha
|
||||
} else {
|
||||
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
|
||||
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package org.schabi.newpipe.error;
|
||||
package org.schabi.newpipe.error
|
||||
|
||||
/**
|
||||
* The user actions that can cause an error.
|
||||
*/
|
||||
public enum UserAction {
|
||||
enum class UserAction(val message: String) {
|
||||
USER_REPORT("user report"),
|
||||
UI_ERROR("ui error"),
|
||||
SUBSCRIPTION_CHANGE("subscription change"),
|
||||
|
@ -31,15 +31,6 @@ public enum UserAction {
|
|||
PREFERENCES_MIGRATION("migration of preferences"),
|
||||
SHARE_TO_NEWPIPE("share to newpipe"),
|
||||
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
|
||||
OPEN_INFO_ITEM_DIALOG("open info item dialog");
|
||||
OPEN_INFO_ITEM_DIALOG("open info item dialog")
|
||||
|
||||
private final String message;
|
||||
|
||||
UserAction(final String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
package org.schabi.newpipe.fragments;
|
||||
package org.schabi.newpipe.fragments
|
||||
|
||||
/**
|
||||
* Indicates that the current fragment can handle back presses.
|
||||
*/
|
||||
public interface BackPressable {
|
||||
open interface BackPressable {
|
||||
/**
|
||||
* A back press was delegated to this fragment.
|
||||
*
|
||||
* @return if the back press was handled
|
||||
*/
|
||||
boolean onBackPressed();
|
||||
fun onBackPressed(): Boolean
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorPanelHelper;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
@State
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||
protected AtomicBoolean isLoading = new AtomicBoolean();
|
||||
|
||||
@Nullable
|
||||
protected View emptyStateView;
|
||||
@Nullable
|
||||
protected TextView emptyStateMessageView;
|
||||
@Nullable
|
||||
private ProgressBar loadingProgressBar;
|
||||
|
||||
private ErrorPanelHelper errorPanelHelper;
|
||||
@Nullable
|
||||
@State
|
||||
protected ErrorInfo lastPanelError = null;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
doInitialLoadLogic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
wasLoading.set(isLoading.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (lastPanelError != null) {
|
||||
showError(lastPanelError);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
||||
emptyStateMessageView = rootView.findViewById(R.id.empty_state_message);
|
||||
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
|
||||
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (errorPanelHelper != null) {
|
||||
errorPanelHelper.dispose();
|
||||
}
|
||||
emptyStateView = null;
|
||||
emptyStateMessageView = null;
|
||||
}
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
reloadContent();
|
||||
}
|
||||
|
||||
public void reloadContent() {
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void doInitialLoadLogic() {
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
protected void startLoading(final boolean forceLoad) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
||||
}
|
||||
showLoading();
|
||||
isLoading.set(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
if (emptyStateView != null) {
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animate(loadingProgressBar, true, 400);
|
||||
}
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
if (emptyStateView != null) {
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) {
|
||||
animate(emptyStateView, true, 200);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
hideErrorPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(final I result) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "handleResult() called with: result = [" + result + "]");
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError() {
|
||||
isLoading.set(false);
|
||||
InfoCache.getInstance().clearCache();
|
||||
if (emptyStateView != null) {
|
||||
animate(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animate(loadingProgressBar, false, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Error handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public final void showError(final ErrorInfo errorInfo) {
|
||||
handleError();
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "showError() is detached or removing = [" + errorInfo + "]");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
errorPanelHelper.showError(errorInfo);
|
||||
lastPanelError = errorInfo;
|
||||
}
|
||||
|
||||
public final void showTextError(@NonNull final String errorString) {
|
||||
handleError();
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
errorPanelHelper.showTextError(errorString);
|
||||
}
|
||||
|
||||
protected void setEmptyStateMessage(@StringRes final int text) {
|
||||
if (emptyStateMessageView != null) {
|
||||
emptyStateMessageView.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public final void hideErrorPanel() {
|
||||
errorPanelHelper.hide();
|
||||
lastPanelError = null;
|
||||
}
|
||||
|
||||
public final boolean isErrorPanelVisible() {
|
||||
return errorPanelHelper.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly calls {@link ErrorUtil#showSnackbar(Fragment, ErrorInfo)}, that shows a snackbar if
|
||||
* a valid view can be found, otherwise creates an error report notification.
|
||||
*
|
||||
* @param errorInfo The error information
|
||||
*/
|
||||
public void showSnackBarError(final ErrorInfo errorInfo) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]");
|
||||
}
|
||||
ErrorUtil.showSnackbar(this, errorInfo);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
package org.schabi.newpipe.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import icepick.State
|
||||
import org.schabi.newpipe.BaseFragment
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.ErrorPanelHelper
|
||||
import org.schabi.newpipe.error.ErrorUtil
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.util.InfoCache
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
abstract class BaseStateFragment<I>() : BaseFragment(), ViewContract<I> {
|
||||
@State
|
||||
protected var wasLoading: AtomicBoolean = AtomicBoolean()
|
||||
protected var isLoading: AtomicBoolean = AtomicBoolean()
|
||||
protected var emptyStateView: View? = null
|
||||
protected var emptyStateMessageView: TextView? = null
|
||||
private var loadingProgressBar: ProgressBar? = null
|
||||
private var errorPanelHelper: ErrorPanelHelper? = null
|
||||
|
||||
@State
|
||||
protected var lastPanelError: ErrorInfo? = null
|
||||
public override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
doInitialLoadLogic()
|
||||
}
|
||||
|
||||
public override fun onPause() {
|
||||
super.onPause()
|
||||
wasLoading.set(isLoading.get())
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
if (lastPanelError != null) {
|
||||
showError(lastPanelError!!)
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
|
||||
super.initViews(rootView, savedInstanceState)
|
||||
emptyStateView = rootView.findViewById(R.id.empty_state_view)
|
||||
emptyStateMessageView = rootView.findViewById(R.id.empty_state_message)
|
||||
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar)
|
||||
errorPanelHelper = ErrorPanelHelper(this, rootView, Runnable({ onRetryButtonClicked() }))
|
||||
}
|
||||
|
||||
public override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
if (errorPanelHelper != null) {
|
||||
errorPanelHelper!!.dispose()
|
||||
}
|
||||
emptyStateView = null
|
||||
emptyStateMessageView = null
|
||||
}
|
||||
|
||||
protected fun onRetryButtonClicked() {
|
||||
reloadContent()
|
||||
}
|
||||
|
||||
open fun reloadContent() {
|
||||
startLoading(true)
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
protected open fun doInitialLoadLogic() {
|
||||
startLoading(true)
|
||||
}
|
||||
|
||||
protected open fun startLoading(forceLoad: Boolean) {
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]")
|
||||
}
|
||||
showLoading()
|
||||
isLoading.set(true)
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
public override fun showLoading() {
|
||||
if (emptyStateView != null) {
|
||||
emptyStateView!!.animate(false, 150)
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
loadingProgressBar!!.animate(true, 400)
|
||||
}
|
||||
hideErrorPanel()
|
||||
}
|
||||
|
||||
public override fun hideLoading() {
|
||||
if (emptyStateView != null) {
|
||||
emptyStateView!!.animate(false, 150)
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
loadingProgressBar!!.animate(false, 0)
|
||||
}
|
||||
hideErrorPanel()
|
||||
}
|
||||
|
||||
public override fun showEmptyState() {
|
||||
isLoading.set(false)
|
||||
if (emptyStateView != null) {
|
||||
emptyStateView!!.animate(true, 200)
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
loadingProgressBar!!.animate(false, 0)
|
||||
}
|
||||
hideErrorPanel()
|
||||
}
|
||||
|
||||
public override fun handleResult(result: I) {
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.d(TAG, "handleResult() called with: result = [" + result + "]")
|
||||
}
|
||||
hideLoading()
|
||||
}
|
||||
|
||||
public override fun handleError() {
|
||||
isLoading.set(false)
|
||||
InfoCache.Companion.getInstance().clearCache()
|
||||
if (emptyStateView != null) {
|
||||
emptyStateView!!.animate(false, 150)
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
loadingProgressBar!!.animate(false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Error handling
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
fun showError(errorInfo: ErrorInfo) {
|
||||
handleError()
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.w(TAG, "showError() is detached or removing = [" + errorInfo + "]")
|
||||
}
|
||||
return
|
||||
}
|
||||
errorPanelHelper!!.showError(errorInfo)
|
||||
lastPanelError = errorInfo
|
||||
}
|
||||
|
||||
fun showTextError(errorString: String) {
|
||||
handleError()
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.w(TAG, "showTextError() is detached or removing = [" + errorString + "]")
|
||||
}
|
||||
return
|
||||
}
|
||||
errorPanelHelper!!.showTextError(errorString)
|
||||
}
|
||||
|
||||
protected fun setEmptyStateMessage(@StringRes text: Int) {
|
||||
if (emptyStateMessageView != null) {
|
||||
emptyStateMessageView!!.setText(text)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideErrorPanel() {
|
||||
errorPanelHelper!!.hide()
|
||||
lastPanelError = null
|
||||
}
|
||||
|
||||
val isErrorPanelVisible: Boolean
|
||||
get() {
|
||||
return errorPanelHelper!!.isVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly calls [ErrorUtil.showSnackbar], that shows a snackbar if
|
||||
* a valid view can be found, otherwise creates an error report notification.
|
||||
*
|
||||
* @param errorInfo The error information
|
||||
*/
|
||||
fun showSnackBarError(errorInfo: ErrorInfo) {
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: errorInfo = [" + errorInfo + "]")
|
||||
}
|
||||
showSnackbar(this, errorInfo)
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class BlankFragment extends BaseFragment {
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
setTitle("NewPipe");
|
||||
return inflater.inflate(R.layout.fragment_blank, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setTitle("NewPipe");
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
// If you are a Copy cat FUCK YOU.
|
||||
// I WILL FIND YOU, AND I WILL ...
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package org.schabi.newpipe.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.schabi.newpipe.BaseFragment
|
||||
import org.schabi.newpipe.R
|
||||
|
||||
class BlankFragment() : BaseFragment() {
|
||||
public override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
setTitle("NewPipe")
|
||||
return inflater.inflate(R.layout.fragment_blank, container, false)
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
setTitle("NewPipe")
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
// If you are a Copy cat FUCK YOU.
|
||||
// I WILL FIND YOU, AND I WILL ...
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class EmptyFragment extends BaseFragment {
|
||||
private static final String SHOW_MESSAGE = "SHOW_MESSAGE";
|
||||
|
||||
public static final EmptyFragment newInstance(final boolean showMessage) {
|
||||
final EmptyFragment emptyFragment = new EmptyFragment();
|
||||
final Bundle bundle = new Bundle(1);
|
||||
bundle.putBoolean(SHOW_MESSAGE, showMessage);
|
||||
emptyFragment.setArguments(bundle);
|
||||
return emptyFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final boolean showMessage = getArguments().getBoolean(SHOW_MESSAGE);
|
||||
final View view = inflater.inflate(R.layout.fragment_empty, container, false);
|
||||
view.findViewById(R.id.empty_state_view).setVisibility(
|
||||
showMessage ? View.VISIBLE : View.GONE);
|
||||
return view;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.schabi.newpipe.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.schabi.newpipe.BaseFragment
|
||||
import org.schabi.newpipe.R
|
||||
|
||||
class EmptyFragment() : BaseFragment() {
|
||||
public override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
val showMessage: Boolean = getArguments()!!.getBoolean(SHOW_MESSAGE)
|
||||
val view: View = inflater.inflate(R.layout.fragment_empty, container, false)
|
||||
view.findViewById<View>(R.id.empty_state_view).setVisibility(
|
||||
if (showMessage) View.VISIBLE else View.GONE)
|
||||
return view
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SHOW_MESSAGE: String = "SHOW_MESSAGE"
|
||||
fun newInstance(showMessage: Boolean): EmptyFragment {
|
||||
val emptyFragment: EmptyFragment = EmptyFragment()
|
||||
val bundle: Bundle = Bundle(1)
|
||||
bundle.putBoolean(SHOW_MESSAGE, showMessage)
|
||||
emptyFragment.setArguments(bundle)
|
||||
return emptyFragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,342 +0,0 @@
|
|||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import static android.widget.RelativeLayout.ABOVE;
|
||||
import static android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM;
|
||||
import static android.widget.RelativeLayout.ALIGN_PARENT_TOP;
|
||||
import static android.widget.RelativeLayout.BELOW;
|
||||
import static com.google.android.material.tabs.TabLayout.INDICATOR_GRAVITY_BOTTOM;
|
||||
import static com.google.android.material.tabs.TabLayout.INDICATOR_GRAVITY_TOP;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentMainBinding;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
|
||||
import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.ScrollableTabLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
||||
private FragmentMainBinding binding;
|
||||
private SelectedTabsPagerAdapter pagerAdapter;
|
||||
|
||||
private final List<Tab> tabsList = new ArrayList<>();
|
||||
private TabsManager tabsManager;
|
||||
|
||||
private boolean hasTabsChanged = false;
|
||||
|
||||
private SharedPreferences prefs;
|
||||
private boolean youtubeRestrictedModeEnabled;
|
||||
private String youtubeRestrictedModeEnabledKey;
|
||||
private boolean mainTabsPositionBottom;
|
||||
private String mainTabsPositionKey;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
tabsManager = TabsManager.getManager(activity);
|
||||
tabsManager.setSavedTabsListener(() -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "TabsManager.SavedTabsChangeListener: "
|
||||
+ "onTabsChanged called, isResumed = " + isResumed());
|
||||
}
|
||||
if (isResumed()) {
|
||||
setupTabs();
|
||||
} else {
|
||||
hasTabsChanged = true;
|
||||
}
|
||||
});
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||
youtubeRestrictedModeEnabled = prefs.getBoolean(youtubeRestrictedModeEnabledKey, false);
|
||||
mainTabsPositionKey = getString(R.string.main_tabs_position_key);
|
||||
mainTabsPositionBottom = prefs.getBoolean(mainTabsPositionKey, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
binding = FragmentMainBinding.bind(rootView);
|
||||
|
||||
binding.mainTabLayout.setupWithViewPager(binding.pager);
|
||||
binding.mainTabLayout.addOnTabSelectedListener(this);
|
||||
|
||||
setupTabs();
|
||||
updateTabLayoutPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
final boolean newYoutubeRestrictedModeEnabled =
|
||||
prefs.getBoolean(youtubeRestrictedModeEnabledKey, false);
|
||||
if (youtubeRestrictedModeEnabled != newYoutubeRestrictedModeEnabled || hasTabsChanged) {
|
||||
youtubeRestrictedModeEnabled = newYoutubeRestrictedModeEnabled;
|
||||
setupTabs();
|
||||
}
|
||||
|
||||
final boolean newMainTabsPosition = prefs.getBoolean(mainTabsPositionKey, false);
|
||||
if (mainTabsPositionBottom != newMainTabsPosition) {
|
||||
mainTabsPositionBottom = newMainTabsPosition;
|
||||
updateTabLayoutPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
tabsManager.unsetSavedTabsListener();
|
||||
if (binding != null) {
|
||||
binding.pager.setAdapter(null);
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
inflater.inflate(R.menu.menu_main_fragment, menu);
|
||||
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_search) {
|
||||
try {
|
||||
NavigationHelper.openSearchFragment(getFM(),
|
||||
ServiceHelper.getSelectedServiceId(activity), "");
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening search fragment", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Tabs
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setupTabs() {
|
||||
tabsList.clear();
|
||||
tabsList.addAll(tabsManager.getTabs());
|
||||
|
||||
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
|
||||
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(),
|
||||
getChildFragmentManager(), tabsList);
|
||||
}
|
||||
|
||||
binding.pager.setAdapter(null);
|
||||
binding.pager.setAdapter(pagerAdapter);
|
||||
|
||||
updateTabsIconAndDescription();
|
||||
updateTitleForTab(binding.pager.getCurrentItem());
|
||||
|
||||
hasTabsChanged = false;
|
||||
}
|
||||
|
||||
private void updateTabsIconAndDescription() {
|
||||
for (int i = 0; i < tabsList.size(); i++) {
|
||||
final TabLayout.Tab tabToSet = binding.mainTabLayout.getTabAt(i);
|
||||
if (tabToSet != null) {
|
||||
final Tab tab = tabsList.get(i);
|
||||
tabToSet.setIcon(tab.getTabIconRes(requireContext()));
|
||||
tabToSet.setContentDescription(tab.getTabName(requireContext()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTitleForTab(final int tabPosition) {
|
||||
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
|
||||
}
|
||||
|
||||
public void commitPlaylistTabs() {
|
||||
pagerAdapter.getLocalPlaylistFragments()
|
||||
.stream()
|
||||
.forEach(LocalPlaylistFragment::saveImmediate);
|
||||
}
|
||||
|
||||
private void updateTabLayoutPosition() {
|
||||
final ScrollableTabLayout tabLayout = binding.mainTabLayout;
|
||||
final ViewPager viewPager = binding.pager;
|
||||
final boolean bottom = mainTabsPositionBottom;
|
||||
|
||||
// change layout params to make the tab layout appear either at the top or at the bottom
|
||||
final var tabParams = (RelativeLayout.LayoutParams) tabLayout.getLayoutParams();
|
||||
final var pagerParams = (RelativeLayout.LayoutParams) viewPager.getLayoutParams();
|
||||
|
||||
tabParams.removeRule(bottom ? ALIGN_PARENT_TOP : ALIGN_PARENT_BOTTOM);
|
||||
tabParams.addRule(bottom ? ALIGN_PARENT_BOTTOM : ALIGN_PARENT_TOP);
|
||||
pagerParams.removeRule(bottom ? BELOW : ABOVE);
|
||||
pagerParams.addRule(bottom ? ABOVE : BELOW, R.id.main_tab_layout);
|
||||
tabLayout.setSelectedTabIndicatorGravity(
|
||||
bottom ? INDICATOR_GRAVITY_TOP : INDICATOR_GRAVITY_BOTTOM);
|
||||
|
||||
tabLayout.setLayoutParams(tabParams);
|
||||
viewPager.setLayoutParams(pagerParams);
|
||||
|
||||
// change the background and icon color of the tab layout:
|
||||
// service-colored at the top, app-background-colored at the bottom
|
||||
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
|
||||
bottom ? R.attr.colorSecondary : R.attr.colorPrimary));
|
||||
|
||||
@ColorInt final int iconColor = bottom
|
||||
? ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)
|
||||
: Color.WHITE;
|
||||
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
|
||||
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
|
||||
tabLayout.setSelectedTabIndicatorColor(iconColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabSelected(final TabLayout.Tab selectedTab) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
|
||||
}
|
||||
updateTitleForTab(selectedTab.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(final TabLayout.Tab tab) { }
|
||||
|
||||
@Override
|
||||
public void onTabReselected(final TabLayout.Tab tab) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
|
||||
}
|
||||
updateTitleForTab(tab.getPosition());
|
||||
}
|
||||
|
||||
public static final class SelectedTabsPagerAdapter
|
||||
extends FragmentStatePagerAdapterMenuWorkaround {
|
||||
private final Context context;
|
||||
private final List<Tab> internalTabsList;
|
||||
/**
|
||||
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
||||
* during runtime and changes are not committed immediately. However, in some cases,
|
||||
* the changes need to be committed immediately by calling
|
||||
* {@link LocalPlaylistFragment#saveImmediate()}.
|
||||
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
|
||||
*/
|
||||
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
|
||||
|
||||
private SelectedTabsPagerAdapter(final Context context,
|
||||
final FragmentManager fragmentManager,
|
||||
final List<Tab> tabsList) {
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.context = context;
|
||||
this.internalTabsList = new ArrayList<>(tabsList);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(final int position) {
|
||||
final Tab tab = internalTabsList.get(position);
|
||||
|
||||
final Fragment fragment;
|
||||
try {
|
||||
fragment = tab.getFragment(context);
|
||||
} catch (final ExtractionException e) {
|
||||
ErrorUtil.showUiErrorSnackbar(context, "Getting fragment item", e);
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
if (fragment instanceof BaseFragment) {
|
||||
((BaseFragment) fragment).useAsFrontPage(true);
|
||||
}
|
||||
|
||||
if (fragment instanceof LocalPlaylistFragment) {
|
||||
localPlaylistFragments.add((LocalPlaylistFragment) fragment);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public List<LocalPlaylistFragment> getLocalPlaylistFragments() {
|
||||
return localPlaylistFragments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(@NonNull final Object object) {
|
||||
// Causes adapter to reload all Fragments when
|
||||
// notifyDataSetChanged is called
|
||||
return POSITION_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return internalTabsList.size();
|
||||
}
|
||||
|
||||
public boolean sameTabs(final List<Tab> tabsToCompare) {
|
||||
return internalTabsList.equals(tabsToCompare);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
package org.schabi.newpipe.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.ActionBar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import org.schabi.newpipe.BaseFragment
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FragmentMainBinding
|
||||
import org.schabi.newpipe.error.ErrorUtil.Companion.showUiErrorSnackbar
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment
|
||||
import org.schabi.newpipe.settings.tabs.Tab
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager.SavedTabsChangeListener
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.views.ScrollableTabLayout
|
||||
import java.util.function.Consumer
|
||||
|
||||
class MainFragment() : BaseFragment(), OnTabSelectedListener {
|
||||
private var binding: FragmentMainBinding? = null
|
||||
private var pagerAdapter: SelectedTabsPagerAdapter? = null
|
||||
private val tabsList: MutableList<Tab?> = ArrayList()
|
||||
private var tabsManager: TabsManager? = null
|
||||
private var hasTabsChanged: Boolean = false
|
||||
private var prefs: SharedPreferences? = null
|
||||
private var youtubeRestrictedModeEnabled: Boolean = false
|
||||
private var youtubeRestrictedModeEnabledKey: String? = null
|
||||
private var mainTabsPositionBottom: Boolean = false
|
||||
private var mainTabsPositionKey: String? = null
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
tabsManager = TabsManager.Companion.getManager((activity)!!)
|
||||
tabsManager!!.setSavedTabsListener(SavedTabsChangeListener({
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.d(TAG, ("TabsManager.SavedTabsChangeListener: "
|
||||
+ "onTabsChanged called, isResumed = " + isResumed()))
|
||||
}
|
||||
if (isResumed()) {
|
||||
setupTabs()
|
||||
} else {
|
||||
hasTabsChanged = true
|
||||
}
|
||||
}))
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled)
|
||||
youtubeRestrictedModeEnabled = prefs.getBoolean(youtubeRestrictedModeEnabledKey, false)
|
||||
mainTabsPositionKey = getString(R.string.main_tabs_position_key)
|
||||
mainTabsPositionBottom = prefs.getBoolean(mainTabsPositionKey, false)
|
||||
}
|
||||
|
||||
public override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_main, container, false)
|
||||
}
|
||||
|
||||
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
|
||||
super.initViews(rootView, savedInstanceState)
|
||||
binding = FragmentMainBinding.bind(rootView)
|
||||
binding!!.mainTabLayout.setupWithViewPager(binding!!.pager)
|
||||
binding!!.mainTabLayout.addOnTabSelectedListener(this)
|
||||
setupTabs()
|
||||
updateTabLayoutPosition()
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
val newYoutubeRestrictedModeEnabled: Boolean = prefs!!.getBoolean(youtubeRestrictedModeEnabledKey, false)
|
||||
if (youtubeRestrictedModeEnabled != newYoutubeRestrictedModeEnabled || hasTabsChanged) {
|
||||
youtubeRestrictedModeEnabled = newYoutubeRestrictedModeEnabled
|
||||
setupTabs()
|
||||
}
|
||||
val newMainTabsPosition: Boolean = prefs!!.getBoolean(mainTabsPositionKey, false)
|
||||
if (mainTabsPositionBottom != newMainTabsPosition) {
|
||||
mainTabsPositionBottom = newMainTabsPosition
|
||||
updateTabLayoutPosition()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
tabsManager!!.unsetSavedTabsListener()
|
||||
if (binding != null) {
|
||||
binding!!.pager.setAdapter(null)
|
||||
binding = null
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
binding = null
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
public override fun onCreateOptionsMenu(menu: Menu,
|
||||
inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.d(TAG, ("onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]"))
|
||||
}
|
||||
inflater.inflate(R.menu.menu_main_fragment, menu)
|
||||
val supportActionBar: ActionBar? = activity!!.getSupportActionBar()
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.getItemId() == R.id.action_search) {
|
||||
try {
|
||||
NavigationHelper.openSearchFragment(getFM(),
|
||||
ServiceHelper.getSelectedServiceId((activity)!!), "")
|
||||
} catch (e: Exception) {
|
||||
showUiErrorSnackbar(this, "Opening search fragment", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Tabs
|
||||
////////////////////////////////////////////////////////////////////////// */
|
||||
private fun setupTabs() {
|
||||
tabsList.clear()
|
||||
tabsList.addAll((tabsManager!!.getTabs())!!)
|
||||
if (pagerAdapter == null || !pagerAdapter!!.sameTabs(tabsList)) {
|
||||
pagerAdapter = SelectedTabsPagerAdapter(requireContext(),
|
||||
getChildFragmentManager(), tabsList)
|
||||
}
|
||||
binding!!.pager.setAdapter(null)
|
||||
binding!!.pager.setAdapter(pagerAdapter)
|
||||
updateTabsIconAndDescription()
|
||||
updateTitleForTab(binding!!.pager.getCurrentItem())
|
||||
hasTabsChanged = false
|
||||
}
|
||||
|
||||
private fun updateTabsIconAndDescription() {
|
||||
for (i in tabsList.indices) {
|
||||
val tabToSet: TabLayout.Tab? = binding!!.mainTabLayout.getTabAt(i)
|
||||
if (tabToSet != null) {
|
||||
val tab: Tab? = tabsList.get(i)
|
||||
tabToSet.setIcon(tab!!.getTabIconRes(requireContext()))
|
||||
tabToSet.setContentDescription(tab.getTabName(requireContext()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTitleForTab(tabPosition: Int) {
|
||||
setTitle(tabsList.get(tabPosition)!!.getTabName(requireContext()))
|
||||
}
|
||||
|
||||
fun commitPlaylistTabs() {
|
||||
pagerAdapter!!.getLocalPlaylistFragments()
|
||||
.stream()
|
||||
.forEach(Consumer({ obj: LocalPlaylistFragment? -> obj!!.saveImmediate() }))
|
||||
}
|
||||
|
||||
private fun updateTabLayoutPosition() {
|
||||
val tabLayout: ScrollableTabLayout = binding!!.mainTabLayout
|
||||
val viewPager: ViewPager = binding!!.pager
|
||||
val bottom: Boolean = mainTabsPositionBottom
|
||||
|
||||
// change layout params to make the tab layout appear either at the top or at the bottom
|
||||
val tabParams: RelativeLayout.LayoutParams = tabLayout.getLayoutParams() as RelativeLayout.LayoutParams
|
||||
val pagerParams: RelativeLayout.LayoutParams = viewPager.getLayoutParams() as RelativeLayout.LayoutParams
|
||||
tabParams.removeRule(if (bottom) RelativeLayout.ALIGN_PARENT_TOP else RelativeLayout.ALIGN_PARENT_BOTTOM)
|
||||
tabParams.addRule(if (bottom) RelativeLayout.ALIGN_PARENT_BOTTOM else RelativeLayout.ALIGN_PARENT_TOP)
|
||||
pagerParams.removeRule(if (bottom) RelativeLayout.BELOW else RelativeLayout.ABOVE)
|
||||
pagerParams.addRule(if (bottom) RelativeLayout.ABOVE else RelativeLayout.BELOW, R.id.main_tab_layout)
|
||||
tabLayout.setSelectedTabIndicatorGravity(
|
||||
if (bottom) TabLayout.INDICATOR_GRAVITY_TOP else TabLayout.INDICATOR_GRAVITY_BOTTOM)
|
||||
tabLayout.setLayoutParams(tabParams)
|
||||
viewPager.setLayoutParams(pagerParams)
|
||||
|
||||
// change the background and icon color of the tab layout:
|
||||
// service-colored at the top, app-background-colored at the bottom
|
||||
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
|
||||
if (bottom) R.attr.colorSecondary else R.attr.colorPrimary))
|
||||
@ColorInt val iconColor: Int = if (bottom) ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent) else Color.WHITE
|
||||
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32))
|
||||
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor))
|
||||
tabLayout.setSelectedTabIndicatorColor(iconColor)
|
||||
}
|
||||
|
||||
public override fun onTabSelected(selectedTab: TabLayout.Tab) {
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]")
|
||||
}
|
||||
updateTitleForTab(selectedTab.getPosition())
|
||||
}
|
||||
|
||||
public override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
public override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
if (BaseFragment.Companion.DEBUG) {
|
||||
Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]")
|
||||
}
|
||||
updateTitleForTab(tab.getPosition())
|
||||
}
|
||||
|
||||
class SelectedTabsPagerAdapter(private val context: Context,
|
||||
fragmentManager: FragmentManager,
|
||||
tabsList: List<Tab?>) : FragmentStatePagerAdapterMenuWorkaround(fragmentManager, FragmentStatePagerAdapterMenuWorkaround.Companion.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
private val internalTabsList: List<Tab?>
|
||||
|
||||
/**
|
||||
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
|
||||
* during runtime and changes are not committed immediately. However, in some cases,
|
||||
* the changes need to be committed immediately by calling
|
||||
* [LocalPlaylistFragment.saveImmediate].
|
||||
* The fragments are removed when [LocalPlaylistFragment.onDestroy] is called.
|
||||
*/
|
||||
private val localPlaylistFragments: MutableList<LocalPlaylistFragment?> = ArrayList()
|
||||
|
||||
init {
|
||||
internalTabsList = ArrayList(tabsList)
|
||||
}
|
||||
|
||||
public override fun getItem(position: Int): Fragment {
|
||||
val tab: Tab? = internalTabsList.get(position)
|
||||
val fragment: Fragment
|
||||
try {
|
||||
fragment = tab!!.getFragment(context)
|
||||
} catch (e: ExtractionException) {
|
||||
showUiErrorSnackbar(context, "Getting fragment item", e)
|
||||
return BlankFragment()
|
||||
}
|
||||
if (fragment is BaseFragment) {
|
||||
fragment.useAsFrontPage(true)
|
||||
}
|
||||
if (fragment is LocalPlaylistFragment) {
|
||||
localPlaylistFragments.add(fragment as LocalPlaylistFragment?)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun getLocalPlaylistFragments(): MutableList<LocalPlaylistFragment?> {
|
||||
return localPlaylistFragments
|
||||
}
|
||||
|
||||
public override fun getItemPosition(`object`: Any): Int {
|
||||
// Causes adapter to reload all Fragments when
|
||||
// notifyDataSetChanged is called
|
||||
return POSITION_NONE
|
||||
}
|
||||
|
||||
public override fun getCount(): Int {
|
||||
return internalTabsList.size
|
||||
}
|
||||
|
||||
fun sameTabs(tabsToCompare: List<Tab?>): Boolean {
|
||||
return (internalTabsList == tabsToCompare)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
/**
|
||||
* Recycler view scroll listener which calls the method {@link #onScrolledDown(RecyclerView)}
|
||||
* if the view is scrolled below the last item.
|
||||
*/
|
||||
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
||||
@Override
|
||||
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (dy > 0) {
|
||||
int pastVisibleItems = 0;
|
||||
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
|
||||
final int visibleItemCount = layoutManager.getChildCount();
|
||||
final int totalItemCount = layoutManager.getItemCount();
|
||||
|
||||
// Already covers the GridLayoutManager case
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
pastVisibleItems = ((LinearLayoutManager) layoutManager)
|
||||
.findFirstVisibleItemPosition();
|
||||
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||
final int[] positions = ((StaggeredGridLayoutManager) layoutManager)
|
||||
.findFirstVisibleItemPositions(null);
|
||||
if (positions != null && positions.length > 0) {
|
||||
pastVisibleItems = positions[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
|
||||
onScrolledDown(recyclerView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the recycler view is scrolled below the last item.
|
||||
*
|
||||
* @param recyclerView the recycler view
|
||||
*/
|
||||
public abstract void onScrolledDown(RecyclerView recyclerView);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.schabi.newpipe.fragments
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
|
||||
/**
|
||||
* Recycler view scroll listener which calls the method [.onScrolledDown]
|
||||
* if the view is scrolled below the last item.
|
||||
*/
|
||||
abstract class OnScrollBelowItemsListener() : RecyclerView.OnScrollListener() {
|
||||
public override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
if (dy > 0) {
|
||||
var pastVisibleItems: Int = 0
|
||||
val layoutManager: RecyclerView.LayoutManager? = recyclerView.getLayoutManager()
|
||||
val visibleItemCount: Int = layoutManager!!.getChildCount()
|
||||
val totalItemCount: Int = layoutManager.getItemCount()
|
||||
|
||||
// Already covers the GridLayoutManager case
|
||||
if (layoutManager is LinearLayoutManager) {
|
||||
pastVisibleItems = layoutManager
|
||||
.findFirstVisibleItemPosition()
|
||||
} else if (layoutManager is StaggeredGridLayoutManager) {
|
||||
val positions: IntArray? = layoutManager
|
||||
.findFirstVisibleItemPositions(null)
|
||||
if (positions != null && positions.size > 0) {
|
||||
pastVisibleItems = positions.get(0)
|
||||
}
|
||||
}
|
||||
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
|
||||
onScrolledDown(recyclerView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the recycler view is scrolled below the last item.
|
||||
*
|
||||
* @param recyclerView the recycler view
|
||||
*/
|
||||
abstract fun onScrolledDown(recyclerView: RecyclerView?)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package org.schabi.newpipe.fragments;
|
||||
|
||||
public interface ViewContract<I> {
|
||||
void showLoading();
|
||||
|
||||
void hideLoading();
|
||||
|
||||
void showEmptyState();
|
||||
|
||||
void handleResult(I result);
|
||||
|
||||
void handleError();
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package org.schabi.newpipe.fragments
|
||||
|
||||
open interface ViewContract<I> {
|
||||
fun showLoading()
|
||||
fun hideLoading()
|
||||
fun showEmptyState()
|
||||
fun handleResult(result: I)
|
||||
fun handleError()
|
||||
}
|
|
@ -1,281 +0,0 @@
|
|||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import com.google.android.material.chip.Chip;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
|
||||
import org.schabi.newpipe.databinding.ItemMetadataBinding;
|
||||
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
|
||||
import org.schabi.newpipe.extractor.Image;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public abstract class BaseDescriptionFragment extends BaseFragment {
|
||||
private final CompositeDisposable descriptionDisposables = new CompositeDisposable();
|
||||
protected FragmentDescriptionBinding binding;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
|
||||
setupDescription();
|
||||
setupMetadata(inflater, binding.detailMetadataLayout);
|
||||
addTagsMetadataItem(inflater, binding.detailMetadataLayout);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
descriptionDisposables.clear();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description to display.
|
||||
* @return description object, if available
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Description getDescription();
|
||||
|
||||
/**
|
||||
* Get the streaming service. Used for generating description links.
|
||||
* @return streaming service
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract StreamingService getService();
|
||||
|
||||
/**
|
||||
* Get the streaming service ID. Used for tag links.
|
||||
* @return service ID
|
||||
*/
|
||||
protected abstract int getServiceId();
|
||||
|
||||
/**
|
||||
* Get the URL of the described video or audio, used to generate description links.
|
||||
* @return stream URL
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract String getStreamUrl();
|
||||
|
||||
/**
|
||||
* Get the list of tags to display below the description.
|
||||
* @return tag list
|
||||
*/
|
||||
@NonNull
|
||||
public abstract List<String> getTags();
|
||||
|
||||
/**
|
||||
* Add additional metadata to display.
|
||||
* @param inflater LayoutInflater
|
||||
* @param layout detailMetadataLayout
|
||||
*/
|
||||
protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout);
|
||||
|
||||
private void setupDescription() {
|
||||
final Description description = getDescription();
|
||||
if (description == null || isEmpty(description.getContent())
|
||||
|| description == Description.EMPTY_DESCRIPTION) {
|
||||
binding.detailDescriptionView.setVisibility(View.GONE);
|
||||
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
// start with disabled state. This also loads description content (!)
|
||||
disableDescriptionSelection();
|
||||
|
||||
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
|
||||
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
|
||||
disableDescriptionSelection();
|
||||
} else {
|
||||
// enable selection only when button is clicked to prevent flickering
|
||||
enableDescriptionSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void enableDescriptionSelection() {
|
||||
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
|
||||
binding.detailDescriptionView.setTextIsSelectable(true);
|
||||
|
||||
final String buttonLabel = getString(R.string.description_select_disable);
|
||||
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
||||
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
||||
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
|
||||
}
|
||||
|
||||
private void disableDescriptionSelection() {
|
||||
// show description content again, otherwise some links are not clickable
|
||||
final Description description = getDescription();
|
||||
if (description != null) {
|
||||
TextLinkifier.fromDescription(binding.detailDescriptionView,
|
||||
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
getService(), getStreamUrl(),
|
||||
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
||||
}
|
||||
|
||||
binding.detailDescriptionNoteView.setVisibility(View.GONE);
|
||||
binding.detailDescriptionView.setTextIsSelectable(false);
|
||||
|
||||
final String buttonLabel = getString(R.string.description_select_enable);
|
||||
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
|
||||
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
|
||||
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
|
||||
}
|
||||
|
||||
protected void addMetadataItem(final LayoutInflater inflater,
|
||||
final LinearLayout layout,
|
||||
final boolean linkifyContent,
|
||||
@StringRes final int type,
|
||||
@NonNull final String content) {
|
||||
if (isBlank(content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ItemMetadataBinding itemBinding =
|
||||
ItemMetadataBinding.inflate(inflater, layout, false);
|
||||
|
||||
itemBinding.metadataTypeView.setText(type);
|
||||
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
|
||||
ShareUtils.copyToClipboard(requireContext(), content);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (linkifyContent) {
|
||||
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
|
||||
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
|
||||
} else {
|
||||
itemBinding.metadataContentView.setText(content);
|
||||
}
|
||||
|
||||
itemBinding.metadataContentView.setClickable(true);
|
||||
|
||||
layout.addView(itemBinding.getRoot());
|
||||
}
|
||||
|
||||
private String imageSizeToText(final int heightOrWidth) {
|
||||
if (heightOrWidth < 0) {
|
||||
return getString(R.string.question_mark);
|
||||
} else {
|
||||
return String.valueOf(heightOrWidth);
|
||||
}
|
||||
}
|
||||
|
||||
protected void addImagesMetadataItem(final LayoutInflater inflater,
|
||||
final LinearLayout layout,
|
||||
@StringRes final int type,
|
||||
final List<Image> images) {
|
||||
final String preferredImageUrl = ImageStrategy.choosePreferredImage(images);
|
||||
if (preferredImageUrl == null) {
|
||||
return; // null will be returned in case there is no image
|
||||
}
|
||||
|
||||
final ItemMetadataBinding itemBinding =
|
||||
ItemMetadataBinding.inflate(inflater, layout, false);
|
||||
itemBinding.metadataTypeView.setText(type);
|
||||
|
||||
final SpannableStringBuilder urls = new SpannableStringBuilder();
|
||||
for (final Image image : images) {
|
||||
if (urls.length() != 0) {
|
||||
urls.append(", ");
|
||||
}
|
||||
final int entryBegin = urls.length();
|
||||
|
||||
if (image.getHeight() != Image.HEIGHT_UNKNOWN
|
||||
|| image.getWidth() != Image.WIDTH_UNKNOWN
|
||||
// if even the resolution level is unknown, ?x? will be shown
|
||||
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
|
||||
urls.append(imageSizeToText(image.getHeight()));
|
||||
urls.append('x');
|
||||
urls.append(imageSizeToText(image.getWidth()));
|
||||
} else {
|
||||
switch (image.getEstimatedResolutionLevel()) {
|
||||
case LOW -> urls.append(getString(R.string.image_quality_low));
|
||||
case MEDIUM -> urls.append(getString(R.string.image_quality_medium));
|
||||
case HIGH -> urls.append(getString(R.string.image_quality_high));
|
||||
default -> {
|
||||
// unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
urls.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull final View widget) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), image.getUrl());
|
||||
}
|
||||
}, entryBegin, urls.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
if (preferredImageUrl.equals(image.getUrl())) {
|
||||
urls.setSpan(new StyleSpan(Typeface.BOLD), entryBegin, urls.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
itemBinding.metadataContentView.setText(urls);
|
||||
itemBinding.metadataContentView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
layout.addView(itemBinding.getRoot());
|
||||
}
|
||||
|
||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||
final List<String> tags = getTags();
|
||||
|
||||
if (!tags.isEmpty()) {
|
||||
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||
|
||||
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
|
||||
itemBinding.metadataTagsChips, false);
|
||||
chip.setText(tag);
|
||||
chip.setOnClickListener(this::onTagClick);
|
||||
chip.setOnLongClickListener(this::onTagLongClick);
|
||||
itemBinding.metadataTagsChips.addView(chip);
|
||||
});
|
||||
|
||||
layout.addView(itemBinding.getRoot());
|
||||
}
|
||||
}
|
||||
|
||||
private void onTagClick(final View chip) {
|
||||
if (getParentFragment() != null) {
|
||||
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
|
||||
getServiceId(), ((Chip) chip).getText().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onTagLongClick(final View chip) {
|
||||
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
package org.schabi.newpipe.fragments.detail
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.schabi.newpipe.BaseFragment
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FragmentDescriptionBinding
|
||||
import org.schabi.newpipe.databinding.ItemMetadataBinding
|
||||
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding
|
||||
import org.schabi.newpipe.extractor.Image
|
||||
import org.schabi.newpipe.extractor.Image.ResolutionLevel
|
||||
import org.schabi.newpipe.extractor.StreamingService
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.extractor.utils.Utils
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import org.schabi.newpipe.util.image.ImageStrategy
|
||||
import org.schabi.newpipe.util.text.TextLinkifier
|
||||
import java.util.function.Consumer
|
||||
|
||||
abstract class BaseDescriptionFragment() : BaseFragment() {
|
||||
private val descriptionDisposables: CompositeDisposable = CompositeDisposable()
|
||||
protected var binding: FragmentDescriptionBinding? = null
|
||||
public override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
binding = FragmentDescriptionBinding.inflate(inflater, container, false)
|
||||
setupDescription()
|
||||
setupMetadata(inflater, binding!!.detailMetadataLayout)
|
||||
addTagsMetadataItem(inflater, binding!!.detailMetadataLayout)
|
||||
return binding!!.getRoot()
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
descriptionDisposables.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description to display.
|
||||
* @return description object, if available
|
||||
*/
|
||||
protected abstract fun getDescription(): Description?
|
||||
|
||||
/**
|
||||
* Get the streaming service. Used for generating description links.
|
||||
* @return streaming service
|
||||
*/
|
||||
protected abstract fun getService(): StreamingService
|
||||
|
||||
/**
|
||||
* Get the streaming service ID. Used for tag links.
|
||||
* @return service ID
|
||||
*/
|
||||
protected abstract fun getServiceId(): Int
|
||||
|
||||
/**
|
||||
* Get the URL of the described video or audio, used to generate description links.
|
||||
* @return stream URL
|
||||
*/
|
||||
protected abstract fun getStreamUrl(): String?
|
||||
|
||||
/**
|
||||
* Get the list of tags to display below the description.
|
||||
* @return tag list
|
||||
*/
|
||||
abstract fun getTags(): List<String?>
|
||||
|
||||
/**
|
||||
* Add additional metadata to display.
|
||||
* @param inflater LayoutInflater
|
||||
* @param layout detailMetadataLayout
|
||||
*/
|
||||
protected abstract fun setupMetadata(inflater: LayoutInflater?, layout: LinearLayout?)
|
||||
private fun setupDescription() {
|
||||
val description: Description? = getDescription()
|
||||
if (((description == null) || TextUtils.isEmpty(description.getContent())
|
||||
|| (description === Description.EMPTY_DESCRIPTION))) {
|
||||
binding!!.detailDescriptionView.setVisibility(View.GONE)
|
||||
binding!!.detailSelectDescriptionButton.setVisibility(View.GONE)
|
||||
return
|
||||
}
|
||||
|
||||
// start with disabled state. This also loads description content (!)
|
||||
disableDescriptionSelection()
|
||||
binding!!.detailSelectDescriptionButton.setOnClickListener(View.OnClickListener({ v: View? ->
|
||||
if (binding!!.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
|
||||
disableDescriptionSelection()
|
||||
} else {
|
||||
// enable selection only when button is clicked to prevent flickering
|
||||
enableDescriptionSelection()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
private fun enableDescriptionSelection() {
|
||||
binding!!.detailDescriptionNoteView.setVisibility(View.VISIBLE)
|
||||
binding!!.detailDescriptionView.setTextIsSelectable(true)
|
||||
val buttonLabel: String = getString(R.string.description_select_disable)
|
||||
binding!!.detailSelectDescriptionButton.setContentDescription(buttonLabel)
|
||||
TooltipCompat.setTooltipText(binding!!.detailSelectDescriptionButton, buttonLabel)
|
||||
binding!!.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close)
|
||||
}
|
||||
|
||||
private fun disableDescriptionSelection() {
|
||||
// show description content again, otherwise some links are not clickable
|
||||
val description: Description? = getDescription()
|
||||
if (description != null) {
|
||||
TextLinkifier.fromDescription(binding!!.detailDescriptionView,
|
||||
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
getService(), getStreamUrl(),
|
||||
descriptionDisposables, TextLinkifier.SET_LINK_MOVEMENT_METHOD)
|
||||
}
|
||||
binding!!.detailDescriptionNoteView.setVisibility(View.GONE)
|
||||
binding!!.detailDescriptionView.setTextIsSelectable(false)
|
||||
val buttonLabel: String = getString(R.string.description_select_enable)
|
||||
binding!!.detailSelectDescriptionButton.setContentDescription(buttonLabel)
|
||||
TooltipCompat.setTooltipText(binding!!.detailSelectDescriptionButton, buttonLabel)
|
||||
binding!!.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all)
|
||||
}
|
||||
|
||||
protected fun addMetadataItem(inflater: LayoutInflater?,
|
||||
layout: LinearLayout,
|
||||
linkifyContent: Boolean,
|
||||
@StringRes type: Int,
|
||||
content: String) {
|
||||
if (Utils.isBlank(content)) {
|
||||
return
|
||||
}
|
||||
val itemBinding: ItemMetadataBinding = ItemMetadataBinding.inflate((inflater)!!, layout, false)
|
||||
itemBinding.metadataTypeView.setText(type)
|
||||
itemBinding.metadataTypeView.setOnLongClickListener(OnLongClickListener({ v: View? ->
|
||||
ShareUtils.copyToClipboard(requireContext(), content)
|
||||
true
|
||||
}))
|
||||
if (linkifyContent) {
|
||||
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
|
||||
descriptionDisposables, TextLinkifier.SET_LINK_MOVEMENT_METHOD)
|
||||
} else {
|
||||
itemBinding.metadataContentView.setText(content)
|
||||
}
|
||||
itemBinding.metadataContentView.setClickable(true)
|
||||
layout.addView(itemBinding.getRoot())
|
||||
}
|
||||
|
||||
private fun imageSizeToText(heightOrWidth: Int): String {
|
||||
if (heightOrWidth < 0) {
|
||||
return getString(R.string.question_mark)
|
||||
} else {
|
||||
return heightOrWidth.toString()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun addImagesMetadataItem(inflater: LayoutInflater?,
|
||||
layout: LinearLayout,
|
||||
@StringRes type: Int,
|
||||
images: List<Image?>) {
|
||||
val preferredImageUrl: String? = ImageStrategy.choosePreferredImage(images)
|
||||
if (preferredImageUrl == null) {
|
||||
return // null will be returned in case there is no image
|
||||
}
|
||||
val itemBinding: ItemMetadataBinding = ItemMetadataBinding.inflate((inflater)!!, layout, false)
|
||||
itemBinding.metadataTypeView.setText(type)
|
||||
val urls: SpannableStringBuilder = SpannableStringBuilder()
|
||||
for (image: Image? in images) {
|
||||
if (urls.length != 0) {
|
||||
urls.append(", ")
|
||||
}
|
||||
val entryBegin: Int = urls.length
|
||||
if ((image!!.getHeight() != Image.HEIGHT_UNKNOWN
|
||||
) || (image.getWidth() != Image.WIDTH_UNKNOWN // if even the resolution level is unknown, ?x? will be shown
|
||||
) || (image.getEstimatedResolutionLevel() == ResolutionLevel.UNKNOWN)) {
|
||||
urls.append(imageSizeToText(image.getHeight()))
|
||||
urls.append('x')
|
||||
urls.append(imageSizeToText(image.getWidth()))
|
||||
} else {
|
||||
when (image.getEstimatedResolutionLevel()) {
|
||||
ResolutionLevel.LOW -> urls.append(getString(R.string.image_quality_low))
|
||||
ResolutionLevel.MEDIUM -> urls.append(getString(R.string.image_quality_medium))
|
||||
ResolutionLevel.HIGH -> urls.append(getString(R.string.image_quality_high))
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
urls.setSpan(object : ClickableSpan() {
|
||||
public override fun onClick(widget: View) {
|
||||
ShareUtils.openUrlInBrowser(requireContext(), image.getUrl())
|
||||
}
|
||||
}, entryBegin, urls.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
if ((preferredImageUrl == image.getUrl())) {
|
||||
urls.setSpan(StyleSpan(Typeface.BOLD), entryBegin, urls.length,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
itemBinding.metadataContentView.setText(urls)
|
||||
itemBinding.metadataContentView.setMovementMethod(LinkMovementMethod.getInstance())
|
||||
layout.addView(itemBinding.getRoot())
|
||||
}
|
||||
|
||||
private fun addTagsMetadataItem(inflater: LayoutInflater, layout: LinearLayout) {
|
||||
val tags: List<String?> = getTags()
|
||||
if (!tags.isEmpty()) {
|
||||
val itemBinding: ItemMetadataTagsBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false)
|
||||
tags.stream().sorted(java.lang.String.CASE_INSENSITIVE_ORDER).forEach(Consumer({ tag: String? ->
|
||||
val chip: Chip = inflater.inflate(R.layout.chip,
|
||||
itemBinding.metadataTagsChips, false) as Chip
|
||||
chip.setText(tag)
|
||||
chip.setOnClickListener(View.OnClickListener({ chip: View -> onTagClick(chip) }))
|
||||
chip.setOnLongClickListener(OnLongClickListener({ chip: View -> onTagLongClick(chip) }))
|
||||
itemBinding.metadataTagsChips.addView(chip)
|
||||
}))
|
||||
layout.addView(itemBinding.getRoot())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTagClick(chip: View) {
|
||||
if (getParentFragment() != null) {
|
||||
NavigationHelper.openSearchFragment(getParentFragment()!!.getParentFragmentManager(),
|
||||
getServiceId(), (chip as Chip).getText().toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTagLongClick(chip: View): Boolean {
|
||||
ShareUtils.copyToClipboard(requireContext(), (chip as Chip).getText().toString())
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.util.Localization.getAppLocale;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
public class DescriptionFragment extends BaseDescriptionFragment {
|
||||
|
||||
@State
|
||||
StreamInfo streamInfo;
|
||||
|
||||
public DescriptionFragment(final StreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
}
|
||||
|
||||
public DescriptionFragment() {
|
||||
// keep empty constructor for IcePick when resuming fragment from memory
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Description getDescription() {
|
||||
return streamInfo.getDescription();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected StreamingService getService() {
|
||||
return streamInfo.getService();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getServiceId() {
|
||||
return streamInfo.getServiceId();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected String getStreamUrl() {
|
||||
return streamInfo.getUrl();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<String> getTags() {
|
||||
return streamInfo.getTags();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupMetadata(final LayoutInflater inflater,
|
||||
final LinearLayout layout) {
|
||||
if (streamInfo != null && streamInfo.getUploadDate() != null) {
|
||||
binding.detailUploadDateView.setText(Localization
|
||||
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
|
||||
} else {
|
||||
binding.detailUploadDateView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (streamInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_category,
|
||||
streamInfo.getCategory());
|
||||
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_licence,
|
||||
streamInfo.getLicence());
|
||||
|
||||
addPrivacyMetadataItem(inflater, layout);
|
||||
|
||||
if (streamInfo.getAgeLimit() != NO_AGE_LIMIT) {
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_age_limit,
|
||||
String.valueOf(streamInfo.getAgeLimit()));
|
||||
}
|
||||
|
||||
if (streamInfo.getLanguageInfo() != null) {
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_language,
|
||||
streamInfo.getLanguageInfo().getDisplayLanguage(getAppLocale(getContext())));
|
||||
}
|
||||
|
||||
addMetadataItem(inflater, layout, true, R.string.metadata_support,
|
||||
streamInfo.getSupportInfo());
|
||||
addMetadataItem(inflater, layout, true, R.string.metadata_host,
|
||||
streamInfo.getHost());
|
||||
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_thumbnails,
|
||||
streamInfo.getThumbnails());
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_uploader_avatars,
|
||||
streamInfo.getUploaderAvatars());
|
||||
addImagesMetadataItem(inflater, layout, R.string.metadata_subchannel_avatars,
|
||||
streamInfo.getSubChannelAvatars());
|
||||
}
|
||||
|
||||
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||
if (streamInfo.getPrivacy() != null) {
|
||||
@StringRes final int contentRes;
|
||||
switch (streamInfo.getPrivacy()) {
|
||||
case PUBLIC:
|
||||
contentRes = R.string.metadata_privacy_public;
|
||||
break;
|
||||
case UNLISTED:
|
||||
contentRes = R.string.metadata_privacy_unlisted;
|
||||
break;
|
||||
case PRIVATE:
|
||||
contentRes = R.string.metadata_privacy_private;
|
||||
break;
|
||||
case INTERNAL:
|
||||
contentRes = R.string.metadata_privacy_internal;
|
||||
break;
|
||||
case OTHER:
|
||||
default:
|
||||
contentRes = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (contentRes != 0) {
|
||||
addMetadataItem(inflater, layout, false, R.string.metadata_privacy,
|
||||
getString(contentRes));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package org.schabi.newpipe.fragments.detail
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.StringRes
|
||||
import icepick.State
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.StreamingService
|
||||
import org.schabi.newpipe.extractor.stream.Description
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor.Privacy
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.util.Localization
|
||||
|
||||
class DescriptionFragment : BaseDescriptionFragment {
|
||||
@State
|
||||
var streamInfo: StreamInfo? = null
|
||||
|
||||
constructor(streamInfo: StreamInfo?) {
|
||||
this.streamInfo = streamInfo
|
||||
}
|
||||
|
||||
constructor()
|
||||
|
||||
override fun getDescription(): Description? {
|
||||
return streamInfo!!.getDescription()
|
||||
}
|
||||
|
||||
override fun getService(): StreamingService {
|
||||
return streamInfo!!.getService()
|
||||
}
|
||||
|
||||
override fun getServiceId(): Int {
|
||||
return streamInfo!!.getServiceId()
|
||||
}
|
||||
|
||||
override fun getStreamUrl(): String {
|
||||
return streamInfo!!.getUrl()
|
||||
}
|
||||
|
||||
public override fun getTags(): List<String?> {
|
||||
return streamInfo!!.getTags()
|
||||
}
|
||||
|
||||
override fun setupMetadata(inflater: LayoutInflater?,
|
||||
layout: LinearLayout?) {
|
||||
if (streamInfo != null && streamInfo!!.getUploadDate() != null) {
|
||||
binding!!.detailUploadDateView.setText(Localization.localizeUploadDate((activity)!!, streamInfo!!.getUploadDate().offsetDateTime()))
|
||||
} else {
|
||||
binding!!.detailUploadDateView.setVisibility(View.GONE)
|
||||
}
|
||||
if (streamInfo == null) {
|
||||
return
|
||||
}
|
||||
addMetadataItem(inflater, (layout)!!, false, R.string.metadata_category,
|
||||
streamInfo!!.getCategory())
|
||||
addMetadataItem(inflater, (layout), false, R.string.metadata_licence,
|
||||
streamInfo!!.getLicence())
|
||||
addPrivacyMetadataItem(inflater, layout)
|
||||
if (streamInfo!!.getAgeLimit() != StreamExtractor.NO_AGE_LIMIT) {
|
||||
addMetadataItem(inflater, (layout), false, R.string.metadata_age_limit, streamInfo!!.getAgeLimit().toString())
|
||||
}
|
||||
if (streamInfo!!.getLanguageInfo() != null) {
|
||||
addMetadataItem(inflater, (layout), false, R.string.metadata_language,
|
||||
streamInfo!!.getLanguageInfo().getDisplayLanguage(Localization.getAppLocale((getContext())!!)))
|
||||
}
|
||||
addMetadataItem(inflater, (layout), true, R.string.metadata_support,
|
||||
streamInfo!!.getSupportInfo())
|
||||
addMetadataItem(inflater, (layout), true, R.string.metadata_host,
|
||||
streamInfo!!.getHost())
|
||||
addImagesMetadataItem(inflater, (layout), R.string.metadata_thumbnails,
|
||||
streamInfo!!.getThumbnails())
|
||||
addImagesMetadataItem(inflater, (layout), R.string.metadata_uploader_avatars,
|
||||
streamInfo!!.getUploaderAvatars())
|
||||
addImagesMetadataItem(inflater, (layout), R.string.metadata_subchannel_avatars,
|
||||
streamInfo!!.getSubChannelAvatars())
|
||||
}
|
||||
|
||||
private fun addPrivacyMetadataItem(inflater: LayoutInflater?, layout: LinearLayout?) {
|
||||
if (streamInfo!!.getPrivacy() != null) {
|
||||
@StringRes val contentRes: Int
|
||||
when (streamInfo!!.getPrivacy()) {
|
||||
Privacy.PUBLIC -> contentRes = R.string.metadata_privacy_public
|
||||
Privacy.UNLISTED -> contentRes = R.string.metadata_privacy_unlisted
|
||||
Privacy.PRIVATE -> contentRes = R.string.metadata_privacy_private
|
||||
Privacy.INTERNAL -> contentRes = R.string.metadata_privacy_internal
|
||||
Privacy.OTHER -> contentRes = 0
|
||||
else -> contentRes = 0
|
||||
}
|
||||
if (contentRes != 0) {
|
||||
addMetadataItem(inflater, (layout)!!, false, R.string.metadata_privacy,
|
||||
getString(contentRes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
class StackItem implements Serializable {
|
||||
private final int serviceId;
|
||||
private String url;
|
||||
private String title;
|
||||
private PlayQueue playQueue;
|
||||
|
||||
StackItem(final int serviceId, final String url,
|
||||
final String title, final PlayQueue playQueue) {
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.playQueue = playQueue;
|
||||
}
|
||||
|
||||
public void setUrl(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public void setPlayQueue(final PlayQueue queue) {
|
||||
this.playQueue = queue;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(final String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public PlayQueue getPlayQueue() {
|
||||
return playQueue;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getServiceId() + ":" + getUrl() + " > " + getTitle();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package org.schabi.newpipe.fragments.detail
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue
|
||||
import java.io.Serializable
|
||||
|
||||
internal class StackItem(private val serviceId: Int, private var url: String?,
|
||||
private var title: String, private var playQueue: PlayQueue?) : Serializable {
|
||||
fun setUrl(url: String?) {
|
||||
this.url = url
|
||||
}
|
||||
|
||||
fun setPlayQueue(queue: PlayQueue?) {
|
||||
playQueue = queue
|
||||
}
|
||||
|
||||
fun getServiceId(): Int {
|
||||
return serviceId
|
||||
}
|
||||
|
||||
fun getTitle(): String? {
|
||||
return title
|
||||
}
|
||||
|
||||
fun setTitle(title: String) {
|
||||
this.title = title
|
||||
}
|
||||
|
||||
fun getUrl(): String? {
|
||||
return url
|
||||
}
|
||||
|
||||
fun getPlayQueue(): PlayQueue? {
|
||||
return playQueue
|
||||
}
|
||||
|
||||
public override fun toString(): String {
|
||||
return getServiceId().toString() + ":" + getUrl() + " > " + getTitle()
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TabAdapter extends FragmentPagerAdapter {
|
||||
private final List<Fragment> mFragmentList = new ArrayList<>();
|
||||
private final List<String> mFragmentTitleList = new ArrayList<>();
|
||||
private final FragmentManager fragmentManager;
|
||||
|
||||
public TabAdapter(final FragmentManager fm) {
|
||||
// if changed to BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT => crash if enqueueing stream in
|
||||
// the background and then clicking on it to open VideoDetailFragment:
|
||||
// "Cannot setMaxLifecycle for Fragment not attached to FragmentManager"
|
||||
super(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
|
||||
this.fragmentManager = fm;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(final int position) {
|
||||
return mFragmentList.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mFragmentList.size();
|
||||
}
|
||||
|
||||
public void addFragment(final Fragment fragment, final String title) {
|
||||
mFragmentList.add(fragment);
|
||||
mFragmentTitleList.add(title);
|
||||
}
|
||||
|
||||
public void clearAllItems() {
|
||||
mFragmentList.clear();
|
||||
mFragmentTitleList.clear();
|
||||
}
|
||||
|
||||
public void removeItem(final int position) {
|
||||
mFragmentList.remove(position == 0 ? 0 : position - 1);
|
||||
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
|
||||
}
|
||||
|
||||
public void updateItem(final int position, final Fragment fragment) {
|
||||
mFragmentList.set(position, fragment);
|
||||
}
|
||||
|
||||
public void updateItem(final String title, final Fragment fragment) {
|
||||
final int index = mFragmentTitleList.indexOf(title);
|
||||
if (index != -1) {
|
||||
updateItem(index, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(@NonNull final Object object) {
|
||||
if (mFragmentList.contains(object)) {
|
||||
return mFragmentList.indexOf(object);
|
||||
} else {
|
||||
return POSITION_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemPositionByTitle(final String title) {
|
||||
return mFragmentTitleList.indexOf(title);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getItemTitle(final int position) {
|
||||
if (position < 0 || position >= mFragmentTitleList.size()) {
|
||||
return null;
|
||||
}
|
||||
return mFragmentTitleList.get(position);
|
||||
}
|
||||
|
||||
public void notifyDataSetUpdate() {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull final ViewGroup container,
|
||||
final int position,
|
||||
@NonNull final Object object) {
|
||||
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package org.schabi.newpipe.fragments.detail
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
|
||||
class TabAdapter // if changed to BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT => crash if enqueueing stream in
|
||||
// the background and then clicking on it to open VideoDetailFragment:
|
||||
// "Cannot setMaxLifecycle for Fragment not attached to FragmentManager"
|
||||
(private val fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_SET_USER_VISIBLE_HINT) {
|
||||
private val mFragmentList: MutableList<Fragment> = ArrayList()
|
||||
private val mFragmentTitleList: MutableList<String?> = ArrayList()
|
||||
public override fun getItem(position: Int): Fragment {
|
||||
return mFragmentList.get(position)
|
||||
}
|
||||
|
||||
public override fun getCount(): Int {
|
||||
return mFragmentList.size
|
||||
}
|
||||
|
||||
fun addFragment(fragment: Fragment, title: String?) {
|
||||
mFragmentList.add(fragment)
|
||||
mFragmentTitleList.add(title)
|
||||
}
|
||||
|
||||
fun clearAllItems() {
|
||||
mFragmentList.clear()
|
||||
mFragmentTitleList.clear()
|
||||
}
|
||||
|
||||
fun removeItem(position: Int) {
|
||||
mFragmentList.removeAt(if (position == 0) 0 else position - 1)
|
||||
mFragmentTitleList.removeAt(if (position == 0) 0 else position - 1)
|
||||
}
|
||||
|
||||
fun updateItem(position: Int, fragment: Fragment) {
|
||||
mFragmentList.set(position, fragment)
|
||||
}
|
||||
|
||||
fun updateItem(title: String?, fragment: Fragment) {
|
||||
val index: Int = mFragmentTitleList.indexOf(title)
|
||||
if (index != -1) {
|
||||
updateItem(index, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun getItemPosition(`object`: Any): Int {
|
||||
if (mFragmentList.contains(`object`)) {
|
||||
return mFragmentList.indexOf(`object`)
|
||||
} else {
|
||||
return POSITION_NONE
|
||||
}
|
||||
}
|
||||
|
||||
fun getItemPositionByTitle(title: String?): Int {
|
||||
return mFragmentTitleList.indexOf(title)
|
||||
}
|
||||
|
||||
fun getItemTitle(position: Int): String? {
|
||||
if (position < 0 || position >= mFragmentTitleList.size) {
|
||||
return null
|
||||
}
|
||||
return mFragmentTitleList.get(position)
|
||||
}
|
||||
|
||||
fun notifyDataSetUpdate() {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
public override fun destroyItem(container: ViewGroup,
|
||||
position: Int,
|
||||
`object`: Any) {
|
||||
fragmentManager.beginTransaction().remove((`object` as Fragment?)!!).commitNowAllowingStateLoss()
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue