Notifications about new streams
This commit is contained in:
parent
6a1d81fcf3
commit
da9bd1d420
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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>
|
Binary file not shown.
After Width: | Height: | Size: 413 B |
Binary file not shown.
After Width: | Height: | Size: 294 B |
|
@ -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>
|
Binary file not shown.
After Width: | Height: | Size: 522 B |
Binary file not shown.
After Width: | Height: | Size: 731 B |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.
Loading…
Reference in New Issue