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
26 changes: 17 additions & 9 deletions app/src/main/java/eu/faircode/netguard/AdapterRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import net.kollnig.missioncontrol.Common;
import net.kollnig.missioncontrol.DetailsActivity;
import net.kollnig.missioncontrol.R;
import net.kollnig.missioncontrol.data.AppProtectionState;
import net.kollnig.missioncontrol.data.BlockingMode;
import net.kollnig.missioncontrol.data.InternetBlocklist;

Expand Down Expand Up @@ -242,10 +243,16 @@ public void onBindViewHolder(final ViewHolder holder, int position) {

// Get rule
final Rule rule = listFiltered.get(position);
// In minimal mode, tracker_protect is not user-controllable
final boolean active = BlockingMode.isMinimalMode(context)
? rule.apply
: rule.apply && rule.tracker_protect;
final InternetBlocklist internetBlocklist = InternetBlocklist.getInstance(context);
final boolean blockedInternet = internetBlocklist.blockedInternet(rule.uid);
final AppProtectionState state = AppProtectionState.resolve(
rule.apply, rule.tracker_protect, blockedInternet);
// The Internet toggle below is available for every app TrackerControl
// still sees. Reading it off the shared state model keeps the main list
// and the details screen in agreement — in particular, an app with its
// tracker protection turned off can still have its Internet blocked,
// which the old apply && tracker_protect test denied outside Minimal.
final boolean active = state != AppProtectionState.BYPASSED;

// Show if non default rules
holder.itemView.setBackgroundColor(rule.changed ? colorChanged : Color.TRANSPARENT);
Expand All @@ -270,8 +277,6 @@ public void onBindViewHolder(final ViewHolder holder, int position) {

// Show if Internet access blocked
final ImageView iv = holder.ivIcon;
InternetBlocklist internetBlocklist = InternetBlocklist.getInstance(context);
boolean blockedInternet = internetBlocklist.blockedInternet(rule.uid);
setGreyscale(iv, rule.internet && active && blockedInternet);
updateToggleContentDescription(iv, context, rule, active, blockedInternet);
holder.ivIcon.setOnClickListener(view -> {
Expand All @@ -287,10 +292,10 @@ public void onBindViewHolder(final ViewHolder holder, int position) {

boolean wasBlocked = internetBlocklist.blockedInternet(rule.uid);
if (wasBlocked) {
internetBlocklist.unblock(rule.uid);
internetBlocklist.unblock(context, rule.uid);
Toast.makeText(context, R.string.internet_unblocked, Toast.LENGTH_SHORT).show();
} else {
internetBlocklist.block(rule.uid);
internetBlocklist.block(context, rule.uid);
Toast.makeText(context, R.string.internet_blocked, Toast.LENGTH_SHORT).show();
}
setGreyscale(iv, !wasBlocked);
Expand Down Expand Up @@ -376,7 +381,10 @@ private void updateRule(Context context, Rule rule, boolean root, List<Rule> lis
SharedPreferences notify = context.getSharedPreferences("notify", Context.MODE_PRIVATE);

apply.edit().putBoolean(rule.packageName, rule.apply).apply();
BlockingMode.clearAutoExcludedApp(context, rule.packageName);
// Only a re-included app leaves the mode-managed exclusion set; clearing
// it on exclusion would make a Minimal-mode auto-exclusion permanent.
if (rule.apply)
BlockingMode.clearAutoExcludedApp(context, rule.packageName);
tracker_protect.edit().putBoolean(rule.packageName, rule.tracker_protect).apply();

if (rule.notify)
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java
Original file line number Diff line number Diff line change
Expand Up @@ -2923,6 +2923,23 @@ private void handlePackageChanged(Context context, Intent intent) {

int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
if (uid > 0) {
// The internet block is keyed by UID, not by package, so it
// may still be in use by another package of a shared UID.
// Drop it only once the UID has no package left, otherwise a
// reinstall silently comes back with its Internet blocked.
// A SecurityException (other user/profile on Android 16+)
// means "unknown", which must not be read as "none left".
try {
String[] remaining = context.getPackageManager().getPackagesForUid(uid);
if (remaining == null || remaining.length == 0) {
InternetBlocklist internetBlocklist = InternetBlocklist.getInstance(context);
if (internetBlocklist.blockedInternet(uid))
internetBlocklist.unblock(context, uid);
}
} catch (SecurityException ex) {
Log.w(TAG, "Keeping internet block uid=" + uid + ": " + ex.getMessage());
}

DatabaseHelper dh = DatabaseHelper.getInstance(context);
dh.clearLog(uid);
dh.clearAccess(uid, false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* TrackerControl is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TrackerControl is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Copyright © 2026
*/

package net.kollnig.missioncontrol.data;

/**
* The single per-app protection state shown in the app details header.
* <p>
* It is a derived view over three pre-existing stores — the per-package
* "apply" and "tracker_protect" preferences and the per-UID internet
* blocklist — none of which change format here. Those stores are written by
* more actors than the two UI paths (bulk settings actions, the beta
* vpn_exclude migration, blocking-mode exclusion sync, XML import), so every
* one of the eight combinations can legitimately exist on disk.
* {@link #resolve} is therefore a total derivation, not an invariant.
*/
public enum AppProtectionState {
/** Routed through the VPN, trackers blocked and recorded. */
PROTECTED,
/** Routed through the VPN, but trackers are neither blocked nor recorded. */
TRACKERS_ALLOWED,
/** Routed through the VPN, but all connections are dropped. */
NO_INTERNET,
/** Outside the VPN entirely; TrackerControl sees nothing of this app. */
BYPASSED;

/**
* Derive the state from the three stores.
* <p>
* Precedence is Bypass, then No-internet, then the protection flag. Bypass
* dominates for a mechanical reason rather than as a UI convention: an app
* with {@code apply == false} is handed to
* {@code Builder#addDisallowedApplication}, so its packets never reach the
* tun and the per-UID internet block cannot be enforced for it.
*
* @param apply the per-package "apply" preference
* @param trackerProtect the resolved tracker protection flag, i.e. the
* per-package preference with the browser default
* already applied (see
* {@link BlockingMode#isTrackerProtectionEnabled})
* @param internetBlocked whether the app's UID is in the internet blocklist
*/
public static AppProtectionState resolve(boolean apply,
boolean trackerProtect,
boolean internetBlocked) {
if (!apply)
return BYPASSED;
if (internetBlocked)
return NO_INTERNET;
return trackerProtect ? PROTECTED : TRACKERS_ALLOWED;
}

/**
* The writes that move an app into {@code target}.
* <p>
* States that do not depend on a store leave it alone, so switching to
* "No internet" and back does not silently discard the app's tracker
* protection choice.
*/
public static Change of(AppProtectionState target) {
switch (target) {
case PROTECTED:
return new Change(true, Boolean.TRUE, Boolean.FALSE);
case TRACKERS_ALLOWED:
return new Change(true, Boolean.FALSE, Boolean.FALSE);
case NO_INTERNET:
return new Change(true, null, Boolean.TRUE);
case BYPASSED:
return new Change(false, null, null);
default:
throw new IllegalArgumentException("Unknown state " + target);
}
}

/**
* A write plan over the three stores. A {@code null} field means the store
* is left untouched.
*/
public static final class Change {
public final boolean apply;
public final Boolean trackerProtect;
public final Boolean internetBlocked;

Change(boolean apply, Boolean trackerProtect, Boolean internetBlocked) {
this.apply = apply;
this.trackerProtect = trackerProtect;
this.internetBlocked = internetBlocked;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ public void clear() {
blockmap.clear();
}

/**
* Persist the current blocklist.
* <p>
* The list is otherwise only written when the details screen is paused, so
* a change made from the main screen — or made shortly before the process
* dies — would be lost. Both UI paths write through
* {@link #block(Context, int)} / {@link #unblock(Context, int)} instead.
*
* @param c Context
*/
public synchronized void saveSettings(Context c) {
SharedPreferences prefs = c.getSharedPreferences(PREF_BLOCKLIST, Context.MODE_PRIVATE);
Set<String> set = new HashSet<>();
for (Integer uid : blockmap)
set.add(Integer.toString(uid));

prefs.edit().putStringSet(SHARED_PREFS_INTERNET_BLOCKLIST_APPS_KEY, set).apply();
}

/**
* Block internet for a given app
*
Expand All @@ -96,6 +115,17 @@ public synchronized void block(int uid) {
blockmap.add(uid);
}

/**
* Block internet for a given app and persist the change immediately.
*
* @param c Context
* @param uid Uid of app to block internet
*/
public synchronized void block(Context c, int uid) {
block(uid);
saveSettings(c);
}

/**
* Unblock internet for a given app
*
Expand All @@ -105,6 +135,36 @@ public synchronized void unblock(int uid) {
blockmap.remove(uid);
}

/**
* Unblock internet for a given app and persist the change immediately.
*
* @param c Context
* @param uid Uid of app to unblock internet
*/
public synchronized void unblock(Context c, int uid) {
unblock(uid);
saveSettings(c);
}

/**
* Apply a resolved protection state's internet decision, if it has one.
*
* @param c Context
* @param uid Uid of the app
* @param internetBlocked the {@code internetBlocked} field of an
* {@link AppProtectionState.Change}; {@code null}
* leaves the blocklist untouched
*/
public void apply(Context c, int uid, Boolean internetBlocked) {
if (internetBlocked == null)
return;

if (internetBlocked)
block(c, uid);
else
unblock(c, uid);
}

/**
* Check if internet is blocked for given app
*
Expand Down
Loading