]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
android: Add Quick Settings tile to toggle VPN state
authorTobias Brunner <tobias@strongswan.org>
Fri, 8 Jun 2018 12:22:52 +0000 (14:22 +0200)
committerTobias Brunner <tobias@strongswan.org>
Tue, 3 Jul 2018 09:31:35 +0000 (11:31 +0200)
Only if there is no currently active (or previously active) profile does
this currently operate on the configured (or stored most recently used)
profile.  This way it's possible to use a different connection and
quickly disable and re-enable it again.  When unlocked the profile name
is shown, when locked a generic text is used (this detection doesn't seem
to work 100% reliably).  To disconnect, the user is forced to unlock the
device, connecting is possible without, if the credentials are available
and no fatal error occurs (it even works with the system credential store,
at least on Android 8.1).

Note that the tile is not available right after a reboot.  It seems that
the system has to be unlocked once to activate third-party tiles (will
be interesting to see how this works together with Always-on VPN).

12 files changed:
src/frontends/android/app/src/main/AndroidManifest.xml
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnTileService.java [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable-hdpi/ic_notification_disconnected.png [new file with mode: 0755]
src/frontends/android/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png [new file with mode: 0755]
src/frontends/android/app/src/main/res/drawable-xhdpi/ic_notification_disconnected.png [new file with mode: 0755]
src/frontends/android/app/src/main/res/values-de/strings.xml
src/frontends/android/app/src/main/res/values-pl/strings.xml
src/frontends/android/app/src/main/res/values-ru/strings.xml
src/frontends/android/app/src/main/res/values-ua/strings.xml
src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml
src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml
src/frontends/android/app/src/main/res/values/strings.xml

index 5d827a34def6cfccdefbf4b28fdd9ee0376a57f3..6b0db3782b83fe95628d0ef6116e1295cb616ab0 100644 (file)
@@ -36,6 +36,9 @@
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
+            </intent-filter>
         </activity>
         <activity
             android:name=".ui.VpnProfileControlActivity"
                 <action android:name="android.net.VpnService" />
             </intent-filter>
         </service>
+        <service
+            android:name=".ui.VpnTileService"
+            android:label="@string/tile_default"
+            android:icon="@drawable/ic_notification"
+            android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+            <intent-filter>
+                <action android:name="android.service.quicksettings.action.QS_TILE" />
+            </intent-filter>
+        </service>
 
         <provider
             android:name=".data.LogContentProvider"
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnTileService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnTileService.java
new file mode 100644 (file)
index 0000000..9641d17
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2018 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program 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 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program 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.
+ */
+
+package org.strongswan.android.ui;
+
+import android.annotation.TargetApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Icon;
+import android.os.Build;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.logic.VpnStateService;
+import org.strongswan.android.utils.Constants;
+
+@TargetApi(Build.VERSION_CODES.N)
+public class VpnTileService extends TileService implements VpnStateService.VpnStateListener
+{
+       private boolean mListening;
+       private VpnProfileDataSource mDataSource;
+       private VpnStateService mService;
+       private final ServiceConnection mServiceConnection = new ServiceConnection()
+       {
+               @Override
+               public void onServiceDisconnected(ComponentName name)
+               {
+                       mService = null;
+               }
+
+               @Override
+               public void onServiceConnected(ComponentName name, IBinder service)
+               {
+                       mService = ((VpnStateService.LocalBinder)service).getService();
+                       if (mListening)
+                       {
+                               mService.registerListener(VpnTileService.this);
+                               updateTile();
+                       }
+               }
+       };
+
+       @Override
+       public void onCreate()
+       {
+               super.onCreate();
+
+               Context context = getApplicationContext();
+               context.bindService(new Intent(context, VpnStateService.class),
+                                                       mServiceConnection, Service.BIND_AUTO_CREATE);
+
+               mDataSource = new VpnProfileDataSource(this);
+               mDataSource.open();
+       }
+
+       @Override
+       public void onDestroy()
+       {
+               super.onDestroy();
+               if (mService != null)
+               {
+                       getApplicationContext().unbindService(mServiceConnection);
+               }
+               mDataSource.close();
+       }
+
+       @Override
+       public void onStartListening()
+       {
+               super.onStartListening();
+               mListening = true;
+               if (mService != null)
+               {
+                       mService.registerListener(this);
+                       updateTile();
+               }
+       }
+
+       @Override
+       public void onStopListening()
+       {
+               super.onStopListening();
+               mListening = false;
+               if (mService != null)
+               {
+                       mService.unregisterListener(this);
+               }
+       }
+
+       private VpnProfile getProfile()
+       {
+               SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
+               String uuid = pref.getString(Constants.PREF_DEFAULT_VPN_PROFILE, null);
+               if (uuid == null || uuid.equals(Constants.PREF_DEFAULT_VPN_PROFILE_MRU))
+               {
+                       uuid = pref.getString(Constants.PREF_MRU_VPN_PROFILE, null);
+               }
+
+               return mDataSource.getVpnProfile(uuid);
+       }
+
+       @Override
+       public void onClick()
+       {
+               if (mService != null)
+               {
+                       /* we operate on the current/most recently used profile, but fall back to configuration */
+                       VpnProfile profile = mService.getProfile();
+                       if (profile == null)
+                       {
+                               profile = getProfile();
+                       }
+
+                       /* open the main activity in case of an error. since the state is still CONNECTING
+                        * there is a popup confirmation dialog if we connect again, disconnect would work
+                        * but doing two operations is not ideal */
+                       if (mService.getErrorState() == VpnStateService.ErrorState.NO_ERROR)
+                       {
+                               switch (mService.getState())
+                               {
+                                       case CONNECTING:
+                                       case CONNECTED:
+                                               Runnable disconnect = new Runnable()
+                                               {
+                                                       @Override
+                                                       public void run()
+                                                       {
+                                                               mService.disconnect();
+                                                       }
+                                               };
+                                               if (isLocked())
+                                               {
+                                                       unlockAndRun(disconnect);
+                                               }
+                                               else
+                                               {
+                                                       disconnect.run();
+                                               }
+                                               return;
+                               }
+                               if (profile != null)
+                               {
+                                       Intent intent = new Intent(this, VpnProfileControlActivity.class);
+                                       intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                                       intent.setAction(VpnProfileControlActivity.START_PROFILE);
+                                       intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_ID, profile.getUUID().toString());
+                                       startActivity(intent);
+                                       return;
+                               }
+                       }
+               }
+               Intent intent = new Intent(this, MainActivity.class);
+               intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+               startActivityAndCollapse(intent);
+       }
+
+       @Override
+       public void stateChanged()
+       {
+               updateTile();
+       }
+
+       private void updateTile()
+       {
+               VpnProfile profile = mService.getProfile();
+               VpnStateService.State state = mService.getState();
+               VpnStateService.ErrorState error = mService.getErrorState();
+
+               /* same as above, only use the configured profile if we have no active profile */
+               if (profile == null)
+               {
+                       profile = getProfile();
+               }
+
+               Tile tile = getQsTile();
+
+               if (error != VpnStateService.ErrorState.NO_ERROR)
+               {
+                       tile.setState(Tile.STATE_INACTIVE);
+                       tile.setIcon(Icon.createWithResource(this, R.drawable.ic_notification_warning));
+                       tile.setLabel(getString(R.string.tile_connect));
+               }
+               else
+               {
+                       switch (state)
+                       {
+                               case DISCONNECTING:
+                               case DISABLED:
+                                       tile.setState(Tile.STATE_INACTIVE);
+                                       tile.setIcon(Icon.createWithResource(this, R.drawable.ic_notification_disconnected));
+                                       tile.setLabel(getString(R.string.tile_connect));
+                                       break;
+                               case CONNECTING:
+                                       tile.setState(Tile.STATE_ACTIVE);
+                                       tile.setIcon(Icon.createWithResource(this, R.drawable.ic_notification_connecting));
+                                       tile.setLabel(getString(R.string.tile_disconnect));
+                                       break;
+                               case CONNECTED:
+                                       tile.setState(Tile.STATE_ACTIVE);
+                                       tile.setIcon(Icon.createWithResource(this, R.drawable.ic_notification));
+                                       tile.setLabel(getString(R.string.tile_disconnect));
+                                       break;
+                       }
+               }
+               if (profile != null && !isSecure())
+               {
+                       tile.setLabel(profile.getName());
+               }
+               tile.updateTile();
+       }
+}
diff --git a/src/frontends/android/app/src/main/res/drawable-hdpi/ic_notification_disconnected.png b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_notification_disconnected.png
new file mode 100755 (executable)
index 0000000..039877b
Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_notification_disconnected.png differ
diff --git a/src/frontends/android/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png
new file mode 100755 (executable)
index 0000000..1adedbe
Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_notification_disconnected.png differ
diff --git a/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_notification_disconnected.png b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_notification_disconnected.png
new file mode 100755 (executable)
index 0000000..e5ddb24
Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_notification_disconnected.png differ
index 97df885f86cab204636fb1f37fad56c2333ca1b6..564a6cc2e431212824a3910754304e10d46c3fe9 100644 (file)
     <string name="disconnect_active_connection">Dies trennt die aktuelle VPN Verbindung!</string>
     <string name="connect">Verbinden</string>
 
+    <!-- Quick Settings tile -->
+    <string name="tile_default">VPN umschalten</string>
+    <string name="tile_connect">VPN verbinden</string>
+    <string name="tile_disconnect">VPN trennen</string>
+
 </resources>
index e9c52f14a0a256d12b8d9298ee2698dd0dae3d6f..8441c0a7a299ee619db143e5ede13f638d3e19cd 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">Połącz</string>
 
+    <!-- Quick Settings tile -->
+    <string name="tile_default">Toggle VPN</string>
+    <string name="tile_connect">Connect VPN</string>
+    <string name="tile_disconnect">Disconnect VPN</string>
+
 </resources>
index 5f16eecc8112a536b6b002f117f195ee061748ac..20259ef887728c7c34f7b97893d81c8b50824f81 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">Соединить</string>
 
+    <!-- Quick Settings tile -->
+    <string name="tile_default">Toggle VPN</string>
+    <string name="tile_connect">Connect VPN</string>
+    <string name="tile_disconnect">Disconnect VPN</string>
+
 </resources>
index 53bcf7164056f586f9e907eaa32d4899e5214f3d..1fc1c2926c049eb609aa802c194472dd81d99bca 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">Підключити</string>
 
+    <!-- Quick Settings tile -->
+    <string name="tile_default">Toggle VPN</string>
+    <string name="tile_connect">Connect VPN</string>
+    <string name="tile_disconnect">Disconnect VPN</string>
+
 </resources>
index 72098cc4796f7db5ad676823fd8371923361174c..8a886c5d812d6e87508bd9260f3128a02ca6e6ca 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">连接</string>
 
+    <!-- Quick Settings tile -->
+    <string name="tile_default">Toggle VPN</string>
+    <string name="tile_connect">Connect VPN</string>
+    <string name="tile_disconnect">Disconnect VPN</string>
+
 </resources>
index ba9fdd044a6a9dc485f91161779603aff941bc66..f2529a519d06cd5d396d16816e528a7085977177 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">連線</string>
 
+    <!-- Quick Settings tile -->
+    <string name="tile_default">Toggle VPN</string>
+    <string name="tile_connect">Connect VPN</string>
+    <string name="tile_disconnect">Disconnect VPN</string>
+
 </resources>
index cb507beae4cde28457da13149ab1d56fd3ac282c..584a2d9a2d8d62cc1269569227c2def62e393dd3 100644 (file)
     <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
     <string name="connect">Connect</string>
 
+    <!-- Quick Settings tile -->
+    <string name="tile_default">Toggle VPN</string>
+    <string name="tile_connect">Connect VPN</string>
+    <string name="tile_disconnect">Disconnect VPN</string>
+
 </resources>