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 5d827a3..6b0db37 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 97df885..564a6cc 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 e9c52f1..8441c0a 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 5f16eec..20259ef 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 53bcf71..1fc1c29 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 72098cc..8a886c5 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 ba9fdd0..f2529a5 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 cb507be..584a2d9 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>