mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Notifications about new streams
This commit is contained in:
		| @@ -108,6 +108,7 @@ ext { | |||||||
|     leakCanaryVersion = '2.5' |     leakCanaryVersion = '2.5' | ||||||
|     stethoVersion = '1.6.0' |     stethoVersion = '1.6.0' | ||||||
|     mockitoVersion = '3.6.0' |     mockitoVersion = '3.6.0' | ||||||
|  |     workVersion = '2.5.0' | ||||||
| } | } | ||||||
|  |  | ||||||
| configurations { | configurations { | ||||||
| @@ -213,6 +214,8 @@ dependencies { | |||||||
|     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' |     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' | ||||||
|     implementation 'androidx.webkit:webkit:1.4.0' |     implementation 'androidx.webkit:webkit:1.4.0' | ||||||
|     implementation 'com.google.android.material:material:1.2.1' |     implementation 'com.google.android.material:material:1.2.1' | ||||||
|  |     implementation "androidx.work:work-runtime:${workVersion}" | ||||||
|  |     implementation "androidx.work:work-rxjava2:${workVersion}" | ||||||
|  |  | ||||||
| /** Third-party libraries **/ | /** Third-party libraries **/ | ||||||
|     // Instance state boilerplate elimination |     // Instance state boilerplate elimination | ||||||
|   | |||||||
| @@ -40,6 +40,12 @@ | |||||||
|         android:title="@string/settings_category_notification_title" |         android:title="@string/settings_category_notification_title" | ||||||
|         app:iconSpaceReserved="false" /> |         app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|  |     <PreferenceScreen | ||||||
|  |         android:fragment="org.schabi.newpipe.settings.NotificationsSettingsFragment" | ||||||
|  |         android:icon="@drawable/ic_notifications" | ||||||
|  |         android:title="@string/notifications" | ||||||
|  |         app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|     <PreferenceScreen |     <PreferenceScreen | ||||||
|         android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment" |         android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment" | ||||||
|         android:icon="@drawable/ic_cloud_download" |         android:icon="@drawable/ic_cloud_download" | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| package org.schabi.newpipe; | package org.schabi.newpipe; | ||||||
|  |  | ||||||
|  | import android.app.NotificationChannel; | ||||||
|  | import android.app.NotificationManager; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| @@ -108,7 +110,6 @@ public class App extends MultiDexApplication { | |||||||
|                 && prefs.getBoolean(getString(R.string.show_image_indicators_key), false)); |                 && prefs.getBoolean(getString(R.string.show_image_indicators_key), false)); | ||||||
|  |  | ||||||
|         configureRxJavaErrorHandler(); |         configureRxJavaErrorHandler(); | ||||||
|  |  | ||||||
|         // Check for new version |         // Check for new version | ||||||
|         disposable = CheckForNewAppVersion.checkNewVersion(this); |         disposable = CheckForNewAppVersion.checkNewVersion(this); | ||||||
|     } |     } | ||||||
| @@ -249,9 +250,20 @@ public class App extends MultiDexApplication { | |||||||
|                 .setDescription(getString(R.string.hash_channel_description)) |                 .setDescription(getString(R.string.hash_channel_description)) | ||||||
|                 .build(); |                 .build(); | ||||||
|  |  | ||||||
|  |         final NotificationChannel newStreamsChannel = new NotificationChannel( | ||||||
|  |                 getString(R.string.streams_notification_channel_id), | ||||||
|  |                 getString(R.string.streams_notification_channel_name), | ||||||
|  |                 NotificationManager.IMPORTANCE_DEFAULT | ||||||
|  |         ); | ||||||
|  |         newStreamsChannel.setDescription( | ||||||
|  |                 getString(R.string.streams_notification_channel_description) | ||||||
|  |         ); | ||||||
|  |         newStreamsChannel.enableVibration(false); | ||||||
|  |  | ||||||
|         final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); |         final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); | ||||||
|         notificationManager.createNotificationChannelsCompat(Arrays.asList(mainChannel, |         notificationManager.createNotificationChannels( | ||||||
|                 appUpdateChannel, hashChannel)); |                 Arrays.asList(mainChannel, appUpdateChannel, hashChannel, newStreamsChannel) | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected boolean isDisposedRxExceptionsReported() { |     protected boolean isDisposedRxExceptionsReported() { | ||||||
|   | |||||||
| @@ -69,6 +69,7 @@ import org.schabi.newpipe.fragments.BackPressable; | |||||||
| import org.schabi.newpipe.fragments.MainFragment; | import org.schabi.newpipe.fragments.MainFragment; | ||||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||||
| import org.schabi.newpipe.fragments.list.search.SearchFragment; | import org.schabi.newpipe.fragments.list.search.SearchFragment; | ||||||
|  | import org.schabi.newpipe.notifications.NotificationWorker; | ||||||
| import org.schabi.newpipe.player.Player; | import org.schabi.newpipe.player.Player; | ||||||
| import org.schabi.newpipe.player.event.OnKeyDownListener; | import org.schabi.newpipe.player.event.OnKeyDownListener; | ||||||
| import org.schabi.newpipe.player.helper.PlayerHolder; | import org.schabi.newpipe.player.helper.PlayerHolder; | ||||||
| @@ -158,11 +159,11 @@ public class MainActivity extends AppCompatActivity { | |||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e); |             ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (DeviceUtils.isTv(this)) { |         if (DeviceUtils.isTv(this)) { | ||||||
|             FocusOverlayView.setupFocusObserver(this); |             FocusOverlayView.setupFocusObserver(this); | ||||||
|         } |         } | ||||||
|         openMiniPlayerUponPlayerStarted(); |         openMiniPlayerUponPlayerStarted(); | ||||||
|  |         NotificationWorker.schedule(this); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void setupDrawer() throws Exception { |     private void setupDrawer() throws Exception { | ||||||
|   | |||||||
| @@ -1,5 +1,11 @@ | |||||||
| package org.schabi.newpipe; | package org.schabi.newpipe; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; | ||||||
|  | import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2; | ||||||
|  | import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3; | ||||||
|  | import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4; | ||||||
|  | import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5; | ||||||
|  |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.database.Cursor; | import android.database.Cursor; | ||||||
|  |  | ||||||
| @@ -8,11 +14,6 @@ import androidx.room.Room; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.database.AppDatabase; | import org.schabi.newpipe.database.AppDatabase; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; |  | ||||||
| import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2; |  | ||||||
| import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3; |  | ||||||
| import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4; |  | ||||||
|  |  | ||||||
| public final class NewPipeDatabase { | public final class NewPipeDatabase { | ||||||
|     private static volatile AppDatabase databaseInstance; |     private static volatile AppDatabase databaseInstance; | ||||||
|  |  | ||||||
| @@ -23,7 +24,7 @@ public final class NewPipeDatabase { | |||||||
|     private static AppDatabase getDatabase(final Context context) { |     private static AppDatabase getDatabase(final Context context) { | ||||||
|         return Room |         return Room | ||||||
|                 .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) |                 .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) | ||||||
|                 .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) |                 .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) | ||||||
|                 .build(); |                 .build(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| package org.schabi.newpipe.database; | package org.schabi.newpipe.database; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.database.Migrations.DB_VER_5; | ||||||
|  |  | ||||||
| import androidx.room.Database; | import androidx.room.Database; | ||||||
| import androidx.room.RoomDatabase; | import androidx.room.RoomDatabase; | ||||||
| import androidx.room.TypeConverters; | import androidx.room.TypeConverters; | ||||||
| @@ -27,8 +29,6 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity; | |||||||
| import org.schabi.newpipe.database.subscription.SubscriptionDAO; | import org.schabi.newpipe.database.subscription.SubscriptionDAO; | ||||||
| import org.schabi.newpipe.database.subscription.SubscriptionEntity; | import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.database.Migrations.DB_VER_4; |  | ||||||
|  |  | ||||||
| @TypeConverters({Converters.class}) | @TypeConverters({Converters.class}) | ||||||
| @Database( | @Database( | ||||||
|         entities = { |         entities = { | ||||||
| @@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_4; | |||||||
|                 FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, |                 FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, | ||||||
|                 FeedLastUpdatedEntity.class |                 FeedLastUpdatedEntity.class | ||||||
|         }, |         }, | ||||||
|         version = DB_VER_4 |         version = DB_VER_5 | ||||||
| ) | ) | ||||||
| public abstract class AppDatabase extends RoomDatabase { | public abstract class AppDatabase extends RoomDatabase { | ||||||
|     public static final String DATABASE_NAME = "newpipe.db"; |     public static final String DATABASE_NAME = "newpipe.db"; | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ public final class Migrations { | |||||||
|     public static final int DB_VER_2 = 2; |     public static final int DB_VER_2 = 2; | ||||||
|     public static final int DB_VER_3 = 3; |     public static final int DB_VER_3 = 3; | ||||||
|     public static final int DB_VER_4 = 4; |     public static final int DB_VER_4 = 4; | ||||||
|  |     public static final int DB_VER_5 = 5; | ||||||
|  |  | ||||||
|     private static final String TAG = Migrations.class.getName(); |     private static final String TAG = Migrations.class.getName(); | ||||||
|     public static final boolean DEBUG = MainActivity.DEBUG; |     public static final boolean DEBUG = MainActivity.DEBUG; | ||||||
| @@ -179,5 +180,14 @@ public final class Migrations { | |||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     private Migrations() { } |     public static final Migration MIGRATION_4_5 = new Migration(DB_VER_4, DB_VER_5) { | ||||||
|  |         @Override | ||||||
|  |         public void migrate(@NonNull final SupportSQLiteDatabase database) { | ||||||
|  |             database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " | ||||||
|  |                      + "INTEGER NOT NULL DEFAULT 0"); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     private Migrations() { | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -39,6 +39,9 @@ abstract class StreamDAO : BasicDAO<StreamEntity> { | |||||||
|     @Insert(onConflict = OnConflictStrategy.IGNORE) |     @Insert(onConflict = OnConflictStrategy.IGNORE) | ||||||
|     internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long> |     internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long> | ||||||
|  |  | ||||||
|  |     @Query("SELECT COUNT(*) != 0 FROM streams WHERE url = :url AND service_id = :serviceId") | ||||||
|  |     internal abstract fun exists(serviceId: Long, url: String?): Boolean | ||||||
|  |  | ||||||
|     @Query( |     @Query( | ||||||
|         """ |         """ | ||||||
|         SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration  |         SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration  | ||||||
|   | |||||||
| @@ -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 | ||||||
|  | } | ||||||
| @@ -26,6 +26,7 @@ public class SubscriptionEntity { | |||||||
|     public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url"; |     public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url"; | ||||||
|     public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count"; |     public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count"; | ||||||
|     public static final String SUBSCRIPTION_DESCRIPTION = "description"; |     public static final String SUBSCRIPTION_DESCRIPTION = "description"; | ||||||
|  |     public static final String SUBSCRIPTION_NOTIFICATION_MODE  = "notification_mode"; | ||||||
|  |  | ||||||
|     @PrimaryKey(autoGenerate = true) |     @PrimaryKey(autoGenerate = true) | ||||||
|     private long uid = 0; |     private long uid = 0; | ||||||
| @@ -48,6 +49,9 @@ public class SubscriptionEntity { | |||||||
|     @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) |     @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) | ||||||
|     private String description; |     private String description; | ||||||
|  |  | ||||||
|  |     @ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE) | ||||||
|  |     private int notificationMode; | ||||||
|  |  | ||||||
|     @Ignore |     @Ignore | ||||||
|     public static SubscriptionEntity from(@NonNull final ChannelInfo info) { |     public static SubscriptionEntity from(@NonNull final ChannelInfo info) { | ||||||
|         final SubscriptionEntity result = new SubscriptionEntity(); |         final SubscriptionEntity result = new SubscriptionEntity(); | ||||||
| @@ -114,6 +118,15 @@ public class SubscriptionEntity { | |||||||
|         this.description = description; |         this.description = description; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @NotificationMode | ||||||
|  |     public int getNotificationMode() { | ||||||
|  |         return notificationMode; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setNotificationMode(@NotificationMode final int notificationMode) { | ||||||
|  |         this.notificationMode = notificationMode; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Ignore |     @Ignore | ||||||
|     public void setData(final String n, final String au, final String d, final Long sc) { |     public void setData(final String n, final String au, final String d, final Long sc) { | ||||||
|         this.setName(n); |         this.setName(n); | ||||||
|   | |||||||
| @@ -1,6 +1,11 @@ | |||||||
| package org.schabi.newpipe.fragments.list.channel; | 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.content.Context; | ||||||
|  | import android.graphics.Color; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.text.TextUtils; | import android.text.TextUtils; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| @@ -19,9 +24,11 @@ import androidx.appcompat.app.ActionBar; | |||||||
| import androidx.core.content.ContextCompat; | import androidx.core.content.ContextCompat; | ||||||
| import androidx.viewbinding.ViewBinding; | import androidx.viewbinding.ViewBinding; | ||||||
|  |  | ||||||
|  | import com.google.android.material.snackbar.Snackbar; | ||||||
| import com.jakewharton.rxbinding4.view.RxView; | import com.jakewharton.rxbinding4.view.RxView; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.database.subscription.NotificationMode; | ||||||
| import org.schabi.newpipe.database.subscription.SubscriptionEntity; | import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||||
| import org.schabi.newpipe.databinding.ChannelHeaderBinding; | import org.schabi.newpipe.databinding.ChannelHeaderBinding; | ||||||
| import org.schabi.newpipe.databinding.FragmentChannelBinding; | 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.fragments.list.BaseListInfoFragment; | ||||||
| import org.schabi.newpipe.ktx.AnimationType; | import org.schabi.newpipe.ktx.AnimationType; | ||||||
| import org.schabi.newpipe.local.subscription.SubscriptionManager; | 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.ChannelPlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.util.ExtractorHelper; | 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.functions.Function; | ||||||
| import io.reactivex.rxjava3.schedulers.Schedulers; | 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<ChannelInfo> | public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | ||||||
|         implements View.OnClickListener { |         implements View.OnClickListener { | ||||||
|  |  | ||||||
| @@ -84,6 +88,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|     private PlaylistControlBinding playlistControlBinding; |     private PlaylistControlBinding playlistControlBinding; | ||||||
|  |  | ||||||
|     private MenuItem menuRssButton; |     private MenuItem menuRssButton; | ||||||
|  |     private MenuItem menuNotifyButton; | ||||||
|  |  | ||||||
|     public static ChannelFragment getInstance(final int serviceId, final String url, |     public static ChannelFragment getInstance(final int serviceId, final String url, | ||||||
|                                               final String name) { |                                               final String name) { | ||||||
| @@ -181,6 +186,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|                         + "menu = [" + menu + "], inflater = [" + inflater + "]"); |                         + "menu = [" + menu + "], inflater = [" + inflater + "]"); | ||||||
|             } |             } | ||||||
|             menuRssButton = menu.findItem(R.id.menu_item_rss); |             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<ChannelInfo> | |||||||
|             case R.id.action_settings: |             case R.id.action_settings: | ||||||
|                 NavigationHelper.openSettings(requireContext()); |                 NavigationHelper.openSettings(requireContext()); | ||||||
|                 break; |                 break; | ||||||
|  |             case R.id.menu_item_notify: | ||||||
|  |                 final boolean value = !item.isChecked(); | ||||||
|  |                 item.setEnabled(false); | ||||||
|  |                 setNotify(value); | ||||||
|  |                 break; | ||||||
|             case R.id.menu_item_rss: |             case R.id.menu_item_rss: | ||||||
|                 openRssFeed(); |                 openRssFeed(); | ||||||
|                 break; |                 break; | ||||||
| @@ -238,15 +249,22 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|                 .subscribe(getSubscribeUpdateMonitor(info), onError)); |                 .subscribe(getSubscribeUpdateMonitor(info), onError)); | ||||||
|  |  | ||||||
|         disposables.add(observable |         disposables.add(observable | ||||||
|                 // Some updates are very rapid |                 .map(List::isEmpty) | ||||||
|                 // (for example when calling the updateSubscription(info)) |                 .distinctUntilChanged() | ||||||
|                 // so only update the UI for the latest emission |  | ||||||
|                 // ("sync" the subscribe button's state) |  | ||||||
|                 .debounce(100, TimeUnit.MILLISECONDS) |  | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|                 .subscribe((List<SubscriptionEntity> subscriptionEntities) -> |                 .subscribe((Boolean isEmpty) -> updateSubscribeButton(!isEmpty), onError)); | ||||||
|                         updateSubscribeButton(!subscriptionEntities.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<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription, |     private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription, | ||||||
| @@ -326,6 +344,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|                         info.getAvatarUrl(), |                         info.getAvatarUrl(), | ||||||
|                         info.getDescription(), |                         info.getDescription(), | ||||||
|                         info.getSubscriberCount()); |                         info.getSubscriberCount()); | ||||||
|  |                 updateNotifyButton(null); | ||||||
|                 subscribeButtonMonitor = monitorSubscribeButton( |                 subscribeButtonMonitor = monitorSubscribeButton( | ||||||
|                         headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info)); |                         headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info)); | ||||||
|             } else { |             } else { | ||||||
| @@ -333,6 +352,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|                     Log.d(TAG, "Found subscription to this channel!"); |                     Log.d(TAG, "Found subscription to this channel!"); | ||||||
|                 } |                 } | ||||||
|                 final SubscriptionEntity subscription = subscriptionEntities.get(0); |                 final SubscriptionEntity subscription = subscriptionEntities.get(0); | ||||||
|  |                 updateNotifyButton(subscription); | ||||||
|                 subscribeButtonMonitor = monitorSubscribeButton( |                 subscribeButtonMonitor = monitorSubscribeButton( | ||||||
|                         headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription)); |                         headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription)); | ||||||
|             } |             } | ||||||
| @@ -375,6 +395,41 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> | |||||||
|                 AnimationType.LIGHT_SCALE_AND_ALPHA); |                 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 |     // Load and handle | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ import io.reactivex.rxjava3.core.Flowable | |||||||
| import io.reactivex.rxjava3.schedulers.Schedulers | import io.reactivex.rxjava3.schedulers.Schedulers | ||||||
| import org.schabi.newpipe.NewPipeDatabase | import org.schabi.newpipe.NewPipeDatabase | ||||||
| import org.schabi.newpipe.database.feed.model.FeedGroupEntity | 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.SubscriptionDAO | ||||||
| import org.schabi.newpipe.database.subscription.SubscriptionEntity | import org.schabi.newpipe.database.subscription.SubscriptionEntity | ||||||
| import org.schabi.newpipe.extractor.ListInfo | 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.feed.FeedInfo | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem | import org.schabi.newpipe.extractor.stream.StreamInfoItem | ||||||
| import org.schabi.newpipe.local.feed.FeedDatabaseManager | import org.schabi.newpipe.local.feed.FeedDatabaseManager | ||||||
|  | import org.schabi.newpipe.util.ExtractorHelper | ||||||
|  |  | ||||||
| class SubscriptionManager(context: Context) { | class SubscriptionManager(context: Context) { | ||||||
|     private val database = NewPipeDatabase.getInstance(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<StreamInfoItem>) { |     fun updateFromInfo(subscriptionId: Long, info: ListInfo<StreamInfoItem>) { | ||||||
|         val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId) |         val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId) | ||||||
|  |  | ||||||
| @@ -94,4 +107,14 @@ class SubscriptionManager(context: Context) { | |||||||
|     fun deleteSubscription(subscriptionEntity: SubscriptionEntity) { |     fun deleteSubscription(subscriptionEntity: SubscriptionEntity) { | ||||||
|         subscriptionTable.delete(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() | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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<SubscriptionEntity>) { | ||||||
|  |         var notified = 0 | ||||||
|  |         for (subscription in subscriptions) { | ||||||
|  |             if (subscription.notificationMode != NotificationMode.DISABLED) { | ||||||
|  |                 notified++ | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         val preference = findPreference<Preference>(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") | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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() | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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<SubscriptionHolder>() { | ||||||
|  |  | ||||||
|  |     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<SubscriptionEntity>) { | ||||||
|  |         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<SubscriptionItem>() { | ||||||
|  |  | ||||||
|  |         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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -571,6 +571,12 @@ public final class NavigationHelper { | |||||||
|         return getOpenIntent(context, url, service.getServiceId(), linkType); |         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. |      * Start an activity to install Kore. | ||||||
|      * |      * | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								app/src/main/res/drawable-anydpi-v24/ic_stat_newpipe.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/src/main/res/drawable-anydpi-v24/ic_stat_newpipe.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportWidth="98.91304" | ||||||
|  |     android:viewportHeight="98.91304" | ||||||
|  |     android:tint="#FFFFFF"> | ||||||
|  |   <group android:translateX="-6.7255435" | ||||||
|  |       android:translateY="-0.54347825"> | ||||||
|  |     <path | ||||||
|  |         android:pathData="m23.909,10.211v78.869c0,0 7.7,-4.556 12.4,-7.337V67.477,56.739 31.686c0,0 3.707,2.173 8.948,5.24 6.263,3.579 14.57,8.565 21.473,12.655 -9.358,5.483 -16.8,9.876 -22.496,13.234V77.053C57.974,68.927 75.176,58.762 90.762,49.581 75.551,40.634 57.144,29.768 43.467,21.715 31.963,14.94 23.909,10.211 23.909,10.211Z" | ||||||
|  |         android:strokeWidth="1.2782383" | ||||||
|  |         android:fillColor="#ffffff"/> | ||||||
|  |   </group> | ||||||
|  | </vector> | ||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_stat_newpipe.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_stat_newpipe.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 413 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_stat_newpipe.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_stat_newpipe.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 294 B | 
							
								
								
									
										9
									
								
								app/src/main/res/drawable-night/ic_notifications.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable-night/ic_notifications.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportWidth="24" | ||||||
|  |     android:viewportHeight="24"> | ||||||
|  |     <path | ||||||
|  |         android:fillColor="#FFFFFFFF" | ||||||
|  |         android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" /> | ||||||
|  | </vector> | ||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_stat_newpipe.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/ic_stat_newpipe.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 522 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_stat_newpipe.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_stat_newpipe.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 731 B | 
							
								
								
									
										9
									
								
								app/src/main/res/drawable/ic_notifications.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/src/main/res/drawable/ic_notifications.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportWidth="24" | ||||||
|  |     android:viewportHeight="24"> | ||||||
|  |     <path | ||||||
|  |         android:fillColor="#FF000000" | ||||||
|  |         android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" /> | ||||||
|  | </vector> | ||||||
							
								
								
									
										14
									
								
								app/src/main/res/layout/fragment_channels_notifications.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/src/main/res/layout/fragment_channels_notifications.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/recycler_view" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:scrollbars="vertical" | ||||||
|  |         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> | ||||||
|  |      | ||||||
|  | </FrameLayout> | ||||||
							
								
								
									
										16
									
								
								app/src/main/res/layout/item_notification_config.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/res/layout/item_notification_config.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="?listPreferredItemHeightSmall" | ||||||
|  |     android:background="?selectableItemBackground" | ||||||
|  |     android:ellipsize="end" | ||||||
|  |     android:gravity="center_vertical" | ||||||
|  |     android:maxLines="2" | ||||||
|  |     android:orientation="horizontal" | ||||||
|  |     android:paddingStart="?listPreferredItemPaddingStart" | ||||||
|  |     android:paddingEnd="?listPreferredItemPaddingEnd" | ||||||
|  |     android:textAppearance="@style/TextAppearance.AppCompat.Body1" | ||||||
|  |     android:drawableEnd="?android:listChoiceIndicatorMultiple" | ||||||
|  |     tools:text="@tools:sample/lorem[4]" /> | ||||||
| @@ -17,6 +17,13 @@ | |||||||
|         android:title="@string/share" |         android:title="@string/share" | ||||||
|         app:showAsAction="ifRoom" /> |         app:showAsAction="ifRoom" /> | ||||||
|  |  | ||||||
|  |     <item | ||||||
|  |         android:id="@+id/menu_item_notify" | ||||||
|  |         android:checkable="true" | ||||||
|  |         android:visible="false" | ||||||
|  |         android:title="@string/get_notified" | ||||||
|  |         app:showAsAction="never" /> | ||||||
|  |  | ||||||
|     <item |     <item | ||||||
|         android:id="@+id/action_settings" |         android:id="@+id/action_settings" | ||||||
|         android:orderInCategory="1" |         android:orderInCategory="1" | ||||||
|   | |||||||
| @@ -169,6 +169,11 @@ | |||||||
|         <item quantity="many">%s видео</item> |         <item quantity="many">%s видео</item> | ||||||
|         <item quantity="other">%s видео</item> |         <item quantity="other">%s видео</item> | ||||||
|     </plurals> |     </plurals> | ||||||
|  |     <plurals name="new_streams"> | ||||||
|  |         <item quantity="one">%s новое видео</item> | ||||||
|  |         <item quantity="few">%s новых видео</item> | ||||||
|  |         <item quantity="many">%s новых видео</item> | ||||||
|  |     </plurals> | ||||||
|     <string name="delete_item_search_history">Удалить этот элемент из истории поиска?</string> |     <string name="delete_item_search_history">Удалить этот элемент из истории поиска?</string> | ||||||
|     <string name="main_page_content">Главная страница</string> |     <string name="main_page_content">Главная страница</string> | ||||||
|     <string name="blank_page_summary">Пустая страница</string> |     <string name="blank_page_summary">Пустая страница</string> | ||||||
| @@ -683,4 +688,20 @@ | |||||||
|     <string name="main_page_content_swipe_remove">Удалять элементы смахиванием</string> |     <string name="main_page_content_swipe_remove">Удалять элементы смахиванием</string> | ||||||
|     <string name="start_main_player_fullscreen_summary">Не начинать просмотр видео в мини-плеере, но сразу переключиться в полноэкранный режим, если автовращение экрана заблокировано. Вы можете переключиться на мини-плеер, выйдя из полноэкранного режима.</string> |     <string name="start_main_player_fullscreen_summary">Не начинать просмотр видео в мини-плеере, но сразу переключиться в полноэкранный режим, если автовращение экрана заблокировано. Вы можете переключиться на мини-плеер, выйдя из полноэкранного режима.</string> | ||||||
|     <string name="start_main_player_fullscreen_title">Начинать просмотр в полноэкранном режиме</string> |     <string name="start_main_player_fullscreen_title">Начинать просмотр в полноэкранном режиме</string> | ||||||
|  |     <string name="notifications">Уведомления</string> | ||||||
|  |     <string name="streams_notification_channel_name">Новые видео</string> | ||||||
|  |     <string name="streams_notification_channel_description">Уведомления о новых видео в подписках</string> | ||||||
|  |     <string name="streams_notifications_interval_title">Частота проверки</string> | ||||||
|  |     <string name="enable_streams_notifications_title">Уведомлять о новых видео</string> | ||||||
|  |     <string name="enable_streams_notifications_summary">Получать уведомления о новых видео из каналов, на которые Вы подписаны</string> | ||||||
|  |     <string name="every_hour">Каждый час</string> | ||||||
|  |     <string name="every_two_hours">Каждые 2 часа</string> | ||||||
|  |     <string name="every_three_hours">Каждые 3 часа</string> | ||||||
|  |     <string name="twice_per_day">Дважды в день</string> | ||||||
|  |     <string name="every_day">Каждый день</string> | ||||||
|  |     <string name="streams_notifications_network_title">Тип подключения</string> | ||||||
|  |     <string name="any_network">Любая сеть</string> | ||||||
|  |     <string name="notifications_disabled">Уведомления отключены</string> | ||||||
|  |     <string name="get_notified">Уведомлять</string> | ||||||
|  |     <string name="you_successfully_subscribed">Вы подписались на канал</string> | ||||||
| </resources> | </resources> | ||||||
| @@ -443,4 +443,5 @@ | |||||||
|     <string name="seek_duration_title">快进 / 快退的单位时间</string> |     <string name="seek_duration_title">快进 / 快退的单位时间</string> | ||||||
|     <string name="clear_download_history">清除下载历史记录</string> |     <string name="clear_download_history">清除下载历史记录</string> | ||||||
|     <string name="delete_downloaded_files">删除下载了的文件</string> |     <string name="delete_downloaded_files">删除下载了的文件</string> | ||||||
|  |     <string name="enumeration_comma">、</string> | ||||||
| </resources> | </resources> | ||||||
| @@ -132,4 +132,5 @@ | |||||||
|     <string name="use_inexact_seek_title">使用粗略快查</string> |     <string name="use_inexact_seek_title">使用粗略快查</string> | ||||||
|     <string name="controls_add_to_playlist_title">添加到</string> |     <string name="controls_add_to_playlist_title">添加到</string> | ||||||
|     <string name="tab_choose">選擇標籤</string> |     <string name="tab_choose">選擇標籤</string> | ||||||
|  |     <string name="enumeration_comma">、</string> | ||||||
| </resources> | </resources> | ||||||
| @@ -1260,4 +1260,34 @@ | |||||||
|     </string-array> |     </string-array> | ||||||
|  |  | ||||||
|     <string name="recaptcha_cookies_key" translatable="false">recaptcha_cookies_key</string> |     <string name="recaptcha_cookies_key" translatable="false">recaptcha_cookies_key</string> | ||||||
|  |     <string name="enable_streams_notifications" translatable="false">enable_streams_notifications</string> | ||||||
|  |     <string name="streams_notifications_interval_key" translatable="false">streams_notifications_interval</string> | ||||||
|  |     <string name="streams_notifications_interval_default" translatable="false">3</string> | ||||||
|  |     <string-array name="streams_notifications_interval_values"> | ||||||
|  |         <item>1</item> | ||||||
|  |         <item>2</item> | ||||||
|  |         <item>3</item> | ||||||
|  |         <item>12</item> | ||||||
|  |         <item>24</item> | ||||||
|  |     </string-array> | ||||||
|  |     <string-array name="streams_notifications_interval_description"> | ||||||
|  |         <item>@string/every_hour</item> | ||||||
|  |         <item>@string/every_two_hours</item> | ||||||
|  |         <item>@string/every_three_hours</item> | ||||||
|  |         <item>@string/twice_per_day</item> | ||||||
|  |         <item>@string/every_day</item> | ||||||
|  |     </string-array> | ||||||
|  |     <string name="streams_notifications_network_key" translatable="false">streams_notifications_network</string> | ||||||
|  |     <string name="streams_notifications_network_any" translatable="false">any</string> | ||||||
|  |     <string name="streams_notifications_network_wifi" translatable="false">wifi</string> | ||||||
|  |     <string name="streams_notifications_network_default" translatable="false">@string/streams_notifications_network_wifi</string> | ||||||
|  |     <string-array name="streams_notifications_network_values"> | ||||||
|  |         <item>@string/streams_notifications_network_any</item> | ||||||
|  |         <item>@string/streams_notifications_network_wifi</item> | ||||||
|  |     </string-array> | ||||||
|  |     <string-array name="streams_notifications_network_description"> | ||||||
|  |         <item>@string/any_network</item> | ||||||
|  |         <item>@string/wifi_only</item> | ||||||
|  |     </string-array> | ||||||
|  |     <string name="streams_notifications_channels_key" translatable="false">streams_notifications_channels</string> | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <resources xmlns:tools="http://schemas.android.com/tools" | <resources xmlns:tools="http://schemas.android.com/tools" | ||||||
|         tools:ignore="MissingTranslation"> |         tools:ignore="MissingTranslation"> | ||||||
|  |  | ||||||
| @@ -180,6 +180,7 @@ | |||||||
|     <string name="always">Always</string> |     <string name="always">Always</string> | ||||||
|     <string name="just_once">Just Once</string> |     <string name="just_once">Just Once</string> | ||||||
|     <string name="file">File</string> |     <string name="file">File</string> | ||||||
|  |     <string name="notifications">Notifications</string> | ||||||
|     <string name="notification_channel_id" translatable="false">newpipe</string> |     <string name="notification_channel_id" translatable="false">newpipe</string> | ||||||
|     <string name="notification_channel_name">NewPipe Notification</string> |     <string name="notification_channel_name">NewPipe Notification</string> | ||||||
|     <string name="notification_channel_description">Notifications for NewPipe background and popup players</string> |     <string name="notification_channel_description">Notifications for NewPipe background and popup players</string> | ||||||
| @@ -189,6 +190,9 @@ | |||||||
|     <string name="hash_channel_id" translatable="false">newpipeHash</string> |     <string name="hash_channel_id" translatable="false">newpipeHash</string> | ||||||
|     <string name="hash_channel_name">Video Hash Notification</string> |     <string name="hash_channel_name">Video Hash Notification</string> | ||||||
|     <string name="hash_channel_description">Notifications for video hashing progress</string> |     <string name="hash_channel_description">Notifications for video hashing progress</string> | ||||||
|  |     <string name="streams_notification_channel_id" translatable="false">newpipeNewStreams</string> | ||||||
|  |     <string name="streams_notification_channel_name">New streams</string> | ||||||
|  |     <string name="streams_notification_channel_description">Notifications about new streams for subscriptions</string> | ||||||
|     <string name="unknown_content">[Unknown]</string> |     <string name="unknown_content">[Unknown]</string> | ||||||
|     <string name="switch_to_background">Switch to Background</string> |     <string name="switch_to_background">Switch to Background</string> | ||||||
|     <string name="switch_to_popup">Switch to Popup</string> |     <string name="switch_to_popup">Switch to Popup</string> | ||||||
| @@ -309,6 +313,10 @@ | |||||||
|     </plurals> |     </plurals> | ||||||
|     <string name="no_comments">No comments</string> |     <string name="no_comments">No comments</string> | ||||||
|     <string name="comments_are_disabled">Comments are disabled</string> |     <string name="comments_are_disabled">Comments are disabled</string> | ||||||
|  |     <plurals name="new_streams"> | ||||||
|  |         <item quantity="one">%s new stream</item> | ||||||
|  |         <item quantity="other">%s new streams</item> | ||||||
|  |     </plurals> | ||||||
|     <!-- Missions --> |     <!-- Missions --> | ||||||
|     <string name="start">Start</string> |     <string name="start">Start</string> | ||||||
|     <string name="pause">Pause</string> |     <string name="pause">Pause</string> | ||||||
| @@ -513,6 +521,17 @@ | |||||||
|         <item>240p</item> |         <item>240p</item> | ||||||
|         <item>144p</item> |         <item>144p</item> | ||||||
|     </string-array> |     </string-array> | ||||||
|  |     <!-- Notifications settings --> | ||||||
|  |     <string name="enable_streams_notifications_title">New streams notifications</string> | ||||||
|  |     <string name="enable_streams_notifications_summary">Notify about new streams from subscriptions</string> | ||||||
|  |     <string name="streams_notifications_interval_title">Checking frequency</string> | ||||||
|  |     <string name="every_hour">Every hour</string> | ||||||
|  |     <string name="every_two_hours">Every 2 hours</string> | ||||||
|  |     <string name="every_three_hours">Every 3 hours</string> | ||||||
|  |     <string name="twice_per_day">Twice per day</string> | ||||||
|  |     <string name="every_day">Every day</string> | ||||||
|  |     <string name="streams_notifications_network_title">Required network connection</string> | ||||||
|  |     <string name="any_network">Any network</string> | ||||||
|     <!-- Updates Settings --> |     <!-- Updates Settings --> | ||||||
|     <string name="updates_setting_title">Updates</string> |     <string name="updates_setting_title">Updates</string> | ||||||
|     <string name="updates_setting_description">Show a notification to prompt app update when a new version is available</string> |     <string name="updates_setting_description">Show a notification to prompt app update when a new version is available</string> | ||||||
| @@ -703,4 +722,10 @@ | |||||||
|     <!-- Show Channel Details --> |     <!-- Show Channel Details --> | ||||||
|     <string name="error_show_channel_details">Error at Show Channel Details</string> |     <string name="error_show_channel_details">Error at Show Channel Details</string> | ||||||
|     <string name="loading_channel_details">Loading Channel Details…</string> |     <string name="loading_channel_details">Loading Channel Details…</string> | ||||||
|  |     <string name="notifications_disabled">Notifications are disabled</string> | ||||||
|  |     <string name="get_notified">Get notified</string> | ||||||
|  |     <string name="you_successfully_subscribed">You now subscribed to this channel</string> | ||||||
|  |     <string name="enumeration_comma">,</string> | ||||||
|  |     <string name="notification_title_pattern" translatable="false">%s • %s</string> | ||||||
|  |     <string name="streams_notifications_channels_summary" translatable="false">%d/%d</string> | ||||||
| </resources> | </resources> | ||||||
							
								
								
									
										41
									
								
								app/src/main/res/xml/notifications_settings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								app/src/main/res/xml/notifications_settings.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  | 	xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  | 	android:key="general_preferences" | ||||||
|  | 	android:title="@string/notifications"> | ||||||
|  |  | ||||||
|  | 	<SwitchPreference | ||||||
|  | 		android:defaultValue="false" | ||||||
|  | 		android:key="@string/enable_streams_notifications" | ||||||
|  | 		android:summary="@string/enable_streams_notifications_summary" | ||||||
|  | 		android:title="@string/enable_streams_notifications_title" | ||||||
|  | 		app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|  | 	<ListPreference | ||||||
|  | 		android:defaultValue="@string/streams_notifications_interval_default" | ||||||
|  | 		android:dependency="@string/enable_streams_notifications" | ||||||
|  | 		android:entries="@array/streams_notifications_interval_description" | ||||||
|  | 		android:entryValues="@array/streams_notifications_interval_values" | ||||||
|  | 		android:key="@string/streams_notifications_interval_key" | ||||||
|  | 		android:summary="%s" | ||||||
|  | 		android:title="@string/streams_notifications_interval_title" | ||||||
|  | 		app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|  | 	<ListPreference | ||||||
|  | 		android:defaultValue="@string/streams_notifications_network_default" | ||||||
|  | 		android:dependency="@string/enable_streams_notifications" | ||||||
|  | 		android:entries="@array/streams_notifications_network_description" | ||||||
|  | 		android:entryValues="@array/streams_notifications_network_values" | ||||||
|  | 		android:key="@string/streams_notifications_network_key" | ||||||
|  | 		android:summary="%s" | ||||||
|  | 		android:title="@string/streams_notifications_network_title" | ||||||
|  | 		app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|  | 	<Preference | ||||||
|  | 		android:fragment="org.schabi.newpipe.settings.notifications.NotificationsChannelsConfigFragment" | ||||||
|  | 		android:dependency="@string/enable_streams_notifications" | ||||||
|  | 		android:key="@string/streams_notifications_channels_key" | ||||||
|  | 		android:title="@string/channels" | ||||||
|  | 		app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|  | </PreferenceScreen> | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" | <PreferenceScreen | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|     android:key="general_preferences" |     android:key="general_preferences" | ||||||
|     android:title="@string/settings"> |     android:title="@string/settings"> | ||||||
| @@ -40,6 +41,12 @@ | |||||||
|         android:title="@string/settings_category_notification_title" |         android:title="@string/settings_category_notification_title" | ||||||
|         app:iconSpaceReserved="false" /> |         app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|  |     <PreferenceScreen | ||||||
|  |         android:fragment="org.schabi.newpipe.settings.NotificationsSettingsFragment" | ||||||
|  |         android:icon="@drawable/ic_notifications" | ||||||
|  |         android:title="@string/notifications" | ||||||
|  |         app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|     <PreferenceScreen |     <PreferenceScreen | ||||||
|         android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment" |         android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment" | ||||||
|         android:icon="@drawable/ic_cloud_download" |         android:icon="@drawable/ic_cloud_download" | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								assets/db.dia
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/db.dia
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user
	 Vasiliy
					Vasiliy