android: VPN notification as in the Final 2026 shade: protected title, pause and change actions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-12 10:12:41 +07:00
1 parent 6652970925
commit db6c806d58
5 files changed
+53 -2

No files matched your search

@@ -734,6 +734,12 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
/**
* Build a notification matching the current state
*/
/** Whether «RU sites direct» is on (any mode). */
private static boolean isRuSplitOn(Context context)
{
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(Constants.PREF_RUSPLIT_GEOIP_ENABLED, false);
}
private Notification buildNotification(boolean publicVersion)
{
/* the state service may not be bound yet when startForeground() is
@@ -818,7 +824,14 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
break;
}
}
builder.setContentTitle(getString(s));
if (state == State.CONNECTED && error == ErrorState.NO_ERROR && !publicVersion && profile != null)
{ /* «Protected · Amsterdam» like the mockup shade */
builder.setContentTitle(getString(R.string.notif_protected, name));
}
else
{
builder.setContentTitle(getString(s));
}
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
@@ -827,6 +840,17 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
}
if (!publicVersion)
{
if (state == State.CONNECTED && error == ErrorState.NO_ERROR && profile != null)
{ /* «Pause 15 min» and «Change» as on the mockup shade */
Intent pause = new Intent(getApplicationContext(), PauseReceiver.class).setAction(PauseManager.ACTION_PAUSE);
builder.addAction(R.drawable.ic_notification_connecting, getString(R.string.notif_pause),
PendingIntent.getBroadcast(getApplicationContext(), 1, pause, flags));
Intent change = new Intent(getApplicationContext(), MainActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(MainActivity.EXTRA_OPEN_SERVERS, true);
builder.addAction(R.drawable.ic_notification, getString(R.string.notif_change),
PendingIntent.getActivity(getApplicationContext(), 2, change, flags));
}
if (add_action)
{
Intent intent = new Intent(getApplicationContext(), VpnProfileControlActivity.class);
@@ -837,7 +861,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
}
if (error == ErrorState.NO_ERROR)
{
builder.setContentText(name);
builder.setContentText(state == State.CONNECTED && isRuSplitOn(this) ? getString(R.string.notif_ru_direct) : name);
}
builder.setPublicVersion(buildNotification(true));
}
@@ -40,6 +40,8 @@ object PauseManager
private const val PREF_UNTIL = "pref_pause_until"
private const val PREF_PROFILE = "pref_pause_profile"
private const val REQUEST_CODE = 45
/** «Pause 15 min» from the VPN notification */
const val ACTION_PAUSE = "org.strongswan.android.action.PAUSE_15"
/* safety net of "until the network changes" */
private const val NETWORK_PAUSE_MAX_MS = 12L * 60 * 60 * 1000
@@ -329,6 +331,13 @@ class PauseReceiver : BroadcastReceiver()
{
override fun onReceive(context: Context, intent: Intent?)
{
if (intent?.action == PauseManager.ACTION_PAUSE)
{
val service = VpnStateService.getLiveInstance() ?: return
val profile = service.profile ?: return
PauseManager.pause(context, service, profile.getUUID().toString(), 15)
return
}
PauseManager.onAlarm(context)
}
}
@@ -47,6 +47,11 @@ public class MainActivity extends AppCompatActivity
*/
public static final String EXTRA_ADD_SERVER = "org.strongswan.android.extra.ADD_SERVER";
/**
* Set as an intent extra to open the Servers tab (the «Change» action of the notification)
*/
public static final String EXTRA_OPEN_SERVERS = "org.strongswan.android.extra.OPEN_SERVERS";
/**
* Use "bring your own device" (BYOD) features
*/
@@ -111,5 +116,10 @@ public class MainActivity extends AppCompatActivity
intent.removeExtra(EXTRA_ADD_SERVER);
ShxNavigation.INSTANCE.requestAddServer();
}
if (intent != null && intent.getBooleanExtra(EXTRA_OPEN_SERVERS, false))
{
intent.removeExtra(EXTRA_OPEN_SERVERS);
ShxNavigation.INSTANCE.request(ShxTab.PROFILES);
}
}
}
@@ -1009,4 +1009,8 @@
<string name="perm_tag_optional">по желанию</string>
<string name="perm_allow">Разрешить</string>
<string name="perm_done">Готово</string>
<string name="notif_protected">Защищено · %1$s</string>
<string name="notif_pause">Пауза 15 м</string>
<string name="notif_change">Сменить</string>
<string name="notif_ru_direct">RU-сайты идут напрямую</string>
</resources>
@@ -994,4 +994,8 @@
<string name="perm_tag_optional">optional</string>
<string name="perm_allow">Allow</string>
<string name="perm_done">Done</string>
<string name="notif_protected">Protected · %1$s</string>
<string name="notif_pause">Pause 15 min</string>
<string name="notif_change">Change</string>
<string name="notif_ru_direct">RU sites go direct</string>
</resources>