diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2bd60cf7..922e49af 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,8 @@ android:noHistory="true" tools:ignore="LockedOrientationActivity"> - @@ -13,7 +14,8 @@ - + { try { resetSelectedServer(); diff --git a/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java b/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java index 444f3b81..e9d17be6 100644 --- a/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java +++ b/app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java @@ -108,6 +108,16 @@ public static void saveResetSelectedServerEnabled(Context context, boolean enabl sharedPreferences.edit().putBoolean(Constants.RESET_SELECTED_SERVER_PREF_KEY, enabled).apply(); } + public static boolean getResetSelectedServerOnExceptionEnabled(Context context) { + SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE); + return sharedPreferences.getBoolean(Constants.RESET_SELECTED_SERVER_ON_EXCEPTION_PREF_KEY, false); + } + + public static void saveResetSelectedServerOnExceptionEnabled(Context context, boolean enabled) { + SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE); + sharedPreferences.edit().putBoolean(Constants.RESET_SELECTED_SERVER_ON_EXCEPTION_PREF_KEY, enabled).apply(); + } + public static BypassCensorshipMethod getBypassCensorshipMethod(Context context) { SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE); diff --git a/app/src/main/java/org/fptn/vpn/views/experimentalsettings/ExperimentalSettingsActivity.java b/app/src/main/java/org/fptn/vpn/views/experimentalsettings/ExperimentalSettingsActivity.java new file mode 100644 index 00000000..77fb8a96 --- /dev/null +++ b/app/src/main/java/org/fptn/vpn/views/experimentalsettings/ExperimentalSettingsActivity.java @@ -0,0 +1,228 @@ +package org.fptn.vpn.views.experimentalsettings; + +import android.annotation.SuppressLint; +import android.app.StatusBarManager; +import android.content.ComponentName; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SwitchCompat; + +import org.fptn.vpn.R; +import org.fptn.vpn.services.tile.FptnTileService; +import org.fptn.vpn.utils.SharedPrefUtils; + +public class ExperimentalSettingsActivity extends AppCompatActivity { + private final String TAG = this.getClass().getSimpleName(); + + private SwitchCompat switchNetworkType; + private SwitchCompat switchIPAddress; + private SeekBar seekBarAttemptsCount; + private SeekBar seekBarDelayBetween; + private SwitchCompat resetServerAfterDisconnectSwitch; + private SwitchCompat resetServerAfterDisconnectOnException; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.experimental_settings_layout); + + initializeVariable(); + } + + @SuppressLint("InlinedApi") + private void initializeVariable() { + switchNetworkType = findViewById(R.id.reconnect_on_change_network_type_switch); + switchNetworkType.setChecked(SharedPrefUtils.getReconnectOnChangeNetworkTypeEnabled(this)); + + switchIPAddress = findViewById(R.id.reconnect_on_change_ip_address_switch); + switchIPAddress.setChecked(SharedPrefUtils.getReconnectOnChangeIPEnabled(this)); + + // Reconnects attempts count + seekBarAttemptsCount = findViewById(R.id.seekBarAttemptsCount); + TextView textViewAttemptsCount = findViewById(R.id.textViewAttemptsCount); + seekBarAttemptsCount.setOnSeekBarChangeListener(new SimpleSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (progress == 3) { + textViewAttemptsCount.setText("∞"); + } else { + String format = getString(R.string.reconnect_attempts_text); + textViewAttemptsCount.setText(String.format(format, progress * 5)); + } + } + }); + + seekBarAttemptsCount.setProgress(0); + int reconnectAttemptsCount = SharedPrefUtils.getReconnectAttemptsCount(this); + if (reconnectAttemptsCount == Integer.MAX_VALUE) { + seekBarAttemptsCount.setProgress(3); + } else { + seekBarAttemptsCount.setProgress(reconnectAttemptsCount / 5); + } + + // Reconnects delay between in seconds + seekBarDelayBetween = findViewById(R.id.seekBarDelayBetween); + TextView textViewDelayBetween = findViewById(R.id.textViewDelayBetween); + seekBarDelayBetween.setOnSeekBarChangeListener(new SimpleSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + String format = getString(R.string.delay_between_attempts_seconds); + textViewDelayBetween.setText(String.format(format, progress + 1)); + } + }); + + int delayBetweenReconnect = SharedPrefUtils.getDelayBetweenReconnect(this); + seekBarDelayBetween.setProgress(0); + seekBarDelayBetween.setProgress(delayBetweenReconnect - 1); + + // Quick tile request + View tileButtonLayout = findViewById(R.id.tile_layout); + Button buttonRequestTile = findViewById(R.id.quick_settings_tile_button); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + buttonRequestTile.setVisibility(View.VISIBLE); + buttonRequestTile.setEnabled(true); + buttonRequestTile.setOnClickListener(v -> requestQuickSettingsTile(buttonRequestTile)); + + tileButtonLayout.setVisibility(View.VISIBLE); + } else { + buttonRequestTile.setVisibility(View.GONE); // Use GONE to free up layout space + + tileButtonLayout.setVisibility(View.GONE); + } + + // Reset selected server on disconnect + resetServerAfterDisconnectSwitch = findViewById(R.id.reset_selected_server_after_disconnect_switch); + resetServerAfterDisconnectSwitch.setChecked(SharedPrefUtils.getResetSelectedServerEnabled(this)); + + // Reset selected server on disconnect with exception + resetServerAfterDisconnectOnException = findViewById(R.id.reset_selected_server_after_disconnect_with_exception); + resetServerAfterDisconnectOnException.setChecked(SharedPrefUtils.getResetSelectedServerEnabled(this)); + + resetServerAfterDisconnectOnException.setChecked(SharedPrefUtils.getResetSelectedServerOnExceptionEnabled(this)); + updateExceptionVisibility(resetServerAfterDisconnectSwitch.isChecked()); + + // Toggle visibility on change + resetServerAfterDisconnectSwitch.setOnCheckedChangeListener( + (buttonView, isChecked) -> updateExceptionVisibility(isChecked)); + + + // Save and Cancel buttons + Button cancelButton = findViewById(R.id.cancel_button); + cancelButton.setOnClickListener(v -> { + Log.d(TAG, "Cancel button clicked"); + finish(); + }); + + Button saveButton = findViewById(R.id.save_button); + saveButton.setOnClickListener(v -> saveAndFinish()); + } + + private void updateExceptionVisibility(boolean isVisible) { + resetServerAfterDisconnectOnException.setVisibility(isVisible ? View.GONE : View.VISIBLE); + } + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + private void requestQuickSettingsTile(Button buttonRequestTile) { + StatusBarManager statusBarManager = getSystemService(StatusBarManager.class); + if (statusBarManager == null) return; + + // Disable button to prevent double-clicks + buttonRequestTile.setEnabled(false); + + try { + ComponentName componentName = new ComponentName(this, FptnTileService.class); + String label = getString(R.string.app_name); // Or specific tile label + Icon icon = Icon.createWithResource(this, R.drawable.ic_logo); + statusBarManager.requestAddTileService( + componentName, + label, + icon, + getMainExecutor(), + resultCode -> handleTileRequestResult(resultCode, buttonRequestTile) + ); + } catch (Exception e) { + Log.e(TAG, "Failed to request tile addition", e); + Toast.makeText(this, R.string.tile_addition_failed, Toast.LENGTH_SHORT).show(); + buttonRequestTile.setEnabled(true); + } + } + + private void handleTileRequestResult(int resultCode, Button button) { + // Re-enable button unless the tile was successfully added/exists + boolean shouldKeepDisabled = false; + + switch (resultCode) { + case StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED: + Log.d(TAG, "Tile already added successfully."); + Toast.makeText(this, R.string.tile_already_added, Toast.LENGTH_SHORT).show(); + button.setBackgroundResource(R.drawable.round_back_secondary_cancel_100); + shouldKeepDisabled = true; + break; + + case StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED: + Log.d(TAG, "Tile added successfully."); + Toast.makeText(this, R.string.tile_added_successfully, Toast.LENGTH_SHORT).show(); + shouldKeepDisabled = true; + break; + + case StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED: + Log.d(TAG, "User cancelled the request or it failed."); + button.setEnabled(true); + break; + + default: + Log.d(TAG, "Unknown result code: " + resultCode); + button.setEnabled(true); + break; + } + + if (shouldKeepDisabled) { + button.setEnabled(false); + button.setAlpha(0.5f); // Visual cue that it's no longer needed + } + } + + private void saveAndFinish() { + Log.d(TAG, "Save button clicked"); + + SharedPrefUtils.saveReconnectOnChangeNetworkTypeEnabled(this, switchNetworkType.isChecked()); + SharedPrefUtils.saveReconnectOnChangeIPEnabled(this, switchIPAddress.isChecked()); + SharedPrefUtils.saveResetSelectedServerEnabled(this, resetServerAfterDisconnectSwitch.isChecked()); + SharedPrefUtils.saveResetSelectedServerOnExceptionEnabled(this, resetServerAfterDisconnectOnException.isChecked()); + + int attemptsCountProgress = seekBarAttemptsCount.getProgress(); + if (attemptsCountProgress == 3) { + SharedPrefUtils.saveReconnectAttemptsCount(this, Integer.MAX_VALUE); + } else { + SharedPrefUtils.saveReconnectAttemptsCount(this, attemptsCountProgress * 5); + } + + int delayBetweenProgress = seekBarDelayBetween.getProgress(); + SharedPrefUtils.saveDelayBetweenReconnect(this, delayBetweenProgress + 1); + + finish(); + } + + // Helper to reduce boilerplate for SeekBars + private abstract static class SimpleSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener { + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + } +} + diff --git a/app/src/main/java/org/fptn/vpn/views/settings/SettingsActivity.java b/app/src/main/java/org/fptn/vpn/views/settings/SettingsActivity.java index 8e3a1450..fd78ab3e 100644 --- a/app/src/main/java/org/fptn/vpn/views/settings/SettingsActivity.java +++ b/app/src/main/java/org/fptn/vpn/views/settings/SettingsActivity.java @@ -2,15 +2,10 @@ import android.annotation.SuppressLint; import android.app.AlertDialog; -import android.app.StatusBarManager; -import android.content.ComponentName; -import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.graphics.drawable.Icon; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.text.Html; @@ -18,12 +13,9 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.ListAdapter; import android.widget.ListView; -import android.widget.SeekBar; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -33,10 +25,9 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import org.fptn.vpn.R; -import org.fptn.vpn.services.tile.FptnTileService; import org.fptn.vpn.utils.PermissionsUtils; -import org.fptn.vpn.utils.SharedPrefUtils; import org.fptn.vpn.views.CustomBottomNavigationListener; +import org.fptn.vpn.views.experimentalsettings.ExperimentalSettingsActivity; import org.fptn.vpn.views.splash.SplashActivity; import org.fptn.vpn.views.adapter.ServerEntityAdapter; import org.fptn.vpn.views.bypassmethod.BypassMethodsActivity; @@ -115,7 +106,7 @@ private void initializeVariable() { updateTokenLayout.setOnClickListener(this::onUpdateToken); View experimentalFeaturesLayout = findViewById(R.id.experimental_features_layout); - experimentalFeaturesLayout.setOnClickListener(this::showExperimentalSettingsDialog); + experimentalFeaturesLayout.setOnClickListener(this::onExperimentalSettings); View logoutLayout = findViewById(R.id.logout_layout); logoutLayout.setOnClickListener(this::onLogout); @@ -183,6 +174,11 @@ private void requestBackgroundDataTransferPermission() { .show(); } + private void onExperimentalSettings(View view) { + Intent intent = new Intent(SettingsActivity.this, ExperimentalSettingsActivity.class); + startActivity(intent); + } + public void onLogout(View v) { AlertDialog.Builder builder = new AlertDialog.Builder(this) .setTitle(R.string.dialog_logout_title) @@ -204,7 +200,6 @@ public void onUpdateToken(View v) { startActivity(intent); } - // NEW: Bypass methods click handler public void onBypassMethods(View v) { Intent intent = new Intent(SettingsActivity.this, BypassMethodsActivity.class); startActivity(intent); @@ -233,145 +228,4 @@ private static void setListViewHeightBasedOnChildren(ListView listView) { listView.setLayoutParams(params); listView.requestLayout(); } - - public void showExperimentalSettingsDialog(View view) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.experimental_features_label); - builder.setIcon(R.drawable.ic_experimental_features_24); - - View dialogView = getLayoutInflater().inflate(R.layout.experimental_settings_dialog, null); - builder.setView(dialogView); - - /* Reconnect on change network type */ - SwitchCompat switchNetworkType = dialogView.findViewById(R.id.reconnect_on_change_network_type_switch); - switchNetworkType.setChecked(SharedPrefUtils.getReconnectOnChangeNetworkTypeEnabled(this)); - - /* Reconnect on change IP address */ - SwitchCompat switchIPAddress = dialogView.findViewById(R.id.reconnect_on_change_ip_address_switch); - switchIPAddress.setChecked(SharedPrefUtils.getReconnectOnChangeIPEnabled(this)); - - /* Quick tile request */ - Button buttonRequestTile = dialogView.findViewById(R.id.quick_settings_tile_button); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - buttonRequestTile.setOnClickListener(l -> { - @SuppressLint("WrongConstant") StatusBarManager statusBarManager = (StatusBarManager) getSystemService(Context.STATUS_BAR_SERVICE); - try { - // Request to add a custom tile service - statusBarManager.requestAddTileService( - new ComponentName(this, FptnTileService.class), - "FPTN", - Icon.createWithResource(this, R.drawable.ic_logo), - this.getMainExecutor(), - (resultCode) -> { - if (resultCode == StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED) { - Log.d(TAG, "Tile already added successfully. Nothing to do."); - Toast.makeText(this, R.string.tile_already_added, Toast.LENGTH_SHORT) - .show(); - } else if (resultCode == StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED) { - Log.d(TAG, "Tile added successfully."); - Toast.makeText(this, R.string.tile_added_successfully, Toast.LENGTH_SHORT) - .show(); - } else { - Log.d(TAG, "User cancel request."); - } - } - ); - } catch (Exception e) { - Log.e(TAG, "Failed to request tile addition", e); - Toast.makeText(this, R.string.tile_addition_failed, Toast.LENGTH_SHORT) - .show(); - } - }); - buttonRequestTile.setEnabled(true); - buttonRequestTile.setVisibility(View.VISIBLE); - } else { - buttonRequestTile.setEnabled(false); - buttonRequestTile.setVisibility(View.INVISIBLE); - } - - /* Reconnects attempts count */ - SeekBar seekBarAttemptsCount = dialogView.findViewById(R.id.seekBarAttemptsCount); - TextView textViewAttemptsCount = dialogView.findViewById(R.id.textViewAttemptsCount); - seekBarAttemptsCount.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (progress == 3) { - textViewAttemptsCount.setText("∞"); - } else { - String format = getString(R.string.reconnect_attempts_text); - textViewAttemptsCount.setText(String.format(format, progress * 5)); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - - } - }); - - seekBarAttemptsCount.setProgress(0); - int reconnectAttemptsCount = SharedPrefUtils.getReconnectAttemptsCount(this); - if (reconnectAttemptsCount == Integer.MAX_VALUE) { - seekBarAttemptsCount.setProgress(3); - } else { - seekBarAttemptsCount.setProgress(reconnectAttemptsCount / 5); - } - - /* Reconnects delay between in seconds */ - SeekBar seekBarDelayBetween = dialogView.findViewById(R.id.seekBarDelayBetween); - TextView textViewDelayBetween = dialogView.findViewById(R.id.textViewDelayBetween); - seekBarDelayBetween.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - String format = getString(R.string.delay_between_attempts_seconds); - textViewDelayBetween.setText(String.format(format, progress + 1)); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - - } - }); - - int delayBetweenReconnect = SharedPrefUtils.getDelayBetweenReconnect(this); - seekBarDelayBetween.setProgress(0); - seekBarDelayBetween.setProgress(delayBetweenReconnect - 1); - - /* Reset selected server on disconnect */ - SwitchCompat resetServerAfterDisconnectSwitch = dialogView.findViewById(R.id.reset_selected_server_after_disconnect_switch); - resetServerAfterDisconnectSwitch.setChecked(SharedPrefUtils.getResetSelectedServerEnabled(this)); - - builder.setPositiveButton(getString(R.string.save_button), (dialog, which) -> { - Log.d(TAG, "experimentalFeaturesDialog: save"); - SharedPrefUtils.saveReconnectOnChangeNetworkTypeEnabled(this, switchNetworkType.isChecked()); - SharedPrefUtils.saveReconnectOnChangeIPEnabled(this, switchIPAddress.isChecked()); - SharedPrefUtils.saveResetSelectedServerEnabled(this, resetServerAfterDisconnectSwitch.isChecked()); - - int attemptsCountProgress = seekBarAttemptsCount.getProgress(); - if (attemptsCountProgress == 3) { - SharedPrefUtils.saveReconnectAttemptsCount(this, Integer.MAX_VALUE); - } else { - SharedPrefUtils.saveReconnectAttemptsCount(this, attemptsCountProgress * 5); - } - - int delayBetweenProgress = seekBarDelayBetween.getProgress(); - SharedPrefUtils.saveDelayBetweenReconnect(this, delayBetweenProgress + 1); - }); - builder.setNegativeButton(getString(R.string.cancel_button), (dialog, which) -> { - Log.d(TAG, "experimentalFeaturesDialog: cancel"); - dialog.dismiss(); - }); - - builder.create().show(); - } } diff --git a/app/src/main/res/layout/experimental_settings_dialog.xml b/app/src/main/res/layout/experimental_settings_dialog.xml deleted file mode 100644 index b6ede063..00000000 --- a/app/src/main/res/layout/experimental_settings_dialog.xml +++ /dev/null @@ -1,212 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -