Notifications about new streams
This commit is contained in:
parent
be95fa6b2b
commit
51d972eeb9
40 changed files with 1090 additions and 27 deletions
|
|
@ -0,0 +1,46 @@
|
|||
package org.schabi.newpipe.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
|
||||
data class ChannelUpdates(
|
||||
val serviceId: Int,
|
||||
val url: String,
|
||||
val avatarUrl: String,
|
||||
val name: String,
|
||||
val streams: List<StreamInfoItem>
|
||||
) {
|
||||
|
||||
val id = url.hashCode()
|
||||
|
||||
val isNotEmpty: Boolean
|
||||
get() = streams.isNotEmpty()
|
||||
|
||||
val size = streams.size
|
||||
|
||||
fun getText(context: Context): String {
|
||||
val separator = context.resources.getString(R.string.enumeration_comma) + " "
|
||||
return streams.joinToString(separator) { it.name }
|
||||
}
|
||||
|
||||
fun createOpenChannelIntent(context: Context?): Intent {
|
||||
return NavigationHelper.getChannelIntent(context, serviceId, url)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(channel: ChannelInfo, streams: List<StreamInfoItem>): ChannelUpdates {
|
||||
return ChannelUpdates(
|
||||
channel.serviceId,
|
||||
channel.url,
|
||||
channel.avatarUrl,
|
||||
channel.name,
|
||||
streams
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package org.schabi.newpipe.notifications;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class NotificationHelper {
|
||||
|
||||
private final Context context;
|
||||
private final NotificationManager manager;
|
||||
private final CompositeDisposable disposable;
|
||||
|
||||
public NotificationHelper(final Context context) {
|
||||
this.context = context;
|
||||
this.disposable = new CompositeDisposable();
|
||||
this.manager = (NotificationManager) context.getSystemService(
|
||||
Context.NOTIFICATION_SERVICE
|
||||
);
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether notifications are not disabled by user via system settings.
|
||||
*
|
||||
* @param context Context
|
||||
* @return true if notifications are allowed, false otherwise
|
||||
*/
|
||||
public static boolean isNotificationsEnabledNative(final Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
final String channelId = context.getString(R.string.streams_notification_channel_id);
|
||||
final NotificationManager manager = (NotificationManager) context
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (manager != null) {
|
||||
final NotificationChannel channel = manager.getNotificationChannel(channelId);
|
||||
return channel != null
|
||||
&& channel.getImportance() != NotificationManager.IMPORTANCE_NONE;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return NotificationManagerCompat.from(context).areNotificationsEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isNewStreamsNotificationsEnabled(@NonNull final Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.enable_streams_notifications), false)
|
||||
&& isNotificationsEnabledNative(context);
|
||||
}
|
||||
|
||||
public static void openNativeSettingsScreen(final Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
final String channelId = context.getString(R.string.streams_notification_channel_id);
|
||||
final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName())
|
||||
.putExtra(Settings.EXTRA_CHANNEL_ID, channelId);
|
||||
context.startActivity(intent);
|
||||
} else {
|
||||
final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
public void notify(final ChannelUpdates data) {
|
||||
final String summary = context.getResources().getQuantityString(
|
||||
R.plurals.new_streams, data.getSize(), data.getSize()
|
||||
);
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context,
|
||||
context.getString(R.string.streams_notification_channel_id))
|
||||
.setContentTitle(
|
||||
context.getString(R.string.notification_title_pattern,
|
||||
data.getName(),
|
||||
summary)
|
||||
)
|
||||
.setContentText(data.getText(context))
|
||||
.setNumber(data.getSize())
|
||||
.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setSmallIcon(R.drawable.ic_stat_newpipe)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(context.getResources(),
|
||||
R.drawable.ic_newpipe_triangle_white))
|
||||
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
|
||||
.setColorized(true)
|
||||
.setAutoCancel(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_SOCIAL);
|
||||
final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
|
||||
for (final StreamInfoItem stream : data.getStreams()) {
|
||||
style.addLine(stream.getName());
|
||||
}
|
||||
style.setSummaryText(summary);
|
||||
style.setBigContentTitle(data.getName());
|
||||
builder.setStyle(style);
|
||||
builder.setContentIntent(PendingIntent.getActivity(
|
||||
context,
|
||||
data.getId(),
|
||||
data.createOpenChannelIntent(context),
|
||||
0
|
||||
));
|
||||
|
||||
disposable.add(
|
||||
Single.create(new NotificationIcon(context, data.getAvatarUrl()))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doAfterTerminate(() -> manager.notify(data.getId(), builder.build()))
|
||||
.subscribe(builder::setLargeIcon, throwable -> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package org.schabi.newpipe.notifications;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageSize;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import io.reactivex.rxjava3.annotations.NonNull;
|
||||
import io.reactivex.rxjava3.core.SingleEmitter;
|
||||
import io.reactivex.rxjava3.core.SingleOnSubscribe;
|
||||
|
||||
final class NotificationIcon implements SingleOnSubscribe<Bitmap> {
|
||||
|
||||
private final String url;
|
||||
private final int size;
|
||||
|
||||
NotificationIcon(final Context context, final String url) {
|
||||
this.url = url;
|
||||
this.size = getIconSize(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(@NonNull final SingleEmitter<Bitmap> emitter) throws Throwable {
|
||||
ImageLoader.getInstance().loadImage(
|
||||
url,
|
||||
new ImageSize(size, size),
|
||||
new SimpleImageLoadingListener() {
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(final String imageUri,
|
||||
final View view,
|
||||
final FailReason failReason) {
|
||||
emitter.onError(failReason.getCause());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(final String imageUri,
|
||||
final View view,
|
||||
final Bitmap loadedImage) {
|
||||
emitter.onSuccess(loadedImage);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static int getIconSize(final Context context) {
|
||||
final ActivityManager activityManager = (ActivityManager) context.getSystemService(
|
||||
Context.ACTIVITY_SERVICE
|
||||
);
|
||||
final int size2 = activityManager != null ? activityManager.getLauncherLargeIconSize() : 0;
|
||||
final int size1 = context.getResources()
|
||||
.getDimensionPixelSize(android.R.dimen.app_icon_size);
|
||||
return Math.max(size2, size1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package org.schabi.newpipe.notifications
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.RxWorker
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
import org.schabi.newpipe.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NotificationWorker(
|
||||
appContext: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : RxWorker(appContext, workerParams) {
|
||||
|
||||
private val notificationHelper by lazy {
|
||||
NotificationHelper(appContext)
|
||||
}
|
||||
|
||||
override fun createWork() = if (isEnabled(applicationContext)) {
|
||||
Flowable.create(
|
||||
SubscriptionUpdates(applicationContext),
|
||||
BackpressureStrategy.BUFFER
|
||||
).doOnNext { notificationHelper.notify(it) }
|
||||
.toList()
|
||||
.map { Result.success() }
|
||||
.onErrorReturnItem(Result.failure())
|
||||
} else Single.just(Result.success())
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "notifications"
|
||||
|
||||
private fun isEnabled(context: Context): Boolean {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(
|
||||
context.getString(R.string.enable_streams_notifications),
|
||||
false
|
||||
) && NotificationHelper.isNotificationsEnabledNative(context)
|
||||
}
|
||||
|
||||
fun schedule(context: Context, options: ScheduleOptions, force: Boolean = false) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(
|
||||
if (options.isRequireNonMeteredNetwork) {
|
||||
NetworkType.UNMETERED
|
||||
} else {
|
||||
NetworkType.CONNECTED
|
||||
}
|
||||
).build()
|
||||
val request = PeriodicWorkRequest.Builder(
|
||||
NotificationWorker::class.java,
|
||||
options.interval,
|
||||
TimeUnit.MILLISECONDS
|
||||
).setConstraints(constraints)
|
||||
.addTag(TAG)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniquePeriodicWork(
|
||||
TAG,
|
||||
if (force) {
|
||||
ExistingPeriodicWorkPolicy.REPLACE
|
||||
} else {
|
||||
ExistingPeriodicWorkPolicy.KEEP
|
||||
},
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun schedule(context: Context) = schedule(context, ScheduleOptions.from(context))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package org.schabi.newpipe.notifications
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.schabi.newpipe.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class ScheduleOptions(
|
||||
val interval: Long,
|
||||
val isRequireNonMeteredNetwork: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(context: Context): ScheduleOptions {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return ScheduleOptions(
|
||||
interval = TimeUnit.HOURS.toMillis(
|
||||
preferences.getString(
|
||||
context.getString(R.string.streams_notifications_interval_key),
|
||||
context.getString(R.string.streams_notifications_interval_default)
|
||||
)?.toLongOrNull() ?: context.getString(
|
||||
R.string.streams_notifications_interval_default
|
||||
).toLong()
|
||||
),
|
||||
isRequireNonMeteredNetwork = preferences.getString(
|
||||
context.getString(R.string.streams_notifications_network_key),
|
||||
context.getString(R.string.streams_notifications_network_default)
|
||||
) == context.getString(R.string.streams_notifications_network_wifi)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package org.schabi.newpipe.notifications
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.FlowableEmitter
|
||||
import io.reactivex.FlowableOnSubscribe
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
|
||||
class SubscriptionUpdates(context: Context) : FlowableOnSubscribe<ChannelUpdates?> {
|
||||
|
||||
private val subscriptionManager = SubscriptionManager(context)
|
||||
private val streamTable = NewPipeDatabase.getInstance(context).streamDAO()
|
||||
|
||||
override fun subscribe(emitter: FlowableEmitter<ChannelUpdates?>) {
|
||||
try {
|
||||
val subscriptions = subscriptionManager.subscriptions().blockingFirst()
|
||||
for (subscription in subscriptions) {
|
||||
if (subscription.notificationMode != NotificationMode.DISABLED) {
|
||||
val channel = ExtractorHelper.getChannelInfo(
|
||||
subscription.serviceId,
|
||||
subscription.url, true
|
||||
).blockingGet()
|
||||
val updates = ChannelUpdates.from(channel, filterStreams(channel.relatedItems))
|
||||
if (updates.isNotEmpty) {
|
||||
emitter.onNext(updates)
|
||||
// prevent duplicated notifications
|
||||
streamTable.upsertAll(updates.streams.map { StreamEntity(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
emitter.onComplete()
|
||||
} catch (e: Exception) {
|
||||
emitter.onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterStreams(list: List<*>): List<StreamInfoItem> {
|
||||
val streams = ArrayList<StreamInfoItem>(list.size)
|
||||
for (o in list) {
|
||||
if (o is StreamInfoItem) {
|
||||
if (streamTable.exists(o.serviceId.toLong(), o.url)) {
|
||||
break
|
||||
}
|
||||
streams.add(o)
|
||||
}
|
||||
}
|
||||
return streams
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue