mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-01-08 00:10:32 +00:00
Convert subscription export service to a worker
This commit is contained in:
parent
00e2d66dd5
commit
173ad83e0e
@ -9,6 +9,7 @@ plugins {
|
|||||||
alias libs.plugins.kotlin.compose
|
alias libs.plugins.kotlin.compose
|
||||||
alias libs.plugins.kotlin.kapt
|
alias libs.plugins.kotlin.kapt
|
||||||
alias libs.plugins.kotlin.parcelize
|
alias libs.plugins.kotlin.parcelize
|
||||||
|
alias libs.plugins.kotlinx.serialization
|
||||||
alias libs.plugins.checkstyle
|
alias libs.plugins.checkstyle
|
||||||
alias libs.plugins.sonarqube
|
alias libs.plugins.sonarqube
|
||||||
alias libs.plugins.hilt
|
alias libs.plugins.hilt
|
||||||
@ -16,7 +17,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 34
|
compileSdk 35
|
||||||
namespace 'org.schabi.newpipe'
|
namespace 'org.schabi.newpipe'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@ -310,6 +311,9 @@ dependencies {
|
|||||||
// Scroll
|
// Scroll
|
||||||
implementation libs.lazycolumnscrollbar
|
implementation libs.lazycolumnscrollbar
|
||||||
|
|
||||||
|
// Kotlinx Serialization
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
debugImplementation libs.leakcanary.object.watcher
|
debugImplementation libs.leakcanary.object.watcher
|
||||||
|
15
app/proguard-rules.pro
vendored
15
app/proguard-rules.pro
vendored
@ -27,3 +27,18 @@
|
|||||||
|
|
||||||
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
|
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
|
||||||
-keep class org.schabi.newpipe.settings.notifications.** { *; }
|
-keep class org.schabi.newpipe.settings.notifications.** { *; }
|
||||||
|
|
||||||
|
## Keep Kotlinx Serialization classes
|
||||||
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
-keep,includedescriptorclasses class org.schabi.newpipe.**$$serializer { *; }
|
||||||
|
-keepclassmembers class org.schabi.newpipe.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class org.schabi.newpipe.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<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" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<!-- We need to be able to open links in the browser on API 30+ -->
|
<!-- We need to be able to open links in the browser on API 30+ -->
|
||||||
@ -87,8 +88,11 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/title_activity_about" />
|
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.SubscriptionsImportService" />
|
||||||
<service android:name=".local.subscription.services.SubscriptionsExportService" />
|
|
||||||
<service android:name=".local.feed.service.FeedLoadService" />
|
<service android:name=".local.feed.service.FeedLoadService" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
@ -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.GroupsHeader
|
||||||
import org.schabi.newpipe.local.subscription.item.Header
|
import org.schabi.newpipe.local.subscription.item.Header
|
||||||
import org.schabi.newpipe.local.subscription.item.ImportSubscriptionsHintPlaceholderItem
|
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
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
|
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.KEY_VALUE
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
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.NoFileManagerSafeGuard
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||||
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
import org.schabi.newpipe.ui.emptystate.setEmptyStateComposable
|
||||||
@ -224,11 +224,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun requestExportResult(result: ActivityResult) {
|
private fun requestExportResult(result: ActivityResult) {
|
||||||
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
|
val data = result.data?.data
|
||||||
activity.startService(
|
if (data != null && result.resultCode == Activity.RESULT_OK) {
|
||||||
Intent(activity, SubscriptionsExportService::class.java)
|
SubscriptionExportWorker.schedule(activity, data)
|
||||||
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, result.data?.data)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,94 +17,44 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* 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 kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import com.grack.nanojson.JsonAppendableWriter;
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import com.grack.nanojson.JsonArray;
|
import kotlinx.serialization.json.encodeToStream
|
||||||
import com.grack.nanojson.JsonObject;
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException
|
||||||
import com.grack.nanojson.JsonParser;
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionData
|
||||||
import com.grack.nanojson.JsonWriter;
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionItem
|
||||||
|
import java.io.InputStream
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import java.io.OutputStream
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException;
|
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
|
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
|
||||||
* of being able to transfer subscriptions to any device.
|
* of being able to transfer subscriptions to any device.
|
||||||
*/
|
*/
|
||||||
public final class ImportExportJsonHelper {
|
object ImportExportJsonHelper {
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
private val json = Json { encodeDefaults = true }
|
||||||
// Json implementation
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private static final String JSON_APP_VERSION_KEY = "app_version";
|
|
||||||
private static final String JSON_APP_VERSION_INT_KEY = "app_version_int";
|
|
||||||
|
|
||||||
private static final String JSON_SUBSCRIPTIONS_ARRAY_KEY = "subscriptions";
|
|
||||||
|
|
||||||
private static final String JSON_SERVICE_ID_KEY = "service_id";
|
|
||||||
private static final String JSON_URL_KEY = "url";
|
|
||||||
private static final String JSON_NAME_KEY = "name";
|
|
||||||
|
|
||||||
private ImportExportJsonHelper() { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a JSON source through the input stream.
|
* Read a JSON source through the input stream.
|
||||||
*
|
*
|
||||||
* @param in the input stream (e.g. a file)
|
* @param in the input stream (e.g. a file)
|
||||||
* @param eventListener listener for the events generated
|
|
||||||
* @return the parsed subscription items
|
* @return the parsed subscription items
|
||||||
*/
|
*/
|
||||||
public static List<SubscriptionItem> readFrom(
|
@JvmStatic
|
||||||
final InputStream in, @Nullable final ImportExportEventListener eventListener)
|
@Throws(InvalidSourceException::class)
|
||||||
throws InvalidSourceException {
|
fun readFrom(`in`: InputStream?): List<SubscriptionItem> {
|
||||||
if (in == null) {
|
if (`in` == null) {
|
||||||
throw new InvalidSourceException("input is null");
|
throw InvalidSourceException("input is null")
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<SubscriptionItem> channels = new ArrayList<>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final JsonObject parentObject = JsonParser.object().from(in);
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
return json.decodeFromStream<SubscriptionData>(`in`).subscriptions
|
||||||
if (!parentObject.has(JSON_SUBSCRIPTIONS_ARRAY_KEY)) {
|
} catch (e: Throwable) {
|
||||||
throw new InvalidSourceException("Channels array is null");
|
throw InvalidSourceException("Couldn't parse json", e)
|
||||||
}
|
|
||||||
|
|
||||||
final JsonArray channelsArray = parentObject.getArray(JSON_SUBSCRIPTIONS_ARRAY_KEY);
|
|
||||||
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onSizeReceived(channelsArray.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Object o : channelsArray) {
|
|
||||||
if (o instanceof JsonObject) {
|
|
||||||
final JsonObject itemObject = (JsonObject) o;
|
|
||||||
final int serviceId = itemObject.getInt(JSON_SERVICE_ID_KEY, 0);
|
|
||||||
final String url = itemObject.getString(JSON_URL_KEY);
|
|
||||||
final String name = itemObject.getString(JSON_NAME_KEY);
|
|
||||||
|
|
||||||
if (url != null && name != null && !url.isEmpty() && !name.isEmpty()) {
|
|
||||||
channels.add(new SubscriptionItem(serviceId, url, name));
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onItemCompleted(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
throw new InvalidSourceException("Couldn't parse json", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return channels;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,47 +62,13 @@ public final class ImportExportJsonHelper {
|
|||||||
*
|
*
|
||||||
* @param items the list of subscriptions items
|
* @param items the list of subscriptions items
|
||||||
* @param out the output stream (e.g. a file)
|
* @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,
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Nullable final ImportExportEventListener eventListener) {
|
@JvmStatic
|
||||||
final JsonAppendableWriter writer = JsonWriter.on(out);
|
fun writeTo(
|
||||||
writeTo(items, writer, eventListener);
|
items: List<SubscriptionItem>,
|
||||||
writer.done();
|
out: OutputStream,
|
||||||
}
|
) {
|
||||||
|
json.encodeToStream(SubscriptionData(items), out)
|
||||||
/**
|
|
||||||
* @see #writeTo(List, OutputStream, ImportExportEventListener)
|
|
||||||
* @param items the list of subscriptions items
|
|
||||||
* @param writer the output {@link JsonAppendableWriter}
|
|
||||||
* @param eventListener listener for the events generated
|
|
||||||
*/
|
|
||||||
public static void writeTo(final List<SubscriptionItem> items,
|
|
||||||
final JsonAppendableWriter writer,
|
|
||||||
@Nullable final ImportExportEventListener eventListener) {
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onSizeReceived(items.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.object();
|
|
||||||
|
|
||||||
writer.value(JSON_APP_VERSION_KEY, BuildConfig.VERSION_NAME);
|
|
||||||
writer.value(JSON_APP_VERSION_INT_KEY, BuildConfig.VERSION_CODE);
|
|
||||||
|
|
||||||
writer.array(JSON_SUBSCRIPTIONS_ARRAY_KEY);
|
|
||||||
for (final SubscriptionItem item : items) {
|
|
||||||
writer.object();
|
|
||||||
writer.value(JSON_SERVICE_ID_KEY, item.getServiceId());
|
|
||||||
writer.value(JSON_URL_KEY, item.getUrl());
|
|
||||||
writer.value(JSON_NAME_KEY, item.getName());
|
|
||||||
writer.end();
|
|
||||||
|
|
||||||
if (eventListener != null) {
|
|
||||||
eventListener.onItemCompleted(item.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.end();
|
|
||||||
|
|
||||||
writer.end();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -41,8 +41,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
|||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
|
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.ktx.ExceptionUtils;
|
||||||
|
import org.schabi.newpipe.local.subscription.workers.SubscriptionItem;
|
||||||
import org.schabi.newpipe.streams.io.SharpInputStream;
|
import org.schabi.newpipe.streams.io.SharpInputStream;
|
||||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
@ -50,10 +50,10 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
@ -177,18 +177,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
|||||||
private void startImport() {
|
private void startImport() {
|
||||||
showToast(R.string.import_ongoing);
|
showToast(R.string.import_ongoing);
|
||||||
|
|
||||||
Flowable<List<SubscriptionItem>> flowable = null;
|
final var flowable = switch (currentMode) {
|
||||||
switch (currentMode) {
|
case CHANNEL_URL_MODE -> importFromChannelUrl();
|
||||||
case CHANNEL_URL_MODE:
|
case INPUT_STREAM_MODE -> importFromInputStream();
|
||||||
flowable = importFromChannelUrl();
|
case PREVIOUS_EXPORT_MODE -> importFromPreviousExport();
|
||||||
break;
|
default -> null;
|
||||||
case INPUT_STREAM_MODE:
|
};
|
||||||
flowable = importFromInputStream();
|
|
||||||
break;
|
|
||||||
case PREVIOUS_EXPORT_MODE:
|
|
||||||
flowable = importFromPreviousExport();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flowable == null) {
|
if (flowable == null) {
|
||||||
final String message = "Flowable given by \"importFrom\" is 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>>>>,
|
private Function<List<Notification<Pair<ChannelInfo, List<ChannelTabInfo>>>>,
|
||||||
List<SubscriptionEntity>> upsertBatch() {
|
List<SubscriptionEntity>> upsertBatch() {
|
||||||
return notificationList -> {
|
return notificationList -> {
|
||||||
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
|
final var infoList = notificationList.stream()
|
||||||
new ArrayList<>(notificationList.size());
|
.filter(Notification::isOnNext)
|
||||||
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
|
.map(Notification::getValue)
|
||||||
if (n.isOnNext()) {
|
.collect(Collectors.toList());
|
||||||
infoList.add(n.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return subscriptionManager.upsertAll(infoList);
|
return subscriptionManager.upsertAll(infoList);
|
||||||
};
|
};
|
||||||
@ -305,7 +296,11 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
|||||||
private Flowable<List<SubscriptionItem>> importFromChannelUrl() {
|
private Flowable<List<SubscriptionItem>> importFromChannelUrl() {
|
||||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
||||||
.getSubscriptionExtractor()
|
.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() {
|
private Flowable<List<SubscriptionItem>> importFromInputStream() {
|
||||||
@ -314,11 +309,15 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
|||||||
|
|
||||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
||||||
.getSubscriptionExtractor()
|
.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() {
|
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) {
|
protected void handleError(@NonNull final Throwable error) {
|
||||||
|
@ -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
|
||||||
|
)
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -862,4 +862,8 @@
|
|||||||
<item quantity="one">%d comment</item>
|
<item quantity="one">%d comment</item>
|
||||||
<item quantity="other">%d comments</item>
|
<item quantity="other">%d comments</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="export_subscriptions">
|
||||||
|
<item quantity="one">Exporting %d subscription…</item>
|
||||||
|
<item quantity="other">Exporting %d subscriptions…</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -5,7 +5,7 @@ import static org.junit.Assert.fail;
|
|||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
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.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@ -24,7 +24,7 @@ public class ImportExportJsonHelperTest {
|
|||||||
"{\"app_version\":\"0.11.6\",\"app_version_int\": 47,\"subscriptions\":[]}";
|
"{\"app_version\":\"0.11.6\",\"app_version_int\": 47,\"subscriptions\":[]}";
|
||||||
|
|
||||||
final List<SubscriptionItem> items = ImportExportJsonHelper.readFrom(
|
final List<SubscriptionItem> items = ImportExportJsonHelper.readFrom(
|
||||||
new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8)), null);
|
new ByteArrayInputStream(emptySource.getBytes(StandardCharsets.UTF_8)));
|
||||||
assertTrue(items.isEmpty());
|
assertTrue(items.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +40,9 @@ public class ImportExportJsonHelperTest {
|
|||||||
try {
|
try {
|
||||||
if (invalidContent != null) {
|
if (invalidContent != null) {
|
||||||
final byte[] bytes = invalidContent.getBytes(StandardCharsets.UTF_8);
|
final byte[] bytes = invalidContent.getBytes(StandardCharsets.UTF_8);
|
||||||
ImportExportJsonHelper.readFrom((new ByteArrayInputStream(bytes)), null);
|
ImportExportJsonHelper.readFrom(new ByteArrayInputStream(bytes));
|
||||||
} else {
|
} else {
|
||||||
ImportExportJsonHelper.readFrom(null, null);
|
ImportExportJsonHelper.readFrom(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
fail("didn't throw exception");
|
fail("didn't throw exception");
|
||||||
@ -89,7 +89,7 @@ public class ImportExportJsonHelperTest {
|
|||||||
final InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
|
final InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
|
||||||
"import_export_test.json");
|
"import_export_test.json");
|
||||||
final List<SubscriptionItem> itemsFromFile = ImportExportJsonHelper.readFrom(
|
final List<SubscriptionItem> itemsFromFile = ImportExportJsonHelper.readFrom(
|
||||||
inputStream, null);
|
inputStream);
|
||||||
|
|
||||||
if (itemsFromFile.isEmpty()) {
|
if (itemsFromFile.isEmpty()) {
|
||||||
fail("ImportExportJsonHelper.readFrom(input) returned a null or empty list");
|
fail("ImportExportJsonHelper.readFrom(input) returned a null or empty list");
|
||||||
@ -98,10 +98,10 @@ public class ImportExportJsonHelperTest {
|
|||||||
return itemsFromFile;
|
return itemsFromFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String testWriteTo(final List<SubscriptionItem> itemsFromFile) throws Exception {
|
private String testWriteTo(final List<SubscriptionItem> itemsFromFile) {
|
||||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
ImportExportJsonHelper.writeTo(itemsFromFile, out, null);
|
ImportExportJsonHelper.writeTo(itemsFromFile, out);
|
||||||
final String jsonOut = out.toString("UTF-8");
|
final String jsonOut = out.toString(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
if (jsonOut.isEmpty()) {
|
if (jsonOut.isEmpty()) {
|
||||||
fail("JSON returned by writeTo was empty");
|
fail("JSON returned by writeTo was empty");
|
||||||
@ -114,7 +114,7 @@ public class ImportExportJsonHelperTest {
|
|||||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(
|
final ByteArrayInputStream inputStream = new ByteArrayInputStream(
|
||||||
jsonOut.getBytes(StandardCharsets.UTF_8));
|
jsonOut.getBytes(StandardCharsets.UTF_8));
|
||||||
final List<SubscriptionItem> secondReadItems = ImportExportJsonHelper.readFrom(
|
final List<SubscriptionItem> secondReadItems = ImportExportJsonHelper.readFrom(
|
||||||
inputStream, null);
|
inputStream);
|
||||||
|
|
||||||
if (secondReadItems.isEmpty()) {
|
if (secondReadItems.isEmpty()) {
|
||||||
fail("second call to readFrom returned an empty list");
|
fail("second call to readFrom returned an empty list");
|
||||||
|
@ -11,6 +11,7 @@ buildscript {
|
|||||||
classpath libs.kotlin.gradle.plugin
|
classpath libs.kotlin.gradle.plugin
|
||||||
classpath libs.hilt.android.gradle.plugin
|
classpath libs.hilt.android.gradle.plugin
|
||||||
classpath libs.aboutlibraries.plugin
|
classpath libs.aboutlibraries.plugin
|
||||||
|
classpath libs.kotlinx.serialization
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
@ -56,7 +56,7 @@ teamnewpipe-filepicker = "5.0.0"
|
|||||||
teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
teamnewpipe-nanojson = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||||
teamnewpipe-newpipe-extractor = "d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e"
|
teamnewpipe-newpipe-extractor = "d3d5f2b3f03a5f2b479b9f6fdf1c2555cbb9de0e"
|
||||||
viewpager2 = "1.1.0-beta02"
|
viewpager2 = "1.1.0-beta02"
|
||||||
work = "2.8.1"
|
work = "2.10.0"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin" }
|
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-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
kotlin-kapt = { id = "kotlin-kapt" }
|
kotlin-kapt = { id = "kotlin-kapt" }
|
||||||
kotlin-parcelize = { id = "kotlin-parcelize" }
|
kotlin-parcelize = { id = "kotlin-parcelize" }
|
||||||
|
kotlinx-serialization = { id = "kotlinx-serialization" }
|
||||||
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
||||||
|
|
||||||
[libraries]
|
[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-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" }
|
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-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" }
|
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" }
|
||||||
leakcanary-android-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "leakcanary" }
|
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" }
|
leakcanary-object-watcher = { group = "com.squareup.leakcanary", name = "leakcanary-object-watcher-android", version.ref = "leakcanary" }
|
||||||
|
Loading…
Reference in New Issue
Block a user