Fix loading icon in streams notifications
This commit is contained in:
parent
19fd7bc37e
commit
01f3ed0e5e
4 changed files with 67 additions and 102 deletions
|
@ -4,7 +4,6 @@ import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
@ -12,14 +11,11 @@ import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Completable
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for everything related to show notifications about new streams to the user.
|
* Helper for everything related to show notifications about new streams to the user.
|
||||||
|
@ -34,7 +30,7 @@ class NotificationHelper(val context: Context) {
|
||||||
* Show a notification about new streams from a single channel.
|
* Show a notification about new streams from a single channel.
|
||||||
* Opening the notification will open the corresponding channel page.
|
* Opening the notification will open the corresponding channel page.
|
||||||
*/
|
*/
|
||||||
fun displayNewStreamsNotification(data: FeedUpdateInfo): Completable {
|
fun displayNewStreamsNotification(data: FeedUpdateInfo) {
|
||||||
val newStreams: List<StreamInfoItem> = data.newStreams
|
val newStreams: List<StreamInfoItem> = data.newStreams
|
||||||
val summary = context.resources.getQuantityString(
|
val summary = context.resources.getQuantityString(
|
||||||
R.plurals.new_streams, newStreams.size, newStreams.size
|
R.plurals.new_streams, newStreams.size, newStreams.size
|
||||||
|
@ -59,12 +55,6 @@ class NotificationHelper(val context: Context) {
|
||||||
.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||||
.setLargeIcon(
|
|
||||||
BitmapFactory.decodeResource(
|
|
||||||
context.resources,
|
|
||||||
R.drawable.ic_newpipe_triangle_white
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
|
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
|
||||||
.setColorized(true)
|
.setColorized(true)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
@ -87,19 +77,17 @@ class NotificationHelper(val context: Context) {
|
||||||
NavigationHelper
|
NavigationHelper
|
||||||
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
|
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
else
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return Single.create(NotificationIcon(context, data.avatarUrl))
|
PicassoHelper.loadNotificationIcon(data.avatarUrl, context) { bitmap ->
|
||||||
.subscribeOn(Schedulers.io())
|
builder.setLargeIcon(bitmap)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
manager.notify(data.pseudoId, builder.build())
|
||||||
.doOnSuccess { icon ->
|
|
||||||
builder.setLargeIcon(icon)
|
|
||||||
}
|
}
|
||||||
.ignoreElement()
|
|
||||||
.onErrorComplete()
|
|
||||||
.doOnComplete { manager.notify(data.pseudoId, builder.build()) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
package org.schabi.newpipe.local.feed.notifications
|
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import com.squareup.picasso.Picasso
|
|
||||||
import com.squareup.picasso.Target
|
|
||||||
import io.reactivex.rxjava3.core.SingleEmitter
|
|
||||||
import io.reactivex.rxjava3.core.SingleOnSubscribe
|
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class to handle loading and resizing of icons
|
|
||||||
* which are used going to be used in notifications.
|
|
||||||
*/
|
|
||||||
internal class NotificationIcon(
|
|
||||||
context: Context,
|
|
||||||
private val url: String,
|
|
||||||
) : SingleOnSubscribe<Bitmap> {
|
|
||||||
|
|
||||||
private val size = getIconSize(context)
|
|
||||||
|
|
||||||
override fun subscribe(emitter: SingleEmitter<Bitmap>) {
|
|
||||||
val target = SingleEmitterTarget(emitter)
|
|
||||||
PicassoHelper.loadThumbnail(url)
|
|
||||||
.resize(size, size)
|
|
||||||
.centerCrop()
|
|
||||||
.into(target)
|
|
||||||
emitter.setCancellable {
|
|
||||||
PicassoHelper.cancelRequest(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SingleEmitterTarget(private val emitter: SingleEmitter<Bitmap>) : Target {
|
|
||||||
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom?) {
|
|
||||||
if (!emitter.isDisposed) {
|
|
||||||
emitter.onSuccess(bitmap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?) {
|
|
||||||
emitter.tryOnError(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareLoad(placeHolderDrawable: Drawable?) = Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
fun getIconSize(context: Context): Int {
|
|
||||||
val activityManager = context.getSystemService(
|
|
||||||
Context.ACTIVITY_SERVICE
|
|
||||||
) as ActivityManager?
|
|
||||||
val size1 = activityManager?.launcherLargeIconSize ?: 0
|
|
||||||
val size2 = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
|
||||||
return maxOf(size2, size1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.local.feed.notifications
|
package org.schabi.newpipe.local.feed.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
|
@ -12,7 +13,7 @@ import androidx.work.PeriodicWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.rxjava3.RxWorker
|
import androidx.work.rxjava3.RxWorker
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import org.schabi.newpipe.App
|
import org.schabi.newpipe.App
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
@ -34,30 +35,39 @@ class NotificationWorker(
|
||||||
}
|
}
|
||||||
private val feedLoadManager = FeedLoadManager(appContext)
|
private val feedLoadManager = FeedLoadManager(appContext)
|
||||||
|
|
||||||
override fun createWork(): Single<Result> = if (isEnabled(applicationContext)) {
|
override fun createWork(): Single<Result> = if (areNotificationsEnabled(applicationContext)) {
|
||||||
feedLoadManager.startLoading(
|
feedLoadManager.startLoading(
|
||||||
ignoreOutdatedThreshold = true,
|
ignoreOutdatedThreshold = true,
|
||||||
groupId = FeedLoadManager.GROUP_NOTIFICATION_ENABLED
|
groupId = FeedLoadManager.GROUP_NOTIFICATION_ENABLED
|
||||||
)
|
)
|
||||||
|
.doOnSubscribe { showLoadingFeedForegroundNotification() }
|
||||||
.map { feed ->
|
.map { feed ->
|
||||||
feed.mapNotNull { x ->
|
// filter out feedUpdateInfo items (i.e. channels) with nothing new
|
||||||
x.value?.takeIf {
|
feed.mapNotNull {
|
||||||
it.newStreams.isNotEmpty()
|
it.value?.takeIf { feedUpdateInfo ->
|
||||||
|
feedUpdateInfo.newStreams.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.doOnSubscribe { setForegroundAsync(createForegroundInfo()) }
|
.observeOn(AndroidSchedulers.mainThread()) // Picasso requires calls from main thread
|
||||||
.flatMapObservable { Observable.fromIterable(it) }
|
.map { feedUpdateInfoList ->
|
||||||
.flatMapCompletable { x -> notificationHelper.displayNewStreamsNotification(x) }
|
// display notifications for each feedUpdateInfo (i.e. channel)
|
||||||
.toSingleDefault(Result.success())
|
feedUpdateInfoList.forEach { feedUpdateInfo ->
|
||||||
|
notificationHelper.displayNewStreamsNotification(feedUpdateInfo)
|
||||||
|
}
|
||||||
|
return@map Result.success()
|
||||||
|
}
|
||||||
|
.doOnError { throwable ->
|
||||||
|
Log.e(TAG, "Error while displaying streams notifications", throwable)
|
||||||
|
// TODO show error notification
|
||||||
|
}
|
||||||
.onErrorReturnItem(Result.failure())
|
.onErrorReturnItem(Result.failure())
|
||||||
} else {
|
} else {
|
||||||
// Can be the case when the user disables notifications for NewPipe
|
// the user can disable streams notifications in the device's app settings
|
||||||
// in the device's app settings.
|
|
||||||
Single.just(Result.success())
|
Single.just(Result.success())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createForegroundInfo(): ForegroundInfo {
|
private fun showLoadingFeedForegroundNotification() {
|
||||||
val notification = NotificationCompat.Builder(
|
val notification = NotificationCompat.Builder(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
applicationContext.getString(R.string.notification_channel_id)
|
applicationContext.getString(R.string.notification_channel_id)
|
||||||
|
@ -68,14 +78,15 @@ class NotificationWorker(
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setContentTitle(applicationContext.getString(R.string.feed_notification_loading))
|
.setContentTitle(applicationContext.getString(R.string.feed_notification_loading))
|
||||||
.build()
|
.build()
|
||||||
return ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification)
|
setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification))
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = App.PACKAGE_NAME + "_streams_notifications"
|
private val TAG = NotificationWorker::class.java.simpleName
|
||||||
|
private const val WORK_TAG = App.PACKAGE_NAME + "_streams_notifications"
|
||||||
|
|
||||||
private fun isEnabled(context: Context) =
|
private fun areNotificationsEnabled(context: Context) =
|
||||||
NotificationHelper.areNewStreamsNotificationsEnabled(context) &&
|
NotificationHelper.areNewStreamsNotificationsEnabled(context) &&
|
||||||
NotificationHelper.areNotificationsEnabledOnDevice(context)
|
NotificationHelper.areNotificationsEnabledOnDevice(context)
|
||||||
|
|
||||||
|
@ -86,7 +97,7 @@ class NotificationWorker(
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun initialize(context: Context) {
|
fun initialize(context: Context) {
|
||||||
if (isEnabled(context)) {
|
if (areNotificationsEnabled(context)) {
|
||||||
schedule(context)
|
schedule(context)
|
||||||
} else {
|
} else {
|
||||||
cancel(context)
|
cancel(context)
|
||||||
|
@ -114,13 +125,13 @@ class NotificationWorker(
|
||||||
options.interval,
|
options.interval,
|
||||||
TimeUnit.MILLISECONDS
|
TimeUnit.MILLISECONDS
|
||||||
).setConstraints(constraints)
|
).setConstraints(constraints)
|
||||||
.addTag(TAG)
|
.addTag(WORK_TAG)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
WorkManager.getInstance(context)
|
WorkManager.getInstance(context)
|
||||||
.enqueueUniquePeriodicWork(
|
.enqueueUniquePeriodicWork(
|
||||||
TAG,
|
WORK_TAG,
|
||||||
if (force) {
|
if (force) {
|
||||||
ExistingPeriodicWorkPolicy.REPLACE
|
ExistingPeriodicWorkPolicy.REPLACE
|
||||||
} else {
|
} else {
|
||||||
|
@ -139,7 +150,7 @@ class NotificationWorker(
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun runNow(context: Context) {
|
fun runNow(context: Context) {
|
||||||
val request = OneTimeWorkRequestBuilder<NotificationWorker>()
|
val request = OneTimeWorkRequestBuilder<NotificationWorker>()
|
||||||
.addTag(TAG)
|
.addTag(WORK_TAG)
|
||||||
.build()
|
.build()
|
||||||
WorkManager.getInstance(context).enqueue(request)
|
WorkManager.getInstance(context).enqueue(request)
|
||||||
}
|
}
|
||||||
|
@ -149,7 +160,7 @@ class NotificationWorker(
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun cancel(context: Context) {
|
fun cancel(context: Context) {
|
||||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
import com.squareup.picasso.Cache;
|
import com.squareup.picasso.Cache;
|
||||||
import com.squareup.picasso.LruCache;
|
import com.squareup.picasso.LruCache;
|
||||||
|
@ -19,6 +21,7 @@ import org.schabi.newpipe.R;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
@ -161,6 +164,29 @@ public final class PicassoHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void loadNotificationIcon(final String url,
|
||||||
|
final Context context,
|
||||||
|
final Consumer<Bitmap> bitmapConsumer) {
|
||||||
|
loadImageDefault(url, R.drawable.ic_newpipe_triangle_white)
|
||||||
|
.into(new Target() {
|
||||||
|
@Override
|
||||||
|
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
|
||||||
|
bitmapConsumer.accept(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
|
||||||
|
bitmapConsumer.accept(BitmapFactory.decodeResource(context.getResources(),
|
||||||
|
R.drawable.ic_newpipe_triangle_white));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepareLoad(final Drawable placeHolderDrawable) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static RequestCreator loadImageDefault(final String url, final int placeholderResId) {
|
private static RequestCreator loadImageDefault(final String url, final int placeholderResId) {
|
||||||
if (!shouldLoadImages || isBlank(url)) {
|
if (!shouldLoadImages || isBlank(url)) {
|
||||||
return picassoInstance
|
return picassoInstance
|
||||||
|
|
Loading…
Reference in a new issue