Skip to content

Commit 60cb6a1

Browse files
committed
feat: Implement channel blocking feature (#12647)
- Add BlockedChannelsManager utility class for local storage using SharedPreferences - Implement automatic filtering of blocked channels in Trending, Search, and Recommendations - Add 'Block Channel' option in video long-press context menu with undo support - Create Blocked Channels management screen in Settings > Content - Add comprehensive string resources for the feature - Update gradle.properties to use Java 17 for build compatibility Features: * BlockedChannelsManager: Manages blocked channel IDs via SharedPreferences * InfoListAdapter: Filters out videos from blocked channels automatically * StreamDialogDefaultEntry: New BLOCK_CHANNEL entry with Snackbar undo * BlockedChannelsFragment: UI for viewing and managing blocked channels * Settings integration: Accessible via Settings > Content > Blocked Channels Tested: - Debug APK build successful - All code passes checkstyle and ktlint validation - Feature follows MVVM architecture and NewPipe coding standards
1 parent f836f5e commit 60cb6a1

10 files changed

Lines changed: 366 additions & 3 deletions

File tree

app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@
3232
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
3333
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
3434
import org.schabi.newpipe.local.history.HistoryRecordManager;
35+
import org.schabi.newpipe.util.BlockedChannelsManager;
3536
import org.schabi.newpipe.util.FallbackViewHolder;
3637
import org.schabi.newpipe.util.OnClickGesture;
3738

3839
import java.util.ArrayList;
3940
import java.util.List;
4041
import java.util.function.Supplier;
42+
import java.util.stream.Collectors;
4143

4244
/*
4345
* Created by Christian Schabesberger on 01.08.16.
@@ -132,16 +134,33 @@ public void addInfoItemList(@Nullable final List<? extends InfoItem> data) {
132134
+ infoItemList.size() + ", data.size() = " + data.size());
133135
}
134136

137+
// Filter out items from blocked channels
138+
final List<? extends InfoItem> filteredData = data.stream()
139+
.filter(item -> {
140+
if (item instanceof StreamInfoItem) {
141+
final StreamInfoItem streamItem = (StreamInfoItem) item;
142+
final String uploaderUrl = streamItem.getUploaderUrl();
143+
return !BlockedChannelsManager.INSTANCE.isChannelBlocked(
144+
infoItemBuilder.getContext(), uploaderUrl);
145+
}
146+
return true; // Keep non-stream items (channels, playlists, etc.)
147+
})
148+
.collect(Collectors.toList());
149+
150+
if (filteredData.isEmpty()) {
151+
return;
152+
}
153+
135154
final int offsetStart = sizeConsideringHeaderOffset();
136-
infoItemList.addAll(data);
155+
infoItemList.addAll(filteredData);
137156

138157
if (DEBUG) {
139158
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
140159
+ "infoItemList.size() = " + infoItemList.size() + ", "
141160
+ "hasHeader = " + hasHeader() + ", "
142161
+ "showFooter = " + showFooter);
143162
}
144-
notifyItemRangeInserted(offsetStart, data.size());
163+
notifyItemRangeInserted(offsetStart, filteredData.size());
145164

146165
if (showFooter) {
147166
final int footerNow = sizeConsideringHeaderOffset();

app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ public Builder addDefaultEndEntries() {
329329
);
330330
addPlayWithKodiEntryIfNeeded();
331331
addMarkAsWatchedEntryIfNeeded();
332+
addEntry(StreamDialogDefaultEntry.BLOCK_CHANNEL);
332333
addEntry(StreamDialogDefaultEntry.SHOW_CHANNEL_DETAILS);
333334
return this;
334335
}

app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
import androidx.annotation.NonNull;
1111
import androidx.annotation.StringRes;
1212

13+
import com.google.android.material.snackbar.Snackbar;
14+
1315
import org.schabi.newpipe.R;
1416
import org.schabi.newpipe.database.stream.model.StreamEntity;
1517
import org.schabi.newpipe.download.DownloadDialog;
1618
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
1719
import org.schabi.newpipe.local.dialog.PlaylistDialog;
1820
import org.schabi.newpipe.local.history.HistoryRecordManager;
21+
import org.schabi.newpipe.util.BlockedChannelsManager;
1922
import org.schabi.newpipe.util.NavigationHelper;
2023
import org.schabi.newpipe.util.external_communication.KoreUtils;
2124
import org.schabi.newpipe.util.external_communication.ShareUtils;
@@ -135,7 +138,34 @@ public enum StreamDialogDefaultEntry {
135138
.onErrorComplete()
136139
.observeOn(AndroidSchedulers.mainThread())
137140
.subscribe()
138-
);
141+
),
142+
143+
BLOCK_CHANNEL(R.string.block_channel, (fragment, item) -> {
144+
final String uploaderUrl = item.getUploaderUrl();
145+
final String uploaderName = item.getUploaderName();
146+
147+
if (uploaderUrl != null && !uploaderUrl.isEmpty()) {
148+
// Block the channel
149+
BlockedChannelsManager.INSTANCE.blockChannel(
150+
fragment.requireContext(), uploaderUrl, uploaderName);
151+
152+
// Show snackbar with undo action
153+
final Snackbar snackbar = Snackbar.make(
154+
fragment.requireActivity().findViewById(android.R.id.content),
155+
fragment.getString(R.string.channel_blocked,
156+
uploaderName != null ? uploaderName : ""),
157+
Snackbar.LENGTH_LONG
158+
);
159+
160+
snackbar.setAction(R.string.undo, v -> {
161+
// Unblock the channel
162+
BlockedChannelsManager.INSTANCE.unblockChannel(
163+
fragment.requireContext(), uploaderUrl);
164+
});
165+
166+
snackbar.show();
167+
}
168+
});
139169

140170

141171
@StringRes
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package org.schabi.newpipe.settings
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.appcompat.app.AlertDialog
8+
import androidx.fragment.app.Fragment
9+
import androidx.recyclerview.widget.LinearLayoutManager
10+
import androidx.recyclerview.widget.RecyclerView
11+
import com.google.android.material.snackbar.Snackbar
12+
import org.schabi.newpipe.R
13+
import org.schabi.newpipe.databinding.FragmentBlockedChannelsBinding
14+
import org.schabi.newpipe.util.BlockedChannelsManager
15+
16+
/**
17+
* Fragment to display and manage blocked channels
18+
*/
19+
class BlockedChannelsFragment : Fragment() {
20+
private var _binding: FragmentBlockedChannelsBinding? = null
21+
private val binding get() = _binding!!
22+
23+
private lateinit var adapter: BlockedChannelsAdapter
24+
private val blockedChannels = mutableListOf<String>()
25+
26+
override fun onCreateView(
27+
inflater: LayoutInflater,
28+
container: ViewGroup?,
29+
savedInstanceState: Bundle?
30+
): View {
31+
_binding = FragmentBlockedChannelsBinding.inflate(inflater, container, false)
32+
return binding.root
33+
}
34+
35+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
36+
super.onViewCreated(view, savedInstanceState)
37+
38+
setupRecyclerView()
39+
loadBlockedChannels()
40+
}
41+
42+
private fun setupRecyclerView() {
43+
adapter = BlockedChannelsAdapter(blockedChannels) { channelUrl ->
44+
showUnblockDialog(channelUrl)
45+
}
46+
47+
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
48+
binding.recyclerView.adapter = adapter
49+
}
50+
51+
private fun loadBlockedChannels() {
52+
blockedChannels.clear()
53+
blockedChannels.addAll(BlockedChannelsManager.getBlockedChannelsList(requireContext()))
54+
adapter.notifyDataSetChanged()
55+
56+
updateEmptyView()
57+
}
58+
59+
private fun updateEmptyView() {
60+
if (blockedChannels.isEmpty()) {
61+
binding.emptyView.visibility = View.VISIBLE
62+
binding.recyclerView.visibility = View.GONE
63+
} else {
64+
binding.emptyView.visibility = View.GONE
65+
binding.recyclerView.visibility = View.VISIBLE
66+
}
67+
}
68+
69+
private fun showUnblockDialog(channelUrl: String) {
70+
AlertDialog.Builder(requireContext())
71+
.setTitle(R.string.unblock_channel)
72+
.setMessage(R.string.unblock_channel_confirmation)
73+
.setPositiveButton(R.string.unblock_channel) { _, _ ->
74+
unblockChannel(channelUrl)
75+
}
76+
.setNegativeButton(R.string.cancel, null)
77+
.show()
78+
}
79+
80+
private fun unblockChannel(channelUrl: String) {
81+
BlockedChannelsManager.unblockChannel(requireContext(), channelUrl)
82+
loadBlockedChannels()
83+
84+
Snackbar.make(
85+
binding.root,
86+
R.string.channel_unblocked,
87+
Snackbar.LENGTH_SHORT
88+
).show()
89+
}
90+
91+
override fun onDestroyView() {
92+
super.onDestroyView()
93+
_binding = null
94+
}
95+
96+
/**
97+
* RecyclerView adapter for blocked channels
98+
*/
99+
private class BlockedChannelsAdapter(
100+
private val channels: List<String>,
101+
private val onUnblockClick: (String) -> Unit
102+
) : RecyclerView.Adapter<BlockedChannelsAdapter.ViewHolder>() {
103+
104+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
105+
val view = LayoutInflater.from(parent.context)
106+
.inflate(R.layout.item_blocked_channel, parent, false)
107+
return ViewHolder(view)
108+
}
109+
110+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
111+
val channelUrl = channels[position]
112+
holder.bind(channelUrl, onUnblockClick)
113+
}
114+
115+
override fun getItemCount(): Int = channels.size
116+
117+
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
118+
private val channelUrlText: android.widget.TextView = itemView.findViewById(R.id.channel_url)
119+
private val unblockButton: android.widget.Button = itemView.findViewById(R.id.unblock_button)
120+
121+
fun bind(channelUrl: String, onUnblockClick: (String) -> Unit) {
122+
channelUrlText.text = channelUrl
123+
unblockButton.setOnClickListener {
124+
onUnblockClick(channelUrl)
125+
}
126+
}
127+
}
128+
}
129+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package org.schabi.newpipe.util
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import androidx.preference.PreferenceManager
6+
7+
/**
8+
* Manager for handling blocked channels.
9+
* Stores blocked channel IDs in SharedPreferences and provides methods to add, remove, and check blocked channels.
10+
*/
11+
object BlockedChannelsManager {
12+
private const val BLOCKED_CHANNELS_KEY = "blocked_channels"
13+
private const val DELIMITER = ","
14+
15+
/**
16+
* Get the SharedPreferences instance
17+
*/
18+
private fun getPreferences(context: Context): SharedPreferences {
19+
return PreferenceManager.getDefaultSharedPreferences(context)
20+
}
21+
22+
/**
23+
* Get the set of blocked channel IDs
24+
*/
25+
fun getBlockedChannelIds(context: Context): Set<String> {
26+
val prefs = getPreferences(context)
27+
val blockedString = prefs.getString(BLOCKED_CHANNELS_KEY, "") ?: ""
28+
return if (blockedString.isEmpty()) {
29+
emptySet()
30+
} else {
31+
blockedString.split(DELIMITER).toSet()
32+
}
33+
}
34+
35+
/**
36+
* Check if a channel is blocked
37+
*
38+
* @param context Application context
39+
* @param channelUrl The channel URL to check
40+
* @return true if the channel is blocked, false otherwise
41+
*/
42+
fun isChannelBlocked(context: Context, channelUrl: String?): Boolean {
43+
if (channelUrl.isNullOrEmpty()) {
44+
return false
45+
}
46+
return getBlockedChannelIds(context).contains(channelUrl)
47+
}
48+
49+
/**
50+
* Block a channel
51+
*
52+
* @param context Application context
53+
* @param channelUrl The channel URL to block
54+
* @param channelName The channel name (for logging/debugging)
55+
*/
56+
fun blockChannel(context: Context, channelUrl: String, channelName: String? = null) {
57+
val blockedChannels = getBlockedChannelIds(context).toMutableSet()
58+
blockedChannels.add(channelUrl)
59+
saveBlockedChannels(context, blockedChannels)
60+
}
61+
62+
/**
63+
* Unblock a channel
64+
*
65+
* @param context Application context
66+
* @param channelUrl The channel URL to unblock
67+
*/
68+
fun unblockChannel(context: Context, channelUrl: String) {
69+
val blockedChannels = getBlockedChannelIds(context).toMutableSet()
70+
blockedChannels.remove(channelUrl)
71+
saveBlockedChannels(context, blockedChannels)
72+
}
73+
74+
/**
75+
* Get all blocked channels as a list of channel URLs
76+
*
77+
* @param context Application context
78+
* @return List of blocked channel URLs
79+
*/
80+
fun getBlockedChannelsList(context: Context): List<String> {
81+
return getBlockedChannelIds(context).toList()
82+
}
83+
84+
/**
85+
* Clear all blocked channels
86+
*
87+
* @param context Application context
88+
*/
89+
fun clearAllBlockedChannels(context: Context) {
90+
getPreferences(context).edit()
91+
.remove(BLOCKED_CHANNELS_KEY)
92+
.apply()
93+
}
94+
95+
/**
96+
* Save the blocked channels set to SharedPreferences
97+
*/
98+
private fun saveBlockedChannels(context: Context, blockedChannels: Set<String>) {
99+
val blockedString = blockedChannels.joinToString(DELIMITER)
100+
getPreferences(context).edit()
101+
.putString(BLOCKED_CHANNELS_KEY, blockedString)
102+
.apply()
103+
}
104+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
android:layout_width="match_parent"
5+
android:layout_height="match_parent">
6+
7+
<androidx.recyclerview.widget.RecyclerView
8+
android:id="@+id/recyclerView"
9+
android:layout_width="match_parent"
10+
android:layout_height="match_parent"
11+
android:clipToPadding="false"
12+
android:paddingTop="8dp"
13+
android:paddingBottom="8dp" />
14+
15+
<TextView
16+
android:id="@+id/emptyView"
17+
android:layout_width="wrap_content"
18+
android:layout_height="wrap_content"
19+
android:layout_gravity="center"
20+
android:text="@string/no_blocked_channels"
21+
android:textAppearance="?android:attr/textAppearanceMedium"
22+
android:visibility="gone" />
23+
24+
</androidx.coordinatorlayout.widget.CoordinatorLayout>

0 commit comments

Comments
 (0)