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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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
|
* A matching algorithm that is similar to the searching algorithms implemented in editors such
|
||||||
* as Sublime Text, TextMate, Atom and others.
|
* as Sublime Text, TextMate, Atom and others.
|
||||||
*
|
*
|
||||||
* <p>
|
*
|
||||||
|
*
|
||||||
* One point is given for every matched character. Subsequent matches yield two bonus points.
|
* One point is given for every matched character. Subsequent matches yield two bonus points.
|
||||||
* A higher score indicates a higher similarity.
|
* A higher score indicates a higher similarity.
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* <p>
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
* This code has been adapted from Apache Commons Lang 3.3.
|
* This code has been adapted from Apache Commons Lang 3.3.
|
||||||
* </p>
|
*
|
||||||
*
|
*
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
*
|
*
|
||||||
* Note: This class was forked from
|
* Note: This class was forked from
|
||||||
* <a href="https://git.io/JyYJg">
|
* [
|
||||||
* apache/commons-text (8cfdafc) FuzzyScore.java
|
* apache/commons-text (8cfdafc) FuzzyScore.java
|
||||||
* </a>
|
](https://git.io/JyYJg) *
|
||||||
|
*/
|
||||||
|
class FuzzyScore(locale: Locale?) {
|
||||||
|
/**
|
||||||
|
* Gets the locale.
|
||||||
|
*
|
||||||
|
* @return The locale
|
||||||
*/
|
*/
|
||||||
public class FuzzyScore {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Locale used to change the case of text.
|
* 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.
|
* @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
|
* @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) {
|
init {
|
||||||
if (locale == null) {
|
requireNotNull(locale) { "Locale must not be null" }
|
||||||
throw new IllegalArgumentException("Locale must not be null");
|
this.locale = locale
|
||||||
}
|
|
||||||
this.locale = locale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,73 +80,57 @@ public class FuzzyScore {
|
||||||
* score.fuzzyScore("Workshop", "ws") = 2
|
* score.fuzzyScore("Workshop", "ws") = 2
|
||||||
* score.fuzzyScore("Workshop", "wo") = 4
|
* score.fuzzyScore("Workshop", "wo") = 4
|
||||||
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
|
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
|
||||||
* </pre>
|
</pre> *
|
||||||
*
|
*
|
||||||
* @param term a full term that should be matched against, must not be null
|
* @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
|
* @param query the query that will be matched against a term, must not be
|
||||||
* null
|
* null
|
||||||
* @return result score
|
* @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) {
|
fun fuzzyScore(term: CharSequence?, query: CharSequence?): Int {
|
||||||
if (term == null || query == null) {
|
require(!(term == null || query == null)) { "CharSequences must not be null" }
|
||||||
throw new IllegalArgumentException("CharSequences must not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
// fuzzy logic is case insensitive. We normalize the Strings to lower
|
// fuzzy logic is case insensitive. We normalize the Strings to lower
|
||||||
// case right from the start. Turning characters to lower case
|
// case right from the start. Turning characters to lower case
|
||||||
// via Character.toLowerCase(char) is unfortunately insufficient
|
// via Character.toLowerCase(char) is unfortunately insufficient
|
||||||
// as it does not accept a locale.
|
// as it does not accept a locale.
|
||||||
final String termLowerCase = term.toString().toLowerCase(locale);
|
val termLowerCase = term.toString().lowercase(locale)
|
||||||
final String queryLowerCase = query.toString().toLowerCase(locale);
|
val queryLowerCase = query.toString().lowercase(locale)
|
||||||
|
|
||||||
// the resulting score
|
// the resulting score
|
||||||
int score = 0;
|
var score = 0
|
||||||
|
|
||||||
// the position in the term which will be scanned next for potential
|
// the position in the term which will be scanned next for potential
|
||||||
// query character matches
|
// query character matches
|
||||||
int termIndex = 0;
|
var termIndex = 0
|
||||||
|
|
||||||
// index of the previously matched character in the term
|
// index of the previously matched character in the term
|
||||||
int previousMatchingCharacterIndex = Integer.MIN_VALUE;
|
var previousMatchingCharacterIndex = Int.MIN_VALUE
|
||||||
|
for (queryIndex in 0 until queryLowerCase.length) {
|
||||||
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
|
val queryChar = queryLowerCase[queryIndex]
|
||||||
final char queryChar = queryLowerCase.charAt(queryIndex);
|
var termCharacterMatchFound = false
|
||||||
|
while (termIndex < termLowerCase.length
|
||||||
boolean termCharacterMatchFound = false;
|
&& !termCharacterMatchFound) {
|
||||||
for (; termIndex < termLowerCase.length()
|
val termChar = termLowerCase[termIndex]
|
||||||
&& !termCharacterMatchFound; termIndex++) {
|
|
||||||
final char termChar = termLowerCase.charAt(termIndex);
|
|
||||||
|
|
||||||
if (queryChar == termChar) {
|
if (queryChar == termChar) {
|
||||||
// simple character matches result in one point
|
// simple character matches result in one point
|
||||||
score++;
|
score++
|
||||||
|
|
||||||
// subsequent character matches further improve
|
// subsequent character matches further improve
|
||||||
// the score.
|
// the score.
|
||||||
if (previousMatchingCharacterIndex + 1 == termIndex) {
|
if (previousMatchingCharacterIndex + 1 == termIndex) {
|
||||||
score += 2;
|
score += 2
|
||||||
}
|
}
|
||||||
|
previousMatchingCharacterIndex = termIndex
|
||||||
previousMatchingCharacterIndex = termIndex;
|
|
||||||
|
|
||||||
// we can leave the nested loop. Every character in the
|
// we can leave the nested loop. Every character in the
|
||||||
// query can match at most one character in the term.
|
// 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 android.util.Log
|
||||||
|
import androidx.room.migration.Migration
|
||||||
import androidx.annotation.NonNull;
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import androidx.room.migration.Migration;
|
import org.schabi.newpipe.MainActivity
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
|
|
||||||
public final class Migrations {
|
|
||||||
|
|
||||||
|
object Migrations {
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
// Test new migrations manually by importing a database from daily usage //
|
// Test new migrations manually by importing a database from daily usage //
|
||||||
// and checking if the migration works (Use the Database Inspector //
|
// 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 //
|
// If you add a migration point it out in the pull request, so that //
|
||||||
// others remember to test it themselves. //
|
// others remember to test it themselves. //
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
val DB_VER_1: Int = 1
|
||||||
public static final int DB_VER_1 = 1;
|
val DB_VER_2: Int = 2
|
||||||
public static final int DB_VER_2 = 2;
|
val DB_VER_3: Int = 3
|
||||||
public static final int DB_VER_3 = 3;
|
val DB_VER_4: Int = 4
|
||||||
public static final int DB_VER_4 = 4;
|
val DB_VER_5: Int = 5
|
||||||
public static final int DB_VER_5 = 5;
|
val DB_VER_6: Int = 6
|
||||||
public static final int DB_VER_6 = 6;
|
val DB_VER_7: Int = 7
|
||||||
public static final int DB_VER_7 = 7;
|
val DB_VER_8: Int = 8
|
||||||
public static final int DB_VER_8 = 8;
|
val DB_VER_9: Int = 9
|
||||||
public static final int DB_VER_9 = 9;
|
private val TAG: String = Migrations::class.java.getName()
|
||||||
|
val DEBUG: Boolean = MainActivity.Companion.DEBUG
|
||||||
private static final String TAG = Migrations.class.getName();
|
val MIGRATION_1_2: Migration = object : Migration(DB_VER_1, DB_VER_2) {
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
|
||||||
public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
|
||||||
if (DEBUG) {
|
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
|
* 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.
|
// 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.
|
// It's either this or blasting the entire database anew.
|
||||||
database.execSQL("CREATE INDEX `index_search_history_search` "
|
database.execSQL(("CREATE INDEX `index_search_history_search` "
|
||||||
+ "ON `search_history` (`search`)");
|
+ "ON `search_history` (`search`)"))
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS `streams` "
|
||||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
+ "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, "
|
+ "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, "
|
||||||
+ "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, "
|
+ "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, "
|
||||||
+ "`thumbnail_url` TEXT)");
|
+ "`thumbnail_url` TEXT)"))
|
||||||
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` "
|
database.execSQL(("CREATE UNIQUE INDEX `index_streams_service_id_url` "
|
||||||
+ "ON `streams` (`service_id`, `url`)");
|
+ "ON `streams` (`service_id`, `url`)"))
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS `stream_history` "
|
||||||
+ "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, "
|
+ "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, "
|
||||||
+ "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), "
|
+ "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), "
|
||||||
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
||||||
+ "ON UPDATE CASCADE ON DELETE CASCADE )");
|
+ "ON UPDATE CASCADE ON DELETE CASCADE )"))
|
||||||
database.execSQL("CREATE INDEX `index_stream_history_stream_id` "
|
database.execSQL(("CREATE INDEX `index_stream_history_stream_id` "
|
||||||
+ "ON `stream_history` (`stream_id`)");
|
+ "ON `stream_history` (`stream_id`)"))
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS `stream_state` "
|
||||||
+ "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, "
|
+ "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, "
|
||||||
+ "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) "
|
+ "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) "
|
||||||
+ "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
+ "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"))
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS `playlists` "
|
||||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
+ "`name` TEXT, `thumbnail_url` TEXT)");
|
+ "`name` TEXT, `thumbnail_url` TEXT)"))
|
||||||
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
|
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)")
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
|
||||||
+ "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, "
|
+ "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, "
|
||||||
+ "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), "
|
+ "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), "
|
||||||
+ "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) "
|
+ "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) "
|
||||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||||
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
||||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"))
|
||||||
database.execSQL("CREATE UNIQUE INDEX "
|
database.execSQL(("CREATE UNIQUE INDEX "
|
||||||
+ "`index_playlist_stream_join_playlist_id_join_index` "
|
+ "`index_playlist_stream_join_playlist_id_join_index` "
|
||||||
+ "ON `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` "
|
database.execSQL(("CREATE INDEX `index_playlist_stream_join_stream_id` "
|
||||||
+ "ON `playlist_stream_join` (`stream_id`)");
|
+ "ON `playlist_stream_join` (`stream_id`)"))
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS `remote_playlists` "
|
||||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||||
+ "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
|
+ "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"))
|
||||||
database.execSQL("CREATE INDEX `index_remote_playlists_name` "
|
database.execSQL(("CREATE INDEX `index_remote_playlists_name` "
|
||||||
+ "ON `remote_playlists` (`name`)");
|
+ "ON `remote_playlists` (`name`)"))
|
||||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
database.execSQL(("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||||
+ "ON `remote_playlists` (`service_id`, `url`)");
|
+ "ON `remote_playlists` (`service_id`, `url`)"))
|
||||||
|
|
||||||
// Populate streams table with existing entries in watch history
|
// Populate streams table with existing entries in watch history
|
||||||
// Latest data first, thus ignoring older entries with the same indices
|
// 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) "
|
+ "stream_type, duration, uploader, thumbnail_url) "
|
||||||
|
|
||||||
+ "SELECT service_id, url, title, 'VIDEO_STREAM', duration, "
|
+ "SELECT service_id, url, title, 'VIDEO_STREAM', duration, "
|
||||||
+ "uploader, thumbnail_url "
|
+ "uploader, thumbnail_url "
|
||||||
|
|
||||||
+ "FROM watch_history "
|
+ "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
|
// Once the streams have PKs, join them with the normalized history table
|
||||||
// and populate it with the remaining data from watch history
|
// 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 "
|
+ "SELECT uid, creation_date, 1 "
|
||||||
+ "FROM watch_history INNER JOIN streams "
|
+ "FROM watch_history INNER JOIN streams "
|
||||||
+ "ON watch_history.service_id == streams.service_id "
|
+ "ON watch_history.service_id == streams.service_id "
|
||||||
+ "AND watch_history.url == streams.url "
|
+ "AND watch_history.url == streams.url "
|
||||||
+ "ORDER BY creation_date DESC");
|
+ "ORDER BY creation_date DESC"))
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS watch_history")
|
||||||
database.execSQL("DROP TABLE IF EXISTS watch_history");
|
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Stop migrating database");
|
Log.d(TAG, "Stop migrating database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
val MIGRATION_2_3: Migration = object : Migration(DB_VER_2, DB_VER_3) {
|
||||||
public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) {
|
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
|
||||||
// Add NOT NULLs and new fields
|
// 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, "
|
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
+ "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, "
|
+ "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, "
|
||||||
+ "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, "
|
+ "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, "
|
||||||
+ "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, "
|
+ "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, "
|
||||||
+ "textual_upload_date TEXT, upload_date INTEGER, "
|
+ "textual_upload_date TEXT, upload_date INTEGER, "
|
||||||
+ "is_upload_date_approximation INTEGER)");
|
+ "is_upload_date_approximation INTEGER)"))
|
||||||
|
database.execSQL(("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
|
||||||
database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
|
|
||||||
+ "duration, uploader, thumbnail_url, view_count, textual_upload_date, "
|
+ "duration, uploader, thumbnail_url, view_count, textual_upload_date, "
|
||||||
+ "upload_date, is_upload_date_approximation) "
|
+ "upload_date, is_upload_date_approximation) "
|
||||||
|
|
||||||
+ "SELECT uid, service_id, url, ifnull(title, ''), "
|
+ "SELECT uid, service_id, url, ifnull(title, ''), "
|
||||||
+ "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), "
|
+ "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), "
|
||||||
+ "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL "
|
+ "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL "
|
||||||
|
+ "FROM streams WHERE url IS NOT NULL"))
|
||||||
+ "FROM streams WHERE url IS NOT NULL");
|
database.execSQL("DROP TABLE streams")
|
||||||
|
database.execSQL("ALTER TABLE streams_new RENAME TO streams")
|
||||||
database.execSQL("DROP TABLE streams");
|
database.execSQL(("CREATE UNIQUE INDEX index_streams_service_id_url "
|
||||||
database.execSQL("ALTER TABLE streams_new RENAME TO streams");
|
+ "ON streams (service_id, url)"))
|
||||||
database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url "
|
|
||||||
+ "ON streams (service_id, url)");
|
|
||||||
|
|
||||||
// Tables for feed feature
|
// 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, "
|
+ "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
||||||
+ "PRIMARY KEY(stream_id, subscription_id), "
|
+ "PRIMARY KEY(stream_id, subscription_id), "
|
||||||
+ "FOREIGN KEY(stream_id) REFERENCES streams(uid) "
|
+ "FOREIGN KEY(stream_id) REFERENCES streams(uid) "
|
||||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
+ "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)"))
|
||||||
database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)");
|
database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)")
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS feed_group "
|
||||||
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, "
|
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, "
|
||||||
+ "icon_id INTEGER NOT NULL, sort_order INTEGER 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 INDEX index_feed_group_sort_order ON feed_group (sort_order)")
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
|
||||||
+ "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
+ "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
||||||
+ "PRIMARY KEY(group_id, subscription_id), "
|
+ "PRIMARY KEY(group_id, subscription_id), "
|
||||||
+ "FOREIGN KEY(group_id) REFERENCES feed_group(uid) "
|
+ "FOREIGN KEY(group_id) REFERENCES feed_group(uid) "
|
||||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
+ "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)"))
|
||||||
database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id "
|
database.execSQL(("CREATE INDEX index_feed_group_subscription_join_subscription_id "
|
||||||
+ "ON feed_group_subscription_join (subscription_id)");
|
+ "ON feed_group_subscription_join (subscription_id)"))
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated "
|
database.execSQL(("CREATE TABLE IF NOT EXISTS feed_last_updated "
|
||||||
+ "(subscription_id INTEGER NOT NULL, last_updated INTEGER, "
|
+ "(subscription_id INTEGER NOT NULL, last_updated INTEGER, "
|
||||||
+ "PRIMARY KEY(subscription_id), "
|
+ "PRIMARY KEY(subscription_id), "
|
||||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
+ "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)"))
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
val MIGRATION_3_4: Migration = object : Migration(DB_VER_3, DB_VER_4) {
|
||||||
public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
|
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL(
|
database.execSQL(
|
||||||
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
|
"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) {
|
||||||
public static final Migration MIGRATION_5_6 = new Migration(DB_VER_5, DB_VER_6) {
|
database.execSQL(("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` "
|
||||||
@Override
|
+ "INTEGER NOT NULL DEFAULT 0"))
|
||||||
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 static final Migration MIGRATION_6_7 = new Migration(DB_VER_6, DB_VER_7) {
|
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
@Override
|
database.execSQL(("ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` "
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
+ "INTEGER NOT NULL DEFAULT 0"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
// Create a new column thumbnail_stream_id
|
||||||
database.execSQL("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
|
database.execSQL(("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
|
||||||
+ "INTEGER NOT NULL DEFAULT -1");
|
+ "INTEGER NOT NULL DEFAULT -1"))
|
||||||
|
|
||||||
// Migrate the thumbnail_url to the thumbnail_stream_id
|
// 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"
|
+ " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END"
|
||||||
+ " FROM ("
|
+ " FROM ("
|
||||||
+ " SELECT p.uid AS playlist_uid, s.uid AS stream_uid"
|
+ " 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 playlist_stream_join ps ON p.uid = ps.playlist_id"
|
||||||
+ " LEFT JOIN streams s ON s.uid = ps.stream_id"
|
+ " LEFT JOIN streams s ON s.uid = ps.stream_id"
|
||||||
+ " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table"
|
+ " 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
|
// 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, "
|
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
+ "name TEXT, "
|
+ "name TEXT, "
|
||||||
+ "is_thumbnail_permanent INTEGER NOT NULL, "
|
+ "is_thumbnail_permanent INTEGER NOT NULL, "
|
||||||
+ "thumbnail_stream_id INTEGER NOT NULL)");
|
+ "thumbnail_stream_id INTEGER NOT NULL)"))
|
||||||
|
database.execSQL(("INSERT INTO playlists_new"
|
||||||
database.execSQL("INSERT INTO playlists_new"
|
|
||||||
+ " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id "
|
+ " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id "
|
||||||
+ " FROM playlists");
|
+ " FROM playlists"))
|
||||||
|
database.execSQL("DROP TABLE playlists")
|
||||||
|
database.execSQL("ALTER TABLE playlists_new RENAME TO playlists")
|
||||||
database.execSQL("DROP TABLE playlists");
|
database.execSQL(("CREATE INDEX IF NOT EXISTS "
|
||||||
database.execSQL("ALTER TABLE playlists_new RENAME TO playlists");
|
+ "`index_playlists_name` ON `playlists` (`name`)"))
|
||||||
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) {
|
||||||
public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) {
|
database.execSQL(("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT "
|
||||||
@Override
|
+ "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)"))
|
||||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
database.execSQL("UPDATE search_history SET search = trim(search)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val MIGRATION_8_9: Migration = object : Migration(DB_VER_8, DB_VER_9) {
|
||||||
|
public override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
try {
|
try {
|
||||||
database.beginTransaction();
|
database.beginTransaction()
|
||||||
|
|
||||||
// Update playlists.
|
// Update playlists.
|
||||||
// Create a temp table to initialize display_index.
|
// 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, "
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
+ "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, "
|
||||||
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
+ "`thumbnail_stream_id` INTEGER NOT NULL, "
|
||||||
+ "`display_index` INTEGER NOT NULL)");
|
+ "`display_index` INTEGER NOT NULL)"))
|
||||||
database.execSQL("INSERT INTO `playlists_tmp` "
|
database.execSQL(("INSERT INTO `playlists_tmp` "
|
||||||
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
+ "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
+ "`display_index`) "
|
+ "`display_index`) "
|
||||||
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
+ "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, "
|
||||||
+ "-1 "
|
+ "-1 "
|
||||||
+ "FROM `playlists`");
|
+ "FROM `playlists`"))
|
||||||
|
|
||||||
// Replace the old table, note that this also removes the index on the name which
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
// we don't need anymore.
|
// we don't need anymore.
|
||||||
database.execSQL("DROP TABLE `playlists`");
|
database.execSQL("DROP TABLE `playlists`")
|
||||||
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`");
|
database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`")
|
||||||
|
|
||||||
|
|
||||||
// Update remote_playlists.
|
// Update remote_playlists.
|
||||||
// Create a temp table to initialize display_index.
|
// 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, "
|
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||||
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||||
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
+ "`thumbnail_url` TEXT, `uploader` TEXT, "
|
||||||
+ "`display_index` INTEGER NOT NULL,"
|
+ "`display_index` INTEGER NOT NULL,"
|
||||||
+ "`stream_count` INTEGER)");
|
+ "`stream_count` INTEGER)"))
|
||||||
database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
database.execSQL(("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, "
|
||||||
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
+ "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, "
|
||||||
+ "`stream_count`)"
|
+ "`stream_count`)"
|
||||||
+ "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, "
|
+ "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
|
// Replace the old table, note that this also removes the index on the name which
|
||||||
// we don't need anymore.
|
// we don't need anymore.
|
||||||
database.execSQL("DROP TABLE `remote_playlists`");
|
database.execSQL("DROP TABLE `remote_playlists`")
|
||||||
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`");
|
database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`")
|
||||||
|
|
||||||
// Create index on the new table.
|
// Create index on the new table.
|
||||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
database.execSQL(("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||||
+ "ON `remote_playlists` (`service_id`, `url`)");
|
+ "ON `remote_playlists` (`service_id`, `url`)"))
|
||||||
|
database.setTransactionSuccessful()
|
||||||
database.setTransactionSuccessful();
|
|
||||||
} finally {
|
} 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 android.content.Context
|
||||||
|
import org.acra.ReportField
|
||||||
import androidx.annotation.NonNull;
|
import org.acra.data.CrashReportData
|
||||||
|
import org.acra.sender.ReportSender
|
||||||
import org.acra.ReportField;
|
import org.schabi.newpipe.R
|
||||||
import org.acra.data.CrashReportData;
|
import org.schabi.newpipe.error.ErrorUtil.Companion.openActivity
|
||||||
import org.acra.sender.ReportSender;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 13.09.16.
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
class AcraReportSender() : ReportSender {
|
||||||
public class AcraReportSender implements ReportSender {
|
public override fun send(context: Context, report: CrashReportData) {
|
||||||
|
openActivity(context, ErrorInfo(arrayOf<String?>(report.getString(ReportField.STACK_TRACE)),
|
||||||
@Override
|
|
||||||
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
|
|
||||||
ErrorUtil.openActivity(context, new ErrorInfo(
|
|
||||||
new String[]{report.getString(ReportField.STACK_TRACE)},
|
|
||||||
UserAction.UI_ERROR,
|
UserAction.UI_ERROR,
|
||||||
ErrorInfo.SERVICE_NONE,
|
ErrorInfo.SERVICE_NONE,
|
||||||
"ACRA report",
|
"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 android.content.Context
|
||||||
|
import com.google.auto.service.AutoService
|
||||||
import androidx.annotation.NonNull;
|
import org.acra.config.CoreConfiguration
|
||||||
|
import org.acra.sender.ReportSender
|
||||||
import com.google.auto.service.AutoService;
|
import org.acra.sender.ReportSenderFactory
|
||||||
|
import org.schabi.newpipe.App
|
||||||
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.
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* 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)
|
@AutoService(ReportSenderFactory::class)
|
||||||
public class AcraReportSenderFactory implements ReportSenderFactory {
|
class AcraReportSenderFactory() : ReportSenderFactory {
|
||||||
@NonNull
|
public override fun create(context: Context,
|
||||||
public ReportSender create(@NonNull final Context context,
|
config: CoreConfiguration): ReportSender {
|
||||||
@NonNull final CoreConfiguration config) {
|
return AcraReportSender()
|
||||||
return new 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.
|
* The user actions that can cause an error.
|
||||||
*/
|
*/
|
||||||
public enum UserAction {
|
enum class UserAction(val message: String) {
|
||||||
USER_REPORT("user report"),
|
USER_REPORT("user report"),
|
||||||
UI_ERROR("ui error"),
|
UI_ERROR("ui error"),
|
||||||
SUBSCRIPTION_CHANGE("subscription change"),
|
SUBSCRIPTION_CHANGE("subscription change"),
|
||||||
|
@ -31,15 +31,6 @@ public enum UserAction {
|
||||||
PREFERENCES_MIGRATION("migration of preferences"),
|
PREFERENCES_MIGRATION("migration of preferences"),
|
||||||
SHARE_TO_NEWPIPE("share to newpipe"),
|
SHARE_TO_NEWPIPE("share to newpipe"),
|
||||||
CHECK_FOR_NEW_APP_VERSION("check for new app version"),
|
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.
|
* Indicates that the current fragment can handle back presses.
|
||||||
*/
|
*/
|
||||||
public interface BackPressable {
|
open interface BackPressable {
|
||||||
/**
|
/**
|
||||||
* A back press was delegated to this fragment.
|
* A back press was delegated to this fragment.
|
||||||
*
|
*
|
||||||
* @return if the back press was handled
|
* @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