Notifications about new streams
This commit is contained in:
parent
6a1d81fcf3
commit
da9bd1d420
40 changed files with 1090 additions and 27 deletions
|
@ -108,6 +108,7 @@ ext {
|
|||
leakCanaryVersion = '2.5'
|
||||
stethoVersion = '1.6.0'
|
||||
mockitoVersion = '3.6.0'
|
||||
workVersion = '2.5.0'
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
@ -213,6 +214,8 @@ dependencies {
|
|||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation "androidx.work:work-runtime:${workVersion}"
|
||||
implementation "androidx.work:work-rxjava2:${workVersion}"
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
|
|
|
@ -40,6 +40,12 @@
|
|||
android:title="@string/settings_category_notification_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.NotificationsSettingsFragment"
|
||||
android:icon="@drawable/ic_notifications"
|
||||
android:title="@string/notifications"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment"
|
||||
android:icon="@drawable/ic_cloud_download"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
@ -108,7 +110,6 @@ public class App extends MultiDexApplication {
|
|||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
||||
|
||||
configureRxJavaErrorHandler();
|
||||
|
||||
// Check for new version
|
||||
disposable = CheckForNewAppVersion.checkNewVersion(this);
|
||||
}
|
||||
|
@ -249,9 +250,20 @@ public class App extends MultiDexApplication {
|
|||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.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);
|
||||
notificationManager.createNotificationChannelsCompat(Arrays.asList(mainChannel,
|
||||
appUpdateChannel, hashChannel));
|
||||
notificationManager.createNotificationChannels(
|
||||
Arrays.asList(mainChannel, appUpdateChannel, hashChannel, newStreamsChannel)
|
||||
);
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
|
|
|
@ -69,6 +69,7 @@ import org.schabi.newpipe.fragments.BackPressable;
|
|||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
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.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
|
@ -158,11 +159,11 @@ public class MainActivity extends AppCompatActivity {
|
|||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiErrorInSnackbar(this, "Setting up drawer", e);
|
||||
}
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
NotificationWorker.schedule(this);
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
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.database.Cursor;
|
||||
|
||||
|
@ -8,11 +14,6 @@ import androidx.room.Room;
|
|||
|
||||
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 {
|
||||
private static volatile AppDatabase databaseInstance;
|
||||
|
||||
|
@ -23,7 +24,7 @@ public final class NewPipeDatabase {
|
|||
private static AppDatabase getDatabase(final Context context) {
|
||||
return Room
|
||||
.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();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.schabi.newpipe.database;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_5;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.RoomDatabase;
|
||||
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.SubscriptionEntity;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_4;
|
||||
|
||||
@TypeConverters({Converters.class})
|
||||
@Database(
|
||||
entities = {
|
||||
|
@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_4;
|
|||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||
FeedLastUpdatedEntity.class
|
||||
},
|
||||
version = DB_VER_4
|
||||
version = DB_VER_5
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
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_3 = 3;
|
||||
public static final int DB_VER_4 = 4;
|
||||
public static final int DB_VER_5 = 5;
|
||||
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
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)
|
||||
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(
|
||||
"""
|
||||
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_SUBSCRIBER_COUNT = "subscriber_count";
|
||||
public static final String SUBSCRIPTION_DESCRIPTION = "description";
|
||||
public static final String SUBSCRIPTION_NOTIFICATION_MODE = "notification_mode";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long uid = 0;
|
||||
|
@ -48,6 +49,9 @@ public class SubscriptionEntity {
|
|||
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
||||
private String description;
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE)
|
||||
private int notificationMode;
|
||||
|
||||
@Ignore
|
||||
public static SubscriptionEntity from(@NonNull final ChannelInfo info) {
|
||||
final SubscriptionEntity result = new SubscriptionEntity();
|
||||
|
@ -114,6 +118,15 @@ public class SubscriptionEntity {
|
|||
this.description = description;
|
||||
}
|
||||
|
||||
@NotificationMode
|
||||
public int getNotificationMode() {
|
||||
return notificationMode;
|
||||
}
|
||||
|
||||
public void setNotificationMode(@NotificationMode final int notificationMode) {
|
||||
this.notificationMode = notificationMode;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public void setData(final String n, final String au, final String d, final Long sc) {
|
||||
this.setName(n);
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package org.schabi.newpipe.fragments.list.channel;
|
||||
|
||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
@ -19,9 +24,11 @@ import androidx.appcompat.app.ActionBar;
|
|||
import androidx.core.content.ContextCompat;
|
||||
import androidx.viewbinding.ViewBinding;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.NotificationMode;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
|
||||
import org.schabi.newpipe.databinding.FragmentChannelBinding;
|
||||
|
@ -37,6 +44,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.notifications.NotificationHelper;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
@ -60,10 +68,6 @@ import io.reactivex.rxjava3.functions.Consumer;
|
|||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.ktx.TextViewUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
|
||||
|
@ -84,6 +88,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
|||
private PlaylistControlBinding playlistControlBinding;
|
||||
|
||||
private MenuItem menuRssButton;
|
||||
private MenuItem menuNotifyButton;
|
||||
|
||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
|
@ -181,6 +186,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
|||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,6 +203,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
|||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
break;
|
||||
case R.id.menu_item_notify:
|
||||
final boolean value = !item.isChecked();
|
||||
item.setEnabled(false);
|
||||
setNotify(value);
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
openRssFeed();
|
||||
break;
|
||||
|
@ -238,15 +249,22 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
|||
.subscribe(getSubscribeUpdateMonitor(info), onError));
|
||||
|
||||
disposables.add(observable
|
||||
// Some updates are very rapid
|
||||
// (for example when calling the updateSubscription(info))
|
||||
// so only update the UI for the latest emission
|
||||
// ("sync" the subscribe button's state)
|
||||
.debounce(100, TimeUnit.MILLISECONDS)
|
||||
.map(List::isEmpty)
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty()), onError));
|
||||
.subscribe((Boolean isEmpty) -> updateSubscribeButton(!isEmpty), onError));
|
||||
|
||||
disposables.add(observable
|
||||
.map(List::isEmpty)
|
||||
.filter(x -> NotificationHelper.isNewStreamsNotificationsEnabled(requireContext()))
|
||||
.distinctUntilChanged()
|
||||
.skip(1)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(isEmpty -> {
|
||||
if (!isEmpty) {
|
||||
showNotifySnackbar();
|
||||
}
|
||||
}, onError));
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
||||
|
@ -326,6 +344,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
|||
info.getAvatarUrl(),
|
||||
info.getDescription(),
|
||||
info.getSubscriberCount());
|
||||
updateNotifyButton(null);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(
|
||||
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
|
||||
} else {
|
||||
|
@ -333,6 +352,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
|||
Log.d(TAG, "Found subscription to this channel!");
|
||||
}
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
updateNotifyButton(subscription);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(
|
||||
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
}
|
||||
|
@ -375,6 +395,41 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
|||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
|
|
@ -7,6 +7,8 @@ import io.reactivex.rxjava3.core.Flowable
|
|||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import org.schabi.newpipe.extractor.ListInfo
|
||||
|
@ -14,6 +16,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo
|
|||
import org.schabi.newpipe.extractor.feed.FeedInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||
import org.schabi.newpipe.util.ExtractorHelper
|
||||
|
||||
class SubscriptionManager(context: Context) {
|
||||
private val database = NewPipeDatabase.getInstance(context)
|
||||
|
@ -66,6 +69,16 @@ class SubscriptionManager(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateNotificationMode(serviceId: Int, url: String?, @NotificationMode mode: Int): Completable {
|
||||
return subscriptionTable().getSubscription(serviceId, url!!)
|
||||
.flatMapCompletable { entity: SubscriptionEntity ->
|
||||
Completable.fromAction {
|
||||
entity.notificationMode = mode
|
||||
subscriptionTable().update(entity)
|
||||
}.andThen(rememberLastStream(entity))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFromInfo(subscriptionId: Long, info: ListInfo<StreamInfoItem>) {
|
||||
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
|
||||
|
||||
|
@ -94,4 +107,14 @@ class SubscriptionManager(context: Context) {
|
|||
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||
subscriptionTable.delete(subscriptionEntity)
|
||||
}
|
||||
|
||||
private fun rememberLastStream(subscription: SubscriptionEntity): Completable {
|
||||
return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false)
|
||||
.map { channel -> channel.relatedItems.map { stream -> StreamEntity(stream) } }
|
||||
.flatMapCompletable { entities ->
|
||||
Completable.fromAction {
|
||||
database.streamDAO().upsertAll(entities)
|
||||
}
|
||||
}.onErrorComplete()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
|
|
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"
|
||||
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
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="1"
|
||||
|
|
|
@ -169,6 +169,11 @@
|
|||
<item quantity="many">%s видео</item>
|
||||
<item quantity="other">%s видео</item>
|
||||
</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="main_page_content">Главная страница</string>
|
||||
<string name="blank_page_summary">Пустая страница</string>
|
||||
|
@ -683,4 +688,20 @@
|
|||
<string name="main_page_content_swipe_remove">Удалять элементы смахиванием</string>
|
||||
<string name="start_main_player_fullscreen_summary">Не начинать просмотр видео в мини-плеере, но сразу переключиться в полноэкранный режим, если автовращение экрана заблокировано. Вы можете переключиться на мини-плеер, выйдя из полноэкранного режима.</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>
|
|
@ -443,4 +443,5 @@
|
|||
<string name="seek_duration_title">快进 / 快退的单位时间</string>
|
||||
<string name="clear_download_history">清除下载历史记录</string>
|
||||
<string name="delete_downloaded_files">删除下载了的文件</string>
|
||||
<string name="enumeration_comma">、</string>
|
||||
</resources>
|
|
@ -132,4 +132,5 @@
|
|||
<string name="use_inexact_seek_title">使用粗略快查</string>
|
||||
<string name="controls_add_to_playlist_title">添加到</string>
|
||||
<string name="tab_choose">選擇標籤</string>
|
||||
<string name="enumeration_comma">、</string>
|
||||
</resources>
|
|
@ -1260,4 +1260,34 @@
|
|||
</string-array>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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"
|
||||
tools:ignore="MissingTranslation">
|
||||
|
||||
|
@ -180,6 +180,7 @@
|
|||
<string name="always">Always</string>
|
||||
<string name="just_once">Just Once</string>
|
||||
<string name="file">File</string>
|
||||
<string name="notifications">Notifications</string>
|
||||
<string name="notification_channel_id" translatable="false">newpipe</string>
|
||||
<string name="notification_channel_name">NewPipe Notification</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_name">Video Hash Notification</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="switch_to_background">Switch to Background</string>
|
||||
<string name="switch_to_popup">Switch to Popup</string>
|
||||
|
@ -309,6 +313,10 @@
|
|||
</plurals>
|
||||
<string name="no_comments">No comments</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 -->
|
||||
<string name="start">Start</string>
|
||||
<string name="pause">Pause</string>
|
||||
|
@ -513,6 +521,17 @@
|
|||
<item>240p</item>
|
||||
<item>144p</item>
|
||||
</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 -->
|
||||
<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>
|
||||
|
@ -703,4 +722,10 @@
|
|||
<!-- Show Channel Details -->
|
||||
<string name="error_show_channel_details">Error at Show 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>
|
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"?>
|
||||
<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"
|
||||
android:key="general_preferences"
|
||||
android:title="@string/settings">
|
||||
|
@ -40,6 +41,12 @@
|
|||
android:title="@string/settings_category_notification_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.NotificationsSettingsFragment"
|
||||
android:icon="@drawable/ic_notifications"
|
||||
android:title="@string/notifications"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment"
|
||||
android:icon="@drawable/ic_cloud_download"
|
||||
|
|
BIN
assets/db.dia
BIN
assets/db.dia
Binary file not shown.
Loading…
Reference in a new issue