From 60cb6a11a823c1c5c221fb9a07ee29644a32a46b Mon Sep 17 00:00:00 2001 From: Syed Mutaib Date: Tue, 11 Nov 2025 17:40:08 +0000 Subject: [PATCH] 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 --- .../newpipe/info_list/InfoListAdapter.java | 23 +++- .../info_list/dialog/InfoItemDialog.java | 1 + .../dialog/StreamDialogDefaultEntry.java | 32 ++++- .../settings/BlockedChannelsFragment.kt | 129 ++++++++++++++++++ .../newpipe/util/BlockedChannelsManager.kt | 104 ++++++++++++++ .../res/layout/fragment_blocked_channels.xml | 24 ++++ .../main/res/layout/item_blocked_channel.xml | 38 ++++++ app/src/main/res/values/strings.xml | 9 ++ app/src/main/res/xml/content_settings.xml | 8 ++ gradle.properties | 1 + 10 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsFragment.kt create mode 100644 app/src/main/java/org/schabi/newpipe/util/BlockedChannelsManager.kt create mode 100644 app/src/main/res/layout/fragment_blocked_channels.xml create mode 100644 app/src/main/res/layout/item_blocked_channel.xml diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 575568c00..6bc3497df 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -32,12 +32,14 @@ import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.util.BlockedChannelsManager; import org.schabi.newpipe.util.FallbackViewHolder; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; +import java.util.stream.Collectors; /* * Created by Christian Schabesberger on 01.08.16. @@ -132,8 +134,25 @@ public class InfoListAdapter extends RecyclerView.Adapter offsetStart = " + offsetStart + ", " @@ -141,7 +160,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + // Unblock the channel + BlockedChannelsManager.INSTANCE.unblockChannel( + fragment.requireContext(), uploaderUrl); + }); + + snackbar.show(); + } + }); @StringRes diff --git a/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsFragment.kt new file mode 100644 index 000000000..e957159bc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsFragment.kt @@ -0,0 +1,129 @@ +package org.schabi.newpipe.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar +import org.schabi.newpipe.R +import org.schabi.newpipe.databinding.FragmentBlockedChannelsBinding +import org.schabi.newpipe.util.BlockedChannelsManager + +/** + * Fragment to display and manage blocked channels + */ +class BlockedChannelsFragment : Fragment() { + private var _binding: FragmentBlockedChannelsBinding? = null + private val binding get() = _binding!! + + private lateinit var adapter: BlockedChannelsAdapter + private val blockedChannels = mutableListOf() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentBlockedChannelsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupRecyclerView() + loadBlockedChannels() + } + + private fun setupRecyclerView() { + adapter = BlockedChannelsAdapter(blockedChannels) { channelUrl -> + showUnblockDialog(channelUrl) + } + + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + } + + private fun loadBlockedChannels() { + blockedChannels.clear() + blockedChannels.addAll(BlockedChannelsManager.getBlockedChannelsList(requireContext())) + adapter.notifyDataSetChanged() + + updateEmptyView() + } + + private fun updateEmptyView() { + if (blockedChannels.isEmpty()) { + binding.emptyView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE + } else { + binding.emptyView.visibility = View.GONE + binding.recyclerView.visibility = View.VISIBLE + } + } + + private fun showUnblockDialog(channelUrl: String) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.unblock_channel) + .setMessage(R.string.unblock_channel_confirmation) + .setPositiveButton(R.string.unblock_channel) { _, _ -> + unblockChannel(channelUrl) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + private fun unblockChannel(channelUrl: String) { + BlockedChannelsManager.unblockChannel(requireContext(), channelUrl) + loadBlockedChannels() + + Snackbar.make( + binding.root, + R.string.channel_unblocked, + Snackbar.LENGTH_SHORT + ).show() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + /** + * RecyclerView adapter for blocked channels + */ + private class BlockedChannelsAdapter( + private val channels: List, + private val onUnblockClick: (String) -> Unit + ) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_blocked_channel, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val channelUrl = channels[position] + holder.bind(channelUrl, onUnblockClick) + } + + override fun getItemCount(): Int = channels.size + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val channelUrlText: android.widget.TextView = itemView.findViewById(R.id.channel_url) + private val unblockButton: android.widget.Button = itemView.findViewById(R.id.unblock_button) + + fun bind(channelUrl: String, onUnblockClick: (String) -> Unit) { + channelUrlText.text = channelUrl + unblockButton.setOnClickListener { + onUnblockClick(channelUrl) + } + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/BlockedChannelsManager.kt b/app/src/main/java/org/schabi/newpipe/util/BlockedChannelsManager.kt new file mode 100644 index 000000000..8e11b76fd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/BlockedChannelsManager.kt @@ -0,0 +1,104 @@ +package org.schabi.newpipe.util + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager + +/** + * Manager for handling blocked channels. + * Stores blocked channel IDs in SharedPreferences and provides methods to add, remove, and check blocked channels. + */ +object BlockedChannelsManager { + private const val BLOCKED_CHANNELS_KEY = "blocked_channels" + private const val DELIMITER = "," + + /** + * Get the SharedPreferences instance + */ + private fun getPreferences(context: Context): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(context) + } + + /** + * Get the set of blocked channel IDs + */ + fun getBlockedChannelIds(context: Context): Set { + val prefs = getPreferences(context) + val blockedString = prefs.getString(BLOCKED_CHANNELS_KEY, "") ?: "" + return if (blockedString.isEmpty()) { + emptySet() + } else { + blockedString.split(DELIMITER).toSet() + } + } + + /** + * Check if a channel is blocked + * + * @param context Application context + * @param channelUrl The channel URL to check + * @return true if the channel is blocked, false otherwise + */ + fun isChannelBlocked(context: Context, channelUrl: String?): Boolean { + if (channelUrl.isNullOrEmpty()) { + return false + } + return getBlockedChannelIds(context).contains(channelUrl) + } + + /** + * Block a channel + * + * @param context Application context + * @param channelUrl The channel URL to block + * @param channelName The channel name (for logging/debugging) + */ + fun blockChannel(context: Context, channelUrl: String, channelName: String? = null) { + val blockedChannels = getBlockedChannelIds(context).toMutableSet() + blockedChannels.add(channelUrl) + saveBlockedChannels(context, blockedChannels) + } + + /** + * Unblock a channel + * + * @param context Application context + * @param channelUrl The channel URL to unblock + */ + fun unblockChannel(context: Context, channelUrl: String) { + val blockedChannels = getBlockedChannelIds(context).toMutableSet() + blockedChannels.remove(channelUrl) + saveBlockedChannels(context, blockedChannels) + } + + /** + * Get all blocked channels as a list of channel URLs + * + * @param context Application context + * @return List of blocked channel URLs + */ + fun getBlockedChannelsList(context: Context): List { + return getBlockedChannelIds(context).toList() + } + + /** + * Clear all blocked channels + * + * @param context Application context + */ + fun clearAllBlockedChannels(context: Context) { + getPreferences(context).edit() + .remove(BLOCKED_CHANNELS_KEY) + .apply() + } + + /** + * Save the blocked channels set to SharedPreferences + */ + private fun saveBlockedChannels(context: Context, blockedChannels: Set) { + val blockedString = blockedChannels.joinToString(DELIMITER) + getPreferences(context).edit() + .putString(BLOCKED_CHANNELS_KEY, blockedString) + .apply() + } +} diff --git a/app/src/main/res/layout/fragment_blocked_channels.xml b/app/src/main/res/layout/fragment_blocked_channels.xml new file mode 100644 index 000000000..c61c8eaf8 --- /dev/null +++ b/app/src/main/res/layout/fragment_blocked_channels.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_blocked_channel.xml b/app/src/main/res/layout/item_blocked_channel.xml new file mode 100644 index 000000000..99f0abdc9 --- /dev/null +++ b/app/src/main/res/layout/item_blocked_channel.xml @@ -0,0 +1,38 @@ + + + + + + + +