mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-11-04 09:13:00 +00:00 
			
		
		
		
	Merge pull request #11759 from Isira-Seneviratne/Import-export-worker
Rewrite import and export subscriptions functionality using coroutines
This commit is contained in:
		@@ -9,6 +9,7 @@ plugins {
 | 
			
		||||
    alias libs.plugins.kotlin.compose
 | 
			
		||||
    alias libs.plugins.kotlin.kapt
 | 
			
		||||
    alias libs.plugins.kotlin.parcelize
 | 
			
		||||
    alias libs.plugins.kotlinx.serialization
 | 
			
		||||
    alias libs.plugins.checkstyle
 | 
			
		||||
    alias libs.plugins.sonarqube
 | 
			
		||||
    alias libs.plugins.hilt
 | 
			
		||||
@@ -16,7 +17,7 @@ plugins {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileSdk 34
 | 
			
		||||
    compileSdk 35
 | 
			
		||||
    namespace 'org.schabi.newpipe'
 | 
			
		||||
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
@@ -226,7 +227,6 @@ dependencies {
 | 
			
		||||
    implementation libs.androidx.fragment.compose
 | 
			
		||||
    implementation libs.androidx.lifecycle.livedata
 | 
			
		||||
    implementation libs.androidx.lifecycle.viewmodel
 | 
			
		||||
    implementation libs.androidx.localbroadcastmanager
 | 
			
		||||
    implementation libs.androidx.media
 | 
			
		||||
    implementation libs.androidx.preference
 | 
			
		||||
    implementation libs.androidx.recyclerview
 | 
			
		||||
@@ -319,6 +319,9 @@ dependencies {
 | 
			
		||||
    // Scroll
 | 
			
		||||
    implementation libs.lazycolumnscrollbar
 | 
			
		||||
 | 
			
		||||
    // Kotlinx Serialization
 | 
			
		||||
    implementation libs.kotlinx.serialization.json
 | 
			
		||||
 | 
			
		||||
/** Debugging **/
 | 
			
		||||
    // Memory leak detection
 | 
			
		||||
    debugImplementation libs.leakcanary.object.watcher
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							@@ -34,3 +34,18 @@
 | 
			
		||||
 | 
			
		||||
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
 | 
			
		||||
-keep class org.schabi.newpipe.settings.notifications.** { *; }
 | 
			
		||||
 | 
			
		||||
## 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(...);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
 | 
			
		||||
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
 | 
			
		||||
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
 | 
			
		||||
 | 
			
		||||
    <!-- We need to be able to open links in the browser on API 30+ -->
 | 
			
		||||
@@ -90,8 +91,10 @@
 | 
			
		||||
            android:exported="false"
 | 
			
		||||
            android:label="@string/title_activity_about" />
 | 
			
		||||
 | 
			
		||||
        <service android:name=".local.subscription.services.SubscriptionsImportService" />
 | 
			
		||||
        <service android:name=".local.subscription.services.SubscriptionsExportService" />
 | 
			
		||||
        <service
 | 
			
		||||
            android:name="androidx.work.impl.foreground.SystemForegroundService"
 | 
			
		||||
            android:foregroundServiceType="dataSync"
 | 
			
		||||
            tools:node="merge" />
 | 
			
		||||
        <service android:name=".local.feed.service.FeedLoadService" />
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
 | 
			
		||||
    internal abstract fun silentInsertAllInternal(entities: List<SubscriptionEntity>): List<Long>
 | 
			
		||||
 | 
			
		||||
    @Transaction
 | 
			
		||||
    open fun upsertAll(entities: List<SubscriptionEntity>): List<SubscriptionEntity> {
 | 
			
		||||
    open fun upsertAll(entities: List<SubscriptionEntity>) {
 | 
			
		||||
        val insertUidList = silentInsertAllInternal(entities)
 | 
			
		||||
 | 
			
		||||
        insertUidList.forEachIndexed { index: Int, uidFromInsert: Long ->
 | 
			
		||||
@@ -106,7 +106,5 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
 | 
			
		||||
                update(entity)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return entities
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,47 +3,64 @@ package org.schabi.newpipe.local.subscription;
 | 
			
		||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.core.os.BundleCompat;
 | 
			
		||||
import androidx.fragment.app.DialogFragment;
 | 
			
		||||
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.evernote.android.state.State;
 | 
			
		||||
import com.livefront.bridge.Bridge;
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
    @State
 | 
			
		||||
    protected Intent resultServiceIntent;
 | 
			
		||||
    private static final String INPUT = "input";
 | 
			
		||||
 | 
			
		||||
    public static void show(@NonNull final Fragment fragment,
 | 
			
		||||
                            @NonNull final Intent resultServiceIntent) {
 | 
			
		||||
        final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog();
 | 
			
		||||
        confirmationDialog.setResultServiceIntent(resultServiceIntent);
 | 
			
		||||
    public static void show(@NonNull final Fragment fragment, final SubscriptionImportInput input) {
 | 
			
		||||
        final var confirmationDialog = new ImportConfirmationDialog();
 | 
			
		||||
        final var arguments = new Bundle();
 | 
			
		||||
        arguments.putParcelable(INPUT, input);
 | 
			
		||||
        confirmationDialog.setArguments(arguments);
 | 
			
		||||
        confirmationDialog.show(fragment.getParentFragmentManager(), null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setResultServiceIntent(final Intent resultServiceIntent) {
 | 
			
		||||
        this.resultServiceIntent = resultServiceIntent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
 | 
			
		||||
        assureCorrectAppLanguage(getContext());
 | 
			
		||||
        return new AlertDialog.Builder(requireContext())
 | 
			
		||||
        final var context = requireContext();
 | 
			
		||||
        assureCorrectAppLanguage(context);
 | 
			
		||||
        return new AlertDialog.Builder(context)
 | 
			
		||||
                .setMessage(R.string.import_network_expensive_warning)
 | 
			
		||||
                .setCancelable(true)
 | 
			
		||||
                .setNegativeButton(R.string.cancel, null)
 | 
			
		||||
                .setPositiveButton(R.string.ok, (dialogInterface, i) -> {
 | 
			
		||||
                    if (resultServiceIntent != null && getContext() != null) {
 | 
			
		||||
                        getContext().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();
 | 
			
		||||
                })
 | 
			
		||||
                .create();
 | 
			
		||||
@@ -53,10 +70,6 @@ public class ImportConfirmationDialog extends DialogFragment {
 | 
			
		||||
    public void onCreate(@Nullable final Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
 | 
			
		||||
        if (resultServiceIntent == null) {
 | 
			
		||||
            throw new IllegalStateException("Result intent is null");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Bridge.restoreInstanceState(this, savedInstanceState);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ package org.schabi.newpipe.local.subscription
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.DialogInterface
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
@@ -49,11 +48,8 @@ import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
 | 
			
		||||
import org.schabi.newpipe.local.subscription.item.GroupsHeader
 | 
			
		||||
import org.schabi.newpipe.local.subscription.item.Header
 | 
			
		||||
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.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
 | 
			
		||||
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
 | 
			
		||||
@@ -224,21 +220,17 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
            )
 | 
			
		||||
        val data = result.data?.data
 | 
			
		||||
        if (data != null && result.resultCode == Activity.RESULT_OK) {
 | 
			
		||||
            SubscriptionExportWorker.schedule(activity, data)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun requestImportResult(result: ActivityResult) {
 | 
			
		||||
        if (result.data != null && result.resultCode == Activity.RESULT_OK) {
 | 
			
		||||
        val data = result.data?.dataString
 | 
			
		||||
        if (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)
 | 
			
		||||
                this, SubscriptionImportInput.PreviousExportMode(data)
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package org.schabi.newpipe.local.subscription
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.Pair
 | 
			
		||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
 | 
			
		||||
import io.reactivex.rxjava3.core.Completable
 | 
			
		||||
import io.reactivex.rxjava3.core.Flowable
 | 
			
		||||
@@ -48,23 +47,16 @@ class SubscriptionManager(context: Context) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun upsertAll(infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>): List<SubscriptionEntity> {
 | 
			
		||||
        val listEntities = subscriptionTable.upsertAll(
 | 
			
		||||
            infoList.map { SubscriptionEntity.from(it.first) }
 | 
			
		||||
        )
 | 
			
		||||
    fun upsertAll(infoList: List<Pair<ChannelInfo, ChannelTabInfo>>) {
 | 
			
		||||
        val listEntities = infoList.map { SubscriptionEntity.from(it.first) }
 | 
			
		||||
        subscriptionTable.upsertAll(listEntities)
 | 
			
		||||
 | 
			
		||||
        database.runInTransaction {
 | 
			
		||||
            infoList.forEachIndexed { index, info ->
 | 
			
		||||
                info.second.forEach {
 | 
			
		||||
                    feedDatabaseManager.upsertAll(
 | 
			
		||||
                        listEntities[index].uid,
 | 
			
		||||
                        it.relatedItems.filterIsInstance<StreamInfoItem>()
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                val streams = info.second.relatedItems.filterIsInstance<StreamInfoItem>()
 | 
			
		||||
                feedDatabaseManager.upsertAll(listEntities[index].uid, streams)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return listEntities
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateChannelInfo(info: ChannelInfo): Completable =
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,6 @@
 | 
			
		||||
package org.schabi.newpipe.local.subscription;
 | 
			
		||||
 | 
			
		||||
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.content.Intent;
 | 
			
		||||
@@ -37,7 +33,7 @@ import org.schabi.newpipe.error.UserAction;
 | 
			
		||||
import org.schabi.newpipe.extractor.NewPipe;
 | 
			
		||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
 | 
			
		||||
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.StoredFileHelper;
 | 
			
		||||
import org.schabi.newpipe.util.Constants;
 | 
			
		||||
@@ -168,10 +164,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void onImportUrl(final String value) {
 | 
			
		||||
        ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
 | 
			
		||||
                .putExtra(KEY_MODE, CHANNEL_URL_MODE)
 | 
			
		||||
                .putExtra(KEY_VALUE, value)
 | 
			
		||||
                .putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
 | 
			
		||||
        ImportConfirmationDialog.show(this,
 | 
			
		||||
                new SubscriptionImportInput.ChannelUrlMode(currentServiceId, value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void onImportFile() {
 | 
			
		||||
@@ -186,16 +180,10 @@ public class SubscriptionsImportFragment extends BaseFragment {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void requestImportFileResult(final ActivityResult result) {
 | 
			
		||||
        if (result.getData() == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (result.getResultCode() == Activity.RESULT_OK && result.getData().getData() != null) {
 | 
			
		||||
        final String data = result.getData() != null ? result.getData().getDataString() : null;
 | 
			
		||||
        if (result.getResultCode() == Activity.RESULT_OK && data != null) {
 | 
			
		||||
            ImportConfirmationDialog.show(this,
 | 
			
		||||
                    new Intent(activity, SubscriptionsImportService.class)
 | 
			
		||||
                            .putExtra(KEY_MODE, INPUT_STREAM_MODE)
 | 
			
		||||
                            .putExtra(KEY_VALUE, result.getData().getData())
 | 
			
		||||
                            .putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
 | 
			
		||||
                    new SubscriptionImportInput.InputStreamMode(currentServiceId, data));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,233 +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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
        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 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
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import java.io.OutputStream
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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.all
 | 
			
		||||
                    .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,237 @@
 | 
			
		||||
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")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -864,4 +864,16 @@
 | 
			
		||||
        <item quantity="one">%d comment</item>
 | 
			
		||||
        <item quantity="other">%d comments</item>
 | 
			
		||||
    </plurals>
 | 
			
		||||
    <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>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,11 @@ import static org.junit.Assert.fail;
 | 
			
		||||
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
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.ByteArrayOutputStream;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@@ -23,26 +23,22 @@ public class ImportExportJsonHelperTest {
 | 
			
		||||
        final String emptySource =
 | 
			
		||||
                "{\"app_version\":\"0.11.6\",\"app_version_int\": 47,\"subscriptions\":[]}";
 | 
			
		||||
 | 
			
		||||
        final List<SubscriptionItem> items = ImportExportJsonHelper.readFrom(
 | 
			
		||||
                new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8)), null);
 | 
			
		||||
        final var items = ImportExportJsonHelper.readFrom(
 | 
			
		||||
                new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8)));
 | 
			
		||||
        assertTrue(items.isEmpty());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testInvalidSource() {
 | 
			
		||||
        final List<String> invalidList = Arrays.asList(
 | 
			
		||||
                "{}",
 | 
			
		||||
                "",
 | 
			
		||||
                null,
 | 
			
		||||
                "gibberish");
 | 
			
		||||
        final var invalidList = Arrays.asList("{}", "", null, "gibberish");
 | 
			
		||||
 | 
			
		||||
        for (final String invalidContent : invalidList) {
 | 
			
		||||
            try {
 | 
			
		||||
                if (invalidContent != null) {
 | 
			
		||||
                    final byte[] bytes = invalidContent.getBytes(StandardCharsets.UTF_8);
 | 
			
		||||
                    ImportExportJsonHelper.readFrom((new ByteArrayInputStream(bytes)), null);
 | 
			
		||||
                    ImportExportJsonHelper.readFrom(new ByteArrayInputStream(bytes));
 | 
			
		||||
                } else {
 | 
			
		||||
                    ImportExportJsonHelper.readFrom(null, null);
 | 
			
		||||
                    ImportExportJsonHelper.readFrom(null);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                fail("didn't throw exception");
 | 
			
		||||
@@ -58,38 +54,24 @@ public class ImportExportJsonHelperTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    public void ultimateTest() throws Exception {
 | 
			
		||||
        // Read from file
 | 
			
		||||
        final List<SubscriptionItem> itemsFromFile = readFromFile();
 | 
			
		||||
        final var itemsFromFile = readFromFile();
 | 
			
		||||
 | 
			
		||||
        // Test writing to an output
 | 
			
		||||
        final String jsonOut = testWriteTo(itemsFromFile);
 | 
			
		||||
 | 
			
		||||
        // Read again
 | 
			
		||||
        final List<SubscriptionItem> itemsSecondRead = readFromWriteTo(jsonOut);
 | 
			
		||||
        final var itemsSecondRead = readFromWriteTo(jsonOut);
 | 
			
		||||
 | 
			
		||||
        // 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");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 {
 | 
			
		||||
        final InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
 | 
			
		||||
                "import_export_test.json");
 | 
			
		||||
        final List<SubscriptionItem> itemsFromFile = ImportExportJsonHelper.readFrom(
 | 
			
		||||
                inputStream, null);
 | 
			
		||||
        final var inputStream = getClass().getClassLoader()
 | 
			
		||||
                .getResourceAsStream("import_export_test.json");
 | 
			
		||||
        final var itemsFromFile = ImportExportJsonHelper.readFrom(inputStream);
 | 
			
		||||
 | 
			
		||||
        if (itemsFromFile.isEmpty()) {
 | 
			
		||||
            fail("ImportExportJsonHelper.readFrom(input) returned a null or empty list");
 | 
			
		||||
@@ -98,10 +80,10 @@ public class ImportExportJsonHelperTest {
 | 
			
		||||
        return itemsFromFile;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String testWriteTo(final List<SubscriptionItem> itemsFromFile) throws Exception {
 | 
			
		||||
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
 | 
			
		||||
        ImportExportJsonHelper.writeTo(itemsFromFile, out, null);
 | 
			
		||||
        final String jsonOut = out.toString("UTF-8");
 | 
			
		||||
    private String testWriteTo(final List<SubscriptionItem> itemsFromFile) {
 | 
			
		||||
        final var out = new ByteArrayOutputStream();
 | 
			
		||||
        ImportExportJsonHelper.writeTo(itemsFromFile, out);
 | 
			
		||||
        final String jsonOut = out.toString(StandardCharsets.UTF_8);
 | 
			
		||||
 | 
			
		||||
        if (jsonOut.isEmpty()) {
 | 
			
		||||
            fail("JSON returned by writeTo was empty");
 | 
			
		||||
@@ -111,10 +93,8 @@ public class ImportExportJsonHelperTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private List<SubscriptionItem> readFromWriteTo(final String jsonOut) throws Exception {
 | 
			
		||||
        final ByteArrayInputStream inputStream = new ByteArrayInputStream(
 | 
			
		||||
                jsonOut.getBytes(StandardCharsets.UTF_8));
 | 
			
		||||
        final List<SubscriptionItem> secondReadItems = ImportExportJsonHelper.readFrom(
 | 
			
		||||
                inputStream, null);
 | 
			
		||||
        final var inputStream = new ByteArrayInputStream(jsonOut.getBytes(StandardCharsets.UTF_8));
 | 
			
		||||
        final var secondReadItems = ImportExportJsonHelper.readFrom(inputStream);
 | 
			
		||||
 | 
			
		||||
        if (secondReadItems.isEmpty()) {
 | 
			
		||||
            fail("second call to readFrom returned an empty list");
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ buildscript {
 | 
			
		||||
        classpath libs.kotlin.gradle.plugin
 | 
			
		||||
        classpath libs.hilt.android.gradle.plugin
 | 
			
		||||
        classpath libs.aboutlibraries.plugin
 | 
			
		||||
        classpath libs.kotlinx.serialization
 | 
			
		||||
 | 
			
		||||
        // NOTE: Do not place your application dependencies here; they belong
 | 
			
		||||
        // in the individual module build.gradle files
 | 
			
		||||
 
 | 
			
		||||
@@ -24,11 +24,11 @@ jsoup = "1.17.2"
 | 
			
		||||
junit = "4.13.2"
 | 
			
		||||
kotlin = "2.0.21"
 | 
			
		||||
kotlinxCoroutinesRx3 = "1.8.1"
 | 
			
		||||
kotlinxSerializationJson = "1.7.3"
 | 
			
		||||
ktlint = "0.45.2"
 | 
			
		||||
lazycolumnscrollbar = "2.2.0"
 | 
			
		||||
leakcanary = "2.12"
 | 
			
		||||
lifecycle = "2.6.2"
 | 
			
		||||
localbroadcastmanager = "1.1.0"
 | 
			
		||||
markwon = "4.6.2"
 | 
			
		||||
material = "1.11.0"
 | 
			
		||||
media = "1.7.0"
 | 
			
		||||
@@ -60,7 +60,7 @@ teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
 | 
			
		||||
# to cause jitpack to regenerate the artifact.
 | 
			
		||||
teamnewpipe-newpipe-extractor = "v0.24.6"
 | 
			
		||||
webkit = "1.9.0"
 | 
			
		||||
work = "2.8.1"
 | 
			
		||||
work = "2.10.0"
 | 
			
		||||
 | 
			
		||||
[plugins]
 | 
			
		||||
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin" }
 | 
			
		||||
@@ -71,6 +71,7 @@ kotlin-android = { id = "kotlin-android" }
 | 
			
		||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
 | 
			
		||||
kotlin-kapt = { id = "kotlin-kapt" }
 | 
			
		||||
kotlin-parcelize = { id = "kotlin-parcelize" }
 | 
			
		||||
kotlinx-serialization = { id = "kotlinx-serialization" }
 | 
			
		||||
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
 | 
			
		||||
 | 
			
		||||
[libraries]
 | 
			
		||||
@@ -98,7 +99,6 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "a
 | 
			
		||||
androidx-lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
 | 
			
		||||
androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
 | 
			
		||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose" }
 | 
			
		||||
androidx-localbroadcastmanager = { group = "androidx.localbroadcastmanager", name = "localbroadcastmanager", version.ref = "localbroadcastmanager" }
 | 
			
		||||
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
 | 
			
		||||
androidx-media = { group = "androidx.media", name = "media", version.ref = "media" }
 | 
			
		||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
 | 
			
		||||
@@ -136,6 +136,8 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
 | 
			
		||||
kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
 | 
			
		||||
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
 | 
			
		||||
kotlinx-coroutines-rx3 = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-rx3", version.ref = "kotlinxCoroutinesRx3" }
 | 
			
		||||
kotlinx-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
 | 
			
		||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
 | 
			
		||||
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" }
 | 
			
		||||
leakcanary-android-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "leakcanary" }
 | 
			
		||||
leakcanary-object-watcher = { group = "com.squareup.leakcanary", name = "leakcanary-object-watcher-android", version.ref = "leakcanary" }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user