/* * Copyright 2019 Mauricio Colli * FeedLoadService.kt 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.feed.service import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat import androidx.core.app.ServiceCompat import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.functions.Function import org.schabi.newpipe.App import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent import java.util.concurrent.TimeUnit class FeedLoadService : Service() { companion object { private val TAG = FeedLoadService::class.java.simpleName const val NOTIFICATION_ID = 7293450 private const val ACTION_CANCEL = App.PACKAGE_NAME + ".local.feed.service.FeedLoadService.CANCEL" /** * How often the notification will be updated. */ private const val NOTIFICATION_SAMPLING_PERIOD = 1500 const val EXTRA_GROUP_ID: String = "FeedLoadService.EXTRA_GROUP_ID" } private var loadingDisposable: Disposable? = null private var notificationDisposable: Disposable? = null private lateinit var feedLoadManager: FeedLoadManager // ///////////////////////////////////////////////////////////////////////// // Lifecycle // ///////////////////////////////////////////////////////////////////////// override fun onCreate() { super.onCreate() feedLoadManager = FeedLoadManager(this) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (DEBUG) { Log.d( TAG, "onStartCommand() called with: intent = [" + intent + "]," + " flags = [" + flags + "], startId = [" + startId + "]" ) } if (intent == null || loadingDisposable != null) { return START_NOT_STICKY } setupNotification() setupBroadcastReceiver() val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) loadingDisposable = feedLoadManager.startLoading(groupId) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { startForeground(NOTIFICATION_ID, notificationBuilder.build()) } .subscribe { _, error: Throwable? -> // explicitly mark error as nullable if (error != null) { Log.e(TAG, "Error while storing result", error) handleError(error) return@subscribe } stopService() } return START_NOT_STICKY } private fun disposeAll() { unregisterReceiver(broadcastReceiver) loadingDisposable?.dispose() notificationDisposable?.dispose() } private fun stopService() { disposeAll() ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) stopSelf() } override fun onBind(intent: Intent): IBinder? { return null } // ///////////////////////////////////////////////////////////////////////// // Loading & Handling // ///////////////////////////////////////////////////////////////////////// class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) // ///////////////////////////////////////////////////////////////////////// // Notification // ///////////////////////////////////////////////////////////////////////// private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationBuilder: NotificationCompat.Builder private fun createNotification(): NotificationCompat.Builder { val cancelActionIntent = PendingIntentCompat .getBroadcast(this, NOTIFICATION_ID, Intent(ACTION_CANCEL), 0, false) return 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) .addAction(0, getString(R.string.cancel), cancelActionIntent) .setContentTitle(getString(R.string.feed_notification_loading)) } private fun setupNotification() { notificationManager = NotificationManagerCompat.from(this) notificationBuilder = createNotification() val throttleAfterFirstEmission = Function { flow: Flowable -> flow.take(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS)) } notificationDisposable = feedLoadManager.notification .publish(throttleAfterFirstEmission) .observeOn(AndroidSchedulers.mainThread()) .doOnTerminate { notificationManager.cancel(NOTIFICATION_ID) } .subscribe(this::updateNotificationProgress) } private fun updateNotificationProgress(state: FeedLoadState) { notificationBuilder.setProgress(state.maxProgress, state.currentProgress, state.maxProgress == -1) if (state.maxProgress == -1) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) notificationBuilder.setContentInfo(null) if (state.updateDescription.isNotEmpty()) notificationBuilder.setContentText(state.updateDescription) notificationBuilder.setContentText(state.updateDescription) } else { val progressText = state.currentProgress.toString() + "/" + state.maxProgress if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (state.updateDescription.isNotEmpty()) { notificationBuilder.setContentText("${state.updateDescription} ($progressText)") } } else { notificationBuilder.setContentInfo(progressText) if (state.updateDescription.isNotEmpty()) { notificationBuilder.setContentText(state.updateDescription) } } } notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) } // ///////////////////////////////////////////////////////////////////////// // Notification Actions // ///////////////////////////////////////////////////////////////////////// private lateinit var broadcastReceiver: BroadcastReceiver private fun setupBroadcastReceiver() { broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == ACTION_CANCEL) { feedLoadManager.cancel() } } } registerReceiver(broadcastReceiver, IntentFilter(ACTION_CANCEL)) } // ///////////////////////////////////////////////////////////////////////// // Error handling // ///////////////////////////////////////////////////////////////////////// private fun handleError(error: Throwable) { postEvent(ErrorResultEvent(error)) stopService() } }