mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-03-22 23:59:44 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
515bb6e94d | ||
|
|
349000857a | ||
|
|
f1c608b396 | ||
|
|
5f1a270ca4 | ||
|
|
aba2a385fd | ||
|
|
71a3bf2855 | ||
|
|
ebb937934a | ||
|
|
223b240299 | ||
|
|
668af4fc3e | ||
|
|
bfcc31ec89 | ||
|
|
6fa97e17f5 | ||
|
|
0d65733e53 | ||
|
|
9cc6f9fd68 | ||
|
|
79767f95f7 | ||
|
|
d5f941ff3d | ||
|
|
05f09c94d1 | ||
|
|
47624a575a | ||
|
|
3b3348e7a1 | ||
|
|
521f60af85 | ||
|
|
dda219a9e9 | ||
|
|
b8ec9bf412 | ||
|
|
e173bf4252 | ||
|
|
9f45aa571c | ||
|
|
0cdf40cd5f | ||
|
|
0020a02a28 | ||
|
|
e358867da8 | ||
|
|
e6daf45c83 |
@@ -11,6 +11,7 @@ plugins {
|
|||||||
alias(libs.plugins.jetbrains.kotlin.kapt)
|
alias(libs.plugins.jetbrains.kotlin.kapt)
|
||||||
alias(libs.plugins.google.ksp)
|
alias(libs.plugins.google.ksp)
|
||||||
alias(libs.plugins.jetbrains.kotlin.parcelize)
|
alias(libs.plugins.jetbrains.kotlin.parcelize)
|
||||||
|
alias(libs.plugins.jetbrains.kotlinx.serialization)
|
||||||
alias(libs.plugins.sonarqube)
|
alias(libs.plugins.sonarqube)
|
||||||
checkstyle
|
checkstyle
|
||||||
}
|
}
|
||||||
@@ -246,6 +247,12 @@ dependencies {
|
|||||||
implementation(libs.google.android.material)
|
implementation(libs.google.android.material)
|
||||||
implementation(libs.androidx.webkit)
|
implementation(libs.androidx.webkit)
|
||||||
|
|
||||||
|
// Coroutines interop
|
||||||
|
implementation(libs.kotlinx.coroutines.rx3)
|
||||||
|
|
||||||
|
// Kotlinx Serialization
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
implementation(libs.livefront.bridge)
|
implementation(libs.livefront.bridge)
|
||||||
implementation(libs.evernote.statesaver.core)
|
implementation(libs.evernote.statesaver.core)
|
||||||
|
|||||||
15
app/proguard-rules.pro
vendored
15
app/proguard-rules.pro
vendored
@@ -44,3 +44,18 @@
|
|||||||
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## Keep Kotlinx Serialization classes
|
||||||
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
-keep,includedescriptorclasses class org.schabi.newpipe.**$$serializer { *; }
|
||||||
|
-keepclassmembers class org.schabi.newpipe.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class org.schabi.newpipe.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,14 +96,6 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_about" />
|
android:label="@string/title_activity_about" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".local.subscription.services.SubscriptionsImportService"
|
|
||||||
android:foregroundServiceType="dataSync" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".local.subscription.services.SubscriptionsExportService"
|
|
||||||
android:foregroundServiceType="dataSync" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".local.feed.service.FeedLoadService"
|
android:name=".local.feed.service.FeedLoadService"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|||||||
@@ -254,13 +254,6 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
"ByteHamster",
|
"ByteHamster",
|
||||||
"https://github.com/ByteHamster/SearchPreference",
|
"https://github.com/ByteHamster/SearchPreference",
|
||||||
StandardLicenses.MIT
|
StandardLicenses.MIT
|
||||||
),
|
|
||||||
SoftwareComponent(
|
|
||||||
"FreeDroidWarn",
|
|
||||||
"2026",
|
|
||||||
"woheller69",
|
|
||||||
"https://github.com/woheller69/FreeDroidWarn",
|
|
||||||
StandardLicenses.APACHE2
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,9 +216,9 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|
|||||||
|| image.getWidth() != Image.WIDTH_UNKNOWN
|
|| image.getWidth() != Image.WIDTH_UNKNOWN
|
||||||
// if even the resolution level is unknown, ?x? will be shown
|
// if even the resolution level is unknown, ?x? will be shown
|
||||||
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
|
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
|
||||||
urls.append(imageSizeToText(image.getHeight()));
|
|
||||||
urls.append('x');
|
|
||||||
urls.append(imageSizeToText(image.getWidth()));
|
urls.append(imageSizeToText(image.getWidth()));
|
||||||
|
urls.append('x');
|
||||||
|
urls.append(imageSizeToText(image.getHeight()));
|
||||||
} else {
|
} else {
|
||||||
switch (image.getEstimatedResolutionLevel()) {
|
switch (image.getEstimatedResolutionLevel()) {
|
||||||
case LOW -> urls.append(getString(R.string.image_quality_low));
|
case LOW -> urls.append(getString(R.string.image_quality_low));
|
||||||
|
|||||||
@@ -1,41 +1,63 @@
|
|||||||
package org.schabi.newpipe.local.subscription;
|
package org.schabi.newpipe.local.subscription;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.os.BundleCompat;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.work.Constraints;
|
||||||
|
import androidx.work.ExistingWorkPolicy;
|
||||||
|
import androidx.work.NetworkType;
|
||||||
|
import androidx.work.OneTimeWorkRequest;
|
||||||
|
import androidx.work.OutOfQuotaPolicy;
|
||||||
|
import androidx.work.WorkManager;
|
||||||
|
|
||||||
import com.livefront.bridge.Bridge;
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportInput;
|
||||||
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportWorker;
|
||||||
|
|
||||||
public class ImportConfirmationDialog extends DialogFragment {
|
public class ImportConfirmationDialog extends DialogFragment {
|
||||||
protected Intent resultServiceIntent;
|
private static final String INPUT = "input";
|
||||||
private static final String EXTRA_RESULT_SERVICE_INTENT = "extra_result_service_intent";
|
|
||||||
|
|
||||||
public static void show(@NonNull final Fragment fragment,
|
public static void show(@NonNull final Fragment fragment, final SubscriptionImportInput input) {
|
||||||
@NonNull final Intent resultServiceIntent) {
|
final var confirmationDialog = new ImportConfirmationDialog();
|
||||||
final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog();
|
final var arguments = new Bundle();
|
||||||
final Bundle args = new Bundle();
|
arguments.putParcelable(INPUT, input);
|
||||||
args.putParcelable(EXTRA_RESULT_SERVICE_INTENT, resultServiceIntent);
|
confirmationDialog.setArguments(arguments);
|
||||||
confirmationDialog.setArguments(args);
|
|
||||||
confirmationDialog.show(fragment.getParentFragmentManager(), null);
|
confirmationDialog.show(fragment.getParentFragmentManager(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
|
||||||
return new AlertDialog.Builder(requireContext())
|
final var context = requireContext();
|
||||||
|
return new AlertDialog.Builder(context)
|
||||||
.setMessage(R.string.import_network_expensive_warning)
|
.setMessage(R.string.import_network_expensive_warning)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||||
requireContext().startService(resultServiceIntent);
|
final var constraints = new Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build();
|
||||||
|
final var input = BundleCompat.getParcelable(requireArguments(), INPUT,
|
||||||
|
SubscriptionImportInput.class);
|
||||||
|
|
||||||
|
final var req = new OneTimeWorkRequest.Builder(SubscriptionImportWorker.class)
|
||||||
|
.setInputData(input.toData())
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.enqueueUniqueWork(SubscriptionImportWorker.WORK_NAME,
|
||||||
|
ExistingWorkPolicy.APPEND_OR_REPLACE, req);
|
||||||
|
|
||||||
dismiss();
|
dismiss();
|
||||||
})
|
})
|
||||||
.create();
|
.create();
|
||||||
@@ -45,7 +67,7 @@ public class ImportConfirmationDialog extends DialogFragment {
|
|||||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
resultServiceIntent = requireArguments().getParcelable(EXTRA_RESULT_SERVICE_INTENT);
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package org.schabi.newpipe.local.subscription
|
package org.schabi.newpipe.local.subscription
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -15,8 +13,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
@@ -27,9 +23,6 @@ import com.xwray.groupie.GroupAdapter
|
|||||||
import com.xwray.groupie.Section
|
import com.xwray.groupie.Section
|
||||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
|
||||||
import org.schabi.newpipe.databinding.DialogTitleBinding
|
import org.schabi.newpipe.databinding.DialogTitleBinding
|
||||||
@@ -53,13 +46,6 @@ import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
|
|||||||
import org.schabi.newpipe.local.subscription.item.GroupsHeader
|
import org.schabi.newpipe.local.subscription.item.GroupsHeader
|
||||||
import org.schabi.newpipe.local.subscription.item.Header
|
import org.schabi.newpipe.local.subscription.item.Header
|
||||||
import org.schabi.newpipe.local.subscription.item.ImportSubscriptionsHintPlaceholderItem
|
import org.schabi.newpipe.local.subscription.item.ImportSubscriptionsHintPlaceholderItem
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
|
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
|
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
|
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
|
||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.OnClickGesture
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
import org.schabi.newpipe.util.ServiceHelper
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
@@ -72,6 +58,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
|
|
||||||
private lateinit var viewModel: SubscriptionViewModel
|
private lateinit var viewModel: SubscriptionViewModel
|
||||||
private lateinit var subscriptionManager: SubscriptionManager
|
private lateinit var subscriptionManager: SubscriptionManager
|
||||||
|
private lateinit var importExportHelper: SubscriptionsImportExportHelper
|
||||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
|
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
|
||||||
@@ -80,11 +67,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
private lateinit var feedGroupsSortMenuItem: GroupsHeader
|
private lateinit var feedGroupsSortMenuItem: GroupsHeader
|
||||||
private val subscriptionsSection = Section()
|
private val subscriptionsSection = Section()
|
||||||
|
|
||||||
private val requestExportLauncher =
|
|
||||||
registerForActivityResult(StartActivityForResult(), this::requestExportResult)
|
|
||||||
private val requestImportLauncher =
|
|
||||||
registerForActivityResult(StartActivityForResult(), this::requestImportResult)
|
|
||||||
|
|
||||||
@State
|
@State
|
||||||
@JvmField
|
@JvmField
|
||||||
var itemsListState: Parcelable? = null
|
var itemsListState: Parcelable? = null
|
||||||
@@ -104,6 +86,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
subscriptionManager = SubscriptionManager(requireContext())
|
subscriptionManager = SubscriptionManager(requireContext())
|
||||||
|
importExportHelper = SubscriptionsImportExportHelper(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
@@ -143,7 +126,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
// -- Import --
|
// -- Import --
|
||||||
val importSubMenu = menu.addSubMenu(R.string.import_from)
|
val importSubMenu = menu.addSubMenu(R.string.import_from)
|
||||||
|
|
||||||
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { onImportPreviousSelected() }
|
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { importExportHelper.onImportPreviousSelected() }
|
||||||
.setIcon(R.drawable.ic_backup)
|
.setIcon(R.drawable.ic_backup)
|
||||||
|
|
||||||
for (service in ServiceList.all()) {
|
for (service in ServiceList.all()) {
|
||||||
@@ -161,7 +144,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
// -- Export --
|
// -- Export --
|
||||||
val exportSubMenu = menu.addSubMenu(R.string.export_to)
|
val exportSubMenu = menu.addSubMenu(R.string.export_to)
|
||||||
|
|
||||||
addMenuItemToSubmenu(exportSubMenu, R.string.file) { onExportSelected() }
|
addMenuItemToSubmenu(exportSubMenu, R.string.file) { importExportHelper.onExportSelected() }
|
||||||
.setIcon(R.drawable.ic_save)
|
.setIcon(R.drawable.ic_save)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,51 +180,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId)
|
NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onImportPreviousSelected() {
|
|
||||||
NoFileManagerSafeGuard.launchSafe(
|
|
||||||
requestImportLauncher,
|
|
||||||
StoredFileHelper.getPicker(activity, JSON_MIME_TYPE),
|
|
||||||
TAG,
|
|
||||||
requireContext()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onExportSelected() {
|
|
||||||
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
|
|
||||||
val exportName = "newpipe_subscriptions_$date.json"
|
|
||||||
|
|
||||||
NoFileManagerSafeGuard.launchSafe(
|
|
||||||
requestExportLauncher,
|
|
||||||
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null),
|
|
||||||
TAG,
|
|
||||||
requireContext()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openReorderDialog() {
|
private fun openReorderDialog() {
|
||||||
FeedGroupReorderDialog().show(parentFragmentManager, null)
|
FeedGroupReorderDialog().show(parentFragmentManager, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestExportResult(result: ActivityResult) {
|
|
||||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
|
||||||
activity.startService(
|
|
||||||
Intent(activity, SubscriptionsExportService::class.java)
|
|
||||||
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, result.data?.data)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestImportResult(result: ActivityResult) {
|
|
||||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
|
||||||
ImportConfirmationDialog.show(
|
|
||||||
this,
|
|
||||||
Intent(activity, SubscriptionsImportService::class.java)
|
|
||||||
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
|
|
||||||
.putExtra(KEY_VALUE, result.data?.data)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ////////////////////////////////////////////////////////////////////////
|
// ////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment Views
|
// Fragment Views
|
||||||
// ////////////////////////////////////////////////////////////////////////
|
// ////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.schabi.newpipe.local.subscription
|
package org.schabi.newpipe.local.subscription
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Pair
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
@@ -51,23 +50,16 @@ class SubscriptionManager(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upsertAll(infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>): List<SubscriptionEntity> {
|
fun upsertAll(infoList: List<Pair<ChannelInfo, ChannelTabInfo>>) {
|
||||||
val listEntities = subscriptionTable.upsertAll(
|
val listEntities = infoList.map { SubscriptionEntity.from(it.first) }
|
||||||
infoList.map { SubscriptionEntity.from(it.first) }
|
subscriptionTable.upsertAll(listEntities)
|
||||||
)
|
|
||||||
|
|
||||||
database.runInTransaction {
|
database.runInTransaction {
|
||||||
infoList.forEachIndexed { index, info ->
|
infoList.forEachIndexed { index, info ->
|
||||||
info.second.forEach {
|
val streams = info.second.relatedItems.filterIsInstance<StreamInfoItem>()
|
||||||
feedDatabaseManager.upsertAll(
|
feedDatabaseManager.upsertAll(listEntities[index].uid, streams)
|
||||||
listEntities[index].uid,
|
|
||||||
it.relatedItems.filterIsInstance<StreamInfoItem>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return listEntities
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
|
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package org.schabi.newpipe.local.subscription
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import org.schabi.newpipe.local.subscription.SubscriptionFragment.Companion.JSON_MIME_TYPE
|
||||||
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionExportWorker
|
||||||
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportInput
|
||||||
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
||||||
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class has to be created in onAttach() or onCreate().
|
||||||
|
*
|
||||||
|
* It contains registerForActivityResult calls and those
|
||||||
|
* calls are only allowed before a fragment/activity is created.
|
||||||
|
*/
|
||||||
|
class SubscriptionsImportExportHelper(
|
||||||
|
val fragment: Fragment
|
||||||
|
) {
|
||||||
|
val context: Context = fragment.requireContext()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG: String =
|
||||||
|
SubscriptionsImportExportHelper::class.java.simpleName + "@" + Integer.toHexString(
|
||||||
|
hashCode()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requestExportLauncher =
|
||||||
|
fragment.registerForActivityResult(StartActivityForResult(), this::requestExportResult)
|
||||||
|
private val requestImportLauncher =
|
||||||
|
fragment.registerForActivityResult(StartActivityForResult(), this::requestImportResult)
|
||||||
|
|
||||||
|
private fun requestExportResult(result: ActivityResult) {
|
||||||
|
val data = result.data?.data
|
||||||
|
if (data != null && result.resultCode == Activity.RESULT_OK) {
|
||||||
|
SubscriptionExportWorker.schedule(context, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestImportResult(result: ActivityResult) {
|
||||||
|
val data = result.data?.dataString
|
||||||
|
if (data != null && result.resultCode == Activity.RESULT_OK) {
|
||||||
|
ImportConfirmationDialog.show(
|
||||||
|
fragment,
|
||||||
|
SubscriptionImportInput.PreviousExportMode(data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onExportSelected() {
|
||||||
|
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
|
||||||
|
val exportName = "newpipe_subscriptions_$date.json"
|
||||||
|
|
||||||
|
NoFileManagerSafeGuard.launchSafe(
|
||||||
|
requestExportLauncher,
|
||||||
|
StoredFileHelper.getNewPicker(
|
||||||
|
context,
|
||||||
|
exportName,
|
||||||
|
JSON_MIME_TYPE,
|
||||||
|
null
|
||||||
|
),
|
||||||
|
TAG,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onImportPreviousSelected() {
|
||||||
|
NoFileManagerSafeGuard.launchSafe(
|
||||||
|
requestImportLauncher,
|
||||||
|
StoredFileHelper.getPicker(context, JSON_MIME_TYPE),
|
||||||
|
TAG,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
package org.schabi.newpipe.local.subscription;
|
package org.schabi.newpipe.local.subscription;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
||||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
|
|
||||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
|
|
||||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
|
|
||||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -37,7 +33,7 @@ import org.schabi.newpipe.error.UserAction;
|
|||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportInput;
|
||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
@@ -168,10 +164,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onImportUrl(final String value) {
|
public void onImportUrl(final String value) {
|
||||||
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
|
ImportConfirmationDialog.show(this,
|
||||||
.putExtra(KEY_MODE, CHANNEL_URL_MODE)
|
new SubscriptionImportInput.ChannelUrlMode(currentServiceId, value));
|
||||||
.putExtra(KEY_VALUE, value)
|
|
||||||
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onImportFile() {
|
public void onImportFile() {
|
||||||
@@ -186,16 +180,10 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void requestImportFileResult(final ActivityResult result) {
|
private void requestImportFileResult(final ActivityResult result) {
|
||||||
if (result.getData() == null) {
|
final String data = result.getData() != null ? result.getData().getDataString() : null;
|
||||||
return;
|
if (result.getResultCode() == Activity.RESULT_OK && data != null) {
|
||||||
}
|
|
||||||
|
|
||||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData().getData() != null) {
|
|
||||||
ImportConfirmationDialog.show(this,
|
ImportConfirmationDialog.show(this,
|
||||||
new Intent(activity, SubscriptionsImportService.class)
|
new SubscriptionImportInput.InputStreamMode(currentServiceId, data));
|
||||||
.putExtra(KEY_MODE, INPUT_STREAM_MODE)
|
|
||||||
.putExtra(KEY_VALUE, result.getData().getData())
|
|
||||||
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
|
||||||
* BaseImportExportService.java is part of NewPipe
|
|
||||||
*
|
|
||||||
* License: GPL-3.0+
|
|
||||||
* This program 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.
|
|
||||||
*
|
|
||||||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.local.subscription.services;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.core.app.ServiceCompat;
|
|
||||||
|
|
||||||
import org.reactivestreams.Publisher;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
|
||||||
import org.schabi.newpipe.error.UserAction;
|
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.rxjava3.functions.Function;
|
|
||||||
import io.reactivex.rxjava3.processors.PublishProcessor;
|
|
||||||
|
|
||||||
public abstract class BaseImportExportService extends Service {
|
|
||||||
protected final String TAG = this.getClass().getSimpleName();
|
|
||||||
|
|
||||||
protected final CompositeDisposable disposables = new CompositeDisposable();
|
|
||||||
protected final PublishProcessor<String> notificationUpdater = PublishProcessor.create();
|
|
||||||
|
|
||||||
protected NotificationManagerCompat notificationManager;
|
|
||||||
protected NotificationCompat.Builder notificationBuilder;
|
|
||||||
protected SubscriptionManager subscriptionManager;
|
|
||||||
|
|
||||||
private static final int NOTIFICATION_SAMPLING_PERIOD = 2500;
|
|
||||||
|
|
||||||
protected final AtomicInteger currentProgress = new AtomicInteger(-1);
|
|
||||||
protected final AtomicInteger maxProgress = new AtomicInteger(-1);
|
|
||||||
protected final ImportExportEventListener eventListener = new ImportExportEventListener() {
|
|
||||||
@Override
|
|
||||||
public void onSizeReceived(final int size) {
|
|
||||||
maxProgress.set(size);
|
|
||||||
currentProgress.set(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemCompleted(final String itemName) {
|
|
||||||
currentProgress.incrementAndGet();
|
|
||||||
notificationUpdater.onNext(itemName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected Toast toast;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(final Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
subscriptionManager = new SubscriptionManager(this);
|
|
||||||
setupNotification();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
disposeAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void disposeAll() {
|
|
||||||
disposables.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Notification Impl
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
protected abstract int getNotificationId();
|
|
||||||
|
|
||||||
@StringRes
|
|
||||||
public abstract int getTitle();
|
|
||||||
|
|
||||||
protected void setupNotification() {
|
|
||||||
notificationManager = NotificationManagerCompat.from(this);
|
|
||||||
notificationBuilder = createNotification();
|
|
||||||
startForeground(getNotificationId(), notificationBuilder.build());
|
|
||||||
|
|
||||||
final Function<Flowable<String>, Publisher<String>> throttleAfterFirstEmission = flow ->
|
|
||||||
flow.take(1).concatWith(flow.skip(1)
|
|
||||||
.throttleLast(NOTIFICATION_SAMPLING_PERIOD, TimeUnit.MILLISECONDS));
|
|
||||||
|
|
||||||
disposables.add(notificationUpdater
|
|
||||||
.filter(s -> !s.isEmpty())
|
|
||||||
.publish(throttleAfterFirstEmission)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::updateNotification));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void updateNotification(final String text) {
|
|
||||||
notificationBuilder
|
|
||||||
.setProgress(maxProgress.get(), currentProgress.get(), maxProgress.get() == -1);
|
|
||||||
|
|
||||||
final String progressText = currentProgress + "/" + maxProgress;
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
if (!TextUtils.isEmpty(text)) {
|
|
||||||
notificationBuilder.setContentText(text + " (" + progressText + ")");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notificationBuilder.setContentInfo(progressText);
|
|
||||||
notificationBuilder.setContentText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notificationManager.areNotificationsEnabled()) {
|
|
||||||
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void stopService() {
|
|
||||||
postErrorResult(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void stopAndReportError(final Throwable throwable, final String request) {
|
|
||||||
stopService();
|
|
||||||
ErrorUtil.createNotification(this, new ErrorInfo(
|
|
||||||
throwable, UserAction.SUBSCRIPTION_IMPORT_EXPORT, request));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void postErrorResult(final String title, final String text) {
|
|
||||||
disposeAll();
|
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
|
||||||
stopSelf();
|
|
||||||
|
|
||||||
if (title == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String textOrEmpty = text == null ? "" : text;
|
|
||||||
notificationBuilder = new NotificationCompat
|
|
||||||
.Builder(this, getString(R.string.notification_channel_id))
|
|
||||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
|
|
||||||
.setContentText(textOrEmpty);
|
|
||||||
|
|
||||||
if (notificationManager.areNotificationsEnabled()) {
|
|
||||||
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected NotificationCompat.Builder createNotification() {
|
|
||||||
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
|
||||||
.setOngoing(true)
|
|
||||||
.setProgress(-1, -1, true)
|
|
||||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setContentTitle(getString(getTitle()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Toast
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
protected void showToast(@StringRes final int message) {
|
|
||||||
showToast(getString(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void showToast(final String message) {
|
|
||||||
if (toast != null) {
|
|
||||||
toast.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
|
|
||||||
toast.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Error handling
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
protected void handleError(@StringRes final int errorTitle, @NonNull final Throwable error) {
|
|
||||||
String message = getErrorMessage(error);
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(message)) {
|
|
||||||
final String errorClassName = error.getClass().getName();
|
|
||||||
message = getString(R.string.error_occurred_detail, errorClassName);
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast(errorTitle);
|
|
||||||
postErrorResult(getString(errorTitle), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getErrorMessage(final Throwable error) {
|
|
||||||
String message = null;
|
|
||||||
if (error instanceof SubscriptionExtractor.InvalidSourceException) {
|
|
||||||
message = getString(R.string.invalid_source);
|
|
||||||
} else if (error instanceof FileNotFoundException) {
|
|
||||||
message = getString(R.string.invalid_file);
|
|
||||||
} else if (ExceptionUtils.isNetworkRelated(error)) {
|
|
||||||
message = getString(R.string.network_error);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package org.schabi.newpipe.local.subscription.services;
|
|
||||||
|
|
||||||
public interface ImportExportEventListener {
|
|
||||||
/**
|
|
||||||
* Called when the size has been resolved.
|
|
||||||
*
|
|
||||||
* @param size how many items there are to import/export
|
|
||||||
*/
|
|
||||||
void onSizeReceived(int size);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called every time an item has been parsed/resolved.
|
|
||||||
*
|
|
||||||
* @param itemName the name of the subscription item
|
|
||||||
*/
|
|
||||||
void onItemCompleted(String itemName);
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
|
||||||
* ImportExportJsonHelper.java is part of NewPipe
|
|
||||||
*
|
|
||||||
* License: GPL-3.0+
|
|
||||||
* This program 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.
|
|
||||||
*
|
|
||||||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.local.subscription.services;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonAppendableWriter;
|
|
||||||
import com.grack.nanojson.JsonArray;
|
|
||||||
import com.grack.nanojson.JsonObject;
|
|
||||||
import com.grack.nanojson.JsonParser;
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException;
|
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
|
|
||||||
* of being able to transfer subscriptions to any device.
|
|
||||||
*/
|
|
||||||
public final class ImportExportJsonHelper {
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Json implementation
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private static final String JSON_APP_VERSION_KEY = "app_version";
|
|
||||||
private static final String JSON_APP_VERSION_INT_KEY = "app_version_int";
|
|
||||||
|
|
||||||
private static final String JSON_SUBSCRIPTIONS_ARRAY_KEY = "subscriptions";
|
|
||||||
|
|
||||||
private static final String JSON_SERVICE_ID_KEY = "service_id";
|
|
||||||
private static final String JSON_URL_KEY = "url";
|
|
||||||
private static final String JSON_NAME_KEY = "name";
|
|
||||||
|
|
||||||
private ImportExportJsonHelper() { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a JSON source through the input stream.
|
|
||||||
*
|
|
||||||
* @param in the input stream (e.g. a file)
|
|
||||||
* @param eventListener listener for the events generated
|
|
||||||
* @return the parsed subscription items
|
|
||||||
*/
|
|
||||||
public static List<SubscriptionItem> readFrom(
|
|
||||||
final InputStream in, @Nullable final ImportExportEventListener eventListener)
|
|
||||||
throws InvalidSourceException {
|
|
||||||
if (in == null) {
|
|
||||||
throw new InvalidSourceException("input is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<SubscriptionItem> channels = new ArrayList<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final JsonObject parentObject = JsonParser.object().from(in);
|
|
||||||
|
|
||||||
if (!parentObject.has(JSON_SUBSCRIPTIONS_ARRAY_KEY)) {
|
|
||||||
throw new InvalidSourceException("Channels array is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
final JsonArray channelsArray = parentObject.getArray(JSON_SUBSCRIPTIONS_ARRAY_KEY);
|
|
||||||
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onSizeReceived(channelsArray.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Object o : channelsArray) {
|
|
||||||
if (o instanceof JsonObject) {
|
|
||||||
final JsonObject itemObject = (JsonObject) o;
|
|
||||||
final int serviceId = itemObject.getInt(JSON_SERVICE_ID_KEY, 0);
|
|
||||||
final String url = itemObject.getString(JSON_URL_KEY);
|
|
||||||
final String name = itemObject.getString(JSON_NAME_KEY);
|
|
||||||
|
|
||||||
if (url != null && name != null && !url.isEmpty() && !name.isEmpty()) {
|
|
||||||
channels.add(new SubscriptionItem(serviceId, url, name));
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onItemCompleted(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
throw new InvalidSourceException("Couldn't parse json", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the subscriptions items list as JSON to the output.
|
|
||||||
*
|
|
||||||
* @param items the list of subscriptions items
|
|
||||||
* @param out the output stream (e.g. a file)
|
|
||||||
* @param eventListener listener for the events generated
|
|
||||||
*/
|
|
||||||
public static void writeTo(final List<SubscriptionItem> items, final OutputStream out,
|
|
||||||
@Nullable final ImportExportEventListener eventListener) {
|
|
||||||
final JsonAppendableWriter writer = JsonWriter.on(out);
|
|
||||||
writeTo(items, writer, eventListener);
|
|
||||||
writer.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see #writeTo(List, OutputStream, ImportExportEventListener)
|
|
||||||
* @param items the list of subscriptions items
|
|
||||||
* @param writer the output {@link JsonAppendableWriter}
|
|
||||||
* @param eventListener listener for the events generated
|
|
||||||
*/
|
|
||||||
public static void writeTo(final List<SubscriptionItem> items,
|
|
||||||
final JsonAppendableWriter writer,
|
|
||||||
@Nullable final ImportExportEventListener eventListener) {
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onSizeReceived(items.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.object();
|
|
||||||
|
|
||||||
writer.value(JSON_APP_VERSION_KEY, BuildConfig.VERSION_NAME);
|
|
||||||
writer.value(JSON_APP_VERSION_INT_KEY, BuildConfig.VERSION_CODE);
|
|
||||||
|
|
||||||
writer.array(JSON_SUBSCRIPTIONS_ARRAY_KEY);
|
|
||||||
for (final SubscriptionItem item : items) {
|
|
||||||
writer.object();
|
|
||||||
writer.value(JSON_SERVICE_ID_KEY, item.getServiceId());
|
|
||||||
writer.value(JSON_URL_KEY, item.getUrl());
|
|
||||||
writer.value(JSON_NAME_KEY, item.getName());
|
|
||||||
writer.end();
|
|
||||||
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onItemCompleted(item.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.end();
|
|
||||||
|
|
||||||
writer.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
|
||||||
* SubscriptionsExportService.java is part of NewPipe
|
|
||||||
*
|
|
||||||
* License: GPL-3.0+
|
|
||||||
* This program 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.
|
|
||||||
*
|
|
||||||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.local.subscription.services;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.core.content.IntentCompat;
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
|
||||||
import org.reactivestreams.Subscription;
|
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
|
||||||
import org.schabi.newpipe.streams.io.SharpOutputStream;
|
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.functions.Function;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class SubscriptionsExportService extends BaseImportExportService {
|
|
||||||
public static final String KEY_FILE_PATH = "key_file_path";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link LocalBroadcastManager local broadcast} will be made with this action
|
|
||||||
* when the export is successfully completed.
|
|
||||||
*/
|
|
||||||
public static final String EXPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
|
|
||||||
+ ".services.SubscriptionsExportService.EXPORT_COMPLETE";
|
|
||||||
|
|
||||||
private Subscription subscription;
|
|
||||||
private StoredFileHelper outFile;
|
|
||||||
private OutputStream outputStream;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
||||||
if (intent == null || subscription != null) {
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Uri path = IntentCompat.getParcelableExtra(intent, KEY_FILE_PATH, Uri.class);
|
|
||||||
if (path == null) {
|
|
||||||
stopAndReportError(new IllegalStateException(
|
|
||||||
"Exporting to a file, but the path is null"),
|
|
||||||
"Exporting subscriptions");
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
outFile = new StoredFileHelper(this, path, "application/json");
|
|
||||||
// truncate the file before writing to it, otherwise if the new content is smaller than
|
|
||||||
// the previous file size, the file will retain part of the previous content and be
|
|
||||||
// corrupted
|
|
||||||
outputStream = new SharpOutputStream(outFile.openAndTruncateStream());
|
|
||||||
} catch (final IOException e) {
|
|
||||||
handleError(e);
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
startExport();
|
|
||||||
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int getNotificationId() {
|
|
||||||
return 4567;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getTitle() {
|
|
||||||
return R.string.export_ongoing;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void disposeAll() {
|
|
||||||
super.disposeAll();
|
|
||||||
if (subscription != null) {
|
|
||||||
subscription.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startExport() {
|
|
||||||
showToast(R.string.export_ongoing);
|
|
||||||
|
|
||||||
subscriptionManager.subscriptionTable().getAll().take(1)
|
|
||||||
.map(subscriptionEntities -> {
|
|
||||||
final List<SubscriptionItem> result =
|
|
||||||
new ArrayList<>(subscriptionEntities.size());
|
|
||||||
for (final SubscriptionEntity entity : subscriptionEntities) {
|
|
||||||
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
|
|
||||||
entity.getName()));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
})
|
|
||||||
.map(exportToFile())
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(getSubscriber());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Subscriber<StoredFileHelper> getSubscriber() {
|
|
||||||
return new Subscriber<StoredFileHelper>() {
|
|
||||||
@Override
|
|
||||||
public void onSubscribe(final Subscription s) {
|
|
||||||
subscription = s;
|
|
||||||
s.request(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNext(final StoredFileHelper file) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "startExport() success: file = " + file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(final Throwable error) {
|
|
||||||
Log.e(TAG, "onError() called with: error = [" + error + "]", error);
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
LocalBroadcastManager.getInstance(SubscriptionsExportService.this)
|
|
||||||
.sendBroadcast(new Intent(EXPORT_COMPLETE_ACTION));
|
|
||||||
showToast(R.string.export_complete_toast);
|
|
||||||
stopService();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Function<List<SubscriptionItem>, StoredFileHelper> exportToFile() {
|
|
||||||
return subscriptionItems -> {
|
|
||||||
ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener);
|
|
||||||
return outFile;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void handleError(final Throwable error) {
|
|
||||||
super.handleError(R.string.subscriptions_export_unsuccessful, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
|
||||||
* SubscriptionsImportService.java is part of NewPipe
|
|
||||||
*
|
|
||||||
* License: GPL-3.0+
|
|
||||||
* This program 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.
|
|
||||||
*
|
|
||||||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.local.subscription.services;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.IntentCompat;
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
||||||
|
|
||||||
import org.reactivestreams.Subscriber;
|
|
||||||
import org.reactivestreams.Subscription;
|
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
|
||||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
|
||||||
import org.schabi.newpipe.streams.io.SharpInputStream;
|
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|
||||||
import org.schabi.newpipe.util.Constants;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
|
||||||
import io.reactivex.rxjava3.core.Notification;
|
|
||||||
import io.reactivex.rxjava3.functions.Consumer;
|
|
||||||
import io.reactivex.rxjava3.functions.Function;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class SubscriptionsImportService extends BaseImportExportService {
|
|
||||||
public static final int CHANNEL_URL_MODE = 0;
|
|
||||||
public static final int INPUT_STREAM_MODE = 1;
|
|
||||||
public static final int PREVIOUS_EXPORT_MODE = 2;
|
|
||||||
public static final String KEY_MODE = "key_mode";
|
|
||||||
public static final String KEY_VALUE = "key_value";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link LocalBroadcastManager local broadcast} will be made with this action
|
|
||||||
* when the import is successfully completed.
|
|
||||||
*/
|
|
||||||
public static final String IMPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
|
|
||||||
+ ".services.SubscriptionsImportService.IMPORT_COMPLETE";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How many extractions running in parallel.
|
|
||||||
*/
|
|
||||||
public static final int PARALLEL_EXTRACTIONS = 8;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of items to buffer to mass-insert in the subscriptions table,
|
|
||||||
* this leads to a better performance as we can then use db transactions.
|
|
||||||
*/
|
|
||||||
public static final int BUFFER_COUNT_BEFORE_INSERT = 50;
|
|
||||||
|
|
||||||
private Subscription subscription;
|
|
||||||
private int currentMode;
|
|
||||||
private int currentServiceId;
|
|
||||||
@Nullable
|
|
||||||
private String channelUrl;
|
|
||||||
@Nullable
|
|
||||||
private InputStream inputStream;
|
|
||||||
@Nullable
|
|
||||||
private String inputStreamType;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
||||||
if (intent == null || subscription != null) {
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMode = intent.getIntExtra(KEY_MODE, -1);
|
|
||||||
currentServiceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, Constants.NO_SERVICE_ID);
|
|
||||||
|
|
||||||
if (currentMode == CHANNEL_URL_MODE) {
|
|
||||||
channelUrl = intent.getStringExtra(KEY_VALUE);
|
|
||||||
} else {
|
|
||||||
final Uri uri = IntentCompat.getParcelableExtra(intent, KEY_VALUE, Uri.class);
|
|
||||||
if (uri == null) {
|
|
||||||
stopAndReportError(new IllegalStateException(
|
|
||||||
"Importing from input stream, but file path is null"),
|
|
||||||
"Importing subscriptions");
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final StoredFileHelper fileHelper = new StoredFileHelper(this, uri, DEFAULT_MIME);
|
|
||||||
inputStream = new SharpInputStream(fileHelper.getStream());
|
|
||||||
inputStreamType = fileHelper.getType();
|
|
||||||
|
|
||||||
if (inputStreamType == null || inputStreamType.equals(DEFAULT_MIME)) {
|
|
||||||
// mime type could not be determined, just take file extension
|
|
||||||
final String name = fileHelper.getName();
|
|
||||||
final int pointIndex = name.lastIndexOf('.');
|
|
||||||
if (pointIndex == -1 || pointIndex >= name.length() - 1) {
|
|
||||||
inputStreamType = DEFAULT_MIME; // no extension, will fail in the extractor
|
|
||||||
} else {
|
|
||||||
inputStreamType = name.substring(pointIndex + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (final IOException e) {
|
|
||||||
handleError(e);
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentMode == -1 || currentMode == CHANNEL_URL_MODE && channelUrl == null) {
|
|
||||||
final String errorDescription = "Some important field is null or in illegal state: "
|
|
||||||
+ "currentMode=[" + currentMode + "], "
|
|
||||||
+ "channelUrl=[" + channelUrl + "], "
|
|
||||||
+ "inputStream=[" + inputStream + "]";
|
|
||||||
stopAndReportError(new IllegalStateException(errorDescription),
|
|
||||||
"Importing subscriptions");
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
startImport();
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int getNotificationId() {
|
|
||||||
return 4568;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getTitle() {
|
|
||||||
return R.string.import_ongoing;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void disposeAll() {
|
|
||||||
super.disposeAll();
|
|
||||||
if (subscription != null) {
|
|
||||||
subscription.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Imports
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private void startImport() {
|
|
||||||
showToast(R.string.import_ongoing);
|
|
||||||
|
|
||||||
Flowable<List<SubscriptionItem>> flowable = null;
|
|
||||||
switch (currentMode) {
|
|
||||||
case CHANNEL_URL_MODE:
|
|
||||||
flowable = importFromChannelUrl();
|
|
||||||
break;
|
|
||||||
case INPUT_STREAM_MODE:
|
|
||||||
flowable = importFromInputStream();
|
|
||||||
break;
|
|
||||||
case PREVIOUS_EXPORT_MODE:
|
|
||||||
flowable = importFromPreviousExport();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flowable == null) {
|
|
||||||
final String message = "Flowable given by \"importFrom\" is null "
|
|
||||||
+ "(current mode: " + currentMode + ")";
|
|
||||||
stopAndReportError(new IllegalStateException(message), "Importing subscriptions");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
flowable.doOnNext(subscriptionItems ->
|
|
||||||
eventListener.onSizeReceived(subscriptionItems.size()))
|
|
||||||
.flatMap(Flowable::fromIterable)
|
|
||||||
|
|
||||||
.parallel(PARALLEL_EXTRACTIONS)
|
|
||||||
.runOn(Schedulers.io())
|
|
||||||
.map((Function<SubscriptionItem, Notification<Pair<ChannelInfo,
|
|
||||||
List<ChannelTabInfo>>>>) subscriptionItem -> {
|
|
||||||
try {
|
|
||||||
final ChannelInfo channelInfo = ExtractorHelper
|
|
||||||
.getChannelInfo(subscriptionItem.getServiceId(),
|
|
||||||
subscriptionItem.getUrl(), true)
|
|
||||||
.blockingGet();
|
|
||||||
return Notification.createOnNext(new Pair<>(channelInfo,
|
|
||||||
Collections.singletonList(
|
|
||||||
ExtractorHelper.getChannelTab(
|
|
||||||
subscriptionItem.getServiceId(),
|
|
||||||
channelInfo.getTabs().get(0), true).blockingGet()
|
|
||||||
)));
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
return Notification.createOnError(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sequential()
|
|
||||||
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.doOnNext(getNotificationsConsumer())
|
|
||||||
|
|
||||||
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
|
||||||
.map(upsertBatch())
|
|
||||||
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(getSubscriber());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
|
||||||
return new Subscriber<>() {
|
|
||||||
@Override
|
|
||||||
public void onSubscribe(final Subscription s) {
|
|
||||||
subscription = s;
|
|
||||||
s.request(Long.MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNext(final List<SubscriptionEntity> successfulInserted) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "startImport() " + successfulInserted.size()
|
|
||||||
+ " items successfully inserted into the database");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(final Throwable error) {
|
|
||||||
Log.e(TAG, "Got an error!", error);
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
LocalBroadcastManager.getInstance(SubscriptionsImportService.this)
|
|
||||||
.sendBroadcast(new Intent(IMPORT_COMPLETE_ACTION));
|
|
||||||
showToast(R.string.import_complete_toast);
|
|
||||||
stopService();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Consumer<Notification<Pair<ChannelInfo,
|
|
||||||
List<ChannelTabInfo>>>> getNotificationsConsumer() {
|
|
||||||
return notification -> {
|
|
||||||
if (notification.isOnNext()) {
|
|
||||||
final String name = notification.getValue().first.getName();
|
|
||||||
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
|
||||||
} else if (notification.isOnError()) {
|
|
||||||
final Throwable error = notification.getError();
|
|
||||||
final Throwable cause = error.getCause();
|
|
||||||
if (error instanceof IOException) {
|
|
||||||
throw error;
|
|
||||||
} else if (cause instanceof IOException) {
|
|
||||||
throw cause;
|
|
||||||
} else if (ExceptionUtils.isNetworkRelated(error)) {
|
|
||||||
throw new IOException(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
eventListener.onItemCompleted("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Function<List<Notification<Pair<ChannelInfo, List<ChannelTabInfo>>>>,
|
|
||||||
List<SubscriptionEntity>> upsertBatch() {
|
|
||||||
return notificationList -> {
|
|
||||||
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
|
|
||||||
new ArrayList<>(notificationList.size());
|
|
||||||
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
|
|
||||||
if (n.isOnNext()) {
|
|
||||||
infoList.add(n.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return subscriptionManager.upsertAll(infoList);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Flowable<List<SubscriptionItem>> importFromChannelUrl() {
|
|
||||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
|
||||||
.getSubscriptionExtractor()
|
|
||||||
.fromChannelUrl(channelUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Flowable<List<SubscriptionItem>> importFromInputStream() {
|
|
||||||
Objects.requireNonNull(inputStream);
|
|
||||||
Objects.requireNonNull(inputStreamType);
|
|
||||||
|
|
||||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
|
||||||
.getSubscriptionExtractor()
|
|
||||||
.fromInputStream(inputStream, inputStreamType));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {
|
|
||||||
return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void handleError(@NonNull final Throwable error) {
|
|
||||||
super.handleError(R.string.subscriptions_import_unsuccessful, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
||||||
|
* ImportExportJsonHelper.java is part of NewPipe
|
||||||
|
*
|
||||||
|
* License: GPL-3.0+
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.local.subscription.workers
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import kotlinx.serialization.json.encodeToStream
|
||||||
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
|
||||||
|
* of being able to transfer subscriptions to any device.
|
||||||
|
*/
|
||||||
|
object ImportExportJsonHelper {
|
||||||
|
private val json = Json { encodeDefaults = true }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a JSON source through the input stream.
|
||||||
|
*
|
||||||
|
* @param in the input stream (e.g. a file)
|
||||||
|
* @return the parsed subscription items
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(InvalidSourceException::class)
|
||||||
|
fun readFrom(`in`: InputStream?): List<SubscriptionItem> {
|
||||||
|
if (`in` == null) {
|
||||||
|
throw InvalidSourceException("input is null")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
return json.decodeFromStream<SubscriptionData>(`in`).subscriptions
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
throw InvalidSourceException("Couldn't parse json", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the subscriptions items list as JSON to the output.
|
||||||
|
*
|
||||||
|
* @param items the list of subscriptions items
|
||||||
|
* @param out the output stream (e.g. a file)
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@JvmStatic
|
||||||
|
fun writeTo(
|
||||||
|
items: List<SubscriptionItem>,
|
||||||
|
out: OutputStream
|
||||||
|
) {
|
||||||
|
json.encodeToStream(SubscriptionData(items), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package org.schabi.newpipe.local.subscription.workers
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SubscriptionData(
|
||||||
|
val subscriptions: List<SubscriptionItem>
|
||||||
|
) {
|
||||||
|
@SerialName("app_version")
|
||||||
|
private val appVersion = BuildConfig.VERSION_NAME
|
||||||
|
|
||||||
|
@SerialName("app_version_int")
|
||||||
|
private val appVersionInt = BuildConfig.VERSION_CODE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SubscriptionItem(
|
||||||
|
@SerialName("service_id")
|
||||||
|
val serviceId: Int,
|
||||||
|
val url: String,
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package org.schabi.newpipe.local.subscription.workers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.OutOfQuotaPolicy
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.reactive.awaitFirst
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
|
||||||
|
class SubscriptionExportWorker(
|
||||||
|
appContext: Context,
|
||||||
|
params: WorkerParameters
|
||||||
|
) : CoroutineWorker(appContext, params) {
|
||||||
|
// This is needed for API levels < 31 (Android S).
|
||||||
|
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||||
|
return createForegroundInfo(applicationContext.getString(R.string.export_ongoing))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
return try {
|
||||||
|
val uri = inputData.getString(EXPORT_PATH)!!.toUri()
|
||||||
|
val table = NewPipeDatabase.getInstance(applicationContext).subscriptionDAO()
|
||||||
|
val subscriptions =
|
||||||
|
table.getAll()
|
||||||
|
.awaitFirst()
|
||||||
|
.map { SubscriptionItem(it.serviceId, it.url ?: "", it.name ?: "") }
|
||||||
|
|
||||||
|
val qty = subscriptions.size
|
||||||
|
val title = applicationContext.resources.getQuantityString(R.plurals.export_subscriptions, qty, qty)
|
||||||
|
setForeground(createForegroundInfo(title))
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// Truncate file if it already exists
|
||||||
|
applicationContext.contentResolver.openOutputStream(uri, "wt")?.use {
|
||||||
|
ImportExportJsonHelper.writeTo(subscriptions, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.i(TAG, "Exported $qty subscriptions")
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast
|
||||||
|
.makeText(applicationContext, R.string.export_complete_toast, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.e(TAG, "Error while exporting subscriptions", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast
|
||||||
|
.makeText(applicationContext, R.string.subscriptions_export_unsuccessful, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createForegroundInfo(title: String): ForegroundInfo {
|
||||||
|
val notification =
|
||||||
|
NotificationCompat
|
||||||
|
.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setProgress(-1, -1, true)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.build()
|
||||||
|
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0
|
||||||
|
return ForegroundInfo(NOTIFICATION_ID, notification, serviceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SubscriptionExportWork"
|
||||||
|
private const val NOTIFICATION_ID = 4567
|
||||||
|
private const val NOTIFICATION_CHANNEL_ID = "newpipe"
|
||||||
|
private const val WORK_NAME = "exportSubscriptions"
|
||||||
|
private const val EXPORT_PATH = "exportPath"
|
||||||
|
|
||||||
|
fun schedule(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri
|
||||||
|
) {
|
||||||
|
val data = workDataOf(EXPORT_PATH to uri.toString())
|
||||||
|
val workRequest =
|
||||||
|
OneTimeWorkRequestBuilder<SubscriptionExportWorker>()
|
||||||
|
.setInputData(data)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager
|
||||||
|
.getInstance(context)
|
||||||
|
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package org.schabi.newpipe.local.subscription.workers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.rx3.await
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper
|
||||||
|
|
||||||
|
class SubscriptionImportWorker(
|
||||||
|
appContext: Context,
|
||||||
|
params: WorkerParameters
|
||||||
|
) : CoroutineWorker(appContext, params) {
|
||||||
|
// This is needed for API levels < 31 (Android S).
|
||||||
|
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||||
|
return createForegroundInfo(applicationContext.getString(R.string.import_ongoing), null, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val subscriptions =
|
||||||
|
try {
|
||||||
|
loadSubscriptionsFromInput(SubscriptionImportInput.fromData(inputData))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.e(TAG, "Error while loading subscriptions from path", e)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast
|
||||||
|
.makeText(applicationContext, R.string.subscriptions_import_unsuccessful, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mutex = Mutex()
|
||||||
|
var index = 1
|
||||||
|
val qty = subscriptions.size
|
||||||
|
var title =
|
||||||
|
applicationContext.resources.getQuantityString(R.plurals.load_subscriptions, qty, qty)
|
||||||
|
|
||||||
|
val channelInfoList =
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO.limitedParallelism(PARALLEL_EXTRACTIONS)) {
|
||||||
|
subscriptions
|
||||||
|
.map {
|
||||||
|
async {
|
||||||
|
val channelInfo =
|
||||||
|
ExtractorHelper.getChannelInfo(it.serviceId, it.url, true).await()
|
||||||
|
val channelTab =
|
||||||
|
ExtractorHelper.getChannelTab(it.serviceId, channelInfo.tabs[0], true).await()
|
||||||
|
|
||||||
|
val currentIndex = mutex.withLock { index++ }
|
||||||
|
setForeground(createForegroundInfo(title, channelInfo.name, currentIndex, qty))
|
||||||
|
|
||||||
|
channelInfo to channelTab
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.e(TAG, "Error while loading subscription data", e)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(applicationContext, R.string.subscriptions_import_unsuccessful, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
title = applicationContext.resources.getQuantityString(R.plurals.import_subscriptions, qty, qty)
|
||||||
|
setForeground(createForegroundInfo(title, null, 0, 0))
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
val subscriptionManager = SubscriptionManager(applicationContext)
|
||||||
|
for (chunk in channelInfoList.chunked(BUFFER_COUNT_BEFORE_INSERT)) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
subscriptionManager.upsertAll(chunk)
|
||||||
|
}
|
||||||
|
index += chunk.size
|
||||||
|
setForeground(createForegroundInfo(title, null, index, qty))
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(applicationContext, R.string.import_complete_toast, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadSubscriptionsFromInput(input: SubscriptionImportInput): List<SubscriptionItem> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
when (input) {
|
||||||
|
is SubscriptionImportInput.ChannelUrlMode ->
|
||||||
|
NewPipe.getService(input.serviceId).subscriptionExtractor
|
||||||
|
.fromChannelUrl(input.url)
|
||||||
|
.map { SubscriptionItem(it.serviceId, it.url, it.name) }
|
||||||
|
|
||||||
|
is SubscriptionImportInput.InputStreamMode ->
|
||||||
|
applicationContext.contentResolver.openInputStream(input.url.toUri())?.use {
|
||||||
|
val contentType =
|
||||||
|
MimeTypeMap.getFileExtensionFromUrl(input.url).ifEmpty { DEFAULT_MIME }
|
||||||
|
NewPipe.getService(input.serviceId).subscriptionExtractor
|
||||||
|
.fromInputStream(it, contentType)
|
||||||
|
.map { SubscriptionItem(it.serviceId, it.url, it.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is SubscriptionImportInput.PreviousExportMode ->
|
||||||
|
applicationContext.contentResolver.openInputStream(input.url.toUri())?.use {
|
||||||
|
ImportExportJsonHelper.readFrom(it)
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createForegroundInfo(
|
||||||
|
title: String,
|
||||||
|
text: String?,
|
||||||
|
currentProgress: Int,
|
||||||
|
maxProgress: Int
|
||||||
|
): ForegroundInfo {
|
||||||
|
val notification =
|
||||||
|
NotificationCompat
|
||||||
|
.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setProgress(maxProgress, currentProgress, currentProgress == 0)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.addAction(
|
||||||
|
R.drawable.ic_close,
|
||||||
|
applicationContext.getString(R.string.cancel),
|
||||||
|
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
|
||||||
|
).apply {
|
||||||
|
if (currentProgress > 0 && maxProgress > 0) {
|
||||||
|
val progressText = "$currentProgress/$maxProgress"
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
setSubText(progressText)
|
||||||
|
} else {
|
||||||
|
setContentInfo(progressText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0
|
||||||
|
|
||||||
|
return ForegroundInfo(NOTIFICATION_ID, notification, serviceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Log tag length is limited to 23 characters on API levels < 24.
|
||||||
|
private const val TAG = "SubscriptionImport"
|
||||||
|
|
||||||
|
private const val NOTIFICATION_ID = 4568
|
||||||
|
private const val NOTIFICATION_CHANNEL_ID = "newpipe"
|
||||||
|
private const val DEFAULT_MIME = "application/octet-stream"
|
||||||
|
private const val PARALLEL_EXTRACTIONS = 8
|
||||||
|
private const val BUFFER_COUNT_BEFORE_INSERT = 50
|
||||||
|
|
||||||
|
const val WORK_NAME = "SubscriptionImportWorker"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SubscriptionImportInput : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data class ChannelUrlMode(val serviceId: Int, val url: String) : SubscriptionImportInput()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class InputStreamMode(val serviceId: Int, val url: String) : SubscriptionImportInput()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class PreviousExportMode(val url: String) : SubscriptionImportInput()
|
||||||
|
|
||||||
|
fun toData(): Data {
|
||||||
|
val (mode, serviceId, url) = when (this) {
|
||||||
|
is ChannelUrlMode -> Triple(CHANNEL_URL_MODE, serviceId, url)
|
||||||
|
is InputStreamMode -> Triple(INPUT_STREAM_MODE, serviceId, url)
|
||||||
|
is PreviousExportMode -> Triple(PREVIOUS_EXPORT_MODE, null, url)
|
||||||
|
}
|
||||||
|
return workDataOf("mode" to mode, "service_id" to serviceId, "url" to url)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val CHANNEL_URL_MODE = 0
|
||||||
|
private const val INPUT_STREAM_MODE = 1
|
||||||
|
private const val PREVIOUS_EXPORT_MODE = 2
|
||||||
|
|
||||||
|
fun fromData(data: Data): SubscriptionImportInput {
|
||||||
|
val mode = data.getInt("mode", PREVIOUS_EXPORT_MODE)
|
||||||
|
when (mode) {
|
||||||
|
CHANNEL_URL_MODE -> {
|
||||||
|
val serviceId = data.getInt("service_id", -1)
|
||||||
|
if (serviceId == -1) {
|
||||||
|
throw IllegalArgumentException("No service id provided")
|
||||||
|
}
|
||||||
|
val url = data.getString("url")!!
|
||||||
|
return ChannelUrlMode(serviceId, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
INPUT_STREAM_MODE -> {
|
||||||
|
val serviceId = data.getInt("service_id", -1)
|
||||||
|
if (serviceId == -1) {
|
||||||
|
throw IllegalArgumentException("No service id provided")
|
||||||
|
}
|
||||||
|
val url = data.getString("url")!!
|
||||||
|
return InputStreamMode(serviceId, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
PREVIOUS_EXPORT_MODE -> {
|
||||||
|
val url = data.getString("url")!!
|
||||||
|
return PreviousExportMode(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException("Unknown mode: $mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ import androidx.activity.result.ActivityResultLauncher;
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
@@ -27,6 +26,7 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
|
import org.schabi.newpipe.local.subscription.SubscriptionsImportExportHelper;
|
||||||
import org.schabi.newpipe.settings.export.BackupFileLocator;
|
import org.schabi.newpipe.settings.export.BackupFileLocator;
|
||||||
import org.schabi.newpipe.settings.export.ImportExportManager;
|
import org.schabi.newpipe.settings.export.ImportExportManager;
|
||||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||||
@@ -34,12 +34,10 @@ import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ZipHelper;
|
import org.schabi.newpipe.util.ZipHelper;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@@ -57,18 +55,22 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
|||||||
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
|
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
|
||||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
||||||
this::requestExportPathResult);
|
this::requestExportPathResult);
|
||||||
|
private SubscriptionsImportExportHelper importExportHelper;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(@NonNull final Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
importExportHelper = new SubscriptionsImportExportHelper(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
|
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
|
||||||
@Nullable final String rootKey) {
|
@Nullable final String rootKey) {
|
||||||
final File homeDir = ContextCompat.getDataDir(requireContext());
|
manager = new ImportExportManager(new BackupFileLocator(requireContext()));
|
||||||
Objects.requireNonNull(homeDir);
|
|
||||||
manager = new ImportExportManager(new BackupFileLocator(homeDir));
|
|
||||||
|
|
||||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||||
|
|
||||||
|
|
||||||
addPreferencesFromResourceRegistry();
|
addPreferencesFromResourceRegistry();
|
||||||
|
|
||||||
final Preference importDataPreference = requirePreference(R.string.import_data);
|
final Preference importDataPreference = requirePreference(R.string.import_data);
|
||||||
@@ -123,6 +125,21 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
|||||||
alertDialog.show();
|
alertDialog.show();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final Preference exportSubsPreference =
|
||||||
|
requirePreference(R.string.export_subscriptions_key);
|
||||||
|
exportSubsPreference.setOnPreferenceClickListener(reference -> {
|
||||||
|
importExportHelper.onExportSelected();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final Preference importSubsPreference =
|
||||||
|
requirePreference(R.string.import_subscriptions_key);
|
||||||
|
importSubsPreference.setOnPreferenceClickListener(preference -> {
|
||||||
|
importExportHelper.onImportPreviousSelected();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestExportPathResult(final ActivityResult result) {
|
private void requestExportPathResult(final ActivityResult result) {
|
||||||
@@ -181,9 +198,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!manager.ensureDbDirectoryExists()) {
|
manager.ensureDbDirectoryExists();
|
||||||
throw new IOException("Could not create databases dir");
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace the current database
|
// replace the current database
|
||||||
if (!manager.extractDb(file)) {
|
if (!manager.extractDb(file)) {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package org.schabi.newpipe.settings.export
|
package org.schabi.newpipe.settings.export
|
||||||
|
|
||||||
import java.io.File
|
import android.content.Context
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.div
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Locates specific files of NewPipe based on the home directory of the app.
|
* Locates specific files of NewPipe based on the home directory of the app.
|
||||||
*/
|
*/
|
||||||
class BackupFileLocator(private val homeDir: File) {
|
class BackupFileLocator(context: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
const val FILE_NAME_DB = "newpipe.db"
|
const val FILE_NAME_DB = "newpipe.db"
|
||||||
|
|
||||||
@@ -17,13 +19,8 @@ class BackupFileLocator(private val homeDir: File) {
|
|||||||
const val FILE_NAME_JSON_PREFS = "preferences.json"
|
const val FILE_NAME_JSON_PREFS = "preferences.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
val dbDir by lazy { File(homeDir, "/databases") }
|
val db: Path = context.getDatabasePath(FILE_NAME_DB).toPath()
|
||||||
|
val dbJournal: Path = db.resolveSibling("$FILE_NAME_DB-journal")
|
||||||
val db by lazy { File(dbDir, FILE_NAME_DB) }
|
val dbShm: Path = db.resolveSibling("$FILE_NAME_DB-shm")
|
||||||
|
val dbWal: Path = db.resolveSibling("$FILE_NAME_DB-wal")
|
||||||
val dbJournal by lazy { File(dbDir, "$FILE_NAME_DB-journal") }
|
|
||||||
|
|
||||||
val dbShm by lazy { File(dbDir, "$FILE_NAME_DB-shm") }
|
|
||||||
|
|
||||||
val dbWal by lazy { File(dbDir, "$FILE_NAME_DB-wal") }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import java.io.FileNotFoundException
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.ObjectOutputStream
|
import java.io.ObjectOutputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
import kotlin.io.path.createParentDirectories
|
||||||
|
import kotlin.io.path.deleteIfExists
|
||||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
import org.schabi.newpipe.util.ZipHelper
|
import org.schabi.newpipe.util.ZipHelper
|
||||||
@@ -28,11 +30,8 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
|
|||||||
// previous file size, the file will retain part of the previous content and be corrupted
|
// previous file size, the file will retain part of the previous content and be corrupted
|
||||||
ZipOutputStream(SharpOutputStream(file.openAndTruncateStream()).buffered()).use { outZip ->
|
ZipOutputStream(SharpOutputStream(file.openAndTruncateStream()).buffered()).use { outZip ->
|
||||||
// add the database
|
// add the database
|
||||||
ZipHelper.addFileToZip(
|
val name = BackupFileLocator.FILE_NAME_DB
|
||||||
outZip,
|
ZipHelper.addFileToZip(outZip, name, fileLocator.db)
|
||||||
BackupFileLocator.FILE_NAME_DB,
|
|
||||||
fileLocator.db.path
|
|
||||||
)
|
|
||||||
|
|
||||||
// add the legacy vulnerable serialized preferences (will be removed in the future)
|
// add the legacy vulnerable serialized preferences (will be removed in the future)
|
||||||
ZipHelper.addFileToZip(
|
ZipHelper.addFileToZip(
|
||||||
@@ -61,11 +60,10 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to create database directory if it does not exist.
|
* Tries to create database directory if it does not exist.
|
||||||
*
|
|
||||||
* @return Whether the directory exists afterwards.
|
|
||||||
*/
|
*/
|
||||||
fun ensureDbDirectoryExists(): Boolean {
|
@Throws(IOException::class)
|
||||||
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
|
fun ensureDbDirectoryExists() {
|
||||||
|
fileLocator.db.createParentDirectories()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,16 +73,13 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
|
|||||||
* @return true if the database was successfully extracted, false otherwise
|
* @return true if the database was successfully extracted, false otherwise
|
||||||
*/
|
*/
|
||||||
fun extractDb(file: StoredFileHelper): Boolean {
|
fun extractDb(file: StoredFileHelper): Boolean {
|
||||||
val success = ZipHelper.extractFileFromZip(
|
val name = BackupFileLocator.FILE_NAME_DB
|
||||||
file,
|
val success = ZipHelper.extractFileFromZip(file, name, fileLocator.db)
|
||||||
BackupFileLocator.FILE_NAME_DB,
|
|
||||||
fileLocator.db.path
|
|
||||||
)
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
fileLocator.dbJournal.delete()
|
fileLocator.dbJournal.deleteIfExists()
|
||||||
fileLocator.dbWal.delete()
|
fileLocator.dbWal.deleteIfExists()
|
||||||
fileLocator.dbShm.delete()
|
fileLocator.dbShm.deleteIfExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
@@ -37,9 +37,6 @@ import java.util.zip.ZipOutputStream;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public final class ZipHelper {
|
public final class ZipHelper {
|
||||||
|
|
||||||
private static final int BUFFER_SIZE = 2048;
|
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface InputStreamConsumer {
|
public interface InputStreamConsumer {
|
||||||
void acceptStream(InputStream inputStream) throws IOException;
|
void acceptStream(InputStream inputStream) throws IOException;
|
||||||
@@ -55,17 +52,17 @@ public final class ZipHelper {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function helps to create zip files. Caution this will overwrite the original file.
|
* This function helps to create zip files. Caution, this will overwrite the original file.
|
||||||
*
|
*
|
||||||
* @param outZip the ZipOutputStream where the data should be stored in
|
* @param outZip the ZipOutputStream where the data should be stored in
|
||||||
* @param nameInZip the path of the file inside the zip
|
* @param nameInZip the path of the file inside the zip
|
||||||
* @param fileOnDisk the path of the file on the disk that should be added to zip
|
* @param path the path of the file on the disk that should be added to zip
|
||||||
*/
|
*/
|
||||||
public static void addFileToZip(final ZipOutputStream outZip,
|
public static void addFileToZip(final ZipOutputStream outZip,
|
||||||
final String nameInZip,
|
final String nameInZip,
|
||||||
final String fileOnDisk) throws IOException {
|
final Path path) throws IOException {
|
||||||
try (FileInputStream fi = new FileInputStream(fileOnDisk)) {
|
try (var inputStream = Files.newInputStream(path)) {
|
||||||
addFileToZip(outZip, nameInZip, fi);
|
addFileToZip(outZip, nameInZip, inputStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +77,13 @@ public final class ZipHelper {
|
|||||||
final String nameInZip,
|
final String nameInZip,
|
||||||
final OutputStreamConsumer streamConsumer) throws IOException {
|
final OutputStreamConsumer streamConsumer) throws IOException {
|
||||||
final byte[] bytes;
|
final byte[] bytes;
|
||||||
try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream()) {
|
try (var byteOutput = new ByteArrayOutputStream()) {
|
||||||
streamConsumer.acceptStream(byteOutput);
|
streamConsumer.acceptStream(byteOutput);
|
||||||
bytes = byteOutput.toByteArray();
|
bytes = byteOutput.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
try (ByteArrayInputStream byteInput = new ByteArrayInputStream(bytes)) {
|
try (var byteInput = new ByteArrayInputStream(bytes)) {
|
||||||
ZipHelper.addFileToZip(outZip, nameInZip, byteInput);
|
addFileToZip(outZip, nameInZip, byteInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,49 +94,26 @@ public final class ZipHelper {
|
|||||||
* @param nameInZip the path of the file inside the zip
|
* @param nameInZip the path of the file inside the zip
|
||||||
* @param inputStream the content to put inside the file
|
* @param inputStream the content to put inside the file
|
||||||
*/
|
*/
|
||||||
public static void addFileToZip(final ZipOutputStream outZip,
|
private static void addFileToZip(final ZipOutputStream outZip,
|
||||||
final String nameInZip,
|
final String nameInZip,
|
||||||
final InputStream inputStream) throws IOException {
|
final InputStream inputStream) throws IOException {
|
||||||
final byte[] data = new byte[BUFFER_SIZE];
|
outZip.putNextEntry(new ZipEntry(nameInZip));
|
||||||
try (BufferedInputStream bufferedInputStream =
|
inputStream.transferTo(outZip);
|
||||||
new BufferedInputStream(inputStream, BUFFER_SIZE)) {
|
|
||||||
final ZipEntry entry = new ZipEntry(nameInZip);
|
|
||||||
outZip.putNextEntry(entry);
|
|
||||||
int count;
|
|
||||||
while ((count = bufferedInputStream.read(data, 0, BUFFER_SIZE)) != -1) {
|
|
||||||
outZip.write(data, 0, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will extract data from ZipInputStream. Caution this will overwrite the original file.
|
* This will extract data from ZipInputStream. Caution, this will overwrite the original file.
|
||||||
*
|
*
|
||||||
* @param zipFile the zip file to extract from
|
* @param zipFile the zip file to extract from
|
||||||
* @param nameInZip the path of the file inside the zip
|
* @param nameInZip the path of the file inside the zip
|
||||||
* @param fileOnDisk the path of the file on the disk where the data should be extracted to
|
* @param path the path of the file on the disk where the data should be extracted to
|
||||||
* @return will return true if the file was found within the zip file
|
* @return will return true if the file was found within the zip file
|
||||||
*/
|
*/
|
||||||
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
|
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
|
||||||
final String nameInZip,
|
final String nameInZip,
|
||||||
final String fileOnDisk) throws IOException {
|
final Path path) throws IOException {
|
||||||
return extractFileFromZip(zipFile, nameInZip, input -> {
|
return extractFileFromZip(zipFile, nameInZip, input ->
|
||||||
// delete old file first
|
Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING));
|
||||||
final File oldFile = new File(fileOnDisk);
|
|
||||||
if (oldFile.exists()) {
|
|
||||||
if (!oldFile.delete()) {
|
|
||||||
throw new IOException("Could not delete " + fileOnDisk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] data = new byte[BUFFER_SIZE];
|
|
||||||
try (FileOutputStream outFile = new FileOutputStream(fileOnDisk)) {
|
|
||||||
int count;
|
|
||||||
while ((count = input.read(data)) != -1) {
|
|
||||||
outFile.write(data, 0, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -186,7 +186,15 @@ object ImageStrategy {
|
|||||||
fun dbUrlToImageList(url: String?): List<Image> {
|
fun dbUrlToImageList(url: String?): List<Image> {
|
||||||
return when (url) {
|
return when (url) {
|
||||||
null -> listOf()
|
null -> listOf()
|
||||||
else -> listOf(Image(url, -1, -1, ResolutionLevel.UNKNOWN))
|
|
||||||
|
else -> listOf(
|
||||||
|
Image(
|
||||||
|
url,
|
||||||
|
Image.HEIGHT_UNKNOWN,
|
||||||
|
Image.WIDTH_UNKNOWN,
|
||||||
|
ResolutionLevel.UNKNOWN
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,6 +413,8 @@
|
|||||||
<string name="import_export_data_path">import_export_data_path</string>
|
<string name="import_export_data_path">import_export_data_path</string>
|
||||||
<string name="import_data">import_data</string>
|
<string name="import_data">import_data</string>
|
||||||
<string name="export_data">export_data</string>
|
<string name="export_data">export_data</string>
|
||||||
|
<string name="import_subscriptions_key">import_subscriptions_key</string>
|
||||||
|
<string name="export_subscriptions_key">export_subscriptions_key</string>
|
||||||
|
|
||||||
<string name="clear_cookie_key">clear_cookie</string>
|
<string name="clear_cookie_key">clear_cookie</string>
|
||||||
|
|
||||||
|
|||||||
@@ -501,6 +501,18 @@
|
|||||||
<string name="show_error_snackbar">Show an error snackbar</string>
|
<string name="show_error_snackbar">Show an error snackbar</string>
|
||||||
<string name="create_error_notification">Create an error notification</string>
|
<string name="create_error_notification">Create an error notification</string>
|
||||||
<!-- Subscriptions import/export -->
|
<!-- Subscriptions import/export -->
|
||||||
|
<plurals name="export_subscriptions">
|
||||||
|
<item quantity="one">Exporting %d subscription…</item>
|
||||||
|
<item quantity="other">Exporting %d subscriptions…</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="load_subscriptions">
|
||||||
|
<item quantity="one">Loading %d subscription…</item>
|
||||||
|
<item quantity="other">Loading %d subscriptions…</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="import_subscriptions">
|
||||||
|
<item quantity="one">Importing %d subscription…</item>
|
||||||
|
<item quantity="other">Importing %d subscriptions…</item>
|
||||||
|
</plurals>
|
||||||
<string name="import_title">Import</string>
|
<string name="import_title">Import</string>
|
||||||
<string name="import_from">Import from</string>
|
<string name="import_from">Import from</string>
|
||||||
<string name="export_to">Export to</string>
|
<string name="export_to">Export to</string>
|
||||||
@@ -508,6 +520,11 @@
|
|||||||
<string name="export_ongoing">Exporting…</string>
|
<string name="export_ongoing">Exporting…</string>
|
||||||
<string name="import_file_title">Import file</string>
|
<string name="import_file_title">Import file</string>
|
||||||
<string name="previous_export">Previous export</string>
|
<string name="previous_export">Previous export</string>
|
||||||
|
<string name="import_subscriptions_title">Import subscriptions"</string>
|
||||||
|
<string name="export_subscriptions_title">Export subscriptions</string>
|
||||||
|
<string name="import_subscriptions_summary">Import subscriptions from a previous .json export"</string>
|
||||||
|
<string name="export_subscriptions_summary">Export your subscriptions to a .json file</string>
|
||||||
|
<string name="import_from_previous_export">Import from previous export</string>
|
||||||
<string name="subscriptions_import_unsuccessful">Could not import subscriptions</string>
|
<string name="subscriptions_import_unsuccessful">Could not import subscriptions</string>
|
||||||
<string name="subscriptions_export_unsuccessful">Could not export subscriptions</string>
|
<string name="subscriptions_export_unsuccessful">Could not export subscriptions</string>
|
||||||
<string name="import_youtube_instructions">Import YouTube subscriptions from Google takeout:
|
<string name="import_youtube_instructions">Import YouTube subscriptions from Google takeout:
|
||||||
|
|||||||
@@ -22,4 +22,18 @@
|
|||||||
android:summary="@string/reset_settings_summary"
|
android:summary="@string/reset_settings_summary"
|
||||||
app:singleLineTitle="false"
|
app:singleLineTitle="false"
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/export_subscriptions_key"
|
||||||
|
android:title="@string/export_subscriptions_title"
|
||||||
|
android:summary="@string/export_subscriptions_summary"
|
||||||
|
app:singleLineTitle="false"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/import_subscriptions_key"
|
||||||
|
android:title="@string/import_subscriptions_title"
|
||||||
|
android:summary="@string/import_subscriptions_summary"
|
||||||
|
app:singleLineTitle="false"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
@@ -5,11 +5,11 @@ import static org.junit.Assert.fail;
|
|||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
import org.schabi.newpipe.local.subscription.workers.ImportExportJsonHelper;
|
||||||
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionItem;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -23,26 +23,22 @@ public class ImportExportJsonHelperTest {
|
|||||||
final String emptySource =
|
final String emptySource =
|
||||||
"{\"app_version\":\"0.11.6\",\"app_version_int\": 47,\"subscriptions\":[]}";
|
"{\"app_version\":\"0.11.6\",\"app_version_int\": 47,\"subscriptions\":[]}";
|
||||||
|
|
||||||
final List<SubscriptionItem> items = ImportExportJsonHelper.readFrom(
|
final var items = ImportExportJsonHelper.readFrom(
|
||||||
new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8)), null);
|
new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8)));
|
||||||
assertTrue(items.isEmpty());
|
assertTrue(items.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInvalidSource() {
|
public void testInvalidSource() {
|
||||||
final List<String> invalidList = Arrays.asList(
|
final var invalidList = Arrays.asList("{}", "", null, "gibberish");
|
||||||
"{}",
|
|
||||||
"",
|
|
||||||
null,
|
|
||||||
"gibberish");
|
|
||||||
|
|
||||||
for (final String invalidContent : invalidList) {
|
for (final String invalidContent : invalidList) {
|
||||||
try {
|
try {
|
||||||
if (invalidContent != null) {
|
if (invalidContent != null) {
|
||||||
final byte[] bytes = invalidContent.getBytes(StandardCharsets.UTF_8);
|
final byte[] bytes = invalidContent.getBytes(StandardCharsets.UTF_8);
|
||||||
ImportExportJsonHelper.readFrom((new ByteArrayInputStream(bytes)), null);
|
ImportExportJsonHelper.readFrom(new ByteArrayInputStream(bytes));
|
||||||
} else {
|
} else {
|
||||||
ImportExportJsonHelper.readFrom(null, null);
|
ImportExportJsonHelper.readFrom(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
fail("didn't throw exception");
|
fail("didn't throw exception");
|
||||||
@@ -58,38 +54,24 @@ public class ImportExportJsonHelperTest {
|
|||||||
@Test
|
@Test
|
||||||
public void ultimateTest() throws Exception {
|
public void ultimateTest() throws Exception {
|
||||||
// Read from file
|
// Read from file
|
||||||
final List<SubscriptionItem> itemsFromFile = readFromFile();
|
final var itemsFromFile = readFromFile();
|
||||||
|
|
||||||
// Test writing to an output
|
// Test writing to an output
|
||||||
final String jsonOut = testWriteTo(itemsFromFile);
|
final String jsonOut = testWriteTo(itemsFromFile);
|
||||||
|
|
||||||
// Read again
|
// Read again
|
||||||
final List<SubscriptionItem> itemsSecondRead = readFromWriteTo(jsonOut);
|
final var itemsSecondRead = readFromWriteTo(jsonOut);
|
||||||
|
|
||||||
// Check if both lists have the exact same items
|
// Check if both lists have the exact same items
|
||||||
if (itemsFromFile.size() != itemsSecondRead.size()) {
|
if (!itemsFromFile.equals(itemsSecondRead)) {
|
||||||
fail("The list of items were different from each other");
|
fail("The list of items were different from each other");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < itemsFromFile.size(); i++) {
|
|
||||||
final SubscriptionItem item1 = itemsFromFile.get(i);
|
|
||||||
final SubscriptionItem item2 = itemsSecondRead.get(i);
|
|
||||||
|
|
||||||
final boolean equals = item1.getServiceId() == item2.getServiceId()
|
|
||||||
&& item1.getUrl().equals(item2.getUrl())
|
|
||||||
&& item1.getName().equals(item2.getName());
|
|
||||||
|
|
||||||
if (!equals) {
|
|
||||||
fail("The list of items were different from each other");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SubscriptionItem> readFromFile() throws Exception {
|
private List<SubscriptionItem> readFromFile() throws Exception {
|
||||||
final InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
|
final var inputStream = getClass().getClassLoader()
|
||||||
"import_export_test.json");
|
.getResourceAsStream("import_export_test.json");
|
||||||
final List<SubscriptionItem> itemsFromFile = ImportExportJsonHelper.readFrom(
|
final var itemsFromFile = ImportExportJsonHelper.readFrom(inputStream);
|
||||||
inputStream, null);
|
|
||||||
|
|
||||||
if (itemsFromFile.isEmpty()) {
|
if (itemsFromFile.isEmpty()) {
|
||||||
fail("ImportExportJsonHelper.readFrom(input) returned a null or empty list");
|
fail("ImportExportJsonHelper.readFrom(input) returned a null or empty list");
|
||||||
@@ -98,10 +80,10 @@ public class ImportExportJsonHelperTest {
|
|||||||
return itemsFromFile;
|
return itemsFromFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String testWriteTo(final List<SubscriptionItem> itemsFromFile) throws Exception {
|
private String testWriteTo(final List<SubscriptionItem> itemsFromFile) {
|
||||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
final var out = new ByteArrayOutputStream();
|
||||||
ImportExportJsonHelper.writeTo(itemsFromFile, out, null);
|
ImportExportJsonHelper.writeTo(itemsFromFile, out);
|
||||||
final String jsonOut = out.toString("UTF-8");
|
final String jsonOut = out.toString(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
if (jsonOut.isEmpty()) {
|
if (jsonOut.isEmpty()) {
|
||||||
fail("JSON returned by writeTo was empty");
|
fail("JSON returned by writeTo was empty");
|
||||||
@@ -111,10 +93,8 @@ public class ImportExportJsonHelperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<SubscriptionItem> readFromWriteTo(final String jsonOut) throws Exception {
|
private List<SubscriptionItem> readFromWriteTo(final String jsonOut) throws Exception {
|
||||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(
|
final var inputStream = new ByteArrayInputStream(jsonOut.getBytes(StandardCharsets.UTF_8));
|
||||||
jsonOut.getBytes(StandardCharsets.UTF_8));
|
final var secondReadItems = ImportExportJsonHelper.readFrom(inputStream);
|
||||||
final List<SubscriptionItem> secondReadItems = ImportExportJsonHelper.readFrom(
|
|
||||||
inputStream, null);
|
|
||||||
|
|
||||||
if (secondReadItems.isEmpty()) {
|
if (secondReadItems.isEmpty()) {
|
||||||
fail("second call to readFrom returned an empty list");
|
fail("second call to readFrom returned an empty list");
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package org.schabi.newpipe.settings
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.file.Files
|
import kotlin.io.path.createTempFile
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.fileSize
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
@@ -47,10 +49,10 @@ class ImportAllCombinationsTest {
|
|||||||
BackupFileLocator::class.java,
|
BackupFileLocator::class.java,
|
||||||
Mockito.withSettings().stubOnly()
|
Mockito.withSettings().stubOnly()
|
||||||
)
|
)
|
||||||
val db = File.createTempFile("newpipe_", "")
|
val db = createTempFile("newpipe_", "")
|
||||||
val dbJournal = File.createTempFile("newpipe_", "")
|
val dbJournal = createTempFile("newpipe_", "")
|
||||||
val dbWal = File.createTempFile("newpipe_", "")
|
val dbWal = createTempFile("newpipe_", "")
|
||||||
val dbShm = File.createTempFile("newpipe_", "")
|
val dbShm = createTempFile("newpipe_", "")
|
||||||
Mockito.`when`(fileLocator.db).thenReturn(db)
|
Mockito.`when`(fileLocator.db).thenReturn(db)
|
||||||
Mockito.`when`(fileLocator.dbJournal).thenReturn(dbJournal)
|
Mockito.`when`(fileLocator.dbJournal).thenReturn(dbJournal)
|
||||||
Mockito.`when`(fileLocator.dbShm).thenReturn(dbShm)
|
Mockito.`when`(fileLocator.dbShm).thenReturn(dbShm)
|
||||||
@@ -62,7 +64,7 @@ class ImportAllCombinationsTest {
|
|||||||
Assert.assertFalse(dbJournal.exists())
|
Assert.assertFalse(dbJournal.exists())
|
||||||
Assert.assertFalse(dbWal.exists())
|
Assert.assertFalse(dbWal.exists())
|
||||||
Assert.assertFalse(dbShm.exists())
|
Assert.assertFalse(dbShm.exists())
|
||||||
Assert.assertTrue("database file size is zero", Files.size(db.toPath()) > 0)
|
Assert.assertTrue("database file size is zero", db.fileSize() > 0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runTest {
|
runTest {
|
||||||
@@ -70,7 +72,7 @@ class ImportAllCombinationsTest {
|
|||||||
Assert.assertTrue(dbJournal.exists())
|
Assert.assertTrue(dbJournal.exists())
|
||||||
Assert.assertTrue(dbWal.exists())
|
Assert.assertTrue(dbWal.exists())
|
||||||
Assert.assertTrue(dbShm.exists())
|
Assert.assertTrue(dbShm.exists())
|
||||||
Assert.assertEquals(0, Files.size(db.toPath()))
|
Assert.assertEquals(0, db.fileSize())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,15 @@ import android.content.SharedPreferences
|
|||||||
import com.grack.nanojson.JsonParser
|
import com.grack.nanojson.JsonParser
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.ObjectInputStream
|
import java.io.ObjectInputStream
|
||||||
import java.nio.file.Files
|
import java.nio.file.Paths
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
import kotlin.io.path.createTempDirectory
|
||||||
|
import kotlin.io.path.createTempFile
|
||||||
|
import kotlin.io.path.deleteIfExists
|
||||||
|
import kotlin.io.path.div
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.fileSize
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertThrows
|
import org.junit.Assert.assertThrows
|
||||||
@@ -46,7 +53,7 @@ class ImportExportManagerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `The settings must be exported successfully in the correct format`() {
|
fun `The settings must be exported successfully in the correct format`() {
|
||||||
val db = File(classloader.getResource("settings/newpipe.db")!!.file)
|
val db = Paths.get(classloader.getResource("settings/newpipe.db")!!.toURI())
|
||||||
`when`(fileLocator.db).thenReturn(db)
|
`when`(fileLocator.db).thenReturn(db)
|
||||||
|
|
||||||
val expectedPreferences = mapOf("such pref" to "much wow")
|
val expectedPreferences = mapOf("such pref" to "much wow")
|
||||||
@@ -81,29 +88,29 @@ class ImportExportManagerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Ensuring db directory existence must work`() {
|
fun `Ensuring db directory existence must work`() {
|
||||||
val dir = Files.createTempDirectory("newpipe_").toFile()
|
val path = createTempDirectory("newpipe_") / BackupFileLocator.FILE_NAME_DB
|
||||||
Assume.assumeTrue(dir.delete())
|
Assume.assumeTrue(path.parent.deleteIfExists())
|
||||||
`when`(fileLocator.dbDir).thenReturn(dir)
|
`when`(fileLocator.db).thenReturn(path)
|
||||||
|
|
||||||
ImportExportManager(fileLocator).ensureDbDirectoryExists()
|
ImportExportManager(fileLocator).ensureDbDirectoryExists()
|
||||||
assertTrue(dir.exists())
|
assertTrue(path.parent.exists())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Ensuring db directory existence must work when the directory already exists`() {
|
fun `Ensuring db directory existence must work when the directory already exists`() {
|
||||||
val dir = Files.createTempDirectory("newpipe_").toFile()
|
val path = createTempDirectory("newpipe_") / BackupFileLocator.FILE_NAME_DB
|
||||||
`when`(fileLocator.dbDir).thenReturn(dir)
|
`when`(fileLocator.db).thenReturn(path)
|
||||||
|
|
||||||
ImportExportManager(fileLocator).ensureDbDirectoryExists()
|
ImportExportManager(fileLocator).ensureDbDirectoryExists()
|
||||||
assertTrue(dir.exists())
|
assertTrue(path.parent.exists())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `The database must be extracted from the zip file`() {
|
fun `The database must be extracted from the zip file`() {
|
||||||
val db = File.createTempFile("newpipe_", "")
|
val db = createTempFile("newpipe_", "")
|
||||||
val dbJournal = File.createTempFile("newpipe_", "")
|
val dbJournal = createTempFile("newpipe_", "")
|
||||||
val dbWal = File.createTempFile("newpipe_", "")
|
val dbWal = createTempFile("newpipe_", "")
|
||||||
val dbShm = File.createTempFile("newpipe_", "")
|
val dbShm = createTempFile("newpipe_", "")
|
||||||
`when`(fileLocator.db).thenReturn(db)
|
`when`(fileLocator.db).thenReturn(db)
|
||||||
`when`(fileLocator.dbJournal).thenReturn(dbJournal)
|
`when`(fileLocator.dbJournal).thenReturn(dbJournal)
|
||||||
`when`(fileLocator.dbShm).thenReturn(dbShm)
|
`when`(fileLocator.dbShm).thenReturn(dbShm)
|
||||||
@@ -117,15 +124,15 @@ class ImportExportManagerTest {
|
|||||||
assertFalse(dbJournal.exists())
|
assertFalse(dbJournal.exists())
|
||||||
assertFalse(dbWal.exists())
|
assertFalse(dbWal.exists())
|
||||||
assertFalse(dbShm.exists())
|
assertFalse(dbShm.exists())
|
||||||
assertTrue("database file size is zero", Files.size(db.toPath()) > 0)
|
assertTrue("database file size is zero", db.fileSize() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Extracting the database from an empty zip must not work`() {
|
fun `Extracting the database from an empty zip must not work`() {
|
||||||
val db = File.createTempFile("newpipe_", "")
|
val db = createTempFile("newpipe_", "")
|
||||||
val dbJournal = File.createTempFile("newpipe_", "")
|
val dbJournal = createTempFile("newpipe_", "")
|
||||||
val dbWal = File.createTempFile("newpipe_", "")
|
val dbWal = createTempFile("newpipe_", "")
|
||||||
val dbShm = File.createTempFile("newpipe_", "")
|
val dbShm = createTempFile("newpipe_", "")
|
||||||
`when`(fileLocator.db).thenReturn(db)
|
`when`(fileLocator.db).thenReturn(db)
|
||||||
|
|
||||||
val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!)
|
val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!)
|
||||||
@@ -136,7 +143,7 @@ class ImportExportManagerTest {
|
|||||||
assertTrue(dbJournal.exists())
|
assertTrue(dbJournal.exists())
|
||||||
assertTrue(dbWal.exists())
|
assertTrue(dbWal.exists())
|
||||||
assertTrue(dbShm.exists())
|
assertTrue(dbShm.exists())
|
||||||
assertEquals(0, Files.size(db.toPath()))
|
assertEquals(0, db.fileSize())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ plugins {
|
|||||||
alias(libs.plugins.jetbrains.kotlin.kapt) apply false
|
alias(libs.plugins.jetbrains.kotlin.kapt) apply false
|
||||||
alias(libs.plugins.google.ksp) apply false
|
alias(libs.plugins.google.ksp) apply false
|
||||||
alias(libs.plugins.jetbrains.kotlin.parcelize) apply false
|
alias(libs.plugins.jetbrains.kotlin.parcelize) apply false
|
||||||
|
alias(libs.plugins.jetbrains.kotlinx.serialization) apply false
|
||||||
alias(libs.plugins.sonarqube) apply false
|
alias(libs.plugins.sonarqube) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ autoservice-google = "1.1.1"
|
|||||||
autoservice-zacsweers = "1.2.0"
|
autoservice-zacsweers = "1.2.0"
|
||||||
bridge = "v2.0.2"
|
bridge = "v2.0.2"
|
||||||
cardview = "1.0.0"
|
cardview = "1.0.0"
|
||||||
checkstyle = "13.2.0"
|
checkstyle = "13.3.0"
|
||||||
coil = "3.3.0"
|
coil = "3.4.0"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
core = "1.17.0"
|
core = "1.17.0" # Newer versions require minSdk >= 23
|
||||||
desugar = "2.1.5"
|
desugar = "2.1.5"
|
||||||
documentfile = "1.1.0"
|
documentfile = "1.1.0"
|
||||||
exoplayer = "2.19.1"
|
exoplayer = "2.19.1"
|
||||||
@@ -24,7 +24,9 @@ groupie = "2.10.1"
|
|||||||
jsoup = "1.22.1"
|
jsoup = "1.22.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junit-ext = "1.3.0"
|
junit-ext = "1.3.0"
|
||||||
kotlin = "2.3.10"
|
kotlin = "2.3.20"
|
||||||
|
kotlinx-coroutines-rx3 = "1.10.2"
|
||||||
|
kotlinx-serialization-json = "1.10.0"
|
||||||
ksp = "2.3.6"
|
ksp = "2.3.6"
|
||||||
ktlint = "1.8.0"
|
ktlint = "1.8.0"
|
||||||
leakcanary = "2.14"
|
leakcanary = "2.14"
|
||||||
@@ -33,7 +35,7 @@ localbroadcastmanager = "1.1.0"
|
|||||||
markwon = "4.6.2"
|
markwon = "4.6.2"
|
||||||
material = "1.11.0" # TODO: update to newer version after bug is fixed. See https://github.com/TeamNewPipe/NewPipe/pull/13018
|
material = "1.11.0" # TODO: update to newer version after bug is fixed. See https://github.com/TeamNewPipe/NewPipe/pull/13018
|
||||||
media = "1.7.1"
|
media = "1.7.1"
|
||||||
mockitoCore = "5.21.0"
|
mockitoCore = "5.23.0"
|
||||||
okhttp = "5.3.2"
|
okhttp = "5.3.2"
|
||||||
phoenix = "3.0.0"
|
phoenix = "3.0.0"
|
||||||
preference = "1.2.1"
|
preference = "1.2.1"
|
||||||
@@ -44,7 +46,7 @@ runner = "1.7.0"
|
|||||||
rxandroid = "3.0.2"
|
rxandroid = "3.0.2"
|
||||||
rxbinding = "4.0.0"
|
rxbinding = "4.0.0"
|
||||||
rxjava = "3.1.12"
|
rxjava = "3.1.12"
|
||||||
sonarqube = "7.2.2.6593"
|
sonarqube = "7.2.3.7755"
|
||||||
statesaver = "1.4.1" # TODO: Drop because it is deprecated and incompatible with KSP2
|
statesaver = "1.4.1" # TODO: Drop because it is deprecated and incompatible with KSP2
|
||||||
stetho = "1.6.0"
|
stetho = "1.6.0"
|
||||||
swiperefreshlayout = "1.2.0"
|
swiperefreshlayout = "1.2.0"
|
||||||
@@ -110,6 +112,8 @@ jakewharton-phoenix = { module = "com.jakewharton:process-phoenix", version.ref
|
|||||||
jakewharton-rxbinding = { module = "com.jakewharton.rxbinding4:rxbinding", version.ref = "rxbinding" }
|
jakewharton-rxbinding = { module = "com.jakewharton.rxbinding4:rxbinding", version.ref = "rxbinding" }
|
||||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
junit = { module = "junit:junit", version.ref = "junit" }
|
junit = { module = "junit:junit", version.ref = "junit" }
|
||||||
|
kotlinx-coroutines-rx3 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx3", version.ref = "kotlinx-coroutines-rx3" }
|
||||||
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
||||||
lisawray-groupie-core = { module = "com.github.lisawray.groupie:groupie", version.ref = "groupie" }
|
lisawray-groupie-core = { module = "com.github.lisawray.groupie:groupie", version.ref = "groupie" }
|
||||||
lisawray-groupie-viewbinding = { module = "com.github.lisawray.groupie:groupie-viewbinding", version.ref = "groupie" }
|
lisawray-groupie-viewbinding = { module = "com.github.lisawray.groupie:groupie-viewbinding", version.ref = "groupie" }
|
||||||
livefront-bridge = { module = "com.github.livefront:bridge", version.ref = "bridge" }
|
livefront-bridge = { module = "com.github.livefront:bridge", version.ref = "bridge" }
|
||||||
@@ -136,4 +140,5 @@ google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|||||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
jetbrains-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } # Needed for statesaver
|
jetbrains-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } # Needed for statesaver
|
||||||
jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
jetbrains-kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||||
|
jetbrains-kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
|
distributionSha256Sum=60ea723356d81263e8002fec0fcf9e2b0eee0c0850c7a3d7ab0a63f2ccc601f3
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
2
gradlew
vendored
2
gradlew
vendored
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
Reference in New Issue
Block a user