Skip to content
Open
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
115 changes: 112 additions & 3 deletions app/src/main/java/com/psiphon3/LogsTabFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@

package com.psiphon3;

import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import androidx.core.content.FileProvider;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -31,13 +38,28 @@
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.psiphon3.log.LoggingContentProvider;
import com.psiphon3.log.MyLog;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import io.reactivex.disposables.CompositeDisposable;

public class LogsTabFragment extends Fragment {
private LogsListAdapter pagingAdapter;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private MainActivityViewModel viewModel;
private int lastItemCount;
private final ExecutorService executor = Executors.newSingleThreadExecutor();

@Nullable
@Override
Expand All @@ -56,15 +78,14 @@ public void onSaveInstanceState(@NonNull Bundle outState) {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

if (savedInstanceState!= null ) {
if (savedInstanceState != null) {
lastItemCount = savedInstanceState.getInt("lastItemCount", 0);
}

viewModel = new ViewModelProvider(requireActivity(),
new ViewModelProvider.AndroidViewModelFactory(requireActivity().getApplication()))
.get(MainActivityViewModel.class);


RecyclerView recyclerView = view.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
layoutManager.setReverseLayout(true);
Expand All @@ -73,7 +94,6 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
recyclerView.setLayoutManager(layoutManager);
recyclerView.setItemAnimator(null);


pagingAdapter = new LogsListAdapter(new LogsListAdapter.LogEntryComparator());
recyclerView.setAdapter(pagingAdapter);

Expand All @@ -88,8 +108,96 @@ public void onItemRangeInserted(int positionStart, int itemCount) {
lastItemCount = currentItemCount;
}
});

view.findViewById(R.id.btnClearLogs).setOnClickListener(v -> clearLogs());
view.findViewById(R.id.btnShareLogs).setOnClickListener(v -> saveLogs());
}

private void clearLogs() {
Uri deleteUri = LoggingContentProvider.CONTENT_URI.buildUpon()
.appendPath("delete")
.appendPath(String.valueOf(System.currentTimeMillis() + 1000))
.build();
requireContext().getContentResolver().delete(deleteUri, null, null);
}

private void saveLogs() {
executor.execute(() -> {
Uri allLogsUri = LoggingContentProvider.CONTENT_URI.buildUpon()
.appendPath("all")
.appendPath(String.valueOf(System.currentTimeMillis() + 1000))
.build();

StringBuilder sb = new StringBuilder();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.US);

try (Cursor cursor = requireContext().getContentResolver()
.query(allLogsUri, null, null, null, "timestamp ASC")) {
if (cursor != null) {
while (cursor.moveToNext()) {
long ts = cursor.getLong(cursor.getColumnIndexOrThrow("timestamp"));
String logJson = cursor.getString(cursor.getColumnIndexOrThrow("logjson"));
boolean isDiagnostic = cursor.getInt(
cursor.getColumnIndexOrThrow("is_diagnostic")) != 0;

String msg;
if (isDiagnostic) {
try {
JSONObject obj = new JSONObject(logJson);
String m = obj.getString("msg");
JSONObject data = obj.optJSONObject("data");
msg = data == null ? m : m + ":" + data.toString();
} catch (JSONException e) {
msg = logJson;
}
} else {
msg = MyLog.getStatusLogMessageForDisplay(logJson, requireContext());
}

if (msg != null && !msg.isEmpty()) {
sb.append(sdf.format(new Date(ts)))
.append(" ")
.append(msg)
.append("\n");
}
}
}
} catch (Exception ignored) {
}

String text = sb.toString();
String ts2 = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date());
String filename = "ShirOKhorshid-" + ts2 + ".txt";

try {
File dir = requireContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
if (dir != null) {
dir.mkdirs();
File file = new File(dir, filename);
try (FileWriter writer = new FileWriter(file)) {
writer.write(text);
}
Uri fileUri = FileProvider.getUriForFile(
requireContext(),
"com.shirokhorshid.vpn.UpgradeFileProvider",
file);
requireActivity().runOnUiThread(() -> {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
shareIntent.putExtra(Intent.EXTRA_SUBJECT, filename);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(shareIntent,
getString(R.string.share_logs_button)));
});
}
} catch (Exception e) {
requireActivity().runOnUiThread(() ->
Toast.makeText(requireContext(),
"Error: " + e.getMessage(), Toast.LENGTH_LONG).show());
}
});
}

@Override
public void onResume() {
Expand All @@ -103,6 +211,7 @@ public void onResume() {
public void onDestroy() {
super.onDestroy();
compositeDisposable.dispose();
executor.shutdown();
}

@Override
Expand Down
43 changes: 42 additions & 1 deletion app/src/main/java/com/psiphon3/OptionsTabFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,38 @@ private boolean moreSettingsRestartRequired() {
prefs.getBoolean(getString(R.string.shareProxyOnNetworkPreference), false);
boolean shareProxyCurrent =
multiProcessPreferences.getBoolean(getString(R.string.shareProxyOnNetworkPreference), false);
return shareProxyCurrent != shareProxyNew;
if (shareProxyCurrent != shareProxyNew) {
return true;
}

boolean tcpProbeNew = prefs.getBoolean(getString(R.string.tcpReachabilityProbePreference), false);
boolean tcpProbeCurrent = multiProcessPreferences.getBoolean(getString(R.string.tcpReachabilityProbePreference), false);
if (tcpProbeCurrent != tcpProbeNew) {
return true;
}
boolean tcpSkipNew = prefs.getBoolean(getString(R.string.tcpReachabilityProbeSkipDialOnFailPreference), false);
boolean tcpSkipCurrent = multiProcessPreferences.getBoolean(getString(R.string.tcpReachabilityProbeSkipDialOnFailPreference), false);
if (tcpSkipCurrent != tcpSkipNew) {
return true;
}
boolean tcpBoostNew = prefs.getBoolean(getString(R.string.tcpReachabilityProbeBoostPreference), false);
boolean tcpBoostCurrent = multiProcessPreferences.getBoolean(getString(R.string.tcpReachabilityProbeBoostPreference), false);
if (tcpBoostCurrent != tcpBoostNew) {
return true;
}
String tcpTimeoutNew = prefs.getString(getString(R.string.tcpReachabilityProbeTimeoutPreference), "800");
String tcpTimeoutCurrent = multiProcessPreferences.getString(getString(R.string.tcpReachabilityProbeTimeoutPreference), "800");
if (!tcpTimeoutCurrent.equals(tcpTimeoutNew)) {
return true;
}

String protoFilterNew = prefs.getString(getString(R.string.protocolFilterStringPreference), "");
String protoFilterCurrent = multiProcessPreferences.getString(getString(R.string.protocolFilterStringPreference), "");
if (!protoFilterCurrent.equals(protoFilterNew)) {
return true;
}

return false;
}

private void updateVpnSettingsFromPreferences() {
Expand Down Expand Up @@ -462,8 +493,18 @@ private void updateMoreSettingsFromPreferences() {
new SharedPreferencesImport(requireContext(), prefName, getString(R.string.conduitModePreference), getString(R.string.conduitModePreference)),
new SharedPreferencesImport(requireContext(), prefName, getString(R.string.conduitTimeoutPreference), getString(R.string.conduitTimeoutPreference)),
new SharedPreferencesImport(requireContext(), prefName, getString(R.string.rejectCensoredCountryProxiesPreference), getString(R.string.rejectCensoredCountryProxiesPreference)),
new SharedPreferencesImport(requireContext(), prefName, getString(R.string.tcpReachabilityProbePreference), getString(R.string.tcpReachabilityProbePreference)),
new SharedPreferencesImport(requireContext(), prefName, getString(R.string.tcpReachabilityProbeSkipDialOnFailPreference), getString(R.string.tcpReachabilityProbeSkipDialOnFailPreference)),
new SharedPreferencesImport(requireContext(), prefName, getString(R.string.tcpReachabilityProbeBoostPreference), getString(R.string.tcpReachabilityProbeBoostPreference)),
new SharedPreferencesImport(requireContext(), prefName, getString(R.string.tcpReachabilityProbeTimeoutPreference), getString(R.string.tcpReachabilityProbeTimeoutPreference)),
new SharedPreferencesImport(requireContext(), prefName, getString(R.string.shareProxyOnNetworkPreference), getString(R.string.shareProxyOnNetworkPreference)),
new SharedPreferencesImport(requireContext(), prefName, DisguiseManager.PREF_STEALTH_NOTIFICATIONS, DisguiseManager.PREF_STEALTH_NOTIFICATIONS)
);
// Protocol filter must be written directly to Tray (not via migrate()) because
// Tray's migrate() is one-time and will not overwrite an already-migrated key.
SharedPreferences sp = requireActivity()
.getSharedPreferences(prefName, android.content.Context.MODE_PRIVATE);
String filterStr = sp.getString(getString(R.string.protocolFilterStringPreference), "");
multiProcessPreferences.put(getString(R.string.protocolFilterStringPreference), filterStr);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
db.getQueryExecutor().execute(() -> {
db.getOpenHelper().getWritableDatabase().insert("log", SQLiteDatabase.CONFLICT_NONE, values);
if (!values.getAsBoolean("is_diagnostic")) {
context.getContentResolver().notifyChange(uri, null);
context.getContentResolver().notifyChange(CONTENT_URI, null);
}
});
return uri;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,34 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
return true;
});

// Protocol filter: always visible; applies to auto and direct modes.
// The Set<String> from MultiSelectListPreference is stored in UI prefs.
// A comma-joined copy is saved under protocolFilterStringPreference so
// OptionsTabFragment can copy it to Tray via a direct put() (not migrate()).
androidx.preference.MultiSelectListPreference protocolFilterPref =
(androidx.preference.MultiSelectListPreference) preferences.findPreference(
getString(R.string.protocolFilterPreference));
if (protocolFilterPref != null) {
updateProtocolFilterSummary(protocolFilterPref, null);
protocolFilterPref.setOnPreferenceChangeListener((preference, newValue) -> {
@SuppressWarnings("unchecked")
java.util.Set<String> selected = (java.util.Set<String>) newValue;
updateProtocolFilterSummary(
(androidx.preference.MultiSelectListPreference) preference, selected);
String joined = (selected == null || selected.isEmpty())
? "" : android.text.TextUtils.join(",", selected);
getPreferenceManager().getSharedPreferences().edit()
.putString(getString(R.string.protocolFilterStringPreference), joined)
.apply();
return true;
});
}

setupLanguageSelector(preferences);
setupDisguise(preferences);
setupConduitSettings(preferences, preferenceGetter);
setupNetworkSharing(preferences, preferenceGetter);
setupTcpReachability(preferences, preferenceGetter);
setupAbouts(preferences);
}

Expand Down Expand Up @@ -225,6 +249,19 @@ private boolean isConduitRelevantProtocol(String protocol) {
return "auto".equals(protocol) || "conduit".equals(protocol);
}

private void updateProtocolFilterSummary(
androidx.preference.MultiSelectListPreference pref,
java.util.Set<String> selected) {
if (selected == null) {
selected = pref.getValues();
}
if (selected == null || selected.isEmpty()) {
pref.setSummary(getString(R.string.protocolFilterPreferenceSummaryNone));
} else {
pref.setSummary(android.text.TextUtils.join(", ", selected));
}
}

private void setupDisguise(PreferenceScreen preferences) {
// Disguise identity list preference
ListPreference disguiseList = (ListPreference) preferences.findPreference(
Expand Down Expand Up @@ -398,6 +435,58 @@ private void setupNetworkSharing(PreferenceScreen preferences, PreferenceGetter
}
}

private void setupTcpReachability(PreferenceScreen preferences, PreferenceGetter preferenceGetter) {
SwitchPreference tcpProbeSwitch =
(SwitchPreference) preferences.findPreference(getString(R.string.tcpReachabilityProbePreference));
SwitchPreference tcpSkipDialSwitch =
(SwitchPreference) preferences.findPreference(getString(R.string.tcpReachabilityProbeSkipDialOnFailPreference));
SwitchPreference tcpBoostSwitch =
(SwitchPreference) preferences.findPreference(getString(R.string.tcpReachabilityProbeBoostPreference));
ListPreference tcpTimeoutList =
(ListPreference) preferences.findPreference(getString(R.string.tcpReachabilityProbeTimeoutPreference));

if (tcpProbeSwitch == null || tcpSkipDialSwitch == null || tcpBoostSwitch == null || tcpTimeoutList == null) {
return;
}

boolean enabled = preferenceGetter.getBoolean(
getString(R.string.tcpReachabilityProbePreference), false);
tcpProbeSwitch.setChecked(enabled);
tcpSkipDialSwitch.setChecked(preferenceGetter.getBoolean(
getString(R.string.tcpReachabilityProbeSkipDialOnFailPreference), false));
tcpBoostSwitch.setChecked(preferenceGetter.getBoolean(
getString(R.string.tcpReachabilityProbeBoostPreference), false));

String timeoutValue = preferenceGetter.getString(
getString(R.string.tcpReachabilityProbeTimeoutPreference), "800");
tcpTimeoutList.setValue(timeoutValue);
updateTcpTimeoutSummary(tcpTimeoutList, timeoutValue);

tcpSkipDialSwitch.setVisible(enabled);
tcpBoostSwitch.setVisible(enabled);
tcpTimeoutList.setVisible(enabled);

tcpProbeSwitch.setOnPreferenceChangeListener((preference, newValue) -> {
boolean probeEnabled = (Boolean) newValue;
tcpSkipDialSwitch.setVisible(probeEnabled);
tcpBoostSwitch.setVisible(probeEnabled);
tcpTimeoutList.setVisible(probeEnabled);
return true;
});

tcpTimeoutList.setOnPreferenceChangeListener((preference, newValue) -> {
updateTcpTimeoutSummary((ListPreference) preference, (String) newValue);
return true;
});
}

private void updateTcpTimeoutSummary(ListPreference preference, String value) {
int idx = preference.findIndexOfValue(value);
if (idx >= 0 && preference.getEntries() != null && idx < preference.getEntries().length) {
preference.setSummary(preference.getEntries()[idx]);
}
}

private void restartTunnelForNetworkSharing() {
TunnelServiceInteractor tunnelServiceInteractor =
((LocalizedActivities.AppCompatActivity) requireActivity())
Expand Down
Loading