/* * 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.ErrorActivity; import org.schabi.newpipe.error.ErrorInfo; 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(); ErrorActivity.reportError(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; } }