diff --git a/app/build.gradle b/app/build.gradle
index aad6b34d1..cbb9b4a80 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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 {
@@ -310,6 +311,9 @@ dependencies {
// Scroll
implementation libs.lazycolumnscrollbar
+ // Kotlinx Serialization
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
+
/** Debugging **/
// Memory leak detection
debugImplementation libs.leakcanary.object.watcher
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 215df0da5..42d24c5b5 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -27,3 +27,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(...);
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c44f8bf2c..240dd511c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
@@ -87,8 +88,11 @@
android:exported="false"
android:label="@string/title_activity_about" />
+
-
() {
}
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)
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.kt
index 611a1cd30..cd09b477e 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.kt
@@ -17,94 +17,44 @@
* along with this program. If not, see .
*/
-package org.schabi.newpipe.local.subscription.services;
+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;
+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 org.schabi.newpipe.local.subscription.workers.SubscriptionData
+import org.schabi.newpipe.local.subscription.workers.SubscriptionItem
+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.
*/
-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() { }
+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)
- * @param eventListener listener for the events generated
* @return the parsed subscription items
*/
- public static List readFrom(
- final InputStream in, @Nullable final ImportExportEventListener eventListener)
- throws InvalidSourceException {
- if (in == null) {
- throw new InvalidSourceException("input is null");
+ @JvmStatic
+ @Throws(InvalidSourceException::class)
+ fun readFrom(`in`: InputStream?): List {
+ if (`in` == null) {
+ throw InvalidSourceException("input is null")
}
- final List 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);
+ @OptIn(ExperimentalSerializationApi::class)
+ return json.decodeFromStream(`in`).subscriptions
+ } catch (e: Throwable) {
+ throw InvalidSourceException("Couldn't parse json", e)
}
-
- return channels;
}
/**
@@ -112,47 +62,13 @@ public final class ImportExportJsonHelper {
*
* @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 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 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();
+ @OptIn(ExperimentalSerializationApi::class)
+ @JvmStatic
+ fun writeTo(
+ items: List,
+ out: OutputStream,
+ ) {
+ json.encodeToStream(SubscriptionData(items), out)
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java
deleted file mode 100644
index 54809068a..000000000
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright 2018 Mauricio Colli
- * 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 .
- */
-
-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");
- outputStream = new SharpOutputStream(outFile.getStream());
- } 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 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 getSubscriber() {
- return new Subscriber() {
- @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, 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);
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java
index 442c7fddb..fe326da29 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java
@@ -41,8 +41,8 @@ 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.local.subscription.workers.SubscriptionItem;
import org.schabi.newpipe.streams.io.SharpInputStream;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.Constants;
@@ -50,10 +50,10 @@ 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 java.util.stream.Collectors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
@@ -177,18 +177,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
private void startImport() {
showToast(R.string.import_ongoing);
- Flowable> 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;
- }
+ final var flowable = switch (currentMode) {
+ case CHANNEL_URL_MODE -> importFromChannelUrl();
+ case INPUT_STREAM_MODE -> importFromInputStream();
+ case PREVIOUS_EXPORT_MODE -> importFromPreviousExport();
+ default -> null;
+ };
if (flowable == null) {
final String message = "Flowable given by \"importFrom\" is null "
@@ -290,13 +284,10 @@ public class SubscriptionsImportService extends BaseImportExportService {
private Function>>>,
List> upsertBatch() {
return notificationList -> {
- final List>> infoList =
- new ArrayList<>(notificationList.size());
- for (final Notification>> n : notificationList) {
- if (n.isOnNext()) {
- infoList.add(n.getValue());
- }
- }
+ final var infoList = notificationList.stream()
+ .filter(Notification::isOnNext)
+ .map(Notification::getValue)
+ .collect(Collectors.toList());
return subscriptionManager.upsertAll(infoList);
};
@@ -305,7 +296,11 @@ public class SubscriptionsImportService extends BaseImportExportService {
private Flowable> importFromChannelUrl() {
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
.getSubscriptionExtractor()
- .fromChannelUrl(channelUrl));
+ .fromChannelUrl(channelUrl))
+ .map(list -> list.stream()
+ .map(item -> new SubscriptionItem(item.getServiceId(), item.getUrl(),
+ item.getName()))
+ .collect(Collectors.toList()));
}
private Flowable> importFromInputStream() {
@@ -314,11 +309,15 @@ public class SubscriptionsImportService extends BaseImportExportService {
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
.getSubscriptionExtractor()
- .fromInputStream(inputStream, inputStreamType));
+ .fromInputStream(inputStream, inputStreamType))
+ .map(list -> list.stream()
+ .map(item -> new SubscriptionItem(item.getServiceId(), item.getUrl(),
+ item.getName()))
+ .collect(Collectors.toList()));
}
private Flowable> importFromPreviousExport() {
- return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream, null));
+ return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream));
}
protected void handleError(@NonNull final Throwable error) {
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionData.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionData.kt
new file mode 100644
index 000000000..dbb49508e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionData.kt
@@ -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
+) {
+ @SerialName("app_version")
+ private val appVersion = BuildConfig.VERSION_NAME
+
+ @SerialName("app_version_int")
+ private val appVersionInt = BuildConfig.VERSION_CODE
+}
+
+@Serializable
+class SubscriptionItem(
+ @SerialName("service_id")
+ val serviceId: Int,
+ val url: String,
+ val name: String
+)
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionExportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionExportWorker.kt
new file mode 100644
index 000000000..3a83adcb6
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionExportWorker.kt
@@ -0,0 +1,117 @@
+package org.schabi.newpipe.local.subscription.workers
+
+import android.app.Notification
+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
+import org.schabi.newpipe.local.subscription.services.ImportExportJsonHelper
+
+class SubscriptionExportWorker(
+ appContext: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(appContext, params) {
+ // This is needed for API levels < 31 (Android S).
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val notification = createNotification(applicationContext.getString(R.string.export_ongoing))
+ return createForegroundInfo(notification)
+ }
+
+ 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(createNotification(title)))
+
+ withContext(Dispatchers.IO) {
+ applicationContext.contentResolver.openOutputStream(uri)?.use {
+ ImportExportJsonHelper.writeTo(subscriptions, it)
+ }
+ }
+
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "Exported $qty subscriptions")
+ }
+
+ 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 createNotification(title: String): 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()
+
+ private fun createForegroundInfo(notification: Notification): ForegroundInfo {
+ 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()
+ .setInputData(data)
+ .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .build()
+
+ WorkManager
+ .getInstance(context)
+ .enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
+ }
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1bcb98ca9..e7226334d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -862,4 +862,8 @@
- %d comment
- %d comments
+
+ - Exporting %d subscription…
+ - Exporting %d subscriptions…
+
diff --git a/app/src/test/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelperTest.java b/app/src/test/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelperTest.java
index 4f0f125ec..2ccf1f58c 100644
--- a/app/src/test/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelperTest.java
+++ b/app/src/test/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelperTest.java
@@ -5,7 +5,7 @@ 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.SubscriptionItem;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -24,7 +24,7 @@ public class ImportExportJsonHelperTest {
"{\"app_version\":\"0.11.6\",\"app_version_int\": 47,\"subscriptions\":[]}";
final List items = ImportExportJsonHelper.readFrom(
- new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8)), null);
+ new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8)));
assertTrue(items.isEmpty());
}
@@ -40,9 +40,9 @@ public class ImportExportJsonHelperTest {
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");
@@ -89,7 +89,7 @@ public class ImportExportJsonHelperTest {
final InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
"import_export_test.json");
final List itemsFromFile = ImportExportJsonHelper.readFrom(
- inputStream, null);
+ inputStream);
if (itemsFromFile.isEmpty()) {
fail("ImportExportJsonHelper.readFrom(input) returned a null or empty list");
@@ -98,10 +98,10 @@ public class ImportExportJsonHelperTest {
return itemsFromFile;
}
- private String testWriteTo(final List itemsFromFile) throws Exception {
+ private String testWriteTo(final List itemsFromFile) {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
- ImportExportJsonHelper.writeTo(itemsFromFile, out, null);
- final String jsonOut = out.toString("UTF-8");
+ ImportExportJsonHelper.writeTo(itemsFromFile, out);
+ final String jsonOut = out.toString(StandardCharsets.UTF_8);
if (jsonOut.isEmpty()) {
fail("JSON returned by writeTo was empty");
@@ -114,7 +114,7 @@ public class ImportExportJsonHelperTest {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(
jsonOut.getBytes(StandardCharsets.UTF_8));
final List secondReadItems = ImportExportJsonHelper.readFrom(
- inputStream, null);
+ inputStream);
if (secondReadItems.isEmpty()) {
fail("second call to readFrom returned an empty list");
diff --git a/build.gradle b/build.gradle
index f3772ac87..f5e002830 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c39beb6e5..ab724cfa2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -56,7 +56,7 @@ teamnewpipe-filepicker = "5.0.0"
teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
teamnewpipe-newpipe-extractor = "d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e"
viewpager2 = "1.1.0-beta02"
-work = "2.8.1"
+work = "2.10.0"
[plugins]
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin" }
@@ -67,6 +67,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]
@@ -132,6 +133,7 @@ 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" }
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" }