Notifications about new streams

This commit is contained in:
Vasiliy 2019-05-08 20:17:54 +03:00 committed by Koitharu
parent be95fa6b2b
commit 51d972eeb9
40 changed files with 1090 additions and 27 deletions

View file

@ -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
)
}
}
}

View file

@ -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();
}
})
);
}
}

View file

@ -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);
}
}

View file

@ -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))
}
}

View file

@ -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)
)
}
}
}

View file

@ -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
}
}