Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ dependencies {
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.compose.ui:ui-test-junit4'
testImplementation 'androidx.work:work-testing:2.11.2'
testImplementation 'org.robolectric:robolectric:4.17'
testImplementation 'com.squareup.okhttp3:mockwebserver3:5.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class BlocklistsScreenTest {
BlocklistsScreenContent(
model = BlocklistsScreenModel(listOf(first, second)),
callbacks = object : BlocklistsScreenCallbacks {
override fun onDownload() = Unit

override fun onAdd() {
addClicks++
}
Expand Down Expand Up @@ -90,6 +92,8 @@ class BlocklistsScreenTest {
BlocklistsScreenContent(
model = BlocklistsScreenModel(emptyList()),
callbacks = object : BlocklistsScreenCallbacks {
override fun onDownload() = Unit

override fun onAdd() {
addClicks++
}
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/eu/faircode/netguard/ActivitySettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,15 @@ public boolean onPreferenceClick(Preference preference) {
});
}

// Custom lists cannot affect the Play build's Minimal-only policy.
if (BlockingMode.isPlayStoreBuild() && cat_advanced != null) {
for (String key : new String[]{"manage_blocklists", "hosts_download", "hosts_auto_update"}) {
Preference preference = cat_advanced.findPreference(key);
if (preference != null)
cat_advanced.removePreference(preference);
}
}

// Hosts file settings
if (cat_advanced != null) {
Preference pref_use_hosts = screen.findPreference("use_hosts");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.compose.ui.platform.ComposeView;
import androidx.preference.PreferenceManager;
import androidx.work.Constraints;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;

import net.kollnig.missioncontrol.data.Blocklist;
import net.kollnig.missioncontrol.data.BlocklistManager;
import net.kollnig.missioncontrol.data.BlockingMode;
import net.kollnig.missioncontrol.ui.compose.BlocklistRow;
import net.kollnig.missioncontrol.ui.compose.BlocklistsScreen;
import net.kollnig.missioncontrol.ui.compose.BlocklistsScreenCallbacks;
Expand All @@ -28,16 +36,24 @@
import java.util.List;

import eu.faircode.netguard.ServiceSinkhole;
import eu.faircode.netguard.HostsDownloadWorker;
import eu.faircode.netguard.Util;

public class ActivityBlocklists extends AppCompatActivity {
private static final String DOWNLOAD_WORK = "CustomBlocklistsDownload";
private BlocklistManager manager;
private BlocklistsScreenController screenController;
private boolean downloading;
private boolean downloadRunning;

@Override
protected void onCreate(Bundle savedInstanceState) {
Util.setTheme(this);
super.onCreate(savedInstanceState);
if (BlockingMode.isPlayStoreBuild()) {
finish();
return;
}

ComposeView composeView = new ComposeView(this);
// A stable ID gives Compose's saveable state registry a key to persist
Expand Down Expand Up @@ -74,9 +90,29 @@ public void onEnabledChanged(String uuid, boolean enabled) {
public void onDelete(String uuid) {
showDeleteDialog(uuid);
}

@Override
public void onDownload() {
downloadBlocklists();
}
});
WorkManager.getInstance(this).getWorkInfosForUniqueWorkLiveData(DOWNLOAD_WORK)
.observe(this, workInfos -> {
downloading = workInfos != null && workInfos.stream()
.anyMatch(info -> !info.getState().isFinished());
downloadRunning = workInfos != null && workInfos.stream()
.anyMatch(info -> info.getState() == WorkInfo.State.RUNNING);
refreshScreen();
});
}

@Override
protected void onResume() {
super.onResume();
if (screenController != null)
refreshScreen();
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
Expand All @@ -89,13 +125,15 @@ public boolean onOptionsItemSelected(MenuItem item) {
private BlocklistsScreenModel buildScreenModel() {
List<BlocklistRow> rows = new ArrayList<>();
for (Blocklist item : manager.getBlocklists()) {
String lastUpdate = null;
String lastUpdate = getString(R.string.blocklist_not_downloaded);
if (item.lastModified > 0) {
String last = SimpleDateFormat.getDateTimeInstance().format(new Date(item.lastModified));
lastUpdate = getString(R.string.msg_last_update, last);
}

String error = item.lastDownloadSuccess ? null : item.lastErrorMessage;
String error = item.lastDownloadSuccess ? null :
(item.lastErrorMessage == null ? getString(R.string.blocklist_download_failed)
: item.lastErrorMessage);
rows.add(new BlocklistRow(
item.uuid,
item.url,
Expand All @@ -105,7 +143,12 @@ private BlocklistsScreenModel buildScreenModel() {
getString(R.string.blocklist_enable_description, item.url),
getString(R.string.blocklist_delete_description)));
}
return new BlocklistsScreenModel(rows);
String notice = null;
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("log_logcat", false))
notice = getString(R.string.blocklist_research_notice);
else if (BlockingMode.isMinimalMode(this))
notice = getString(R.string.blocklist_minimal_notice);
return new BlocklistsScreenModel(rows, notice, downloading, downloadRunning);
}

private void refreshScreen() {
Expand All @@ -131,6 +174,7 @@ private void showAddDialog(String uuid) {

AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(uuid == null ? R.string.title_add_blocklist : R.string.title_blocklists);
builder.setMessage(R.string.blocklist_format);

final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
Expand Down Expand Up @@ -159,13 +203,14 @@ private void showAddDialog(String uuid) {
item.lastModified = 0;
item.lastDownloadSuccess = true;
item.lastErrorMessage = null;
item.etag = null;
item.lastModifiedHeader = null;
manager.getBlocklistFile(item.uuid).delete();
}
item.url = url;
manager.updateBlocklist(item);
}
applyBlocklists();
refreshScreen();
downloadBlocklists();
} catch (MalformedURLException e) {
Toast.makeText(ActivityBlocklists.this, R.string.msg_invalid_url, Toast.LENGTH_SHORT).show();
}
Expand All @@ -185,6 +230,8 @@ private void updateEnabled(String uuid, boolean enabled) {
manager.updateBlocklist(item);
applyBlocklists();
refreshScreen();
if (enabled && !manager.getBlocklistFile(uuid).exists())
downloadBlocklists();
}

private void showDeleteDialog(String uuid) {
Expand Down Expand Up @@ -214,4 +261,20 @@ private void applyBlocklists() {

ServiceSinkhole.reload("blocklist changed", this, false);
}

private void downloadBlocklists() {
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(HostsDownloadWorker.class)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED).build())
.build();
// Serialise requests from this screen; an offline add remains queued.
WorkManager workManager = WorkManager.getInstance(this);
workManager.enqueueUniqueWork(DOWNLOAD_WORK, ExistingWorkPolicy.APPEND_OR_REPLACE, work);
downloading = true;
refreshScreen();
workManager.getWorkInfoByIdLiveData(work.getId()).observe(this, info -> {
if (info != null && info.getState() == WorkInfo.State.FAILED)
Toast.makeText(this, R.string.blocklist_download_failed, Toast.LENGTH_LONG).show();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.TextButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
Expand All @@ -41,6 +42,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
Expand All @@ -61,8 +63,11 @@ data class BlocklistRow(

/** Immutable presentation model for the Blocklists screen. */
@Immutable
data class BlocklistsScreenModel(
val rows: List<BlocklistRow>
data class BlocklistsScreenModel @JvmOverloads constructor(
val rows: List<BlocklistRow>,
val notice: String? = null,
val downloading: Boolean = false,
val downloadRunning: Boolean = false
)

/** Callback boundary for persistence and dialogs owned by ActivityBlocklists. */
Expand All @@ -71,6 +76,7 @@ interface BlocklistsScreenCallbacks {
fun onEdit(uuid: String)
fun onEnabledChanged(uuid: String, enabled: Boolean)
fun onDelete(uuid: String)
fun onDownload()
}

/** Handle used by ActivityBlocklists to refresh the Compose model. */
Expand Down Expand Up @@ -115,10 +121,14 @@ internal fun BlocklistsScreenContent(
contentWindowInsets = WindowInsets.safeDrawing,
floatingActionButton = {
FloatingActionButton(
onClick = callbacks::onAdd,
modifier = Modifier.testTag("blocklist-add"),
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
onClick = { if (!model.downloadRunning) callbacks.onAdd() },
modifier = Modifier.testTag("blocklist-add").semantics {
if (model.downloadRunning) disabled()
},
containerColor = if (model.downloadRunning) MaterialTheme.colorScheme.surfaceVariant
else MaterialTheme.colorScheme.primary,
contentColor = if (model.downloadRunning) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onPrimary
) {
Icon(
painter = painterResource(android.R.drawable.ic_input_add),
Expand All @@ -127,10 +137,38 @@ internal fun BlocklistsScreenContent(
}
}
) { contentPadding ->
if (model.rows.isEmpty()) {
EmptyBlocklistsContent(contentPadding)
} else {
BlocklistsList(model.rows, contentPadding, callbacks)
Column(Modifier.fillMaxSize().padding(contentPadding)) {
model.notice?.let { notice ->
Text(
text = notice,
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.secondaryContainer)
.padding(16.dp).testTag("blocklist-mode-notice"),
color = MaterialTheme.colorScheme.onSecondaryContainer,
style = MaterialTheme.typography.bodyMedium
)
}
if (model.notice == null) {
Text(
stringResource(R.string.blocklist_browser_notice),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.bodySmall
)
}
if (model.downloading) {
Text(
stringResource(R.string.blocklist_downloading),
modifier = Modifier.padding(16.dp).testTag("blocklist-downloading"),
style = MaterialTheme.typography.bodyMedium
)
}
TextButton(onClick = callbacks::onDownload, enabled = !model.downloading && model.rows.any { it.enabled }) {
Text(stringResource(R.string.blocklist_update))
}
if (model.rows.isEmpty()) {
EmptyBlocklistsContent(PaddingValues())
} else {
BlocklistsList(model.rows, PaddingValues(), callbacks, !model.downloadRunning)
}
}
}
}
Expand Down Expand Up @@ -167,7 +205,8 @@ private fun EmptyBlocklistsContent(contentPadding: PaddingValues) {
private fun BlocklistsList(
rows: List<BlocklistRow>,
contentPadding: PaddingValues,
callbacks: BlocklistsScreenCallbacks
callbacks: BlocklistsScreenCallbacks,
enabled: Boolean
) {
LazyColumn(
modifier = Modifier
Expand All @@ -179,7 +218,7 @@ private fun BlocklistsList(
items = rows,
key = { _, row -> row.uuid }
) { index, row ->
BlocklistListItem(row, callbacks)
BlocklistListItem(row, callbacks, enabled)
if (index < rows.lastIndex) {
HorizontalDivider(
modifier = Modifier.testTag("blocklist-divider"),
Expand All @@ -193,7 +232,8 @@ private fun BlocklistsList(
@Composable
private fun BlocklistListItem(
row: BlocklistRow,
callbacks: BlocklistsScreenCallbacks
callbacks: BlocklistsScreenCallbacks,
enabled: Boolean
) {
ListItem(
headlineContent = {
Expand Down Expand Up @@ -232,6 +272,7 @@ private fun BlocklistListItem(
) {
Switch(
checked = row.enabled,
enabled = enabled,
onCheckedChange = { enabled ->
callbacks.onEnabledChanged(row.uuid, enabled)
},
Expand All @@ -243,6 +284,7 @@ private fun BlocklistListItem(
)
IconButton(
onClick = { callbacks.onDelete(row.uuid) },
enabled = enabled,
modifier = Modifier.testTag("blocklist-delete-${row.uuid}")
) {
Icon(
Expand All @@ -254,7 +296,7 @@ private fun BlocklistListItem(
},
modifier = Modifier
.fillMaxWidth()
.clickable { callbacks.onEdit(row.uuid) }
.clickable(enabled = enabled) { callbacks.onEdit(row.uuid) }
.testTag("blocklist-row-${row.uuid}")
)
}
Expand Down Expand Up @@ -291,6 +333,7 @@ private fun BlocklistsScreenPreview() {
override fun onEdit(uuid: String) = Unit
override fun onEnabledChanged(uuid: String, enabled: Boolean) = Unit
override fun onDelete(uuid: String) = Unit
override fun onDownload() = Unit
}
)
}
Expand Down
21 changes: 15 additions & 6 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -823,15 +823,24 @@ Sincerely,\n\n]]></string>
<string name="onboarding_whatsnew_title" translatable="false">What\'s New</string>
<string name="onboarding_whatsnew_desc" translatable="false">• Blocking Modes: Choose between Minimal, Standard, or Strict blocking to balance privacy and app compatibility.\n• Minimal Mode: DuckDuckGo-compatible blocking that avoids app breakage. Browsers stay routed through the VPN, but tracker protection is off by default; known incompatible apps are excluded.\n• Privacy Fix: SNI extraction is now off by default — it was leaking your IP address to tracker servers.\n• Improved Performance: Faster scrolling, reduced memory usage, and better battery life.\n• Troubleshooting: New troubleshooting screen in settings to diagnose common issues.\n• Better Tracker Detection: Improved handling of shared IPs and DNS-cloaked trackers.</string>

<string name="title_blocklists">Additional host files</string>
<string name="summary_manage_blocklists">These are only for fallback. TrackerControl includes its own host lists that give insights on the companies behind tracking.</string>
<string name="title_add_blocklist">Add host file</string>
<string name="title_delete_blocklist">Delete host file</string>
<string name="msg_delete_blocklist_confirm">Are you sure you want to delete this host file?</string>
<string name="title_blocklists">Custom blocklists</string>
<string name="summary_manage_blocklists">Add blocklists in hosts-file format. Used when the built-in tracker lists do not identify a domain.</string>
<string name="title_add_blocklist">Add blocklist</string>
<string name="title_delete_blocklist">Delete blocklist</string>
<string name="msg_delete_blocklist_confirm">Are you sure you want to delete this blocklist?</string>
<string name="msg_invalid_url">Invalid URL</string>
<string name="msg_apply_blocklists_failed">Failed to apply host files</string>
<string name="msg_apply_blocklists_failed">Failed to apply blocklists</string>
<string name="msg_last_update">Last update: %s</string>

<string name="blocklist_format">Enter an HTTPS URL for a hosts-format blocklist (for example, 0.0.0.0 ads.example.com). Browser filter rules are not supported.</string>
<string name="blocklist_minimal_notice">Custom blocklists are inactive in Minimal mode. Switch to Standard or Strict in Settings to use them.</string>
<string name="blocklist_research_notice">Custom blocklists are inactive while Research mode is on. Turn off Research in Settings to enable tracker blocking.</string>
<string name="blocklist_browser_notice">Browser tracker protection is off by default. Enable it on the browser’s app page to use custom blocklists there.</string>
<string name="blocklist_downloading">Downloading blocklists… Waiting for a connection if offline.</string>
<string name="blocklist_not_downloaded">Not downloaded yet</string>
<string name="blocklist_download_failed">Could not download or apply blocklists. Check the list errors and try updating again.</string>
<string name="blocklist_update">Update blocklists</string>

<!-- Accessibility labels -->
<string name="switch_tracker_control_description">Tracker blocking</string>
<string name="vpn_enabled_switch_description">VPN</string>
Expand Down
Loading
Loading