Merge a2bb804b6f
into c915b6e68b
This commit is contained in:
commit
6edd2f0771
|
@ -1,46 +0,0 @@
|
|||
package org.schabi.newpipe.error;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Instrumented tests for {@link ErrorInfo}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class ErrorInfoTest {
|
||||
|
||||
@Test
|
||||
public void errorInfoTestParcelable() {
|
||||
final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"),
|
||||
UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId());
|
||||
// Obtain a Parcel object and write the parcelable object to it:
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
info.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
final ErrorInfo infoFromParcel = (ErrorInfo) ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertTrue(Arrays.toString(infoFromParcel.getStackTraces())
|
||||
.contains(ErrorInfoTest.class.getSimpleName()));
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
|
||||
infoFromParcel.getServiceName());
|
||||
assertEquals("request", infoFromParcel.getRequest());
|
||||
assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId());
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package org.schabi.newpipe.error
|
||||
|
||||
import android.os.Parcel
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException
|
||||
|
||||
/**
|
||||
* Instrumented tests for [ErrorInfo].
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class ErrorInfoTest {
|
||||
@Test
|
||||
fun errorInfoTestParcelable() {
|
||||
val info = ErrorInfo(ParsingException("Hello"),
|
||||
UserAction.USER_REPORT, "request", ServiceList.YouTube.serviceId)
|
||||
// Obtain a Parcel object and write the parcelable object to it:
|
||||
val parcel = Parcel.obtain()
|
||||
info.writeToParcel(parcel, 0)
|
||||
parcel.setDataPosition(0)
|
||||
val infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel) as ErrorInfo
|
||||
Assert.assertTrue(infoFromParcel.stackTraces.contentToString().contains(ErrorInfoTest::class.java.getSimpleName()))
|
||||
Assert.assertEquals(UserAction.USER_REPORT, infoFromParcel.userAction)
|
||||
Assert.assertEquals(ServiceList.YouTube.serviceInfo.name,
|
||||
infoFromParcel.serviceName)
|
||||
Assert.assertEquals("request", infoFromParcel.request)
|
||||
Assert.assertEquals(R.string.parsing_error.toLong(), infoFromParcel.messageStringId.toLong())
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.testUtil.TestDatabase;
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class SubscriptionManagerTest {
|
||||
private AppDatabase database;
|
||||
private SubscriptionManager manager;
|
||||
|
||||
@Rule
|
||||
public TrampolineSchedulerRule trampolineScheduler = new TrampolineSchedulerRule();
|
||||
|
||||
|
||||
private SubscriptionEntity getAssertOneSubscriptionEntity() {
|
||||
final List<SubscriptionEntity> entities = manager
|
||||
.getSubscriptions(FeedGroupEntity.GROUP_ALL_ID, "", false)
|
||||
.blockingFirst();
|
||||
assertEquals(1, entities.size());
|
||||
return entities.get(0);
|
||||
}
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
database = TestDatabase.Companion.createReplacingNewPipeDatabase();
|
||||
manager = new SubscriptionManager(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanUp() {
|
||||
database.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsert() throws ExtractionException, IOException {
|
||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
|
||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||
|
||||
manager.insertSubscription(subscription);
|
||||
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
|
||||
|
||||
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
||||
assertEquals(subscription.getServiceId(), readSubscription.getServiceId());
|
||||
assertEquals(subscription.getUrl(), readSubscription.getUrl());
|
||||
assertEquals(subscription.getName(), readSubscription.getName());
|
||||
assertEquals(subscription.getAvatarUrl(), readSubscription.getAvatarUrl());
|
||||
assertEquals(subscription.getSubscriberCount(), readSubscription.getSubscriberCount());
|
||||
assertEquals(subscription.getDescription(), readSubscription.getDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateNotificationMode() throws ExtractionException, IOException {
|
||||
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/veritasium");
|
||||
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
|
||||
subscription.setNotificationMode(0);
|
||||
|
||||
manager.insertSubscription(subscription);
|
||||
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
|
||||
.blockingAwait();
|
||||
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
|
||||
|
||||
assertEquals(0, subscription.getNotificationMode());
|
||||
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
|
||||
assertEquals(1, anotherSubscription.getNotificationMode());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package org.schabi.newpipe.local.subscription
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.schabi.newpipe.database.AppDatabase
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity.Companion.from
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||
import org.schabi.newpipe.testUtil.TestDatabase.Companion.createReplacingNewPipeDatabase
|
||||
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||
import java.io.IOException
|
||||
|
||||
class SubscriptionManagerTest {
|
||||
private var database: AppDatabase? = null
|
||||
private var manager: SubscriptionManager? = null
|
||||
|
||||
@Rule
|
||||
var trampolineScheduler = TrampolineSchedulerRule()
|
||||
private val assertOneSubscriptionEntity: SubscriptionEntity
|
||||
private get() {
|
||||
val entities = manager
|
||||
.getSubscriptions(FeedGroupEntity.GROUP_ALL_ID, "", false)
|
||||
.blockingFirst()
|
||||
Assert.assertEquals(1, entities.size.toLong())
|
||||
return entities[0]
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
database = createReplacingNewPipeDatabase()
|
||||
manager = SubscriptionManager(ApplicationProvider.getApplicationContext())
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
database!!.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(ExtractionException::class, IOException::class)
|
||||
fun testInsert() {
|
||||
val info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown")
|
||||
val subscription = from(info)
|
||||
manager!!.insertSubscription(subscription)
|
||||
val readSubscription = assertOneSubscriptionEntity
|
||||
|
||||
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
|
||||
Assert.assertEquals(subscription.getServiceId().toLong(), readSubscription.getServiceId().toLong())
|
||||
Assert.assertEquals(subscription.getUrl(), readSubscription.getUrl())
|
||||
Assert.assertEquals(subscription.getName(), readSubscription.getName())
|
||||
Assert.assertEquals(subscription.getAvatarUrl(), readSubscription.getAvatarUrl())
|
||||
Assert.assertEquals(subscription.getSubscriberCount(), readSubscription.getSubscriberCount())
|
||||
Assert.assertEquals(subscription.getDescription(), readSubscription.getDescription())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(ExtractionException::class, IOException::class)
|
||||
fun testUpdateNotificationMode() {
|
||||
val info = ChannelInfo.getInfo("https://www.youtube.com/c/veritasium")
|
||||
val subscription = from(info)
|
||||
subscription.setNotificationMode(0)
|
||||
manager!!.insertSubscription(subscription)
|
||||
manager!!.updateNotificationMode(subscription.getServiceId(), subscription.getUrl()!!, 1)
|
||||
.blockingAwait()
|
||||
val anotherSubscription = assertOneSubscriptionEntity
|
||||
Assert.assertEquals(0, subscription.getNotificationMode().toLong())
|
||||
Assert.assertEquals(subscription.getUrl(), anotherSubscription.getUrl())
|
||||
Assert.assertEquals(1, anotherSubscription.getNotificationMode().toLong())
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
/**
|
||||
* Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
|
||||
* This class is loaded via reflection by
|
||||
* {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}.
|
||||
*/
|
||||
@SuppressWarnings("unused") // Class is used but loaded via reflection
|
||||
public class DebugSettingsBVDLeakCanary
|
||||
implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI {
|
||||
|
||||
@Override
|
||||
public Intent getNewLeakDisplayActivityIntent() {
|
||||
return LeakCanary.INSTANCE.newLeakDisplayActivityIntent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.schabi.newpipe.settings
|
||||
|
||||
import android.content.Intent
|
||||
import leakcanary.LeakCanary.newLeakDisplayActivityIntent
|
||||
import org.schabi.newpipe.settings.DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI
|
||||
|
||||
/**
|
||||
* Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
|
||||
* This class is loaded via reflection by
|
||||
* [DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI].
|
||||
*/
|
||||
@Suppress("unused") // Class is used but loaded via reflection
|
||||
|
||||
class DebugSettingsBVDLeakCanary : DebugSettingsBVDLeakCanaryAPI {
|
||||
override fun getNewLeakDisplayActivityIntent(): Intent? {
|
||||
return newLeakDisplayActivityIntent()
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue