From 76884123b22dbdd7538add0d931c5f2d8e660ed2 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 16 Jan 2018 13:59:33 +0100 Subject: #8788 implement VpnNotificationManager to handle notifications from VoidVPN and OpenVPN --- .../de/blinkt/openvpn/core/OpenVPNService.java | 241 +++------------- .../java/se/leap/bitmaskclient/BitmaskApp.java | 46 --- .../leap/bitmaskclient/VpnNotificationManager.java | 308 +++++++++++++++++++++ .../se/leap/bitmaskclient/eip/VoidVpnService.java | 139 +++------- .../main/res/layout/custom_notification_layout.xml | 48 ++++ ics-openvpn | 2 +- 6 files changed, 432 insertions(+), 352 deletions(-) create mode 100644 app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java create mode 100644 app/src/main/res/layout/custom_notification_layout.xml diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java index f52c30d9..c15f659a 100644 --- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -8,10 +8,7 @@ package de.blinkt.openvpn.core; import android.Manifest.permission; import android.annotation.TargetApi; import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.app.UiModeManager; -import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; @@ -22,14 +19,12 @@ import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.VpnService; import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.os.Handler.Callback; import android.os.IBinder; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.system.OsConstants; import android.text.TextUtils; @@ -38,7 +33,6 @@ import android.widget.Toast; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; @@ -46,21 +40,19 @@ import java.util.Collection; import java.util.Locale; import java.util.Vector; -import se.leap.bitmaskclient.BuildConfig; -import de.blinkt.openvpn.LaunchVPN; -import se.leap.bitmaskclient.R; import de.blinkt.openvpn.VpnProfile; -import de.blinkt.openvpn.activities.DisconnectVPN; import de.blinkt.openvpn.core.VpnStatus.ByteCountListener; import de.blinkt.openvpn.core.VpnStatus.StateListener; +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.VpnNotificationManager; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; import static de.blinkt.openvpn.core.NetworkSpace.ipAddress; -import se.leap.bitmaskclient.Dashboard; -public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener, IOpenVPNServiceInternal { +public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener, IOpenVPNServiceInternal, VpnNotificationManager.VpnServiceCallback { public static final String START_SERVICE = "de.blinkt.openvpn.START_SERVICE"; public static final String START_SERVICE_STICKY = "de.blinkt.openvpn.START_SERVICE_STICKY"; public static final String ALWAYS_SHOW_NOTIFICATION = "de.blinkt.openvpn.NOTIFICATION_ALWAYS_VISIBLE"; @@ -69,7 +61,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac private static final String RESUME_VPN = "se.leap.bitmaskclient.RESUME_VPN"; public static final String NOTIFICATION_CHANNEL_BG_ID = "openvpn_bg"; public static final String NOTIFICATION_CHANNEL_NEWSTATUS_ID = "openvpn_newstat"; - private String lastChannel; private static boolean mNotificationAlwaysVisible = false; private final Vector mDnslist = new Vector<>(); @@ -93,6 +84,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac private Handler guiHandler; private Toast mlastToast; private Runnable mOpenVPNThread; + private VpnNotificationManager notificationManager; private static final int PRIORITY_MIN = -2; private static final int PRIORITY_DEFAULT = 0; @@ -199,188 +191,11 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } } - - private void showNotification(final String msg, String tickerText, @NonNull String channel, long when, ConnectionStatus status) { - NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - int icon = getIconByConnectionStatus(status); - - android.app.Notification.Builder nbuilder = new Notification.Builder(this); - - int priority; - if (channel.equals(NOTIFICATION_CHANNEL_BG_ID)) - priority = PRIORITY_MIN; - else - priority = PRIORITY_DEFAULT; - - if (mProfile != null) - nbuilder.setContentTitle(getString(R.string.notifcation_title_bitmask, mProfile.mName)); - else - nbuilder.setContentTitle(getString(R.string.notifcation_title_notconnect)); - - nbuilder.setContentText(msg); - nbuilder.setOnlyAlertOnce(true); - nbuilder.setOngoing(true); - - nbuilder.setSmallIcon(icon); - if (status == LEVEL_WAITING_FOR_USER_INPUT) - nbuilder.setContentIntent(getUserInputIntent(msg)); - else - nbuilder.setContentIntent(getGraphPendingIntent()); - - if (when != 0) - nbuilder.setWhen(when); - - - // Try to set the priority available since API 16 (Jellybean) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - - jbNotificationExtras(priority, nbuilder); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - lpNotificationExtras(nbuilder); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - //noinspection NewApi - nbuilder.setChannelId(channel); - if (mProfile != null) - //noinspection NewApi - nbuilder.setShortcutId(mProfile.getUUIDString()); - - } - - if (tickerText != null && !tickerText.equals("")) - nbuilder.setTicker(tickerText); - - @SuppressWarnings("deprecation") - Notification notification = nbuilder.getNotification(); - - int notificationId = channel.hashCode(); - - mNotificationManager.notify(notificationId, notification); - startForeground(notificationId, notification); - - if (lastChannel != null && !channel.equals(lastChannel)) { - // Cancel old notification - mNotificationManager.cancel(lastChannel.hashCode()); - } - - // Check if running on a TV - if (runningOnAndroidTV() && !(priority < 0)) - guiHandler.post(new Runnable() { - - @Override - public void run() { - - if (mlastToast != null) - mlastToast.cancel(); - String toastText = String.format(Locale.getDefault(), "%s - %s", mProfile.mName, msg); - mlastToast = Toast.makeText(getBaseContext(), toastText, Toast.LENGTH_SHORT); - mlastToast.show(); - } - }); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void lpNotificationExtras(Notification.Builder nbuilder) { - nbuilder.setCategory(Notification.CATEGORY_SERVICE); - nbuilder.setLocalOnly(true); - - } - private boolean runningOnAndroidTV() { UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE); return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; } - private int getIconByConnectionStatus(ConnectionStatus level) { - switch (level) { - case LEVEL_CONNECTED: - return R.drawable.ic_stat_vpn; - case LEVEL_AUTH_FAILED: - case LEVEL_NONETWORK: - case LEVEL_NOTCONNECTED: - return R.drawable.ic_stat_vpn_offline; - case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: - case LEVEL_WAITING_FOR_USER_INPUT: - return R.drawable.ic_stat_vpn_outline; - case LEVEL_CONNECTING_SERVER_REPLIED: - return R.drawable.ic_stat_vpn_empty_halo; - case LEVEL_VPNPAUSED: - return android.R.drawable.ic_media_pause; - case UNKNOWN_LEVEL: - default: - return R.drawable.ic_stat_vpn; - - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private void jbNotificationExtras(int priority, - android.app.Notification.Builder nbuilder) { - try { - if (priority != 0) { - Method setpriority = nbuilder.getClass().getMethod("setPriority", int.class); - setpriority.invoke(nbuilder, priority); - - Method setUsesChronometer = nbuilder.getClass().getMethod("setUsesChronometer", boolean.class); - setUsesChronometer.invoke(nbuilder, true); - - } - - Intent disconnectVPN = new Intent(this, Dashboard.class); - disconnectVPN.putExtra(Dashboard.ACTION_ASK_TO_CANCEL_VPN, true); - disconnectVPN.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_SINGLE_TOP); - PendingIntent disconnectPendingIntent = PendingIntent.getActivity(this, 0, disconnectVPN, PendingIntent.FLAG_CANCEL_CURRENT); - - nbuilder.addAction(R.drawable.ic_menu_close_clear_cancel, - getString(R.string.cancel_connection), disconnectPendingIntent); - - /* NO PAUSE VPN functionality for Bitmask (yet) - Intent pauseVPN = new Intent(this, OpenVPNService.class); - if (mDeviceStateReceiver == null || !mDeviceStateReceiver.isUserPaused()) { - pauseVPN.setAction(PAUSE_VPN); - PendingIntent pauseVPNPending = PendingIntent.getService(this, 0, pauseVPN, 0); - nbuilder.addAction(R.drawable.ic_menu_pause, - getString(R.string.pauseVPN), pauseVPNPending); - - } else { - pauseVPN.setAction(RESUME_VPN); - PendingIntent resumeVPNPending = PendingIntent.getService(this, 0, pauseVPN, 0); - nbuilder.addAction(R.drawable.ic_menu_play, - getString(R.string.resumevpn), resumeVPNPending); - } */ - - - //ignore exception - } catch (NoSuchMethodException | IllegalArgumentException | - InvocationTargetException | IllegalAccessException e) { - VpnStatus.logException(e); - } - - } - - PendingIntent getUserInputIntent(String needed) { - Intent intent = new Intent(getApplicationContext(), LaunchVPN.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - intent.putExtra("need", needed); - Bundle b = new Bundle(); - b.putString("need", needed); - PendingIntent pIntent = PendingIntent.getActivity(this, 12, intent, 0); - return pIntent; - } - - PendingIntent getGraphPendingIntent() { - // Let the configure Button show the Log - Intent intent = new Intent(getBaseContext(), Dashboard.class); - intent.putExtra("PAGE", "graph"); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - PendingIntent startLW = PendingIntent.getActivity(this, 0, intent, 0); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - return startLW; - - } - synchronized void registerDeviceStateReceiver(OpenVPNManagement magnagement) { // Registers BroadcastReceiver to track network connection changes. IntentFilter filter = new IntentFilter(); @@ -660,6 +475,8 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac @Override public void onCreate() { super.onCreate(); + notificationManager = new VpnNotificationManager(this, this); + notificationManager.createOpenVpnNotificationChannel(); } @Override @@ -676,6 +493,8 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac // Just in case unregister for state VpnStatus.removeStateListener(this); VpnStatus.flushLog(); + notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_BG_ID); + notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_NEWSTATUS_ID); } private String getTunConfigString() { @@ -837,8 +656,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac mLocalIPv6 = null; mDomain = null; - builder.setConfigureIntent(getGraphPendingIntent()); - try { //Debug.stopMethodTracing(); ParcelFileDescriptor tun = builder.establish(); @@ -1084,8 +901,13 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac // This also mean we are no longer connected, ignore bytecount messages until next // CONNECTED // Does not work :( - showNotification(VpnStatus.getLastCleanLogMessage(this), - VpnStatus.getLastCleanLogMessage(this), channel, 0, level); + notificationManager.buildOpenVpnNotification( + mProfile.mName, + VpnStatus.getLastCleanLogMessage(this), + VpnStatus.getLastCleanLogMessage(this), + level, + 0, + channel); } } @@ -1110,9 +932,13 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, getResources()), humanReadableByteCount(out, false, getResources()), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, getResources())); - - - showNotification(netstat, null, NOTIFICATION_CHANNEL_BG_ID, mConnecttime, LEVEL_CONNECTED); + notificationManager.buildOpenVpnNotification( + mProfile.mName, + netstat, + null, + LEVEL_CONNECTED, + mConnecttime, + NOTIFICATION_CHANNEL_BG_ID); } } @@ -1149,6 +975,23 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac public void requestInputFromUser(int resid, String needed) { VpnStatus.updateStateString("NEED", "need " + needed, resid, LEVEL_WAITING_FOR_USER_INPUT); - showNotification(getString(resid), getString(resid), NOTIFICATION_CHANNEL_NEWSTATUS_ID, 0, LEVEL_WAITING_FOR_USER_INPUT); + notificationManager.buildOpenVpnNotification( + mProfile.mName, + getString(resid), + getString(resid), + LEVEL_WAITING_FOR_USER_INPUT, + 0, + NOTIFICATION_CHANNEL_BG_ID); + + } + + @Override + public void onNotificationBuild(int notificationId, Notification notification) { + startForeground(notificationId, notification); + } + + @Override + public void onNotificationStop() { + stopForeground(true); } -} +} \ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java index d7f574b2..88a01b62 100644 --- a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java +++ b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java @@ -1,16 +1,6 @@ package se.leap.bitmaskclient; -import android.annotation.TargetApi; import android.app.Application; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.content.Context; -import android.graphics.Color; -import android.os.Build; - -import de.blinkt.openvpn.core.OpenVPNService; - -import static android.os.Build.VERSION_CODES.O; /** * Created by cyberta on 24.10.17. @@ -23,42 +13,6 @@ public class BitmaskApp extends Application { super.onCreate(); PRNGFixes.apply(); //TODO: add LeakCanary! - if (Build.VERSION.SDK_INT >= O) - createNotificationChannelsForOpenvpn(); - } - - - @TargetApi(O) - private void createNotificationChannelsForOpenvpn() { - NotificationManager mNotificationManager = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - - // Background message - CharSequence name = getString(R.string.channel_name_background); - NotificationChannel mChannel = new NotificationChannel(OpenVPNService.NOTIFICATION_CHANNEL_BG_ID, - name, NotificationManager.IMPORTANCE_MIN); - - mChannel.setDescription(getString(R.string.channel_description_background)); - mChannel.enableLights(false); - - mChannel.setLightColor(Color.DKGRAY); - mNotificationManager.createNotificationChannel(mChannel); - - // Connection status change messages - - name = getString(R.string.channel_name_status); - mChannel = new NotificationChannel(OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, - name, NotificationManager.IMPORTANCE_DEFAULT); - - - mChannel.setDescription(getString(R.string.channel_description_status)); - mChannel.enableLights(true); - - mChannel.setLightColor(Color.BLUE); - mNotificationManager.createNotificationChannel(mChannel); - } - - } diff --git a/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java b/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java new file mode 100644 index 00000000..5b089524 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java @@ -0,0 +1,308 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.widget.RemoteViews; + +import de.blinkt.openvpn.LaunchVPN; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.OpenVPNService; +import se.leap.bitmaskclient.eip.VoidVpnService; + +import static android.os.Build.VERSION_CODES.O; +import static android.support.v4.app.NotificationCompat.PRIORITY_HIGH; +import static android.support.v4.app.NotificationCompat.PRIORITY_MAX; +import static android.support.v4.app.NotificationCompat.PRIORITY_MIN; +import static android.text.TextUtils.isEmpty; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN; + +/** + * Created by cyberta on 14.01.18. + */ + +public class VpnNotificationManager { + + Context context; + private VpnServiceCallback vpnServiceCallback; + private NotificationManager notificationManager; + private NotificationManagerCompat compatNotificationManager; + private String[] notificationChannels = { + OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, + OpenVPNService.NOTIFICATION_CHANNEL_BG_ID, + VoidVpnService.NOTIFICATION_CHANNEL_NEWSTATUS_ID}; + private String lastNotificationChannel = ""; + + public interface VpnServiceCallback { + void onNotificationBuild(int notificationId, Notification notification); + void onNotificationStop(); + } + + public VpnNotificationManager(@NonNull Context context, @NonNull VpnServiceCallback vpnServiceCallback) { + this.context = context; + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + compatNotificationManager = NotificationManagerCompat.from(context); + this.vpnServiceCallback = vpnServiceCallback; + } + + public void buildVoidVpnNotification(final String msg, String tickerText, ConnectionStatus status) { + //TODO: implement extra Dashboard.ACTION_ASK_TO_CANCEL_BLOCKING_VPN + NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.Builder(R.drawable.ic_menu_close_clear_cancel, + context.getString(R.string.vpn_button_turn_off_blocking), getStopVoidVpnIntent()); + + buildVpnNotification( + context.getString(R.string.void_vpn_title), + msg, + tickerText, + status, + VoidVpnService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, + PRIORITY_MAX, + 0, + getDashboardIntent(), + actionBuilder.build()); + } + + public void stopNotifications(String notificationChannelNewstatusId) { + vpnServiceCallback.onNotificationStop(); + compatNotificationManager.cancel(notificationChannelNewstatusId.hashCode()); + } + + public void deleteNotificationChannel(String notificationChannel) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + notificationManager.getNotificationChannel(notificationChannel) != null) { + notificationManager.deleteNotificationChannel(notificationChannel); + } + } + + /** + * @param msg + * @param tickerText + * @param status + * @param when + */ + public void buildOpenVpnNotification(String profileName, final String msg, String tickerText, ConnectionStatus status, long when, String notificationChannelNewstatusId) { + NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action. + Builder(R.drawable.ic_menu_close_clear_cancel, context.getString(R.string.cancel_connection), getDisconnectIntent()); + String title; + if (isEmpty(profileName)) { + title = context.getString(R.string.app_name); + } else { + title = context.getString(R.string.notifcation_title_bitmask, profileName); + } + + PendingIntent contentIntent; + if (status == LEVEL_WAITING_FOR_USER_INPUT) + contentIntent = getUserInputIntent(msg); + else + contentIntent = getDashboardIntent(); + + int priority; + if (OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID.equals(notificationChannelNewstatusId)) { + priority = PRIORITY_HIGH; + } else { + // background channel + priority = PRIORITY_MIN; + } + + buildVpnNotification( + title, + msg, + tickerText, + status, + notificationChannelNewstatusId, + priority, + when, + contentIntent, + actionBuilder.build()); + } + + + @TargetApi(O) + public void createVoidVpnNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + + // Connection status change messages + CharSequence name = context.getString(R.string.channel_name_status); + NotificationChannel mChannel = new NotificationChannel(VoidVpnService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, + name, NotificationManager.IMPORTANCE_DEFAULT); + + mChannel.setDescription(context.getString(R.string.channel_description_status)); + mChannel.enableLights(true); + + mChannel.setLightColor(Color.BLUE); + notificationManager.createNotificationChannel(mChannel); + } + + @TargetApi(O) + public void createOpenVpnNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + + // Background message + CharSequence name = context.getString(R.string.channel_name_background); + NotificationChannel mChannel = new NotificationChannel(OpenVPNService.NOTIFICATION_CHANNEL_BG_ID, + name, NotificationManager.IMPORTANCE_MIN); + + mChannel.setDescription(context.getString(R.string.channel_description_background)); + mChannel.enableLights(false); + + mChannel.setLightColor(Color.DKGRAY); + notificationManager.createNotificationChannel(mChannel); + + // Connection status change messages + name = context.getString(R.string.channel_name_status); + mChannel = new NotificationChannel(OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, + name, NotificationManager.IMPORTANCE_DEFAULT); + + + mChannel.setDescription(context.getString(R.string.channel_description_status)); + mChannel.enableLights(true); + + mChannel.setLightColor(Color.BLUE); + notificationManager.createNotificationChannel(mChannel); + } + + /** + * @return a custom remote view for notifications for API 16 - 19 + */ + private RemoteViews getKitkatCustomRemoteView(ConnectionStatus status, String title, String message) { + int iconResource = getIconByConnectionStatus(status); + RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.custom_notification_layout); + remoteViews.setImageViewResource(R.id.image_icon, iconResource); + remoteViews.setTextViewText(R.id.message, message); + remoteViews.setTextViewText(R.id.title, title); + + return remoteViews; + } + + private void buildVpnNotification(String title, final String msg, String tickerText, ConnectionStatus status, String notificationChannelNewstatusId, int priority, long when, PendingIntent contentIntent, NotificationCompat.Action notificationAction) { + NotificationCompat.Builder nCompatBuilder = new NotificationCompat.Builder(context, notificationChannelNewstatusId); + int icon = getIconByConnectionStatus(status); + + // this is a workaround to avoid confusion between the Android's system vpn notification + // showing a filled out key icon and the bitmask icon indicating a different state. + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT && + notificationChannelNewstatusId.equals(OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID) && + status != LEVEL_NONETWORK + ) { + // removes the icon from the system status bar + icon = android.R.color.transparent; + // adds the icon to the notification in the notification drawer + nCompatBuilder.setContent(getKitkatCustomRemoteView(status, title, msg)); + } else { + nCompatBuilder.addAction(notificationAction); + } + + nCompatBuilder.setContentTitle(title); + nCompatBuilder.setCategory(NotificationCompat.CATEGORY_SERVICE); + nCompatBuilder.setLocalOnly(true); + nCompatBuilder.setContentText(msg); + nCompatBuilder.setOnlyAlertOnce(true); + nCompatBuilder.setSmallIcon(icon); + nCompatBuilder.setPriority(priority); + nCompatBuilder.setOngoing(true); + nCompatBuilder.setUsesChronometer(true); + nCompatBuilder.setWhen(when); + nCompatBuilder.setContentIntent(contentIntent); + if (!isEmpty(tickerText)) { + nCompatBuilder.setTicker(tickerText); + } + + Notification notification = nCompatBuilder.build(); + int notificationId = notificationChannelNewstatusId.hashCode(); + + if (!notificationChannelNewstatusId.equals(lastNotificationChannel)) { + // Cancel old notification + for (String channel : notificationChannels) { + stopNotifications(channel); + } + } + + compatNotificationManager.notify(notificationId, notification); + vpnServiceCallback.onNotificationBuild(notificationId, notification); + lastNotificationChannel = notificationChannelNewstatusId; + } + + private PendingIntent getDashboardIntent() { + Intent startDashboard = new Intent(context, Dashboard.class); + startDashboard.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_SINGLE_TOP); + return PendingIntent.getActivity(context, 0, startDashboard, PendingIntent.FLAG_CANCEL_CURRENT); + } + + private PendingIntent getStopVoidVpnIntent() { + Intent stopVoidVpnIntent = new Intent (context, VoidVpnService.class); + stopVoidVpnIntent.setAction(EIP_ACTION_STOP_BLOCKING_VPN); + return PendingIntent.getService(context, 0, stopVoidVpnIntent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + private PendingIntent getDisconnectIntent() { + Intent disconnectVPN = new Intent(context, Dashboard.class); + disconnectVPN.setAction(Intent.ACTION_MAIN); //needs to be set that actual action can get triggered + disconnectVPN.putExtra(Dashboard.ACTION_ASK_TO_CANCEL_VPN, true); + disconnectVPN.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + return PendingIntent.getActivity(context, 0, disconnectVPN, PendingIntent.FLAG_CANCEL_CURRENT); + } + + private PendingIntent getUserInputIntent(String needed) { + Intent intent = new Intent(context, LaunchVPN.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + intent.putExtra("need", needed); + Bundle b = new Bundle(); + b.putString("need", needed); + PendingIntent pIntent = PendingIntent.getActivity(context, 12, intent, 0); + return pIntent; + } + + private int getIconByConnectionStatus(ConnectionStatus level) { + switch (level) { + case LEVEL_CONNECTED: + return R.drawable.ic_stat_vpn; + case LEVEL_AUTH_FAILED: + case LEVEL_NONETWORK: + case LEVEL_NOTCONNECTED: + return R.drawable.ic_stat_vpn_offline; + case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: + case LEVEL_WAITING_FOR_USER_INPUT: + case LEVEL_CONNECTING_SERVER_REPLIED: + return R.drawable.ic_stat_vpn_outline; + case LEVEL_BLOCKING: + return R.drawable.ic_stat_vpn_blocking; + case UNKNOWN_LEVEL: + default: + return R.drawable.ic_stat_vpn_offline; + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java index 792de2cb..6d49d83d 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java @@ -1,19 +1,27 @@ +/** + * Copyright (c) 2013 LEAP Encryption Access Project and contributers + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package se.leap.bitmaskclient.eip; -import android.annotation.TargetApi; import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.Color; import android.net.VpnService; import android.os.Build; import android.os.ParcelFileDescriptor; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; import android.util.Log; import java.io.IOException; @@ -22,10 +30,9 @@ import java.util.Observer; import de.blinkt.openvpn.core.ConnectionStatus; import de.blinkt.openvpn.core.VpnStatus; -import se.leap.bitmaskclient.Dashboard; import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.VpnNotificationManager; -import static android.os.Build.VERSION_CODES.O; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_ALWAYS_ON_EIP; import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_BLOCKING_VPN; import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN; @@ -33,7 +40,7 @@ import static se.leap.bitmaskclient.Constants.EIP_IS_ALWAYS_ON; import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; -public class VoidVpnService extends VpnService implements Observer { +public class VoidVpnService extends VpnService implements Observer, VpnNotificationManager.VpnServiceCallback { static final String TAG = VoidVpnService.class.getSimpleName(); static ParcelFileDescriptor fd; @@ -42,16 +49,15 @@ public class VoidVpnService extends VpnService implements Observer { private static final String STATE_ESTABLISH = "ESTABLISHVOIDVPN"; public static final String NOTIFICATION_CHANNEL_NEWSTATUS_ID = "bitmask_void_vpn_news"; private EipStatus eipStatus; - NotificationManager notificationManager; - NotificationManagerCompat compatNotificationManager; + private VpnNotificationManager notificationManager; @Override public void onCreate() { super.onCreate(); - notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - compatNotificationManager = NotificationManagerCompat.from(this); eipStatus = EipStatus.getInstance(); eipStatus.addObserver(this); + notificationManager = new VpnNotificationManager(this, this); + notificationManager.createVoidVpnNotificationChannel(); } @Override @@ -91,24 +97,9 @@ public class VoidVpnService extends VpnService implements Observer { closeFd(); } - @TargetApi(O) - private void createNotificationChannel() { - - // Connection status change messages - CharSequence name = getString(R.string.channel_name_status); - NotificationChannel mChannel = new NotificationChannel(VoidVpnService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, - name, NotificationManagerCompat.IMPORTANCE_DEFAULT); - - mChannel.setDescription(getString(R.string.channel_description_status)); - mChannel.enableLights(true); - - mChannel.setLightColor(Color.BLUE); - notificationManager.createNotificationChannel(mChannel); - } - - private void stop() { - stopNotifications(); + notificationManager.stopNotifications(NOTIFICATION_CHANNEL_NEWSTATUS_ID); + notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_NEWSTATUS_ID); if (thread != null) { thread.interrupt(); } @@ -174,87 +165,23 @@ public class VoidVpnService extends VpnService implements Observer { } if (eipStatus.isBlockingVpnEstablished()) { - showNotification(getString(eipStatus.getLocalizedResId()), - getString(eipStatus.getLocalizedResId()), eipStatus.getLevel()); + notificationManager.buildVoidVpnNotification( + getString(eipStatus.getLocalizedResId()), + getString(eipStatus.getLocalizedResId()), + eipStatus.getLevel()); } else { - stopNotifications(); + notificationManager.stopNotifications(NOTIFICATION_CHANNEL_NEWSTATUS_ID); } } - private void stopNotifications() { - stopForeground(true); - compatNotificationManager.cancel(NOTIFICATION_CHANNEL_NEWSTATUS_ID.hashCode()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && - notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_NEWSTATUS_ID) != null) { - notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_NEWSTATUS_ID); - } - } - - /** - * @param msg - * @param tickerText - * @param status - */ - private void showNotification(final String msg, String tickerText, ConnectionStatus status) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel(); - } - - int icon = getIconByConnectionStatus(status); - NotificationCompat.Builder nCompatBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_NEWSTATUS_ID); - - nCompatBuilder.setContentTitle(getString(R.string.notifcation_title_bitmask, getString(R.string.void_vpn_title))); - nCompatBuilder.setCategory(NotificationCompat.CATEGORY_SERVICE); - nCompatBuilder.setLocalOnly(true); - nCompatBuilder.setContentText(msg); - nCompatBuilder.setOnlyAlertOnce(true); - nCompatBuilder.setSmallIcon(icon); - if (tickerText != null && !tickerText.equals("")) { - nCompatBuilder.setTicker(tickerText); - } - - nCompatBuilder.setContentIntent(getDashboardIntent()); - //TODO: implement extra Dashboard.ACTION_ASK_TO_CANCEL_BLOCKING_VPN - NotificationCompat.Action.Builder builder = new NotificationCompat.Action.Builder(R.drawable.ic_menu_close_clear_cancel, getString(R.string.vpn_button_turn_off_blocking), getStopVoidVpnIntent()); - nCompatBuilder.addAction(builder.build()); - - Notification notification = nCompatBuilder.build(); - int notificationId = NOTIFICATION_CHANNEL_NEWSTATUS_ID.hashCode(); - compatNotificationManager.notify(notificationId, notification); + @Override + public void onNotificationBuild(int notificationId, Notification notification) { startForeground(notificationId, notification); } - private PendingIntent getDashboardIntent() { - Intent startDashboard = new Intent(this, Dashboard.class); - startDashboard.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_SINGLE_TOP); - return PendingIntent.getActivity(this, 0, startDashboard, PendingIntent.FLAG_CANCEL_CURRENT); - } - - private PendingIntent getStopVoidVpnIntent() { - Intent stopVoidVpnIntent = new Intent (this, VoidVpnService.class); - stopVoidVpnIntent.setAction(EIP_ACTION_STOP_BLOCKING_VPN); - return PendingIntent.getService(this, 0, stopVoidVpnIntent, PendingIntent.FLAG_CANCEL_CURRENT); + @Override + public void onNotificationStop() { + stopForeground(true); } - //TODO: replace with getIconByEipLevel(EipLevel level) - private int getIconByConnectionStatus(ConnectionStatus level) { - switch (level) { - case LEVEL_CONNECTED: - return R.drawable.ic_stat_vpn; - case LEVEL_AUTH_FAILED: - case LEVEL_NONETWORK: - case LEVEL_NOTCONNECTED: - return R.drawable.ic_stat_vpn_offline; - case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: - case LEVEL_WAITING_FOR_USER_INPUT: - case LEVEL_CONNECTING_SERVER_REPLIED: - return R.drawable.ic_stat_vpn_outline; - case LEVEL_BLOCKING: - return R.drawable.ic_stat_vpn_blocking; - case UNKNOWN_LEVEL: - default: - return R.drawable.ic_stat_vpn_offline; - } - } } diff --git a/app/src/main/res/layout/custom_notification_layout.xml b/app/src/main/res/layout/custom_notification_layout.xml new file mode 100644 index 00000000..e97fcbe2 --- /dev/null +++ b/app/src/main/res/layout/custom_notification_layout.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ics-openvpn b/ics-openvpn index f2edf86a..498e9a62 160000 --- a/ics-openvpn +++ b/ics-openvpn @@ -1 +1 @@ -Subproject commit f2edf86aeb4ca90ad7f2e9589fa2ffa1f477dfc7 +Subproject commit 498e9a6264e51ce36c11df7587d6f82395172ac7 -- cgit v1.2.3