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
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class TrackersFragment extends Fragment {
private int mAppUid;

private SwipeRefreshLayout swipeRefresh;
private RecyclerView recyclerView;
private TrackersListAdapter adapter;

private boolean running = false;
Expand Down Expand Up @@ -79,7 +80,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,

Context c = v.getContext();
trackerList = TrackerList.getInstance(c);
RecyclerView recyclerView = v.findViewById(R.id.transmissions_list);
recyclerView = v.findViewById(R.id.transmissions_list);
recyclerView.setLayoutManager(new LinearLayoutManager(c));
adapter = new TrackersListAdapter(getActivity(), recyclerView, mAppUid, mAppId);
recyclerView.setAdapter(adapter);
Expand Down Expand Up @@ -212,10 +213,19 @@ private void suggestLaunchingApp() {

@Override
public void onDestroyView() {
super.onDestroyView();
// Explicitly detach the adapter while the RecyclerView still exists.
// This invokes onDetachedFromRecyclerView(), which dismisses any open
// BottomSheetDialog before the view's Activity context can be retained.
if (recyclerView != null) {
recyclerView.setAdapter(null);
recyclerView = null;
}
adapter = null;

// Avoid leaking the SwipeRefreshLayout (and its Activity context) beyond
// the view's lifecycle when the Fragment instance is retained.
swipeRefresh = null;
super.onDestroyView();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RadioGroup;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.materialswitch.MaterialSwitch;
import android.widget.TextView;
import android.widget.Toast;
Expand Down Expand Up @@ -94,6 +95,9 @@ public class TrackersListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
private TextView mTvAnalysisProgress;
private ProgressBar mPbTrackerDetection;

// At most one of the app-controls bottom sheets is open at a time.
private BottomSheetDialog mOpenSheet;

public TrackersListAdapter(Context c,
RecyclerView v,
Integer appUid,
Expand Down Expand Up @@ -455,29 +459,8 @@ private void updateText(TextView tv, Tracker t) {

holder.mLibraryExplanation.setText(R.string.trackers_static_explanation);

// The internet block is keyed by UID, so it necessarily covers every
// package sharing that UID. Say so rather than letting it surprise.
String relatedApps = getRelatedApps();
if (relatedApps == null) {
holder.mNoInternetExplanation.setText(R.string.app_state_no_internet_explanation);
} else {
String explanation = mContext.getString(R.string.app_state_no_internet_explanation_shared,
mContext.getString(R.string.app_state_no_internet_explanation),
mContext.getString(R.string.app_state_no_internet_shared_uid, relatedApps));
holder.mNoInternetExplanation.setText(explanation);
}

AppProtectionState state = currentState(w);
holder.mAppState.setOnCheckedChangeListener(null);
holder.mAppState.check(radioIdFor(state));
holder.mAppState.setOnCheckedChangeListener((group, checkedId) -> {
AppProtectionState selected = stateForRadioId(checkedId);
if (selected == null || selected == currentState(w))
return;

applyState(selected, w);
notifyDataSetChanged();
});
holder.mAppStateValue.setText(stateLabelRes(currentState(w)));
holder.mRowAppState.setOnClickListener(v -> showProtectionSheet(w));

bindRemoteRouting(holder);
}
Expand All @@ -498,34 +481,129 @@ private void bindRemoteRouting(VHHeader holder) {
RemoteRoutingLogic.Unavailable unavailable =
RemoteRoutingLogic.getUnavailableReason(wgEnabled, defaultRoutes, applyApp);
if (unavailable != null) {
holder.mAppRoute.setVisibility(View.GONE);
holder.mAppRouteUnavailable.setVisibility(View.VISIBLE);
holder.mAppRouteUnavailable.setText(explainUnavailable(unavailable));
holder.mRowAppRoute.setOnClickListener(null);
holder.mRowAppRoute.setClickable(false);
holder.mRowAppRoute.setEnabled(false);
holder.mAppRouteChevron.setVisibility(View.GONE);
holder.mAppRouteValue.setText(explainUnavailable(unavailable));
return;
}

holder.mAppRouteUnavailable.setVisibility(View.GONE);
holder.mAppRoute.setVisibility(View.VISIBLE);
String mode = RemoteRoutingLogic.normalizeMode(
prefs.getString(Rule.PREF_WG_ROUTE_MODE, RemoteRoutingLogic.getDefaultMode()));
boolean tunnelled = RemoteRoutingLogic.routesThroughTunnel(mode, getRouteOverride(), true);

holder.mRowAppRoute.setEnabled(true);
holder.mRowAppRoute.setClickable(true);
holder.mAppRouteChevron.setVisibility(View.VISIBLE);
holder.mAppRouteValue.setText(tunnelled ? R.string.app_route_through : R.string.app_route_direct);
holder.mRowAppRoute.setOnClickListener(v -> showRouteSheet());
}

/**
* Shows the protection-state bottom sheet, wiring the shared-UID
* No-Internet explanation into the sheet's own description view.
*/
private void showProtectionSheet(InternetBlocklist w) {
BottomSheetDialog sheet = new BottomSheetDialog(mContext);
View view = LayoutInflater.from(mContext).inflate(R.layout.bottom_sheet_app_state, null);
sheet.setContentView(view);

// The internet block is keyed by UID, so it necessarily covers every
// package sharing that UID. Say so rather than letting it surprise.
TextView noInternetDesc = view.findViewById(R.id.tvStateNoInternetDesc);
String relatedApps = getRelatedApps();
if (relatedApps == null) {
noInternetDesc.setText(R.string.app_state_no_internet_explanation);
} else {
String explanation = mContext.getString(R.string.app_state_no_internet_explanation_shared,
mContext.getString(R.string.app_state_no_internet_explanation),
mContext.getString(R.string.app_state_no_internet_shared_uid, relatedApps));
noInternetDesc.setText(explanation);
}

RadioGroup rgAppState = view.findViewById(R.id.rgAppState);
rgAppState.check(radioIdFor(currentState(w)));
rgAppState.setOnCheckedChangeListener((group, checkedId) -> {
AppProtectionState selected = stateForRadioId(checkedId);

// Dismiss first: applyState() may trigger a reload, and the sheet
// shouldn't linger on screen while that happens.
sheet.dismiss();

if (selected != null && selected != currentState(w)) {
applyState(selected, w);
notifyDataSetChanged();
}
});

showSheet(sheet);
}

/**
* Shows the remote-routing bottom sheet, recomputing the current mode the
* same way {@link #bindRemoteRouting(VHHeader)} does.
*/
private void showRouteSheet() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
String mode = RemoteRoutingLogic.normalizeMode(
prefs.getString(Rule.PREF_WG_ROUTE_MODE, RemoteRoutingLogic.getDefaultMode()));
boolean tunnelled = RemoteRoutingLogic.routesThroughTunnel(mode, getRouteOverride(), true);

holder.mAppRoute.setOnCheckedChangeListener(null);
holder.mAppRoute.check(tunnelled ? R.id.rbRouteTunnel : R.id.rbRouteDirect);
holder.mAppRoute.setOnCheckedChangeListener((group, checkedId) -> {
BottomSheetDialog sheet = new BottomSheetDialog(mContext);
View view = LayoutInflater.from(mContext).inflate(R.layout.bottom_sheet_app_route, null);
sheet.setContentView(view);

RadioGroup rgAppRoute = view.findViewById(R.id.rgAppRoute);
rgAppRoute.check(tunnelled ? R.id.rbRouteTunnel : R.id.rbRouteDirect);
rgAppRoute.setOnCheckedChangeListener((group, checkedId) -> {
boolean wantsTunnel = (checkedId == R.id.rbRouteTunnel);
if (wantsTunnel == RemoteRoutingLogic.routesThroughTunnel(mode, getRouteOverride(), true))
return;

mContext.getSharedPreferences(Rule.PREF_WG_ROUTE, Context.MODE_PRIVATE)
.edit().putBoolean(mAppId, wantsTunnel).apply();
// Dismiss first: the reload triggered below shouldn't hold the
// sheet open while it runs.
sheet.dismiss();

if (wantsTunnel != RemoteRoutingLogic.routesThroughTunnel(mode, getRouteOverride(), true)) {
mContext.getSharedPreferences(Rule.PREF_WG_ROUTE, Context.MODE_PRIVATE)
.edit().putBoolean(mAppId, wantsTunnel).apply();

AsyncTask.execute(() -> {
Rule.clearCache(mContext);
ServiceSinkhole.reload("app routing changed", mContext, false);
});

// The row subtitle now depends on this value.
notifyDataSetChanged();
}
});

showSheet(sheet);
}

AsyncTask.execute(() -> {
Rule.clearCache(mContext);
ServiceSinkhole.reload("app routing changed", mContext, false);
});
/**
* Only one sheet should be open at a time, and it must not outlive the
* RecyclerView that hosts the row that opened it.
*/
private void showSheet(BottomSheetDialog sheet) {
if (mOpenSheet != null)
mOpenSheet.dismiss();

mOpenSheet = sheet;
sheet.setOnDismissListener(d -> {
if (mOpenSheet == d)
mOpenSheet = null;
});
sheet.show();
}

@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);

if (mOpenSheet != null) {
mOpenSheet.dismiss();
mOpenSheet = null;
}
}

@Nullable
Expand Down Expand Up @@ -657,6 +735,20 @@ private static AppProtectionState stateForRadioId(int checkedId) {
return null;
}

private static int stateLabelRes(AppProtectionState state) {
switch (state) {
case TRACKERS_ALLOWED:
return R.string.app_state_trackers_allowed;
case NO_INTERNET:
return R.string.app_state_no_internet;
case BYPASSED:
return R.string.app_state_bypassed;
case PROTECTED:
default:
return R.string.app_state_protected;
}
}

@Override
public int getItemCount() {
return mValues.size() + 1;
Expand Down Expand Up @@ -698,21 +790,21 @@ static class VHItem extends RecyclerView.ViewHolder {
static class VHHeader extends RecyclerView.ViewHolder {
final TextView mLibraryExplanation;
final TextView mLibraryDisclaimer;
final RadioGroup mAppState;
final TextView mNoInternetExplanation;
final View mAppRouteCard;
final RadioGroup mAppRoute;
final TextView mAppRouteUnavailable;
final View mRowAppState;
final TextView mAppStateValue;
final View mRowAppRoute;
final TextView mAppRouteValue;
final View mAppRouteChevron;

VHHeader(View view) {
super(view);
mLibraryExplanation = view.findViewById(R.id.tvLibraryExplanation);
mLibraryDisclaimer = view.findViewById(R.id.tvLibraryDisclaimer);
mAppState = view.findViewById(R.id.rgAppState);
mNoInternetExplanation = view.findViewById(R.id.tvStateNoInternetDesc);
mAppRouteCard = view.findViewById(R.id.cardAppRoute);
mAppRoute = view.findViewById(R.id.rgAppRoute);
mAppRouteUnavailable = view.findViewById(R.id.tvAppRouteUnavailable);
mRowAppState = view.findViewById(R.id.rowAppState);
mAppStateValue = view.findViewById(R.id.tvAppStateValue);
mRowAppRoute = view.findViewById(R.id.rowAppRoute);
mAppRouteValue = view.findViewById(R.id.tvAppRouteValue);
mAppRouteChevron = view.findViewById(R.id.ivAppRouteChevron);
}
}
}
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_chevron_right.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path
android:fillColor="#000000"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
</vector>
75 changes: 75 additions & 0 deletions app/src/main/res/layout/bottom_sheet_app_route.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
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="wrap_content">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingBottom="24dp">

<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="@string/app_route_heading"
android:textColor="?android:textColorPrimary"
android:textSize="18sp"
android:textStyle="bold" />

<RadioGroup
android:id="@+id/rgAppRoute"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<RadioButton
android:id="@+id/rbRouteTunnel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:contentDescription="@string/app_route_through"
android:text="@string/app_route_through"
android:textStyle="bold" />

<TextView
android:id="@+id/tvRouteTunnelDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginBottom="12dp"
android:text="@string/app_route_through_explanation"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall" />

<RadioButton
android:id="@+id/rbRouteDirect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:contentDescription="@string/app_route_direct"
android:text="@string/app_route_direct"
android:textStyle="bold" />

<TextView
android:id="@+id/tvRouteDirectDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:text="@string/app_route_direct_explanation"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall" />

</RadioGroup>

</LinearLayout>

</androidx.core.widget.NestedScrollView>
Loading