From 173ad83e0ee166597dd177654d1870361aeb5669 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 26 Nov 2024 07:55:37 +0530 Subject: [PATCH] Convert subscription export service to a worker --- app/build.gradle | 6 +- app/proguard-rules.pro | 15 ++ app/src/main/AndroidManifest.xml | 6 +- .../subscription/SubscriptionFragment.kt | 10 +- .../services/ImportExportJsonHelper.kt | 140 +++------------ .../services/SubscriptionsExportService.java | 168 ------------------ .../services/SubscriptionsImportService.java | 47 +++-- .../subscription/workers/SubscriptionData.kt | 24 +++ .../workers/SubscriptionExportWorker.kt | 117 ++++++++++++ app/src/main/res/values/strings.xml | 4 + .../services/ImportExportJsonHelperTest.java | 18 +- build.gradle | 1 + gradle/libs.versions.toml | 4 +- 13 files changed, 238 insertions(+), 322 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java create mode 100644 app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionData.kt create mode 100644 app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionExportWorker.kt 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" }