diff options
Diffstat (limited to 'app/src/main/java/se/leap')
11 files changed, 713 insertions, 226 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java index 953a559d..d7f574b2 100644 --- a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java +++ b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java @@ -1,6 +1,16 @@ 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. @@ -13,5 +23,42 @@ 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/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java index 8723191c..a7ab56fd 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java @@ -15,15 +15,20 @@ public interface Constants { ///////////////////////////////////////////// String EIP_ACTION_CHECK_CERT_VALIDITY = "EIP.CHECK_CERT_VALIDITY"; - String EIP_ACTION_START = "EIP.START"; - String EIP_ACTION_STOP = "EIP.STOP"; - String EIP_ACTION_UPDATE = "EIP.UPDATE"; - String EIP_ACTION_IS_RUNNING = "EIP.IS_RUNNING"; - String EIP_ACTION_BLOCK_VPN_PROFILE = "EIP.ACTION_BLOCK_VPN_PROFILE"; + String EIP_ACTION_START = "se.leap.bitmaskclient.EIP.START"; + String EIP_ACTION_STOP = "se.leap.bitmaskclient.EIP.STOP"; + String EIP_ACTION_UPDATE = "se.leap.bitmaskclient.EIP.UPDATE"; + String EIP_ACTION_IS_RUNNING = "se.leap.bitmaskclient.EIP.IS_RUNNING"; + String EIP_ACTION_START_ALWAYS_ON_EIP = "se.leap.bitmaskclient.START_ALWAYS_ON_EIP"; + String EIP_ACTION_START_BLOCKING_VPN = "se.leap.bitmaskclient.EIP_ACTION_START_BLOCKING_VPN"; + String EIP_ACTION_STOP_BLOCKING_VPN = "se.leap.bitmaskclient.EIP_ACTION_STOP_BLOCKING_VPN"; String EIP_NOTIFICATION = "EIP.NOTIFICATION"; String EIP_RECEIVER = "EIP.RECEIVER"; String EIP_REQUEST = "EIP.REQUEST"; + String EIP_RESTART_ON_BOOT = "EIP.RESTART_ON_BOOT"; + String EIP_IS_ALWAYS_ON = "EIP.EIP_IS_ALWAYS_ON"; + ////////////////////////////////////////////// diff --git a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java index 1f0477c9..f1e7b3bd 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java +++ b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java @@ -45,6 +45,9 @@ import se.leap.bitmaskclient.userstatus.SessionDialog; import se.leap.bitmaskclient.userstatus.User; import se.leap.bitmaskclient.userstatus.UserStatusFragment; +import static se.leap.bitmaskclient.Constants.EIP_IS_ALWAYS_ON; +import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; + /** * The main user facing Activity of Bitmask Android, consisting of status, controls, * and access to preferences. @@ -59,14 +62,23 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec public static final String TAG = Dashboard.class.getSimpleName(); public static final String ACTION_QUIT = "quit"; + + /** + * When "Disconnect" is clicked from the notification this extra gets added to the calling intent. + */ public static final String ACTION_ASK_TO_CANCEL_VPN = "ask to cancel vpn"; + /** + * if always-on feature is enabled, but there's no provider configured the EIP Service + * adds this intent extra. ACTION_CONFIGURE_ALWAYS_ON_PROFILE + * serves to start the Configuration Wizard on top of the Dashboard Activity. + */ + public static final String ACTION_CONFIGURE_ALWAYS_ON_PROFILE = "configure always-on profile"; public static final String REQUEST_CODE = "request_code"; public static final String PARAMETERS = "dashboard parameters"; - public static final String START_ON_BOOT = "dashboard start on boot"; - //FIXME: remove OR FIX ON_BOOT - public static final String ON_BOOT = "dashboard on boot"; + public static final String APP_VERSION = "bitmask version"; - private static Context app; + //FIXME: context classes in static fields lead to memory leaks! + private static Context dashboardContext; protected static SharedPreferences preferences; private FragmentManagerEnhanced fragment_manager; @@ -87,28 +99,18 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler(), this); - if (app == null) { - app = this; + if (dashboardContext == null) { + dashboardContext = this; handleVersion(); } - boolean provider_exists = previousProviderExists(savedInstanceState); - if (provider_exists) { - provider = getProvider(savedInstanceState); - if(!provider.isConfigured()) - startActivityForResult(new Intent(this, ConfigurationWizard.class), CONFIGURE_LEAP); - else { - buildDashboard(getIntent().getBooleanExtra(ON_BOOT, false)); - user_status_fragment.restoreSessionStatus(savedInstanceState); - } - } else { - startActivityForResult(new Intent(this, ConfigurationWizard.class), CONFIGURE_LEAP); - } + + prepareEIP(savedInstanceState); } @Override protected void onResume() { super.onResume(); - handleVPNCancellation(getIntent()); + handleVpnCancellation(getIntent()); } private boolean previousProviderExists(Bundle savedInstanceState) { @@ -172,7 +174,19 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); - handleVPNCancellation(intent); + handleIntentExtras(intent); + } + + private void handleIntentExtras(Intent intent) { + if (intent.hasExtra(ACTION_ASK_TO_CANCEL_VPN)) { + handleVpnCancellation(intent); + } else if (intent.hasExtra(EIP_RESTART_ON_BOOT)) { + Log.d(TAG, "Dashboard: EIP_RESTART_ON_BOOT"); + prepareEIP(null); + } else if (intent.hasExtra(ACTION_CONFIGURE_ALWAYS_ON_PROFILE)) { + Log.d(TAG, "Dashboard: ACTION_CONFIGURE_ALWAYS_ON_PROFILE"); + handleConfigureAlwaysOn(getIntent()); + } } @Override @@ -195,15 +209,44 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec } } - private void handleVPNCancellation(Intent intent) { + private void handleVpnCancellation(Intent intent) { if (intent.hasExtra(Dashboard.ACTION_ASK_TO_CANCEL_VPN)) { eip_fragment.askToStopEIP(); intent.removeExtra(ACTION_ASK_TO_CANCEL_VPN); } } + private void handleConfigureAlwaysOn(Intent intent) { + intent.removeExtra(ACTION_CONFIGURE_ALWAYS_ON_PROFILE); + Log.d(TAG, "start Configuration wizard!"); + startActivityForResult(new Intent(this, ConfigurationWizard.class), CONFIGURE_LEAP); + } + + private void prepareEIP(Bundle savedInstanceState) { + boolean provider_exists = previousProviderExists(savedInstanceState); + if (provider_exists) { + provider = getProvider(savedInstanceState); + if(!provider.isConfigured()) { + configureLeapProvider(); + } else { + Log.d(TAG, "vpn provider is configured"); + buildDashboard(getIntent().getBooleanExtra(EIP_RESTART_ON_BOOT, false)); + user_status_fragment.restoreSessionStatus(savedInstanceState); + } + } else { + configureLeapProvider(); + } + } + + private void configureLeapProvider() { + if (getIntent().hasExtra(ACTION_CONFIGURE_ALWAYS_ON_PROFILE)) { + getIntent().removeExtra(ACTION_CONFIGURE_ALWAYS_ON_PROFILE); + } + startActivityForResult(new Intent(this, ConfigurationWizard.class), CONFIGURE_LEAP); + } @SuppressLint("CommitPrefEdits") private void providerToPreferences(Provider provider) { + //FIXME: figure out why .commit() is used and try refactor that cause, currently runs on UI thread preferences.edit().putBoolean(Constants.PROVIDER_CONFIGURED, true).commit(); preferences.edit().putString(Provider.MAIN_URL, provider.mainUrl().toString()).apply(); preferences.edit().putString(Provider.KEY, provider.definition().toString()).apply(); @@ -235,7 +278,10 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec * Inflates permanent UI elements of the View and contains logic for what * service dependent UI elements to include. */ - private void buildDashboard(boolean hide_and_turn_on_eip) { + //TODO: REFACTOR ME! Consider implementing a manager that handles most of VpnFragment's logic about handling EIP commands. + //This way, we could avoid to create UI elements (like fragment_manager.replace(R.id.servicesCollection, eip_fragment, VpnFragment.TAG); ) + // just to start services and destroy them afterwards + private void buildDashboard(boolean hideAndTurnOnEipOnBoot) { setContentView(R.layout.dashboard); ButterKnife.inject(this); @@ -249,24 +295,43 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec if (provider.hasEIP()) { fragment_manager.removePreviousFragment(VpnFragment.TAG); - eip_fragment = new VpnFragment(); - - if (hide_and_turn_on_eip) { - //TODO: remove line below if not in use anymore... - preferences.edit().remove(Dashboard.START_ON_BOOT).apply(); - //FIXME: always start on Boot? Why do we keep shared preferences then? - Bundle arguments = new Bundle(); - arguments.putBoolean(VpnFragment.START_ON_BOOT, true); - if (eip_fragment != null) eip_fragment.setArguments(arguments); - } - + eip_fragment = prepareEipFragment(hideAndTurnOnEipOnBoot); fragment_manager.replace(R.id.servicesCollection, eip_fragment, VpnFragment.TAG); - if (hide_and_turn_on_eip) { + if (hideAndTurnOnEipOnBoot) { onBackPressed(); } } } + /** + * + * @param hideAndTurnOnEipOnBoot Flag that indicates if system intent android.intent.action.BOOT_COMPLETED + * has caused to start Dashboard + * @return + */ + private VpnFragment prepareEipFragment(boolean hideAndTurnOnEipOnBoot) { + VpnFragment eip_fragment = new VpnFragment(); + + if (hideAndTurnOnEipOnBoot && !isAlwaysOn()) { + preferences.edit().remove(EIP_RESTART_ON_BOOT).apply(); + Bundle arguments = new Bundle(); + arguments.putBoolean(VpnFragment.START_EIP_ON_BOOT, true); + Log.d(TAG, "set START_EIP_ON_BOOT argument for eip_fragment"); + eip_fragment.setArguments(arguments); + + } + return eip_fragment; + } + + /** + * checks if Android's VPN feature 'always-on' is enabled for Bitmask + * @return + */ + private boolean isAlwaysOn() { + return preferences.getBoolean(EIP_IS_ALWAYS_ON, false); + } + + @Override public boolean onPrepareOptionsMenu(Menu menu) { if (provider.allowsRegistration()) { @@ -369,7 +434,7 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec } public static Context getContext() { - return app; + return dashboardContext; } public static Provider getProvider() { return provider; } diff --git a/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java b/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java index 9394e1b1..f9aa2660 100644 --- a/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java +++ b/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java @@ -1,7 +1,15 @@ package se.leap.bitmaskclient; -import android.content.*; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; +import static android.content.Intent.ACTION_BOOT_COMPLETED; +import static se.leap.bitmaskclient.Constants.EIP_IS_ALWAYS_ON; +import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; +import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; public class OnBootReceiver extends BroadcastReceiver { @@ -10,15 +18,33 @@ public class OnBootReceiver extends BroadcastReceiver { // Debug: am broadcast -a android.intent.action.BOOT_COMPLETED @Override public void onReceive(Context context, Intent intent) { + //Lint complains if we're not checking the intent action + if (intent == null || !ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + return; + } preferences = context.getSharedPreferences(Constants.SHARED_PREFERENCES, Context.MODE_PRIVATE); - boolean provider_configured = !preferences.getString(Provider.KEY, "").isEmpty(); - boolean start_on_boot = preferences.getBoolean(Dashboard.START_ON_BOOT, false); - if (provider_configured && start_on_boot) { - Intent dashboard_intent = new Intent(context, Dashboard.class); - dashboard_intent.setAction(Constants.EIP_ACTION_START); - dashboard_intent.putExtra(Dashboard.ON_BOOT, true); - dashboard_intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(dashboard_intent); + boolean providerConfigured = !preferences.getString(PROVIDER_VPN_CERTIFICATE, "").isEmpty(); + boolean startOnBoot = preferences.getBoolean(EIP_RESTART_ON_BOOT, false); + boolean isAlwaysOnConfigured = preferences.getBoolean(EIP_IS_ALWAYS_ON, false); + Log.d("OpenVPN", "OpenVPN onBoot intent received. Provider configured? " + providerConfigured + " Start on boot? " + startOnBoot + " isAlwaysOn feature configured: " + isAlwaysOnConfigured); + if (providerConfigured) { + if (isAlwaysOnConfigured) { + //exit because the app is already setting up the vpn + return; + } + if (startOnBoot) { + Intent dashboard_intent = new Intent(context, Dashboard.class); + dashboard_intent.putExtra(EIP_RESTART_ON_BOOT, true); + dashboard_intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(dashboard_intent); + } + } else { + if (isAlwaysOnConfigured) { + Intent dashboard_intent = new Intent(context, Dashboard.class); + dashboard_intent.putExtra(Dashboard.ACTION_CONFIGURE_ALWAYS_ON_PROFILE, true); + dashboard_intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(dashboard_intent); + } } } } diff --git a/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java b/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java index f787a955..f1a15efd 100644 --- a/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java @@ -16,26 +16,53 @@ */ package se.leap.bitmaskclient; -import android.app.*; -import android.content.*; -import android.os.*; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ResultReceiver; import android.util.Log; -import android.view.*; -import android.widget.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; -import org.jetbrains.annotations.*; +import java.util.Observable; +import java.util.Observer; -import java.util.*; - -import butterknife.*; +import butterknife.ButterKnife; +import butterknife.InjectView; +import butterknife.OnClick; import de.blinkt.openvpn.core.IOpenVPNServiceInternal; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.ProfileManager; import de.blinkt.openvpn.core.VpnStatus; +import mbanje.kurt.fabbutton.FabButton; import se.leap.bitmaskclient.eip.EIP; import se.leap.bitmaskclient.eip.EipStatus; import se.leap.bitmaskclient.eip.VoidVpnService; -import se.leap.bitmaskclient.userstatus.FabButton; + +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_UPDATE; +import static se.leap.bitmaskclient.Constants.EIP_NOTIFICATION; +import static se.leap.bitmaskclient.Constants.EIP_RECEIVER; +import static se.leap.bitmaskclient.Constants.EIP_REQUEST; +import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; +import static se.leap.bitmaskclient.Constants.PROVIDER_ALLOWED_REGISTERED; +import static se.leap.bitmaskclient.Constants.PROVIDER_ALLOW_ANONYMOUS; +import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; public class VpnFragment extends Fragment implements Observer { @@ -43,14 +70,14 @@ public class VpnFragment extends Fragment implements Observer { public static final String IS_PENDING = TAG + ".is_pending"; protected static final String IS_CONNECTED = TAG + ".is_connected"; - public static final String START_ON_BOOT = "start on boot"; + public static final String START_EIP_ON_BOOT = "start on boot"; @InjectView(R.id.vpn_status_image) FabButton vpn_status_image; @InjectView(R.id.vpn_main_button) Button main_button; - private static Dashboard dashboard; + private Dashboard dashboard; private static EIPReceiver eip_receiver; private static EipStatus eip_status; private boolean wants_to_connect; @@ -96,26 +123,19 @@ public class VpnFragment extends Fragment implements Observer { ButterKnife.inject(this, view); Bundle arguments = getArguments(); - if (arguments != null && arguments.containsKey(START_ON_BOOT) && arguments.getBoolean(START_ON_BOOT)) + if (arguments != null && arguments.containsKey(START_EIP_ON_BOOT) && arguments.getBoolean(START_EIP_ON_BOOT)) { startEipFromScratch(); - if (savedInstanceState != null) restoreState(savedInstanceState); + } return view; } - private void restoreState(@NotNull Bundle savedInstanceState) { - if (savedInstanceState.getBoolean(IS_PENDING)) - eip_status.setConnecting(); - else if (savedInstanceState.getBoolean(IS_CONNECTED)) - eip_status.setConnectedOrDisconnected(); - } - @Override public void onResume() { super.onResume(); //FIXME: avoid race conditions while checking certificate an logging in at about the same time //eipCommand(Constants.EIP_ACTION_CHECK_CERT_VALIDITY); - handleNewState(eip_status); + handleNewState(); bindOpenVpnService(); } @@ -127,14 +147,12 @@ public class VpnFragment extends Fragment implements Observer { @Override public void onSaveInstanceState(Bundle outState) { - outState.putBoolean(IS_PENDING, eip_status.isConnecting()); outState.putBoolean(IS_CONNECTED, eip_status.isConnected()); super.onSaveInstanceState(outState); } - private void saveStatus() { - boolean is_on = eip_status.isConnected() || eip_status.isConnecting(); - Dashboard.preferences.edit().putBoolean(Dashboard.START_ON_BOOT, is_on).commit(); + private void saveStatus(boolean restartOnBoot) { + Dashboard.preferences.edit().putBoolean(EIP_RESTART_ON_BOOT, restartOnBoot).apply(); } @OnClick(R.id.vpn_main_button) @@ -143,8 +161,6 @@ public class VpnFragment extends Fragment implements Observer { handleSwitchOff(); else handleSwitchOn(); - - saveStatus(); } private void handleSwitchOn() { @@ -153,7 +169,6 @@ public class VpnFragment extends Fragment implements Observer { else if (canLogInToStartEIP()) { wants_to_connect = true; Bundle bundle = new Bundle(); - bundle.putBoolean(IS_PENDING, true); dashboard.sessionDialog(bundle); } else { Log.d(TAG, "WHAT IS GOING ON HERE?!"); @@ -163,13 +178,13 @@ public class VpnFragment extends Fragment implements Observer { } private boolean canStartEIP() { - boolean certificateExists = !Dashboard.preferences.getString(Constants.PROVIDER_VPN_CERTIFICATE, "").isEmpty(); - boolean isAllowedAnon = Dashboard.preferences.getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, false); + boolean certificateExists = !Dashboard.preferences.getString(PROVIDER_VPN_CERTIFICATE, "").isEmpty(); + boolean isAllowedAnon = Dashboard.preferences.getBoolean(PROVIDER_ALLOW_ANONYMOUS, false); return (isAllowedAnon || certificateExists) && !eip_status.isConnected() && !eip_status.isConnecting(); } private boolean canLogInToStartEIP() { - boolean isAllowedRegistered = Dashboard.preferences.getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, false); + boolean isAllowedRegistered = Dashboard.preferences.getBoolean(PROVIDER_ALLOWED_REGISTERED, false); boolean isLoggedIn = !LeapSRPSession.getToken().isEmpty(); return isAllowedRegistered && !isLoggedIn && !eip_status.isConnecting() && !eip_status.isConnected(); } @@ -179,8 +194,9 @@ public class VpnFragment extends Fragment implements Observer { askPendingStartCancellation(); } else if (eip_status.isConnected()) { askToStopEIP(); - } else + } else { updateIcon(); + } } private void askPendingStartCancellation() { @@ -203,25 +219,30 @@ public class VpnFragment extends Fragment implements Observer { public void startEipFromScratch() { wants_to_connect = false; - eip_status.setConnecting(); - - saveStatus(); - eipCommand(Constants.EIP_ACTION_START); + saveStatus(true); + eipCommand(EIP_ACTION_START); } private void stop() { - if (eip_status.isConnecting()) - VoidVpnService.stop(); + saveStatus(false); + if (eip_status.isBlockingVpnEstablished()) { + stopBlockingVpn(); + } disconnect(); } + private void stopBlockingVpn() { + Log.d(TAG, "stop VoidVpn!"); + Intent stopVoidVpnIntent = new Intent(dashboard, VoidVpnService.class); + stopVoidVpnIntent.setAction(EIP_ACTION_STOP_BLOCKING_VPN); + dashboard.startService(stopVoidVpnIntent); + } + private void disconnect() { - eip_status.setDisconnecting(); ProfileManager.setConntectedVpnProfileDisconnected(dashboard); if (mService != null) { try { mService.stopVPN(false); - eip_status.setConnectedOrDisconnected(); } catch (RemoteException e) { VpnStatus.logException(e); } @@ -229,7 +250,8 @@ public class VpnFragment extends Fragment implements Observer { } protected void stopEipIfPossible() { - eipCommand(Constants.EIP_ACTION_STOP); + //FIXME: no need to start a service here! + eipCommand(EIP_ACTION_STOP); } private void downloadEIPServiceConfig() { @@ -257,7 +279,7 @@ public class VpnFragment extends Fragment implements Observer { } protected void updateEipService() { - eipCommand(Constants.EIP_ACTION_UPDATE); + eipCommand(EIP_ACTION_UPDATE); } /** @@ -270,7 +292,7 @@ public class VpnFragment extends Fragment implements Observer { // TODO validate "action"...how do we get the list of intent-filters for a class via Android API? Intent vpn_intent = new Intent(dashboard.getApplicationContext(), EIP.class); vpn_intent.setAction(action); - vpn_intent.putExtra(Constants.EIP_RECEIVER, eip_receiver); + vpn_intent.putExtra(EIP_RECEIVER, eip_receiver); dashboard.startService(vpn_intent); } @@ -278,36 +300,33 @@ public class VpnFragment extends Fragment implements Observer { public void update(Observable observable, Object data) { if (observable instanceof EipStatus) { eip_status = (EipStatus) observable; - final EipStatus eip_status = (EipStatus) observable; dashboard.runOnUiThread(new Runnable() { @Override public void run() { - handleNewState(eip_status); + handleNewState(); } }); } } - private void handleNewState(EipStatus eip_status) { - Context context = dashboard.getApplicationContext(); - String error = eip_status.lastError(5, context); - - if (!error.isEmpty()) VoidVpnService.stop(); + private void handleNewState() { updateIcon(); updateButton(); } private void updateIcon() { - if (eip_status.isConnected() || eip_status.isConnecting()) { - if(eip_status.isConnecting()) { - vpn_status_image.showProgress(true); - vpn_status_image.setIcon(R.drawable.ic_stat_vpn_empty_halo, R.drawable.ic_stat_vpn_empty_halo); - vpn_status_image.setTag(R.drawable.ic_stat_vpn_empty_halo); - } else { - vpn_status_image.showProgress(false); - vpn_status_image.setIcon(R.drawable.ic_stat_vpn, R.drawable.ic_stat_vpn); - vpn_status_image.setTag(R.drawable.ic_stat_vpn); - } + if (eip_status.isBlocking()) { + vpn_status_image.showProgress(false); + vpn_status_image.setIcon(R.drawable.ic_stat_vpn_blocking, R.drawable.ic_stat_vpn_blocking); + vpn_status_image.setTag(R.drawable.ic_stat_vpn_blocking); + } else if (eip_status.isConnecting()) { + vpn_status_image.showProgress(true); + vpn_status_image.setIcon(R.drawable.ic_stat_vpn_empty_halo, R.drawable.ic_stat_vpn_empty_halo); + vpn_status_image.setTag(R.drawable.ic_stat_vpn_empty_halo); + } else if (eip_status.isConnected()){ + vpn_status_image.showProgress(false); + vpn_status_image.setIcon(R.drawable.ic_stat_vpn, R.drawable.ic_stat_vpn); + vpn_status_image.setTag(R.drawable.ic_stat_vpn); } else { vpn_status_image.setIcon(R.drawable.ic_stat_vpn_offline, R.drawable.ic_stat_vpn_offline); vpn_status_image.setTag(R.drawable.ic_stat_vpn_offline); @@ -316,17 +335,28 @@ public class VpnFragment extends Fragment implements Observer { } private void updateButton() { - if (eip_status.isConnected() || eip_status.isConnecting()) { - if(eip_status.isConnecting()) { - main_button.setText(dashboard.getString(android.R.string.cancel)); - } else { - main_button.setText(dashboard.getString(R.string.vpn_button_turn_off)); - } + if (eip_status.isConnecting()) { + main_button.setText(dashboard.getString(android.R.string.cancel)); + } else if (eip_status.isConnected() || isOpenVpnRunningWithoutNetwork()) { + main_button.setText(dashboard.getString(R.string.vpn_button_turn_off)); } else { main_button.setText(dashboard.getString(R.string.vpn_button_turn_on)); } } + private boolean isOpenVpnRunningWithoutNetwork() { + boolean isRunning = false; + try { + isRunning = eip_status.getLevel() == LEVEL_NONETWORK && + mService.isVpnRunning(); + } catch (Exception e) { + //eat me + e.printStackTrace(); + } + + return isRunning; + } + private void bindOpenVpnService() { Intent intent = new Intent(dashboard, OpenVPNService.class); intent.setAction(OpenVPNService.START_SERVICE); @@ -343,9 +373,9 @@ public class VpnFragment extends Fragment implements Observer { protected void onReceiveResult(int resultCode, Bundle resultData) { super.onReceiveResult(resultCode, resultData); - String request = resultData.getString(Constants.EIP_REQUEST); + String request = resultData.getString(EIP_REQUEST); - if (request.equals(Constants.EIP_ACTION_START)) { + if (request.equals(EIP_ACTION_START)) { switch (resultCode) { case Activity.RESULT_OK: break; @@ -353,7 +383,7 @@ public class VpnFragment extends Fragment implements Observer { break; } - } else if (request.equals(Constants.EIP_ACTION_STOP)) { + } else if (request.equals(EIP_ACTION_STOP)) { switch (resultCode) { case Activity.RESULT_OK: stop(); @@ -361,14 +391,14 @@ public class VpnFragment extends Fragment implements Observer { case Activity.RESULT_CANCELED: break; } - } else if (request.equals(Constants.EIP_NOTIFICATION)) { + } else if (request.equals(EIP_NOTIFICATION)) { switch (resultCode) { case Activity.RESULT_OK: break; case Activity.RESULT_CANCELED: break; } - } else if (request.equals(Constants.EIP_ACTION_CHECK_CERT_VALIDITY)) { + } else if (request.equals(EIP_ACTION_CHECK_CERT_VALIDITY)) { switch (resultCode) { case Activity.RESULT_OK: break; @@ -376,14 +406,14 @@ public class VpnFragment extends Fragment implements Observer { dashboard.downloadVpnCertificate(); break; } - } else if (request.equals(Constants.EIP_ACTION_UPDATE)) { + } else if (request.equals(EIP_ACTION_UPDATE)) { switch (resultCode) { case Activity.RESULT_OK: if (wants_to_connect) startEipFromScratch(); break; case Activity.RESULT_CANCELED: - handleNewState(eip_status); + handleNewState(); break; } } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java index 0b35dc3d..a84ab941 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java @@ -19,12 +19,25 @@ package se.leap.bitmaskclient.eip; import android.app.*; import android.content.*; import android.os.*; +import android.util.Log; import org.json.*; import de.blinkt.openvpn.*; import se.leap.bitmaskclient.*; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_IS_RUNNING; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_ALWAYS_ON_EIP; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_UPDATE; +import static se.leap.bitmaskclient.Constants.EIP_RECEIVER; +import static se.leap.bitmaskclient.Constants.EIP_REQUEST; +import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; + /** * EIP is the abstract base class for interacting with and managing the Encrypted * Internet Proxy connection. Connections are started, stopped, and queried through @@ -55,9 +68,8 @@ public final class EIP extends IntentService { @Override public void onCreate() { super.onCreate(); - context = getApplicationContext(); - preferences = getSharedPreferences(Constants.SHARED_PREFERENCES, MODE_PRIVATE); + preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); eip_definition = eipDefinitionFromPreferences(); if (gateways_manager.isEmpty()) gatewaysFromPreferences(); @@ -66,17 +78,19 @@ public final class EIP extends IntentService { @Override protected void onHandleIntent(Intent intent) { String action = intent.getAction(); - mReceiver = intent.getParcelableExtra(Constants.EIP_RECEIVER); + mReceiver = intent.getParcelableExtra(EIP_RECEIVER); - if (action.equals(Constants.EIP_ACTION_START)) + if (action.equals(EIP_ACTION_START)) startEIP(); - else if (action.equals(Constants.EIP_ACTION_STOP)) + else if (action.equals(EIP_ACTION_START_ALWAYS_ON_EIP)) + startAlwaysOnEIP(); + else if (action.equals(EIP_ACTION_STOP)) stopEIP(); - else if (action.equals(Constants.EIP_ACTION_IS_RUNNING)) + else if (action.equals(EIP_ACTION_IS_RUNNING)) isRunning(); - else if (action.equals(Constants.EIP_ACTION_UPDATE)) + else if (action.equals(EIP_ACTION_UPDATE)) updateEIPService(); - else if (action.equals(Constants.EIP_ACTION_CHECK_CERT_VALIDITY)) + else if (action.equals(EIP_ACTION_CHECK_CERT_VALIDITY)) checkCertValidity(); } @@ -88,15 +102,38 @@ public final class EIP extends IntentService { private void startEIP() { if (gateways_manager.isEmpty()) updateEIPService(); - earlyRoutes(); + if (!EipStatus.getInstance().isBlockingVpnEstablished()) { + earlyRoutes(); + } gateway = gateways_manager.select(); if (gateway != null && gateway.getProfile() != null) { mReceiver = VpnFragment.getReceiver(); launchActiveGateway(); - tellToReceiver(Constants.EIP_ACTION_START, Activity.RESULT_OK); + tellToReceiver(EIP_ACTION_START, Activity.RESULT_OK); } else - tellToReceiver(Constants.EIP_ACTION_START, Activity.RESULT_CANCELED); + tellToReceiver(EIP_ACTION_START, Activity.RESULT_CANCELED); + } + + /** + * Tries to start the last used vpn profile when the OS was rebooted and always-on-VPN is enabled. + * The {@link OnBootReceiver} will care if there is no profile. + */ + private void startAlwaysOnEIP() { + Log.d(TAG, "startAlwaysOnEIP vpn"); + + if (gateways_manager.isEmpty()) + updateEIPService(); + + gateway = gateways_manager.select(); + + if (gateway != null && gateway.getProfile() != null) { + //mReceiver = VpnFragment.getReceiver(); + Log.d(TAG, "startAlwaysOnEIP eip launch avtive gateway vpn"); + launchActiveGateway(); + } else { + Log.d(TAG, "startAlwaysOnEIP no active profile available!"); + } } /** @@ -124,7 +161,7 @@ public final class EIP extends IntentService { if (eip_status.isConnected() || eip_status.isConnecting()) result_code = Activity.RESULT_OK; - tellToReceiver(Constants.EIP_ACTION_STOP, result_code); + tellToReceiver(EIP_ACTION_STOP, result_code); } /** @@ -137,7 +174,7 @@ public final class EIP extends IntentService { int resultCode = (eip_status.isConnected()) ? Activity.RESULT_OK : Activity.RESULT_CANCELED; - tellToReceiver(Constants.EIP_ACTION_IS_RUNNING, resultCode); + tellToReceiver(EIP_ACTION_IS_RUNNING, resultCode); } /** @@ -148,13 +185,13 @@ public final class EIP extends IntentService { eip_definition = eipDefinitionFromPreferences(); if (eip_definition.length() > 0) updateGateways(); - tellToReceiver(Constants.EIP_ACTION_UPDATE, Activity.RESULT_OK); + tellToReceiver(EIP_ACTION_UPDATE, Activity.RESULT_OK); } private JSONObject eipDefinitionFromPreferences() { JSONObject result = new JSONObject(); try { - String eip_definition_string = preferences.getString(Constants.PROVIDER_KEY, ""); + String eip_definition_string = preferences.getString(PROVIDER_KEY, ""); if (!eip_definition_string.isEmpty()) { result = new JSONObject(eip_definition_string); } @@ -184,17 +221,17 @@ public final class EIP extends IntentService { } private void checkCertValidity() { - VpnCertificateValidator validator = new VpnCertificateValidator(preferences.getString(Constants.PROVIDER_VPN_CERTIFICATE, "")); + VpnCertificateValidator validator = new VpnCertificateValidator(preferences.getString(PROVIDER_VPN_CERTIFICATE, "")); int resultCode = validator.isValid() ? Activity.RESULT_OK : Activity.RESULT_CANCELED; - tellToReceiver(Constants.EIP_ACTION_CHECK_CERT_VALIDITY, resultCode); + tellToReceiver(EIP_ACTION_CHECK_CERT_VALIDITY, resultCode); } private void tellToReceiver(String action, int resultCode) { if (mReceiver != null) { Bundle resultData = new Bundle(); - resultData.putString(Constants.EIP_REQUEST, action); + resultData.putString(EIP_REQUEST, action); mReceiver.send(resultCode, resultData); } } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java index 501543b8..ddf152d2 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java @@ -17,19 +17,37 @@ package se.leap.bitmaskclient.eip; import android.content.*; +import android.os.AsyncTask; +import android.support.annotation.VisibleForTesting; import java.util.*; import de.blinkt.openvpn.core.*; +/** + * EipStatus is a Singleton that represents a reduced set of a vpn's ConnectionStatus. + * EipStatus changes it's state (EipLevel) when ConnectionStatus gets updated by OpenVpnService or + * by VoidVpnService. + */ public class EipStatus extends Observable implements VpnStatus.StateListener { public static String TAG = EipStatus.class.getSimpleName(); private static EipStatus current_status; + public enum EipLevel { + CONNECTING, + DISCONNECTING, + CONNECTED, + DISCONNECTED, + BLOCKING, + UNKNOWN + } - private static ConnectionStatus level = ConnectionStatus.LEVEL_NOTCONNECTED; - private static boolean - wants_to_disconnect = false, - is_connecting = false; + /** + * vpn_level holds the connection status of the openvpn vpn and the traffic blocking + * void vpn. LEVEL_BLOCKING is set when the latter vpn is up. All other states are set by + * openvpn. + */ + private ConnectionStatus vpn_level = ConnectionStatus.LEVEL_NOTCONNECTED; + private static EipLevel current_eip_level = EipLevel.DISCONNECTED; int last_error_line = 0; private String state, log_message; @@ -48,64 +66,137 @@ public class EipStatus extends Observable implements VpnStatus.StateListener { @Override public void updateState(final String state, final String logmessage, final int localizedResId, final ConnectionStatus level) { - updateStatus(state, logmessage, localizedResId, level); - if (isConnected() || isDisconnected() || wantsToDisconnect()) { - setConnectedOrDisconnected(); - } else - setConnecting(); + ConnectionStatus tmp = current_status.getLevel(); + current_status = getInstance(); + current_status.setState(state); + current_status.setLogMessage(logmessage); + current_status.setLocalizedResId(localizedResId); + current_status.setLevel(level); + current_status.setEipLevel(level); + if (tmp != current_status.getLevel()) { + current_status.setChanged(); + current_status.notifyObservers(); + } } @Override public void setConnectedVPN(String uuid) { } - private void updateStatus(final String state, final String logmessage, final int localizedResId, final ConnectionStatus level) { - current_status = getInstance(); - current_status.setState(state); - current_status.setLogMessage(logmessage); - current_status.setLocalizedResId(localizedResId); - current_status.setLevel(level); - current_status.setChanged(); + + private void setEipLevel(ConnectionStatus level) { + switch (level) { + case LEVEL_CONNECTED: + current_eip_level = EipLevel.CONNECTED; + break; + case LEVEL_VPNPAUSED: + throw new IllegalStateException("Ics-Openvpn's VPNPAUSED state is not supported by Bitmask"); + case LEVEL_CONNECTING_SERVER_REPLIED: + case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: + case LEVEL_WAITING_FOR_USER_INPUT: + case LEVEL_START: + current_eip_level = EipLevel.CONNECTING; + break; + case LEVEL_AUTH_FAILED: + case LEVEL_NOTCONNECTED: + current_eip_level = EipLevel.DISCONNECTED; + break; + case LEVEL_NONETWORK: + case LEVEL_BLOCKING: + setEipLevelWithDelay(level); + break; + case UNKNOWN_LEVEL: + current_eip_level = EipLevel.UNKNOWN; //?? + break; + } } - public boolean wantsToDisconnect() { - return wants_to_disconnect; + @VisibleForTesting + EipLevel getEipLevel() { + return current_eip_level; } - public boolean isConnecting() { - return is_connecting; + /** + * This is a debouncing method ignoring states that are valid for less than a second. + * This way flickering UI changes can be avoided. + * + * @param futureLevel + */ + private void setEipLevelWithDelay(ConnectionStatus futureLevel) { + new DelayTask(current_status.getLevel(), futureLevel).execute(); } - public boolean isConnected() { - return level == ConnectionStatus.LEVEL_CONNECTED; + private static class DelayTask extends AsyncTask<Void, Void, Void> { + + private final ConnectionStatus currentLevel; + private final ConnectionStatus futureLevel; + + public DelayTask(ConnectionStatus currentLevel, ConnectionStatus futureLevel) { + this.currentLevel = currentLevel; + this.futureLevel = futureLevel; + } + protected Void doInBackground(Void... levels) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.interrupted(); + } + return null; + } + + protected void onPostExecute(Void result) {; + if (currentLevel == current_status.getLevel()) { + switch (futureLevel) { + case LEVEL_NONETWORK: + current_eip_level = EipLevel.DISCONNECTED; + break; + case LEVEL_BLOCKING: + current_eip_level = EipLevel.BLOCKING; + break; + default: + break; + } + current_status.setChanged(); + current_status.notifyObservers(); + } + } } - public boolean isDisconnected() { - return level == ConnectionStatus.LEVEL_NOTCONNECTED; + public boolean isConnecting() { + return current_eip_level == EipLevel.CONNECTING; } - public boolean isPaused() { - return level == ConnectionStatus.LEVEL_VPNPAUSED; + public boolean isConnected() { + return current_eip_level == EipLevel.CONNECTED; } - public void setConnecting() { - is_connecting = true; + /** + * @return true if current_eip_level is for at least a second {@link EipLevel#BLOCKING}. + * See {@link #setEipLevelWithDelay(ConnectionStatus)}. + */ + public boolean isBlocking() { + return current_eip_level == EipLevel.BLOCKING; + } - wants_to_disconnect = false; - current_status.setChanged(); - current_status.notifyObservers(); + /** + * + * @return true immediately after traffic blocking VoidVpn was established. + */ + public boolean isBlockingVpnEstablished() { + return vpn_level == ConnectionStatus.LEVEL_BLOCKING; } - public void setConnectedOrDisconnected() { - is_connecting = false; - wants_to_disconnect = false; - current_status.setChanged(); - current_status.notifyObservers(); + public boolean isDisconnected() { + return current_eip_level == EipLevel.DISCONNECTED; } - public void setDisconnecting() { - wants_to_disconnect = true; - is_connecting = false; + /** + * ics-openvpn's paused state is not implemented yet + * @return + */ + @Deprecated + public boolean isPaused() { + return vpn_level == ConnectionStatus.LEVEL_VPNPAUSED; } public String getState() { @@ -121,7 +212,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener { } public ConnectionStatus getLevel() { - return level; + return vpn_level; } private void setState(String state) { @@ -137,7 +228,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener { } private void setLevel(ConnectionStatus level) { - EipStatus.level = level; + this.vpn_level = level; } public boolean errorInLast(int lines, Context context) { @@ -169,7 +260,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener { @Override public String toString() { - return "State: " + state + " Level: " + level.toString(); + return "State: " + state + " Level: " + vpn_level.toString(); } } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnLauncher.java b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnLauncher.java index b1aab79c..9a3c8f85 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnLauncher.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnLauncher.java @@ -1,11 +1,12 @@ package se.leap.bitmaskclient.eip; -import android.app.*; -import android.content.*; -import android.net.*; -import android.os.*; +import android.app.Activity; +import android.content.Intent; +import android.net.VpnService; +import android.os.Build; +import android.os.Bundle; -import se.leap.bitmaskclient.Constants; +import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_BLOCKING_VPN; public class VoidVpnLauncher extends Activity { @@ -30,8 +31,12 @@ public class VoidVpnLauncher extends Activity { if (requestCode == VPN_USER_PERMISSION) { if (resultCode == RESULT_OK) { Intent void_vpn_service = new Intent(getApplicationContext(), VoidVpnService.class); - void_vpn_service.setAction(Constants.EIP_ACTION_BLOCK_VPN_PROFILE); - startService(void_vpn_service); + void_vpn_service.setAction(EIP_ACTION_START_BLOCKING_VPN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(void_vpn_service); + } else { + startService(void_vpn_service); + } } } finish(); 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 ff375553..792de2cb 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java @@ -1,43 +1,88 @@ package se.leap.bitmaskclient.eip; -import android.content.*; -import android.net.*; -import android.os.*; +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.*; +import java.io.IOException; +import java.util.Observable; +import java.util.Observer; -import se.leap.bitmaskclient.Constants; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.Dashboard; +import se.leap.bitmaskclient.R; -public class VoidVpnService extends VpnService { +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; +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 { static final String TAG = VoidVpnService.class.getSimpleName(); static ParcelFileDescriptor fd; - static Thread thread; + private final int ALWAYS_ON_MIN_API_LEVEL = Build.VERSION_CODES.N; + 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; + + @Override + public void onCreate() { + super.onCreate(); + notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + compatNotificationManager = NotificationManagerCompat.from(this); + eipStatus = EipStatus.getInstance(); + eipStatus.addObserver(this); + } @Override public int onStartCommand(Intent intent, int flags, int startId) { String action = intent != null ? intent.getAction() : ""; - if (action == Constants.EIP_ACTION_BLOCK_VPN_PROFILE) { + if (action.equals(EIP_ACTION_START_BLOCKING_VPN)) { + thread = new Thread(new Runnable() { + public void run() { + establishBlockingVpn(); + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putBoolean(EIP_IS_ALWAYS_ON, false).commit(); + Log.d(TAG, "start blocking vpn profile - always on = false"); + } + }); + thread.run(); + } else if (action.equals("android.net.VpnService") && Build.VERSION.SDK_INT >= ALWAYS_ON_MIN_API_LEVEL) { + //only always-on feature triggers this thread = new Thread(new Runnable() { public void run() { - Builder builder = new Builder(); - builder.setSession("Blocking until running"); - builder.addAddress("10.42.0.8", 16); - builder.addRoute("0.0.0.0", 1); - builder.addRoute("192.168.1.0", 24); - builder.addDnsServer("10.42.0.1"); - try { - fd = builder.establish(); - - } catch (Exception e) { - e.printStackTrace(); - } + establishBlockingVpn(); + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putBoolean(EIP_IS_ALWAYS_ON, true).commit(); + requestVpnWithLastSelectedProfile(); + Log.d(TAG, "start blocking vpn profile - always on = true"); } }); thread.run(); + } else if (action.equals(EIP_ACTION_STOP_BLOCKING_VPN)) { + stop(); } - return 0; + return START_STICKY; } @Override @@ -46,9 +91,27 @@ public class VoidVpnService extends VpnService { closeFd(); } - public static void stop() { - if (thread != null) + @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(); + if (thread != null) { thread.interrupt(); + } closeFd(); } @@ -64,4 +127,134 @@ public class VoidVpnService extends VpnService { e.printStackTrace(); } } + + private Builder prepareBlockingVpnProfile() { + Builder builder = new Builder(); + builder.setSession("Blocking until running"); + builder.addRoute("0.0.0.0", 1); + builder.addRoute("192.168.1.0", 24); + builder.addDnsServer("10.42.0.1"); + builder.addAddress("10.42.0.8", 16); + return builder; + + } + + private void establishBlockingVpn() { + try { + VpnStatus.logInfo(getString(R.string.void_vpn_establish)); + VpnStatus.updateStateString(STATE_ESTABLISH, "", + R.string.void_vpn_establish, ConnectionStatus.LEVEL_BLOCKING); + Builder builder = prepareBlockingVpnProfile(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + builder.addDisallowedApplication(getPackageName()); + } + + fd = builder.establish(); + } catch (Exception e) { + // Catch any exception + e.printStackTrace(); + VpnStatus.logError(R.string.void_vpn_error_establish); + } + } + + private void requestVpnWithLastSelectedProfile() { + Intent startEIP = new Intent(getApplicationContext(), EIP.class); + startEIP.setAction(EIP_ACTION_START_ALWAYS_ON_EIP); + getApplicationContext().startService(startEIP); + } + + @Override + public void update(Observable observable, Object arg) { + if (observable instanceof EipStatus) { + eipStatus = (EipStatus) observable; + } + + if (thread == null) { + return; + } + + if (eipStatus.isBlockingVpnEstablished()) { + showNotification(getString(eipStatus.getLocalizedResId()), + getString(eipStatus.getLocalizedResId()), eipStatus.getLevel()); + } else { + stopNotifications(); + } + } + + 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); + 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); + } + + //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/java/se/leap/bitmaskclient/userstatus/SessionDialog.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java index d124c395..61349490 100644 --- a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java +++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java @@ -60,8 +60,6 @@ public class SessionDialog extends DialogFragment { @InjectView(R.id.password_entered) EditText password_field; - private static boolean is_eip_pending = false; - public static SessionDialog getInstance(Provider provider, Bundle arguments) { SessionDialog dialog = new SessionDialog(); if (provider.getName().equalsIgnoreCase("riseup")) { @@ -100,7 +98,6 @@ public class SessionDialog extends DialogFragment { .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); - interface_with_Dashboard.cancelLoginOrSignup(); } }) .setNeutralButton(R.string.signup_button, new DialogInterface.OnClickListener() { @@ -116,7 +113,6 @@ public class SessionDialog extends DialogFragment { } private void setUp(Bundle arguments) { - is_eip_pending = arguments.getBoolean(VpnFragment.IS_PENDING, false); if (arguments.containsKey(ERRORS.PASSWORD_INVALID_LENGTH.toString())) password_field.setError(getString(R.string.error_not_valid_password_user_message)); else if (arguments.containsKey(ERRORS.RISEUP_WARNING.toString())) { @@ -160,7 +156,6 @@ public class SessionDialog extends DialogFragment { public void signUp(String username, String password); - public void cancelLoginOrSignup(); } SessionDialogInterface interface_with_Dashboard; @@ -177,10 +172,4 @@ public class SessionDialog extends DialogFragment { } } - @Override - public void onCancel(DialogInterface dialog) { - super.onCancel(dialog); - if (is_eip_pending) - interface_with_Dashboard.cancelLoginOrSignup(); - } } diff --git a/app/src/main/java/se/leap/bitmaskclient/userstatus/UserStatusFragment.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/UserStatusFragment.java index 20189904..14323f8e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/userstatus/UserStatusFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/UserStatusFragment.java @@ -24,7 +24,6 @@ import se.leap.bitmaskclient.ProviderAPI; import se.leap.bitmaskclient.ProviderAPICommand; import se.leap.bitmaskclient.ProviderAPIResultReceiver; import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.eip.EipStatus; public class UserStatusFragment extends Fragment implements Observer, SessionDialog.SessionDialogInterface { @@ -169,7 +168,7 @@ public class UserStatusFragment extends Fragment implements Observer, SessionDia } public void cancelLoginOrSignup() { - EipStatus.getInstance().setConnectedOrDisconnected(); + //EipStatus.getInstance().setConnectedOrDisconnected(); } private Bundle bundlePassword(String password) { |