Convert subscription export service to a worker

This commit is contained in:
Isira Seneviratne 2024-11-26 07:55:37 +05:30
parent 00e2d66dd5
commit 173ad83e0e
13 changed files with 238 additions and 322 deletions

View File

@ -9,6 +9,7 @@ plugins {
alias libs.plugins.kotlin.compose
alias libs.plugins.kotlin.kapt
alias libs.plugins.kotlin.parcelize
alias libs.plugins.kotlinx.serialization
alias libs.plugins.checkstyle
alias libs.plugins.sonarqube
alias libs.plugins.hilt
@ -16,7 +17,7 @@ plugins {
}
android {
compileSdk 34
compileSdk 35
namespace 'org.schabi.newpipe'
defaultConfig {
@ -310,6 +311,9 @@ dependencies {
// Scroll
implementation libs.lazycolumnscrollbar
// Kotlinx Serialization
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
/** Debugging **/
// Memory leak detection
debugImplementation libs.leakcanary.object.watcher

View File

@ -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(...);
}

View File

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- We need to be able to open links in the browser on API 30+ -->
@ -87,8 +88,11 @@
android:exported="false"
android:label="@string/title_activity_about" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<service android:name=".local.subscription.services.SubscriptionsImportService" />
<service android:name=".local.subscription.services.SubscriptionsExportService" />
<service android:name=".local.feed.service.FeedLoadService" />
<activity

View File

@ -49,11 +49,11 @@ import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
import org.schabi.newpipe.local.subscription.item.GroupsHeader
import org.schabi.newpipe.local.subscription.item.Header
import org.schabi.newpipe.local.subscription.item.ImportSubscriptionsHintPlaceholderItem
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.local.subscription.workers.SubscriptionExportWorker
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
@ -224,11 +224,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
private fun requestExportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
activity.startService(
Intent(activity, SubscriptionsExportService::class.java)
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, result.data?.data)
)
val data = result.data?.data
if (data != null && result.resultCode == Activity.RESULT_OK) {
SubscriptionExportWorker.schedule(activity, data)
}
}

View File

@ -17,94 +17,44 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<SubscriptionItem> 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<SubscriptionItem> {
if (`in` == null) {
throw InvalidSourceException("input is null")
}
final List<SubscriptionItem> channels = new ArrayList<>();
try {
final JsonObject parentObject = JsonParser.object().from(in);
if (!parentObject.has(JSON_SUBSCRIPTIONS_ARRAY_KEY)) {
throw new InvalidSourceException("Channels array is null");
}
final JsonArray channelsArray = parentObject.getArray(JSON_SUBSCRIPTIONS_ARRAY_KEY);
if (eventListener != null) {
eventListener.onSizeReceived(channelsArray.size());
}
for (final Object o : channelsArray) {
if (o instanceof JsonObject) {
final JsonObject itemObject = (JsonObject) o;
final int serviceId = itemObject.getInt(JSON_SERVICE_ID_KEY, 0);
final String url = itemObject.getString(JSON_URL_KEY);
final String name = itemObject.getString(JSON_NAME_KEY);
if (url != null && name != null && !url.isEmpty() && !name.isEmpty()) {
channels.add(new SubscriptionItem(serviceId, url, name));
if (eventListener != null) {
eventListener.onItemCompleted(name);
}
}
}
}
} catch (final Throwable e) {
throw new InvalidSourceException("Couldn't parse json", e);
@OptIn(ExperimentalSerializationApi::class)
return json.decodeFromStream<SubscriptionData>(`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<SubscriptionItem> items, final OutputStream out,
@Nullable final ImportExportEventListener eventListener) {
final JsonAppendableWriter writer = JsonWriter.on(out);
writeTo(items, writer, eventListener);
writer.done();
}
/**
* @see #writeTo(List, OutputStream, ImportExportEventListener)
* @param items the list of subscriptions items
* @param writer the output {@link JsonAppendableWriter}
* @param eventListener listener for the events generated
*/
public static void writeTo(final List<SubscriptionItem> items,
final JsonAppendableWriter writer,
@Nullable final ImportExportEventListener eventListener) {
if (eventListener != null) {
eventListener.onSizeReceived(items.size());
}
writer.object();
writer.value(JSON_APP_VERSION_KEY, BuildConfig.VERSION_NAME);
writer.value(JSON_APP_VERSION_INT_KEY, BuildConfig.VERSION_CODE);
writer.array(JSON_SUBSCRIPTIONS_ARRAY_KEY);
for (final SubscriptionItem item : items) {
writer.object();
writer.value(JSON_SERVICE_ID_KEY, item.getServiceId());
writer.value(JSON_URL_KEY, item.getUrl());
writer.value(JSON_NAME_KEY, item.getName());
writer.end();
if (eventListener != null) {
eventListener.onItemCompleted(item.getName());
}
}
writer.end();
writer.end();
@OptIn(ExperimentalSerializationApi::class)
@JvmStatic
fun writeTo(
items: List<SubscriptionItem>,
out: OutputStream,
) {
json.encodeToStream(SubscriptionData(items), out)
}
}

View File

@ -1,168 +0,0 @@
/*
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
* SubscriptionsExportService.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.local.subscription.services;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import androidx.core.content.IntentCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.streams.io.SharpOutputStream;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class SubscriptionsExportService extends BaseImportExportService {
public static final String KEY_FILE_PATH = "key_file_path";
/**
* A {@link LocalBroadcastManager local broadcast} will be made with this action
* when the export is successfully completed.
*/
public static final String EXPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
+ ".services.SubscriptionsExportService.EXPORT_COMPLETE";
private Subscription subscription;
private StoredFileHelper outFile;
private OutputStream outputStream;
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent == null || subscription != null) {
return START_NOT_STICKY;
}
final Uri path = IntentCompat.getParcelableExtra(intent, KEY_FILE_PATH, Uri.class);
if (path == null) {
stopAndReportError(new IllegalStateException(
"Exporting to a file, but the path is null"),
"Exporting subscriptions");
return START_NOT_STICKY;
}
try {
outFile = new StoredFileHelper(this, path, "application/json");
outputStream = new SharpOutputStream(outFile.getStream());
} catch (final IOException e) {
handleError(e);
return START_NOT_STICKY;
}
startExport();
return START_NOT_STICKY;
}
@Override
protected int getNotificationId() {
return 4567;
}
@Override
public int getTitle() {
return R.string.export_ongoing;
}
@Override
protected void disposeAll() {
super.disposeAll();
if (subscription != null) {
subscription.cancel();
}
}
private void startExport() {
showToast(R.string.export_ongoing);
subscriptionManager.subscriptionTable().getAll().take(1)
.map(subscriptionEntities -> {
final List<SubscriptionItem> result =
new ArrayList<>(subscriptionEntities.size());
for (final SubscriptionEntity entity : subscriptionEntities) {
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
entity.getName()));
}
return result;
})
.map(exportToFile())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriber());
}
private Subscriber<StoredFileHelper> getSubscriber() {
return new Subscriber<StoredFileHelper>() {
@Override
public void onSubscribe(final Subscription s) {
subscription = s;
s.request(1);
}
@Override
public void onNext(final StoredFileHelper file) {
if (DEBUG) {
Log.d(TAG, "startExport() success: file = " + file);
}
}
@Override
public void onError(final Throwable error) {
Log.e(TAG, "onError() called with: error = [" + error + "]", error);
handleError(error);
}
@Override
public void onComplete() {
LocalBroadcastManager.getInstance(SubscriptionsExportService.this)
.sendBroadcast(new Intent(EXPORT_COMPLETE_ACTION));
showToast(R.string.export_complete_toast);
stopService();
}
};
}
private Function<List<SubscriptionItem>, StoredFileHelper> exportToFile() {
return subscriptionItems -> {
ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener);
return outFile;
};
}
protected void handleError(final Throwable error) {
super.handleError(R.string.subscriptions_export_unsuccessful, error);
}
}

View File

@ -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<List<SubscriptionItem>> flowable = null;
switch (currentMode) {
case CHANNEL_URL_MODE:
flowable = importFromChannelUrl();
break;
case INPUT_STREAM_MODE:
flowable = importFromInputStream();
break;
case PREVIOUS_EXPORT_MODE:
flowable = importFromPreviousExport();
break;
}
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<Notification<Pair<ChannelInfo, List<ChannelTabInfo>>>>,
List<SubscriptionEntity>> upsertBatch() {
return notificationList -> {
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
new ArrayList<>(notificationList.size());
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
if (n.isOnNext()) {
infoList.add(n.getValue());
}
}
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<List<SubscriptionItem>> 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<List<SubscriptionItem>> 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<List<SubscriptionItem>> importFromPreviousExport() {
return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream, null));
return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream));
}
protected void handleError(@NonNull final Throwable error) {

View File

@ -0,0 +1,24 @@
package org.schabi.newpipe.local.subscription.workers
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.schabi.newpipe.BuildConfig
@Serializable
class SubscriptionData(
val subscriptions: List<SubscriptionItem>
) {
@SerialName("app_version")
private val appVersion = BuildConfig.VERSION_NAME
@SerialName("app_version_int")
private val appVersionInt = BuildConfig.VERSION_CODE
}
@Serializable
class SubscriptionItem(
@SerialName("service_id")
val serviceId: Int,
val url: String,
val name: String
)

View File

@ -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<SubscriptionExportWorker>()
.setInputData(data)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager
.getInstance(context)
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
}
}
}

View File

@ -862,4 +862,8 @@
<item quantity="one">%d comment</item>
<item quantity="other">%d comments</item>
</plurals>
<plurals name="export_subscriptions">
<item quantity="one">Exporting %d subscription…</item>
<item quantity="other">Exporting %d subscriptions…</item>
</plurals>
</resources>

View File

@ -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<SubscriptionItem> 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<SubscriptionItem> 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<SubscriptionItem> itemsFromFile) throws Exception {
private String testWriteTo(final List<SubscriptionItem> 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<SubscriptionItem> secondReadItems = ImportExportJsonHelper.readFrom(
inputStream, null);
inputStream);
if (secondReadItems.isEmpty()) {
fail("second call to readFrom returned an empty list");

View File

@ -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

View File

@ -56,7 +56,7 @@ teamnewpipe-filepicker = "5.0.0"
teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
teamnewpipe-newpipe-extractor = "d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e"
viewpager2 = "1.1.0-beta02"
work = "2.8.1"
work = "2.10.0"
[plugins]
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin" }
@ -67,6 +67,7 @@ kotlin-android = { id = "kotlin-android" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-kapt = { id = "kotlin-kapt" }
kotlin-parcelize = { id = "kotlin-parcelize" }
kotlinx-serialization = { id = "kotlinx-serialization" }
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
[libraries]
@ -132,6 +133,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines-rx3 = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-rx3", version.ref = "kotlinxCoroutinesRx3" }
kotlinx-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" }
leakcanary-android-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "leakcanary" }
leakcanary-object-watcher = { group = "com.squareup.leakcanary", name = "leakcanary-object-watcher-android", version.ref = "leakcanary" }