From 82516dd75c455e2f6329935df03ac105ec1cf12a Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 26 Nov 2024 07:55:36 +0530 Subject: [PATCH 01/17] Rename .java to .kt --- .../{ImportExportJsonHelper.java => ImportExportJsonHelper.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/org/schabi/newpipe/local/subscription/services/{ImportExportJsonHelper.java => ImportExportJsonHelper.kt} (100%) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.kt similarity index 100% rename from app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java rename to app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.kt From 8e9503cfe4d8f8d9d46533d98424bac0e50ca2e3 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 26 Nov 2024 07:55:37 +0530 Subject: [PATCH 02/17] 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 | 171 ------------------ .../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 | 5 +- 13 files changed, 239 insertions(+), 325 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 5830885cb..45336d7bd 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 { @@ -314,6 +315,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 ab1a5a10c..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ /dev/null @@ -1,171 +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"); - // 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 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 b01b8697c..9486e88f4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -863,4 +863,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 5e3f10e5c..2f4fc657c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,8 @@ swiperefreshlayout = "1.1.0" teamnewpipe-filepicker = "5.0.0" teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" teamnewpipe-newpipe-extractor = "v0.24.4" -work = "2.8.1" +viewpager2 = "1.1.0-beta02" +work = "2.10.0" [plugins] aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin" } @@ -66,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] @@ -130,6 +132,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" } From dfb035dfa5d8b4e2c721ffc7dd2bd056740c1c4f Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Thu, 28 Nov 2024 06:34:39 +0530 Subject: [PATCH 03/17] Improve import/export tests --- .../subscription/workers/SubscriptionData.kt | 2 +- .../services/ImportExportJsonHelperTest.java | 43 +++++-------------- 2 files changed, 12 insertions(+), 33 deletions(-) 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 index dbb49508e..174ae7585 100644 --- 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 @@ -16,7 +16,7 @@ class SubscriptionData( } @Serializable -class SubscriptionItem( +data class SubscriptionItem( @SerialName("service_id") val serviceId: Int, val url: String, 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 2ccf1f58c..3243ad9fd 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 @@ -9,7 +9,6 @@ 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,18 +22,14 @@ public class ImportExportJsonHelperTest { final String emptySource = "{\"app_version\":\"0.11.6\",\"app_version_int\": 47,\"subscriptions\":[]}"; - final List items = ImportExportJsonHelper.readFrom( + final var items = ImportExportJsonHelper.readFrom( new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8))); assertTrue(items.isEmpty()); } @Test public void testInvalidSource() { - final List invalidList = Arrays.asList( - "{}", - "", - null, - "gibberish"); + final var invalidList = Arrays.asList("{}", "", null, "gibberish"); for (final String invalidContent : invalidList) { try { @@ -58,38 +53,24 @@ public class ImportExportJsonHelperTest { @Test public void ultimateTest() throws Exception { // Read from file - final List itemsFromFile = readFromFile(); + final var itemsFromFile = readFromFile(); // Test writing to an output final String jsonOut = testWriteTo(itemsFromFile); // Read again - final List 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 readFromFile() throws Exception { - final InputStream inputStream = getClass().getClassLoader().getResourceAsStream( - "import_export_test.json"); - final List itemsFromFile = ImportExportJsonHelper.readFrom( - inputStream); + 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"); @@ -99,7 +80,7 @@ public class ImportExportJsonHelperTest { } private String testWriteTo(final List itemsFromFile) { - final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final var out = new ByteArrayOutputStream(); ImportExportJsonHelper.writeTo(itemsFromFile, out); final String jsonOut = out.toString(StandardCharsets.UTF_8); @@ -111,10 +92,8 @@ public class ImportExportJsonHelperTest { } private List readFromWriteTo(final String jsonOut) throws Exception { - final ByteArrayInputStream inputStream = new ByteArrayInputStream( - jsonOut.getBytes(StandardCharsets.UTF_8)); - final List secondReadItems = ImportExportJsonHelper.readFrom( - inputStream); + 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"); From fc7f1b0af05acb2a24866e6674fc32c4a5bcd49d Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Thu, 28 Nov 2024 09:53:25 +0530 Subject: [PATCH 04/17] Convert subscription import service to a worker --- app/src/main/AndroidManifest.xml | 1 - .../ImportConfirmationDialog.java | 62 +++- .../subscription/SubscriptionFragment.kt | 15 +- .../SubscriptionsImportFragment.java | 26 +- .../services/BaseImportExportService.java | 233 ------------- .../services/ImportExportEventListener.java | 17 - .../services/SubscriptionsImportService.java | 326 ------------------ .../ImportExportJsonHelper.kt | 4 +- .../workers/SubscriptionExportWorker.kt | 1 - .../workers/SubscriptionImportWorker.kt | 153 ++++++++ app/src/main/res/values/strings.xml | 8 + .../services/ImportExportJsonHelperTest.java | 1 + 12 files changed, 222 insertions(+), 625 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java delete mode 100644 app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java delete mode 100644 app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java rename app/src/main/java/org/schabi/newpipe/local/subscription/{services => workers}/ImportExportJsonHelper.kt (92%) create mode 100644 app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 240dd511c..d9a63fcde 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -92,7 +92,6 @@ android:name="androidx.work.impl.foreground.SystemForegroundService" android:foregroundServiceType="dataSync" tools:node="merge" /> - { - if (resultServiceIntent != null && getContext() != null) { - getContext().startService(resultServiceIntent); - } + final var inputData = new Data.Builder() + .putString(SubscriptionImportWorker.KEY_VALUE, value) + .putInt(SubscriptionImportWorker.KEY_MODE, mode) + .putInt(Constants.KEY_SERVICE_ID, serviceId) + .build(); + final var constraints = new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build(); + + final var req = new OneTimeWorkRequest.Builder(SubscriptionImportWorker.class) + .setInputData(inputData) + .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,8 +85,8 @@ 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"); + if (mode == 0 && value == null && serviceId == 0) { + throw new IllegalStateException("Input data not provided"); } Bridge.restoreInstanceState(this, savedInstanceState); diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 25d81287a..cdc7ae179 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -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,14 +48,12 @@ 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.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.SubscriptionImportWorker import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable +import org.schabi.newpipe.util.NO_SERVICE_ID import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.ServiceHelper @@ -231,12 +228,10 @@ class SubscriptionFragment : BaseStateFragment() { } 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, SubscriptionImportWorker.PREVIOUS_EXPORT_MODE, data, NO_SERVICE_ID ) } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index 77a70afa9..a1d244df8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -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.SubscriptionImportWorker; 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, SubscriptionImportWorker.CHANNEL_URL_MODE, value, + currentServiceId); } 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) { - 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)); + final String data = result.getData() != null ? result.getData().getDataString() : null; + if (result.getResultCode() == Activity.RESULT_OK && data != null) { + ImportConfirmationDialog.show(this, SubscriptionImportWorker.INPUT_STREAM_MODE, + data, currentServiceId); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java deleted file mode 100644 index b7c11b160..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2018 Mauricio Colli - * 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 . - */ - -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 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, Publisher> 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; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java deleted file mode 100644 index 7352d1f12..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java +++ /dev/null @@ -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); -} 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 deleted file mode 100644 index fe326da29..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright 2018 Mauricio Colli - * 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 . - */ - -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.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; -import org.schabi.newpipe.util.ExtractorHelper; - -import java.io.IOException; -import java.io.InputStream; -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; -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); - - 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 " - + "(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 -> { - 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> getSubscriber() { - return new Subscriber<>() { - @Override - public void onSubscribe(final Subscription s) { - subscription = s; - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(final List 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>>> 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> upsertBatch() { - return notificationList -> { - final var infoList = notificationList.stream() - .filter(Notification::isOnNext) - .map(Notification::getValue) - .collect(Collectors.toList()); - - return subscriptionManager.upsertAll(infoList); - }; - } - - private Flowable> importFromChannelUrl() { - return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId) - .getSubscriptionExtractor() - .fromChannelUrl(channelUrl)) - .map(list -> list.stream() - .map(item -> new SubscriptionItem(item.getServiceId(), item.getUrl(), - item.getName())) - .collect(Collectors.toList())); - } - - private Flowable> importFromInputStream() { - Objects.requireNonNull(inputStream); - Objects.requireNonNull(inputStreamType); - - return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId) - .getSubscriptionExtractor() - .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)); - } - - protected void handleError(@NonNull final Throwable error) { - super.handleError(R.string.subscriptions_import_unsuccessful, error); - } -} 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/workers/ImportExportJsonHelper.kt similarity index 92% rename from app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.kt rename to app/src/main/java/org/schabi/newpipe/local/subscription/workers/ImportExportJsonHelper.kt index cd09b477e..d71f5fa89 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/ImportExportJsonHelper.kt @@ -17,15 +17,13 @@ * along with this program. If not, see . */ -package org.schabi.newpipe.local.subscription.services +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 org.schabi.newpipe.local.subscription.workers.SubscriptionData -import org.schabi.newpipe.local.subscription.workers.SubscriptionItem import java.io.InputStream import java.io.OutputStream 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 index 3a83adcb6..42b77e21c 100644 --- 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 @@ -23,7 +23,6 @@ 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, diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt new file mode 100644 index 000000000..4e5c2a541 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -0,0 +1,153 @@ +package org.schabi.newpipe.local.subscription.workers + +import android.app.Notification +import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build +import android.util.Pair +import android.webkit.MimeTypeMap +import androidx.core.app.NotificationCompat +import androidx.core.net.toUri +import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +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 org.schabi.newpipe.R +import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.local.subscription.SubscriptionManager +import org.schabi.newpipe.util.ExtractorHelper +import org.schabi.newpipe.util.KEY_SERVICE_ID +import org.schabi.newpipe.util.NO_SERVICE_ID + +class SubscriptionImportWorker( + appContext: Context, + params: WorkerParameters, +) : CoroutineWorker(appContext, params) { + // This is needed for API levels < 31 (Android S). + override suspend fun getForegroundInfo(): ForegroundInfo { + val title = applicationContext.getString(R.string.import_ongoing) + return createForegroundInfo(createNotification(title, null, 0, 0)) + } + + override suspend fun doWork(): Result { + val mode = inputData.getInt(KEY_MODE, CHANNEL_URL_MODE) + val extractor = NewPipe.getService(inputData.getInt(KEY_SERVICE_ID, NO_SERVICE_ID)) + .subscriptionExtractor + val value = inputData.getString(KEY_VALUE) ?: "" + + val subscriptions = withContext(Dispatchers.IO) { + if (mode == CHANNEL_URL_MODE) { + extractor + .fromChannelUrl(value) + .map { SubscriptionItem(it.serviceId, it.url, it.name) } + } else { + applicationContext.contentResolver.openInputStream(value.toUri())?.use { + if (mode == INPUT_STREAM_MODE) { + val contentType = MimeTypeMap.getFileExtensionFromUrl(value).ifEmpty { DEFAULT_MIME } + extractor + .fromInputStream(it, contentType) + .map { SubscriptionItem(it.serviceId, it.url, it.name) } + } else { + ImportExportJsonHelper.readFrom(it) + } + } ?: emptyList() + } + } + + val mutex = Mutex() + var index = 1 + val qty = subscriptions.size + var title = + applicationContext.resources.getQuantityString(R.plurals.load_subscriptions, qty, qty) + + val channelInfoList = 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++ } + val notification = createNotification(title, channelInfo.name, currentIndex, qty) + setForeground(createForegroundInfo(notification)) + + Pair(channelInfo, listOf(channelTab)) + } + }.awaitAll() + } + + title = applicationContext.resources.getQuantityString(R.plurals.import_subscriptions, qty, qty) + setForeground(createForegroundInfo(createNotification(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(createNotification(title, null, index, qty))) + } + + return Result.success() + } + + private fun createNotification( + title: String, + text: String?, + currentProgress: Int, + maxProgress: Int, + ): 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() + + 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 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" + const val CHANNEL_URL_MODE = 0 + const val INPUT_STREAM_MODE = 1 + const val PREVIOUS_EXPORT_MODE = 2 + const val KEY_MODE = "key_mode" + const val KEY_VALUE = "key_value" + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9486e88f4..245782dff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -867,4 +867,12 @@ Exporting %d subscription… Exporting %d subscriptions… + + Loading %d subscription… + Loading %d subscriptions… + + + Importing %d subscription… + Importing %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 3243ad9fd..96bca9733 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,6 +5,7 @@ import static org.junit.Assert.fail; import org.junit.Test; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; +import org.schabi.newpipe.local.subscription.workers.ImportExportJsonHelper; import org.schabi.newpipe.local.subscription.workers.SubscriptionItem; import java.io.ByteArrayInputStream; From c0965a42a1cd937500f52886d34fed767a7e0545 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 29 Nov 2024 07:05:06 +0530 Subject: [PATCH 05/17] Added success toasts --- .../local/subscription/workers/SubscriptionExportWorker.kt | 6 ++++++ .../local/subscription/workers/SubscriptionImportWorker.kt | 7 +++++++ 2 files changed, 13 insertions(+) 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 index 42b77e21c..e41bf3b38 100644 --- 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 @@ -58,6 +58,12 @@ class SubscriptionExportWorker( 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) { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt index 4e5c2a541..e66e2a5df 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -6,6 +6,7 @@ import android.content.pm.ServiceInfo import android.os.Build import android.util.Pair import android.webkit.MimeTypeMap +import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.core.net.toUri import androidx.work.CoroutineWorker @@ -98,6 +99,12 @@ class SubscriptionImportWorker( setForeground(createForegroundInfo(createNotification(title, null, index, qty))) } + withContext(Dispatchers.Main) { + Toast + .makeText(applicationContext, R.string.import_complete_toast, Toast.LENGTH_SHORT) + .show() + } + return Result.success() } From 4e31ccebf8c9b231afa8a3475c60b3fb48a3b439 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 29 Nov 2024 07:23:37 +0530 Subject: [PATCH 06/17] Moved Kotlinx Serialization to library catalog --- app/build.gradle | 2 +- gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 45336d7bd..9e19bb711 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -316,7 +316,7 @@ dependencies { implementation libs.lazycolumnscrollbar // Kotlinx Serialization - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3' + implementation libs.kotlinx.serialization.json /** Debugging **/ // Memory leak detection diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f4fc657c..bf1a2e4ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ 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" @@ -133,6 +134,7 @@ kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-p 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" } From c9d155a33544d69364535b0d1b6ee8b81c37e1c2 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 30 Nov 2024 07:23:37 +0530 Subject: [PATCH 07/17] Combine notification and ForegroundInfo creation methods --- .../workers/SubscriptionExportWorker.kt | 32 +++++----- .../workers/SubscriptionImportWorker.kt | 63 +++++++++---------- 2 files changed, 44 insertions(+), 51 deletions(-) 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 index e41bf3b38..a124fc666 100644 --- 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 @@ -1,6 +1,5 @@ package org.schabi.newpipe.local.subscription.workers -import android.app.Notification import android.content.Context import android.content.pm.ServiceInfo import android.net.Uri @@ -30,8 +29,7 @@ class SubscriptionExportWorker( ) : 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) + return createForegroundInfo(applicationContext.getString(R.string.export_ongoing)) } override suspend fun doWork(): Result { @@ -44,9 +42,8 @@ class SubscriptionExportWorker( .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))) + val title = applicationContext.resources.getQuantityString(R.plurals.export_subscriptions, qty, qty) + setForeground(createForegroundInfo(title)) withContext(Dispatchers.IO) { applicationContext.contentResolver.openOutputStream(uri)?.use { @@ -80,18 +77,17 @@ class SubscriptionExportWorker( } } - 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 { + 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) } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt index e66e2a5df..3556ac883 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -1,6 +1,5 @@ package org.schabi.newpipe.local.subscription.workers -import android.app.Notification import android.content.Context import android.content.pm.ServiceInfo import android.os.Build @@ -33,8 +32,7 @@ class SubscriptionImportWorker( ) : CoroutineWorker(appContext, params) { // This is needed for API levels < 31 (Android S). override suspend fun getForegroundInfo(): ForegroundInfo { - val title = applicationContext.getString(R.string.import_ongoing) - return createForegroundInfo(createNotification(title, null, 0, 0)) + return createForegroundInfo(applicationContext.getString(R.string.import_ongoing), null, 0, 0) } override suspend fun doWork(): Result { @@ -78,8 +76,7 @@ class SubscriptionImportWorker( ExtractorHelper.getChannelTab(it.serviceId, channelInfo.tabs[0], true).await() val currentIndex = mutex.withLock { index++ } - val notification = createNotification(title, channelInfo.name, currentIndex, qty) - setForeground(createForegroundInfo(notification)) + setForeground(createForegroundInfo(title, channelInfo.name, currentIndex, qty)) Pair(channelInfo, listOf(channelTab)) } @@ -87,7 +84,7 @@ class SubscriptionImportWorker( } title = applicationContext.resources.getQuantityString(R.plurals.import_subscriptions, qty, qty) - setForeground(createForegroundInfo(createNotification(title, null, 0, 0))) + setForeground(createForegroundInfo(title, null, 0, 0)) index = 0 val subscriptionManager = SubscriptionManager(applicationContext) @@ -96,7 +93,7 @@ class SubscriptionImportWorker( subscriptionManager.upsertAll(chunk) } index += chunk.size - setForeground(createForegroundInfo(createNotification(title, null, index, qty))) + setForeground(createForegroundInfo(title, null, index, qty)) } withContext(Dispatchers.Main) { @@ -108,38 +105,38 @@ class SubscriptionImportWorker( return Result.success() } - private fun createNotification( + private fun createForegroundInfo( title: String, text: String?, currentProgress: Int, maxProgress: Int, - ): 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) + ): 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() - - private fun createForegroundInfo(notification: Notification): ForegroundInfo { + }.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) } From af8e5646a65d1563916e7b7f3bbcab8bac35f065 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 30 Nov 2024 07:35:12 +0530 Subject: [PATCH 08/17] Remove LocalBroadcastManager --- app/build.gradle | 1 - gradle/libs.versions.toml | 2 -- 2 files changed, 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9e19bb711..fec2e89a1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -223,7 +223,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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf1a2e4ab..2f3dbce97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,6 @@ 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" @@ -96,7 +95,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" } From 095155d35f4f776d561af5f484483278b73f91ad Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 1 Dec 2024 06:24:26 +0530 Subject: [PATCH 09/17] Only get subscription extractor when needed --- .../subscription/workers/SubscriptionImportWorker.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt index 3556ac883..cdcb335ee 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -37,20 +37,19 @@ class SubscriptionImportWorker( override suspend fun doWork(): Result { val mode = inputData.getInt(KEY_MODE, CHANNEL_URL_MODE) - val extractor = NewPipe.getService(inputData.getInt(KEY_SERVICE_ID, NO_SERVICE_ID)) - .subscriptionExtractor - val value = inputData.getString(KEY_VALUE) ?: "" + val serviceId = inputData.getInt(KEY_SERVICE_ID, NO_SERVICE_ID) + val value = inputData.getString(KEY_VALUE)!! val subscriptions = withContext(Dispatchers.IO) { if (mode == CHANNEL_URL_MODE) { - extractor + NewPipe.getService(serviceId).subscriptionExtractor .fromChannelUrl(value) .map { SubscriptionItem(it.serviceId, it.url, it.name) } } else { applicationContext.contentResolver.openInputStream(value.toUri())?.use { if (mode == INPUT_STREAM_MODE) { val contentType = MimeTypeMap.getFileExtensionFromUrl(value).ifEmpty { DEFAULT_MIME } - extractor + NewPipe.getService(serviceId).subscriptionExtractor .fromInputStream(it, contentType) .map { SubscriptionItem(it.serviceId, it.url, it.name) } } else { From 60586c90d638d0030facb5c584f6a249c8279929 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 1 Dec 2024 09:51:12 +0530 Subject: [PATCH 10/17] Improve subscription upsert methods --- .../database/subscription/SubscriptionDAO.kt | 4 +--- .../local/subscription/SubscriptionManager.kt | 17 +++++------------ .../workers/SubscriptionImportWorker.kt | 2 +- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 47b6f4dd9..358741a48 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -90,7 +90,7 @@ abstract class SubscriptionDAO : BasicDAO { internal abstract fun silentInsertAllInternal(entities: List): List @Transaction - open fun upsertAll(entities: List): List { + open fun upsertAll(entities: List) { val insertUidList = silentInsertAllInternal(entities) insertUidList.forEachIndexed { index: Int, uidFromInsert: Long -> @@ -106,7 +106,5 @@ abstract class SubscriptionDAO : BasicDAO { update(entity) } } - - return entities } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 474add4f4..7b666f357 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -48,23 +48,16 @@ class SubscriptionManager(context: Context) { } } - fun upsertAll(infoList: List>>): List { - val listEntities = subscriptionTable.upsertAll( - infoList.map { SubscriptionEntity.from(it.first) } - ) + fun upsertAll(infoList: List>) { + 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() - ) - } + val streams = info.second.relatedItems.filterIsInstance() + feedDatabaseManager.upsertAll(listEntities[index].uid, streams) } } - - return listEntities } fun updateChannelInfo(info: ChannelInfo): Completable = diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt index cdcb335ee..b2a8a3a50 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -77,7 +77,7 @@ class SubscriptionImportWorker( val currentIndex = mutex.withLock { index++ } setForeground(createForegroundInfo(title, channelInfo.name, currentIndex, qty)) - Pair(channelInfo, listOf(channelTab)) + Pair(channelInfo, channelTab) } }.awaitAll() } From 21973b362a6732e3fad2020421f6dc654c2dcf43 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 21 Dec 2024 10:10:42 +0530 Subject: [PATCH 11/17] Use Kotlin Pair --- .../schabi/newpipe/local/subscription/SubscriptionManager.kt | 1 - .../local/subscription/workers/SubscriptionImportWorker.kt | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 7b666f357..8de2c94db 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt index b2a8a3a50..e97eb760e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -3,7 +3,6 @@ package org.schabi.newpipe.local.subscription.workers import android.content.Context import android.content.pm.ServiceInfo import android.os.Build -import android.util.Pair import android.webkit.MimeTypeMap import android.widget.Toast import androidx.core.app.NotificationCompat @@ -77,7 +76,7 @@ class SubscriptionImportWorker( val currentIndex = mutex.withLock { index++ } setForeground(createForegroundInfo(title, channelInfo.name, currentIndex, qty)) - Pair(channelInfo, channelTab) + channelInfo to channelTab } }.awaitAll() } From dbd11a6a8ddb139a506b0aa5ce89763a424ea8eb Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Wed, 22 Jan 2025 15:34:04 +0100 Subject: [PATCH 12/17] SubscriptionImportWorker: inputs as sealed class --- .../ImportConfirmationDialog.java | 32 +----- .../subscription/SubscriptionFragment.kt | 5 +- .../SubscriptionsImportFragment.java | 10 +- .../workers/SubscriptionImportWorker.kt | 106 ++++++++++++++---- 4 files changed, 96 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java index 3db86a203..a193cc1b9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java @@ -11,7 +11,6 @@ import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.work.Constraints; -import androidx.work.Data; import androidx.work.ExistingWorkPolicy; import androidx.work.NetworkType; import androidx.work.OneTimeWorkRequest; @@ -22,31 +21,19 @@ 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; -import org.schabi.newpipe.util.Constants; public class ImportConfirmationDialog extends DialogFragment { @State - protected int mode; - @State - protected String value; - @State - protected int serviceId; + protected SubscriptionImportInput input; - public static void show(@NonNull final Fragment fragment, final int mode, - @Nullable final String value, final int serviceId) { + public static void show(@NonNull final Fragment fragment, final SubscriptionImportInput input) { final var confirmationDialog = new ImportConfirmationDialog(); - confirmationDialog.setData(mode, value, serviceId); + confirmationDialog.input = input; confirmationDialog.show(fragment.getParentFragmentManager(), null); } - @SuppressWarnings("HiddenField") - public void setData(final int mode, final String value, final int serviceId) { - this.mode = mode; - this.value = value; - this.serviceId = serviceId; - } - @NonNull @Override public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { @@ -57,17 +44,12 @@ public class ImportConfirmationDialog extends DialogFragment { .setCancelable(true) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialogInterface, i) -> { - final var inputData = new Data.Builder() - .putString(SubscriptionImportWorker.KEY_VALUE, value) - .putInt(SubscriptionImportWorker.KEY_MODE, mode) - .putInt(Constants.KEY_SERVICE_ID, serviceId) - .build(); final var constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build(); final var req = new OneTimeWorkRequest.Builder(SubscriptionImportWorker.class) - .setInputData(inputData) + .setInputData(input.toData()) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .setConstraints(constraints) .build(); @@ -85,10 +67,6 @@ public class ImportConfirmationDialog extends DialogFragment { public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (mode == 0 && value == null && serviceId == 0) { - throw new IllegalStateException("Input data not provided"); - } - Bridge.restoreInstanceState(this, savedInstanceState); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index cdc7ae179..91ff7cd27 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -49,11 +49,10 @@ 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.workers.SubscriptionExportWorker -import org.schabi.newpipe.local.subscription.workers.SubscriptionImportWorker +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 -import org.schabi.newpipe.util.NO_SERVICE_ID import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.ServiceHelper @@ -231,7 +230,7 @@ class SubscriptionFragment : BaseStateFragment() { val data = result.data?.dataString if (data != null && result.resultCode == Activity.RESULT_OK) { ImportConfirmationDialog.show( - this, SubscriptionImportWorker.PREVIOUS_EXPORT_MODE, data, NO_SERVICE_ID + this, SubscriptionImportInput.PreviousExportMode(data) ) } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index a1d244df8..aee7c0003 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -33,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.workers.SubscriptionImportWorker; +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; @@ -164,8 +164,8 @@ public class SubscriptionsImportFragment extends BaseFragment { } public void onImportUrl(final String value) { - ImportConfirmationDialog.show(this, SubscriptionImportWorker.CHANNEL_URL_MODE, value, - currentServiceId); + ImportConfirmationDialog.show(this, + new SubscriptionImportInput.ChannelUrlMode(currentServiceId, value)); } public void onImportFile() { @@ -182,8 +182,8 @@ public class SubscriptionsImportFragment extends BaseFragment { private void requestImportFileResult(final ActivityResult result) { final String data = result.getData() != null ? result.getData().getDataString() : null; if (result.getResultCode() == Activity.RESULT_OK && data != null) { - ImportConfirmationDialog.show(this, SubscriptionImportWorker.INPUT_STREAM_MODE, - data, currentServiceId); + ImportConfirmationDialog.show(this, + new SubscriptionImportInput.InputStreamMode(currentServiceId, data)); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt index e97eb760e..4bcec5cb3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -3,11 +3,13 @@ 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.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 @@ -18,12 +20,11 @@ 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.R import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.util.ExtractorHelper -import org.schabi.newpipe.util.KEY_SERVICE_ID -import org.schabi.newpipe.util.NO_SERVICE_ID class SubscriptionImportWorker( appContext: Context, @@ -35,27 +36,29 @@ class SubscriptionImportWorker( } override suspend fun doWork(): Result { - val mode = inputData.getInt(KEY_MODE, CHANNEL_URL_MODE) - val serviceId = inputData.getInt(KEY_SERVICE_ID, NO_SERVICE_ID) - val value = inputData.getString(KEY_VALUE)!! + val input = SubscriptionImportInput.fromData(inputData) val subscriptions = withContext(Dispatchers.IO) { - if (mode == CHANNEL_URL_MODE) { - NewPipe.getService(serviceId).subscriptionExtractor - .fromChannelUrl(value) - .map { SubscriptionItem(it.serviceId, it.url, it.name) } - } else { - applicationContext.contentResolver.openInputStream(value.toUri())?.use { - if (mode == INPUT_STREAM_MODE) { - val contentType = MimeTypeMap.getFileExtensionFromUrl(value).ifEmpty { DEFAULT_MIME } - NewPipe.getService(serviceId).subscriptionExtractor + 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) } - } else { + } + + is SubscriptionImportInput.PreviousExportMode -> + applicationContext.contentResolver.openInputStream(input.url.toUri())?.use { ImportExportJsonHelper.readFrom(it) } - } ?: emptyList() - } + } ?: emptyList() } val mutex = Mutex() @@ -146,10 +149,69 @@ class SubscriptionImportWorker( private const val BUFFER_COUNT_BEFORE_INSERT = 50 const val WORK_NAME = "SubscriptionImportWorker" - const val CHANNEL_URL_MODE = 0 - const val INPUT_STREAM_MODE = 1 - const val PREVIOUS_EXPORT_MODE = 2 - const val KEY_MODE = "key_mode" - const val KEY_VALUE = "key_value" + } +} + +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 { + return when (this) { + is ChannelUrlMode -> Data.Builder() + .putInt("mode", CHANNEL_URL_MODE) + .putInt("service_id", serviceId) + .putString("url", url) + .build() + is InputStreamMode -> + Data.Builder() + .putInt("mode", INPUT_STREAM_MODE) + .putInt("service_id", serviceId) + .putString("url", url) + .build() + is PreviousExportMode -> + Data.Builder() + .putInt("mode", PREVIOUS_EXPORT_MODE) + .putString("url", url) + .build() + } + } + + 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") + } + } } } From a79516dfffc787695414f504d24838162e1c6697 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 25 Jan 2025 05:30:14 +0530 Subject: [PATCH 13/17] Use fragment arguments --- .../local/subscription/ImportConfirmationDialog.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java index a193cc1b9..a4b3ea399 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java @@ -8,6 +8,7 @@ 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; @@ -17,7 +18,6 @@ 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; @@ -25,12 +25,13 @@ import org.schabi.newpipe.local.subscription.workers.SubscriptionImportInput; import org.schabi.newpipe.local.subscription.workers.SubscriptionImportWorker; public class ImportConfirmationDialog extends DialogFragment { - @State - protected SubscriptionImportInput input; + private static final String INPUT = "input"; public static void show(@NonNull final Fragment fragment, final SubscriptionImportInput input) { final var confirmationDialog = new ImportConfirmationDialog(); - confirmationDialog.input = input; + final var arguments = new Bundle(); + arguments.putParcelable(INPUT, input); + confirmationDialog.setArguments(arguments); confirmationDialog.show(fragment.getParentFragmentManager(), null); } @@ -47,6 +48,8 @@ public class ImportConfirmationDialog extends DialogFragment { 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()) From af3ed992e5cd8759557a1052739a67af392db882 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 25 Jan 2025 05:40:00 +0530 Subject: [PATCH 14/17] Add error handling for import --- .../workers/SubscriptionImportWorker.kt | 123 +++++++++++------- 1 file changed, 77 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt index 4bcec5cb3..df058b8ee 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -4,6 +4,7 @@ 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 @@ -21,6 +22,7 @@ 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 @@ -36,9 +38,79 @@ class SubscriptionImportWorker( } override suspend fun doWork(): Result { - val input = SubscriptionImportInput.fromData(inputData) + 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 subscriptions = withContext(Dispatchers.IO) { + 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 { + return withContext(Dispatchers.IO) { when (input) { is SubscriptionImportInput.ChannelUrlMode -> NewPipe.getService(input.serviceId).subscriptionExtractor @@ -60,50 +132,6 @@ class SubscriptionImportWorker( } } ?: emptyList() } - - val mutex = Mutex() - var index = 1 - val qty = subscriptions.size - var title = - applicationContext.resources.getQuantityString(R.plurals.load_subscriptions, qty, qty) - - val channelInfoList = 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() - } - - 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 fun createForegroundInfo( @@ -142,6 +170,9 @@ class SubscriptionImportWorker( } 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" From 32a88ab89027248c82512c821fb08df05464db75 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 25 Jan 2025 07:57:01 +0530 Subject: [PATCH 15/17] Truncate existing file in export --- .../local/subscription/workers/SubscriptionExportWorker.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index a124fc666..6de91f174 100644 --- 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 @@ -46,7 +46,8 @@ class SubscriptionExportWorker( setForeground(createForegroundInfo(title)) withContext(Dispatchers.IO) { - applicationContext.contentResolver.openOutputStream(uri)?.use { + // Truncate file if it already exists + applicationContext.contentResolver.openOutputStream(uri, "wt")?.use { ImportExportJsonHelper.writeTo(subscriptions, it) } } From 48d682016e38e1301510cb3bfaee9107fb227949 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 25 Jan 2025 11:21:58 +0530 Subject: [PATCH 16/17] Rm ViewPager version --- gradle/libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f3dbce97..bfafa7a80 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,6 @@ swiperefreshlayout = "1.1.0" teamnewpipe-filepicker = "5.0.0" teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" teamnewpipe-newpipe-extractor = "v0.24.4" -viewpager2 = "1.1.0-beta02" work = "2.10.0" [plugins] From d805679a5ed3704dfff67604197ee9e7b4e8a0bf Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 8 Mar 2025 09:18:30 +0530 Subject: [PATCH 17/17] Use workDataOf --- .../workers/SubscriptionImportWorker.kt | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt index df058b8ee..86b9c739a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/workers/SubscriptionImportWorker.kt @@ -14,6 +14,7 @@ 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 @@ -192,24 +193,12 @@ sealed class SubscriptionImportInput : Parcelable { data class PreviousExportMode(val url: String) : SubscriptionImportInput() fun toData(): Data { - return when (this) { - is ChannelUrlMode -> Data.Builder() - .putInt("mode", CHANNEL_URL_MODE) - .putInt("service_id", serviceId) - .putString("url", url) - .build() - is InputStreamMode -> - Data.Builder() - .putInt("mode", INPUT_STREAM_MODE) - .putInt("service_id", serviceId) - .putString("url", url) - .build() - is PreviousExportMode -> - Data.Builder() - .putInt("mode", PREVIOUS_EXPORT_MODE) - .putString("url", url) - .build() + 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 {