android: Add Quick Settings tile to toggle VPN state

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).
This commit is contained in:
Tobias Brunner 2018-06-08 14:22:52 +02:00
parent 08c79d5112
commit 64b7a6d622
12 changed files with 279 additions and 0 deletions

View 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"
@ -156,6 +159,15 @@
<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"

View File

@ -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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

View File

@ -194,4 +194,9 @@
<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>

View File

@ -194,4 +194,9 @@
<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>

View File

@ -191,4 +191,9 @@
<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>

View File

@ -192,4 +192,9 @@
<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>

View File

@ -191,4 +191,9 @@
<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>

View File

@ -191,4 +191,9 @@
<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>

View File

@ -194,4 +194,9 @@
<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>