From da9bd1d420ebc55c9697ba996caf79d793317a2f Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Wed, 8 May 2019 20:17:54 +0300 Subject: [PATCH] Notifications about new streams --- app/build.gradle | 3 + app/src/debug/res/xml/main_settings.xml | 6 + app/src/main/java/org/schabi/newpipe/App.java | 18 ++- .../java/org/schabi/newpipe/MainActivity.java | 3 +- .../org/schabi/newpipe/NewPipeDatabase.java | 13 +- .../schabi/newpipe/database/AppDatabase.java | 6 +- .../schabi/newpipe/database/Migrations.java | 12 +- .../newpipe/database/stream/dao/StreamDAO.kt | 3 + .../subscription/NotificationMode.java | 14 ++ .../subscription/SubscriptionEntity.java | 13 ++ .../list/channel/ChannelFragment.java | 77 ++++++++-- .../local/subscription/SubscriptionManager.kt | 23 +++ .../newpipe/notifications/ChannelUpdates.kt | 46 ++++++ .../notifications/NotificationHelper.java | 137 ++++++++++++++++++ .../notifications/NotificationIcon.java | 60 ++++++++ .../notifications/NotificationWorker.kt | 82 +++++++++++ .../newpipe/notifications/ScheduleOptions.kt | 33 +++++ .../notifications/SubscriptionUpdates.kt | 53 +++++++ .../settings/NotificationsSettingsFragment.kt | 112 ++++++++++++++ .../NotificationsChannelsConfigFragment.java | 84 +++++++++++ .../NotificationsConfigAdapter.kt | 114 +++++++++++++++ .../schabi/newpipe/util/NavigationHelper.java | 6 + .../drawable-anydpi-v24/ic_stat_newpipe.xml | 14 ++ .../res/drawable-hdpi/ic_stat_newpipe.png | Bin 0 -> 413 bytes .../res/drawable-mdpi/ic_stat_newpipe.png | Bin 0 -> 294 bytes .../res/drawable-night/ic_notifications.xml | 9 ++ .../res/drawable-xhdpi/ic_stat_newpipe.png | Bin 0 -> 522 bytes .../res/drawable-xxhdpi/ic_stat_newpipe.png | Bin 0 -> 731 bytes .../main/res/drawable/ic_notifications.xml | 9 ++ .../fragment_channels_notifications.xml | 14 ++ .../res/layout/item_notification_config.xml | 16 ++ app/src/main/res/menu/menu_channel.xml | 7 + app/src/main/res/values-ru/strings.xml | 21 +++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rHK/strings.xml | 1 + app/src/main/res/values/settings_keys.xml | 30 ++++ app/src/main/res/values/strings.xml | 27 +++- .../main/res/xml/notifications_settings.xml | 41 ++++++ app/src/release/res/xml/main_settings.xml | 9 +- assets/db.dia | Bin 3129 -> 3167 bytes 40 files changed, 1090 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java create mode 100644 app/src/main/java/org/schabi/newpipe/notifications/ChannelUpdates.kt create mode 100644 app/src/main/java/org/schabi/newpipe/notifications/NotificationHelper.java create mode 100644 app/src/main/java/org/schabi/newpipe/notifications/NotificationIcon.java create mode 100644 app/src/main/java/org/schabi/newpipe/notifications/NotificationWorker.kt create mode 100644 app/src/main/java/org/schabi/newpipe/notifications/ScheduleOptions.kt create mode 100644 app/src/main/java/org/schabi/newpipe/notifications/SubscriptionUpdates.kt create mode 100644 app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt create mode 100644 app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationsChannelsConfigFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationsConfigAdapter.kt create mode 100644 app/src/main/res/drawable-anydpi-v24/ic_stat_newpipe.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_stat_newpipe.png create mode 100644 app/src/main/res/drawable-mdpi/ic_stat_newpipe.png create mode 100644 app/src/main/res/drawable-night/ic_notifications.xml create mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_newpipe.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_stat_newpipe.png create mode 100644 app/src/main/res/drawable/ic_notifications.xml create mode 100644 app/src/main/res/layout/fragment_channels_notifications.xml create mode 100644 app/src/main/res/layout/item_notification_config.xml create mode 100644 app/src/main/res/xml/notifications_settings.xml diff --git a/app/build.gradle b/app/build.gradle index 1219aeb33..61a0cdc2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -108,6 +108,7 @@ ext { leakCanaryVersion = '2.5' stethoVersion = '1.6.0' mockitoVersion = '3.6.0' + workVersion = '2.5.0' } configurations { @@ -213,6 +214,8 @@ dependencies { implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.webkit:webkit:1.4.0' implementation 'com.google.android.material:material:1.2.1' + implementation "androidx.work:work-runtime:${workVersion}" + implementation "androidx.work:work-rxjava2:${workVersion}" /** Third-party libraries **/ // Instance state boilerplate elimination diff --git a/app/src/debug/res/xml/main_settings.xml b/app/src/debug/res/xml/main_settings.xml index d482d033c..4e812bb1c 100644 --- a/app/src/debug/res/xml/main_settings.xml +++ b/app/src/debug/res/xml/main_settings.xml @@ -40,6 +40,12 @@ android:title="@string/settings_category_notification_title" app:iconSpaceReserved="false" /> + + { @Insert(onConflict = OnConflictStrategy.IGNORE) internal abstract fun silentInsertAllInternal(streams: List): List + @Query("SELECT COUNT(*) != 0 FROM streams WHERE url = :url AND service_id = :serviceId") + internal abstract fun exists(serviceId: Long, url: String?): Boolean + @Query( """ SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java new file mode 100644 index 000000000..d817032ee --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java @@ -0,0 +1,14 @@ +package org.schabi.newpipe.database.subscription; + +import androidx.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@IntDef({NotificationMode.DISABLED, NotificationMode.ENABLED_DEFAULT}) +@Retention(RetentionPolicy.SOURCE) +public @interface NotificationMode { + + int DISABLED = 0; + int ENABLED_DEFAULT = 1; + //other values reserved for the future +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index 1cf38dbca..0e4bda490 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -26,6 +26,7 @@ public class SubscriptionEntity { public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url"; public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count"; public static final String SUBSCRIPTION_DESCRIPTION = "description"; + public static final String SUBSCRIPTION_NOTIFICATION_MODE = "notification_mode"; @PrimaryKey(autoGenerate = true) private long uid = 0; @@ -48,6 +49,9 @@ public class SubscriptionEntity { @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) private String description; + @ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE) + private int notificationMode; + @Ignore public static SubscriptionEntity from(@NonNull final ChannelInfo info) { final SubscriptionEntity result = new SubscriptionEntity(); @@ -114,6 +118,15 @@ public class SubscriptionEntity { this.description = description; } + @NotificationMode + public int getNotificationMode() { + return notificationMode; + } + + public void setNotificationMode(@NotificationMode final int notificationMode) { + this.notificationMode = notificationMode; + } + @Ignore public void setData(final String n, final String au, final String d, final Long sc) { this.setName(n); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 548ae7b2c..754036dfd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -1,6 +1,11 @@ package org.schabi.newpipe.fragments.list.channel; +import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor; +import static org.schabi.newpipe.ktx.ViewUtils.animate; +import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor; + import android.content.Context; +import android.graphics.Color; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; @@ -19,9 +24,11 @@ import androidx.appcompat.app.ActionBar; import androidx.core.content.ContextCompat; import androidx.viewbinding.ViewBinding; +import com.google.android.material.snackbar.Snackbar; import com.jakewharton.rxbinding4.view.RxView; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.subscription.NotificationMode; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.databinding.ChannelHeaderBinding; import org.schabi.newpipe.databinding.FragmentChannelBinding; @@ -37,6 +44,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.local.subscription.SubscriptionManager; +import org.schabi.newpipe.notifications.NotificationHelper; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.util.ExtractorHelper; @@ -60,10 +68,6 @@ import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.schedulers.Schedulers; -import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor; -import static org.schabi.newpipe.ktx.ViewUtils.animate; -import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor; - public class ChannelFragment extends BaseListInfoFragment implements View.OnClickListener { @@ -84,6 +88,7 @@ public class ChannelFragment extends BaseListInfoFragment private PlaylistControlBinding playlistControlBinding; private MenuItem menuRssButton; + private MenuItem menuNotifyButton; public static ChannelFragment getInstance(final int serviceId, final String url, final String name) { @@ -181,6 +186,7 @@ public class ChannelFragment extends BaseListInfoFragment + "menu = [" + menu + "], inflater = [" + inflater + "]"); } menuRssButton = menu.findItem(R.id.menu_item_rss); + menuNotifyButton = menu.findItem(R.id.menu_item_notify); } } @@ -197,6 +203,11 @@ public class ChannelFragment extends BaseListInfoFragment case R.id.action_settings: NavigationHelper.openSettings(requireContext()); break; + case R.id.menu_item_notify: + final boolean value = !item.isChecked(); + item.setEnabled(false); + setNotify(value); + break; case R.id.menu_item_rss: openRssFeed(); break; @@ -238,15 +249,22 @@ public class ChannelFragment extends BaseListInfoFragment .subscribe(getSubscribeUpdateMonitor(info), onError)); disposables.add(observable - // Some updates are very rapid - // (for example when calling the updateSubscription(info)) - // so only update the UI for the latest emission - // ("sync" the subscribe button's state) - .debounce(100, TimeUnit.MILLISECONDS) + .map(List::isEmpty) + .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) - .subscribe((List subscriptionEntities) -> - updateSubscribeButton(!subscriptionEntities.isEmpty()), onError)); + .subscribe((Boolean isEmpty) -> updateSubscribeButton(!isEmpty), onError)); + disposables.add(observable + .map(List::isEmpty) + .filter(x -> NotificationHelper.isNewStreamsNotificationsEnabled(requireContext())) + .distinctUntilChanged() + .skip(1) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(isEmpty -> { + if (!isEmpty) { + showNotifySnackbar(); + } + }, onError)); } private Function mapOnSubscribe(final SubscriptionEntity subscription, @@ -326,6 +344,7 @@ public class ChannelFragment extends BaseListInfoFragment info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount()); + updateNotifyButton(null); subscribeButtonMonitor = monitorSubscribeButton( headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info)); } else { @@ -333,6 +352,7 @@ public class ChannelFragment extends BaseListInfoFragment Log.d(TAG, "Found subscription to this channel!"); } final SubscriptionEntity subscription = subscriptionEntities.get(0); + updateNotifyButton(subscription); subscribeButtonMonitor = monitorSubscribeButton( headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription)); } @@ -375,6 +395,41 @@ public class ChannelFragment extends BaseListInfoFragment AnimationType.LIGHT_SCALE_AND_ALPHA); } + private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) { + if (menuNotifyButton == null) { + return; + } + if (subscription == null) { + menuNotifyButton.setVisible(false); + } else { + menuNotifyButton.setEnabled( + NotificationHelper.isNewStreamsNotificationsEnabled(requireContext()) + ); + menuNotifyButton.setChecked( + subscription.getNotificationMode() != NotificationMode.DISABLED + ); + menuNotifyButton.setVisible(true); + } + } + + private void setNotify(final boolean isEnabled) { + final int mode = isEnabled ? NotificationMode.ENABLED_DEFAULT : NotificationMode.DISABLED; + disposables.add( + subscriptionManager.updateNotificationMode(currentInfo.getServiceId(), + currentInfo.getUrl(), mode) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe() + ); + } + + private void showNotifySnackbar() { + Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG) + .setAction(R.string.get_notified, v -> setNotify(true)) + .setActionTextColor(Color.YELLOW) + .show(); + } + /*////////////////////////////////////////////////////////////////////////// // Load and handle //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index fb9cffa98..442a867b3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -7,6 +7,8 @@ import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.schedulers.Schedulers import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.subscription.NotificationMode import org.schabi.newpipe.database.subscription.SubscriptionDAO import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.extractor.ListInfo @@ -14,6 +16,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.feed.FeedInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.FeedDatabaseManager +import org.schabi.newpipe.util.ExtractorHelper class SubscriptionManager(context: Context) { private val database = NewPipeDatabase.getInstance(context) @@ -66,6 +69,16 @@ class SubscriptionManager(context: Context) { } } + fun updateNotificationMode(serviceId: Int, url: String?, @NotificationMode mode: Int): Completable { + return subscriptionTable().getSubscription(serviceId, url!!) + .flatMapCompletable { entity: SubscriptionEntity -> + Completable.fromAction { + entity.notificationMode = mode + subscriptionTable().update(entity) + }.andThen(rememberLastStream(entity)) + } + } + fun updateFromInfo(subscriptionId: Long, info: ListInfo) { val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId) @@ -94,4 +107,14 @@ class SubscriptionManager(context: Context) { fun deleteSubscription(subscriptionEntity: SubscriptionEntity) { subscriptionTable.delete(subscriptionEntity) } + + private fun rememberLastStream(subscription: SubscriptionEntity): Completable { + return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false) + .map { channel -> channel.relatedItems.map { stream -> StreamEntity(stream) } } + .flatMapCompletable { entities -> + Completable.fromAction { + database.streamDAO().upsertAll(entities) + } + }.onErrorComplete() + } } diff --git a/app/src/main/java/org/schabi/newpipe/notifications/ChannelUpdates.kt b/app/src/main/java/org/schabi/newpipe/notifications/ChannelUpdates.kt new file mode 100644 index 000000000..9a3b2cbf3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/notifications/ChannelUpdates.kt @@ -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 +) { + + 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): ChannelUpdates { + return ChannelUpdates( + channel.serviceId, + channel.url, + channel.avatarUrl, + channel.name, + streams + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/notifications/NotificationHelper.java b/app/src/main/java/org/schabi/newpipe/notifications/NotificationHelper.java new file mode 100644 index 000000000..6207cd613 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/notifications/NotificationHelper.java @@ -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(); + } + }) + ); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/notifications/NotificationIcon.java b/app/src/main/java/org/schabi/newpipe/notifications/NotificationIcon.java new file mode 100644 index 000000000..fc59b55f0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/notifications/NotificationIcon.java @@ -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 { + + 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 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); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/notifications/NotificationWorker.kt b/app/src/main/java/org/schabi/newpipe/notifications/NotificationWorker.kt new file mode 100644 index 000000000..24dbc82e0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/notifications/NotificationWorker.kt @@ -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)) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/notifications/ScheduleOptions.kt b/app/src/main/java/org/schabi/newpipe/notifications/ScheduleOptions.kt new file mode 100644 index 000000000..b0617b303 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/notifications/ScheduleOptions.kt @@ -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) + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/notifications/SubscriptionUpdates.kt b/app/src/main/java/org/schabi/newpipe/notifications/SubscriptionUpdates.kt new file mode 100644 index 000000000..6f7c3881b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/notifications/SubscriptionUpdates.kt @@ -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 { + + private val subscriptionManager = SubscriptionManager(context) + private val streamTable = NewPipeDatabase.getInstance(context).streamDAO() + + override fun subscribe(emitter: FlowableEmitter) { + 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 { + val streams = ArrayList(list.size) + for (o in list) { + if (o is StreamInfoItem) { + if (streamTable.exists(o.serviceId.toLong(), o.url)) { + break + } + streams.add(o) + } + } + return streams + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt new file mode 100644 index 000000000..62a819e64 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt @@ -0,0 +1,112 @@ +package org.schabi.newpipe.settings + +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.graphics.Color +import android.os.Bundle +import androidx.preference.Preference +import com.google.android.material.snackbar.Snackbar +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.Disposable +import org.schabi.newpipe.R +import org.schabi.newpipe.database.subscription.NotificationMode +import org.schabi.newpipe.database.subscription.SubscriptionEntity +import org.schabi.newpipe.error.ErrorActivity +import org.schabi.newpipe.error.ErrorInfo +import org.schabi.newpipe.error.UserAction +import org.schabi.newpipe.local.subscription.SubscriptionManager +import org.schabi.newpipe.notifications.NotificationHelper +import org.schabi.newpipe.notifications.NotificationWorker +import org.schabi.newpipe.notifications.ScheduleOptions + +class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferenceChangeListener { + + private var notificationWarningSnackbar: Snackbar? = null + private var loader: Disposable? = null + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.notifications_settings) + } + + override fun onStart() { + super.onStart() + defaultPreferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onStop() { + defaultPreferences.unregisterOnSharedPreferenceChangeListener(this) + super.onStop() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + val context = context ?: return + if (key == getString(R.string.streams_notifications_interval_key) || key == getString(R.string.streams_notifications_network_key)) { + NotificationWorker.schedule(context, ScheduleOptions.from(context), true) + } + } + + override fun onResume() { + super.onResume() + val enabled = NotificationHelper.isNotificationsEnabledNative(context) + preferenceScreen.isEnabled = enabled + if (!enabled) { + if (notificationWarningSnackbar == null) { + notificationWarningSnackbar = Snackbar.make( + listView, + R.string.notifications_disabled, + Snackbar.LENGTH_INDEFINITE + ).apply { + setAction(R.string.settings) { v -> + NotificationHelper.openNativeSettingsScreen(v.context) + } + setActionTextColor(Color.YELLOW) + addCallback(object : Snackbar.Callback() { + override fun onDismissed(transientBottomBar: Snackbar, event: Int) { + super.onDismissed(transientBottomBar, event) + notificationWarningSnackbar = null + } + }) + show() + } + } + } else { + notificationWarningSnackbar?.dismiss() + notificationWarningSnackbar = null + } + loader?.dispose() + loader = SubscriptionManager(requireContext()) + .subscriptions() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::updateSubscriptions, this::onError) + } + + override fun onPause() { + loader?.dispose() + loader = null + super.onPause() + } + + private fun updateSubscriptions(subscriptions: List) { + var notified = 0 + for (subscription in subscriptions) { + if (subscription.notificationMode != NotificationMode.DISABLED) { + notified++ + } + } + val preference = findPreference(getString(R.string.streams_notifications_channels_key)) + if (preference != null) { + preference.summary = preference.context.getString( + R.string.streams_notifications_channels_summary, + notified, + subscriptions.size + ) + } + } + + private fun onError(e: Throwable) { + ErrorActivity.reportErrorInSnackbar( + this, + ErrorInfo(e, UserAction.SUBSCRIPTION_GET, "Get subscriptions list") + ) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationsChannelsConfigFragment.java b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationsChannelsConfigFragment.java new file mode 100644 index 000000000..7aa0826e5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationsChannelsConfigFragment.java @@ -0,0 +1,84 @@ +package org.schabi.newpipe.settings.notifications; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.subscription.NotificationMode; +import org.schabi.newpipe.local.subscription.SubscriptionManager; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public final class NotificationsChannelsConfigFragment extends Fragment + implements NotificationsConfigAdapter.ModeToggleListener { + + private NotificationsConfigAdapter adapter; + @Nullable + private Disposable loader = null; + private CompositeDisposable updaters; + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + adapter = new NotificationsConfigAdapter(this); + updaters = new CompositeDisposable(); + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_channels_notifications, container, false); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final RecyclerView recyclerView = view.findViewById(R.id.recycler_view); + recyclerView.setAdapter(adapter); + } + + @Override + public void onActivityCreated(@Nullable final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (loader != null) { + loader.dispose(); + } + loader = new SubscriptionManager(requireContext()) + .subscriptions() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(adapter::update); + } + + @Override + public void onDestroy() { + if (loader != null) { + loader.dispose(); + } + updaters.dispose(); + super.onDestroy(); + } + + @Override + public void onModeToggle(final int position, @NotificationMode final int mode) { + final NotificationsConfigAdapter.SubscriptionItem subscription = adapter.getItem(position); + updaters.add( + new SubscriptionManager(requireContext()) + .updateNotificationMode(subscription.getServiceId(), + subscription.getUrl(), mode) + .subscribeOn(Schedulers.io()) + .subscribe() + ); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationsConfigAdapter.kt b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationsConfigAdapter.kt new file mode 100644 index 000000000..44d2256af --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationsConfigAdapter.kt @@ -0,0 +1,114 @@ +package org.schabi.newpipe.settings.notifications + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CheckedTextView +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import org.schabi.newpipe.R +import org.schabi.newpipe.database.subscription.NotificationMode +import org.schabi.newpipe.database.subscription.SubscriptionEntity +import org.schabi.newpipe.settings.notifications.NotificationsConfigAdapter.SubscriptionHolder + +class NotificationsConfigAdapter( + private val listener: ModeToggleListener +) : RecyclerView.Adapter() { + + private val differ = AsyncListDiffer(this, DiffCallback()) + + init { + setHasStableIds(true) + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): SubscriptionHolder { + val view = LayoutInflater.from(viewGroup.context) + .inflate(R.layout.item_notification_config, viewGroup, false) + return SubscriptionHolder(view, listener) + } + + override fun onBindViewHolder(subscriptionHolder: SubscriptionHolder, i: Int) { + subscriptionHolder.bind(differ.currentList[i]) + } + + fun getItem(position: Int): SubscriptionItem = differ.currentList[position] + + override fun getItemCount() = differ.currentList.size + + override fun getItemId(position: Int): Long { + return differ.currentList[position].id + } + + fun update(newData: List) { + differ.submitList( + newData.map { + SubscriptionItem( + id = it.uid, + title = it.name, + notificationMode = it.notificationMode, + serviceId = it.serviceId, + url = it.url + ) + } + ) + } + + data class SubscriptionItem( + val id: Long, + val title: String, + @NotificationMode + val notificationMode: Int, + val serviceId: Int, + val url: String + ) + + class SubscriptionHolder( + itemView: View, + private val listener: ModeToggleListener + ) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + + private val checkedTextView = itemView as CheckedTextView + + init { + itemView.setOnClickListener(this) + } + + fun bind(data: SubscriptionItem) { + checkedTextView.text = data.title + checkedTextView.isChecked = data.notificationMode != NotificationMode.DISABLED + } + + override fun onClick(v: View) { + val mode = if (checkedTextView.isChecked) { + NotificationMode.DISABLED + } else { + NotificationMode.ENABLED_DEFAULT + } + listener.onModeToggle(adapterPosition, mode) + } + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: SubscriptionItem, newItem: SubscriptionItem): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: SubscriptionItem, newItem: SubscriptionItem): Boolean { + return oldItem == newItem + } + + override fun getChangePayload(oldItem: SubscriptionItem, newItem: SubscriptionItem): Any? { + if (oldItem.notificationMode != newItem.notificationMode) { + return newItem.notificationMode + } else { + return super.getChangePayload(oldItem, newItem) + } + } + } + + interface ModeToggleListener { + fun onModeToggle(position: Int, @NotificationMode mode: Int) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index eba24020f..859bfa31d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -571,6 +571,12 @@ public final class NavigationHelper { return getOpenIntent(context, url, service.getServiceId(), linkType); } + public static Intent getChannelIntent(final Context context, + final int serviceId, + final String url) { + return getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL); + } + /** * Start an activity to install Kore. * diff --git a/app/src/main/res/drawable-anydpi-v24/ic_stat_newpipe.xml b/app/src/main/res/drawable-anydpi-v24/ic_stat_newpipe.xml new file mode 100644 index 000000000..e95f5b4ac --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v24/ic_stat_newpipe.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_stat_newpipe.png b/app/src/main/res/drawable-hdpi/ic_stat_newpipe.png new file mode 100644 index 0000000000000000000000000000000000000000..dc08d67ff89167b42ab133658c4a42d127aca7a5 GIT binary patch literal 413 zcmV;O0b>4%P)QuB#Tw7U`%%oFC2aV032bBx#yy1-$Tux)hO3M%RC0Ua^Em zD?$3XW-RMBuQXQ}vl0~Ar%RtEX?D?LB`CD-u>N3Inm4QExyFkl00000NkvXX Hu0mjfv&*yy literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_stat_newpipe.png b/app/src/main/res/drawable-mdpi/ic_stat_newpipe.png new file mode 100644 index 0000000000000000000000000000000000000000..4af6c74df80b88fb6208df08da68e02ffbc98f94 GIT binary patch literal 294 zcmV+>0oneEP) + + diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_newpipe.png b/app/src/main/res/drawable-xhdpi/ic_stat_newpipe.png new file mode 100644 index 0000000000000000000000000000000000000000..5c5750ce576d073072a26b280b0a95e62305405e GIT binary patch literal 522 zcmV+l0`>igP)*Znnf&%5_L%L)@FOvc!@U8Y?i0hU39R)ID9 zFVA2CM70VyxddIB1)OYyTFnBKH!uqdwF)@71B03coa}=}%>tB9umDQ53OIQHBOphs zfRkg;B2i!-95E%n!4fE!BoH&-4}*KA$P*X?d13_2m?AI--kBm7phJv+Yiu3Z@vuHN zK(!bF*EK!hiYbuO8;{I0dcJPGlT`M=%OGD7>CE&{)kbnXhI+bCDfttAm%|ZsEbepMu+H96xPL(E?&$!iI=(v9wbBXQjrG_ z)*%tXgHhnYiXbWv5eWumNKl&F@56WC1Gm}3J3BV7zXzV%?hHIIGv9pgZV(Vc2qA>5 zkjv#R!way4HS!0#kz3(8tY?vb2Xg#@18^71C?g|n z?NjY7JZ|b9s0@6p@@Op+xj&V zKSYl?(6a13+xaU)jDcRjBY`zy2;^QWFRuAZ<$oIJ4BQ!5X00~u8Hj-xDC7y;$3Uz3 z08a($ojifuCVQ9rK~z9i1zNxVEG!Txxk+UT1JxutsU(CDLI^1ie*xq30c~$9$P@qo N002ovPDHLkV1g)7OiKU& literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml new file mode 100644 index 000000000..024381816 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_channels_notifications.xml b/app/src/main/res/layout/fragment_channels_notifications.xml new file mode 100644 index 000000000..d1ae01bfe --- /dev/null +++ b/app/src/main/res/layout/fragment_channels_notifications.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification_config.xml b/app/src/main/res/layout/item_notification_config.xml new file mode 100644 index 000000000..b68692dd7 --- /dev/null +++ b/app/src/main/res/layout/item_notification_config.xml @@ -0,0 +1,16 @@ + + diff --git a/app/src/main/res/menu/menu_channel.xml b/app/src/main/res/menu/menu_channel.xml index af9020626..d6c54b680 100644 --- a/app/src/main/res/menu/menu_channel.xml +++ b/app/src/main/res/menu/menu_channel.xml @@ -17,6 +17,13 @@ android:title="@string/share" app:showAsAction="ifRoom" /> + + %s видео %s видео + + %s новое видео + %s новых видео + %s новых видео + Удалить этот элемент из истории поиска? Главная страница Пустая страница @@ -683,4 +688,20 @@ Удалять элементы смахиванием Не начинать просмотр видео в мини-плеере, но сразу переключиться в полноэкранный режим, если автовращение экрана заблокировано. Вы можете переключиться на мини-плеер, выйдя из полноэкранного режима. Начинать просмотр в полноэкранном режиме + Уведомления + Новые видео + Уведомления о новых видео в подписках + Частота проверки + Уведомлять о новых видео + Получать уведомления о новых видео из каналов, на которые Вы подписаны + Каждый час + Каждые 2 часа + Каждые 3 часа + Дважды в день + Каждый день + Тип подключения + Любая сеть + Уведомления отключены + Уведомлять + Вы подписались на канал \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e5180c51e..91166c754 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -443,4 +443,5 @@ 快进 / 快退的单位时间 清除下载历史记录 删除下载了的文件 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 63ca8f827..1592b8b59 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -132,4 +132,5 @@ 使用粗略快查 添加到 選擇標籤 + \ No newline at end of file diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 9261dfae1..42d2233c8 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -1260,4 +1260,34 @@ recaptcha_cookies_key + enable_streams_notifications + streams_notifications_interval + 3 + + 1 + 2 + 3 + 12 + 24 + + + @string/every_hour + @string/every_two_hours + @string/every_three_hours + @string/twice_per_day + @string/every_day + + streams_notifications_network + any + wifi + @string/streams_notifications_network_wifi + + @string/streams_notifications_network_any + @string/streams_notifications_network_wifi + + + @string/any_network + @string/wifi_only + + streams_notifications_channels diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5600b5e6..f84cc83b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + @@ -180,6 +180,7 @@ Always Just Once File + Notifications newpipe NewPipe Notification Notifications for NewPipe background and popup players @@ -189,6 +190,9 @@ newpipeHash Video Hash Notification Notifications for video hashing progress + newpipeNewStreams + New streams + Notifications about new streams for subscriptions [Unknown] Switch to Background Switch to Popup @@ -309,6 +313,10 @@ No comments Comments are disabled + + %s new stream + %s new streams + Start Pause @@ -513,6 +521,17 @@ 240p 144p + + New streams notifications + Notify about new streams from subscriptions + Checking frequency + Every hour + Every 2 hours + Every 3 hours + Twice per day + Every day + Required network connection + Any network Updates Show a notification to prompt app update when a new version is available @@ -703,4 +722,10 @@ Error at Show Channel Details Loading Channel Details… + Notifications are disabled + Get notified + You now subscribed to this channel + , + %s • %s + %d/%d \ No newline at end of file diff --git a/app/src/main/res/xml/notifications_settings.xml b/app/src/main/res/xml/notifications_settings.xml new file mode 100644 index 000000000..4390dc48c --- /dev/null +++ b/app/src/main/res/xml/notifications_settings.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/app/src/release/res/xml/main_settings.xml b/app/src/release/res/xml/main_settings.xml index 1d5241102..e999aa8c4 100644 --- a/app/src/release/res/xml/main_settings.xml +++ b/app/src/release/res/xml/main_settings.xml @@ -1,5 +1,6 @@ - @@ -40,6 +41,12 @@ android:title="@string/settings_category_notification_title" app:iconSpaceReserved="false" /> + + 8zj+yD0=v_z5)T{~FC~dkh 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 T|4lfZe);r&fdl#HS9t*dv-=o4