From 6b032b751324a30120cfaabe88940f95171df11f Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 29 Dec 2020 00:54:08 +0100 Subject: new year cleanup: restructure messy project --- app/src/main/AndroidManifest.xml | 24 +- app/src/main/java/de/blinkt/openvpn/LaunchVPN.java | 6 +- .../main/java/de/blinkt/openvpn/VpnProfile.java | 4 +- .../de/blinkt/openvpn/core/OpenVPNService.java | 4 +- .../java/de/blinkt/openvpn/core/VpnStatus.java | 4 +- .../AbstractProviderDetailActivity.java | 107 --- .../bitmaskclient/AddProviderBaseActivity.java | 124 --- .../java/se/leap/bitmaskclient/BitmaskApp.java | 94 -- .../se/leap/bitmaskclient/BitmaskTileService.java | 101 --- .../se/leap/bitmaskclient/ButterKnifeActivity.java | 46 - .../bitmaskclient/ConfigWizardBaseActivity.java | 287 ------- .../main/java/se/leap/bitmaskclient/Constants.java | 166 ---- .../bitmaskclient/CustomProviderSetupActivity.java | 116 --- .../java/se/leap/bitmaskclient/DefaultedURL.java | 48 -- .../java/se/leap/bitmaskclient/DnsResolver.java | 37 - .../leap/bitmaskclient/DrawerSettingsAdapter.java | 0 .../java/se/leap/bitmaskclient/EipFragment.java | 600 ------------- .../se/leap/bitmaskclient/EipSetupListener.java | 12 - .../se/leap/bitmaskclient/EipSetupObserver.java | 375 -------- .../se/leap/bitmaskclient/FeatureVersionCode.java | 6 - .../bitmaskclient/FragmentManagerEnhanced.java | 58 -- .../java/se/leap/bitmaskclient/LeapSRPSession.java | 361 -------- .../java/se/leap/bitmaskclient/LoginActivity.java | 31 - .../java/se/leap/bitmaskclient/MainActivity.java | 363 -------- .../bitmaskclient/MainActivityErrorDialog.java | 171 ---- .../leap/bitmaskclient/OkHttpClientGenerator.java | 182 ---- .../java/se/leap/bitmaskclient/OnBootReceiver.java | 56 -- .../main/java/se/leap/bitmaskclient/PRNGFixes.java | 330 ------- .../main/java/se/leap/bitmaskclient/Provider.java | 593 ------------- .../java/se/leap/bitmaskclient/ProviderAPI.java | 126 --- .../se/leap/bitmaskclient/ProviderAPICommand.java | 84 -- .../leap/bitmaskclient/ProviderApiConnector.java | 98 --- .../leap/bitmaskclient/ProviderApiManagerBase.java | 939 -------------------- .../ProviderApiSetupBroadcastReceiver.java | 81 -- .../ProviderCredentialsBaseActivity.java | 475 ----------- .../se/leap/bitmaskclient/ProviderListAdapter.java | 19 - .../bitmaskclient/ProviderListBaseActivity.java | 186 ---- .../se/leap/bitmaskclient/ProviderManager.java | 270 ------ .../se/leap/bitmaskclient/ProviderObservable.java | 39 - .../se/leap/bitmaskclient/ProviderRenderer.java | 55 -- .../bitmaskclient/ProviderRendererBuilder.java | 19 - .../bitmaskclient/ProviderSetupBaseActivity.java | 231 ----- .../bitmaskclient/ProviderSetupFailedDialog.java | 186 ---- .../leap/bitmaskclient/ProviderSetupInterface.java | 41 - .../java/se/leap/bitmaskclient/SignupActivity.java | 54 -- .../java/se/leap/bitmaskclient/SrpCredentials.java | 26 - .../se/leap/bitmaskclient/SrpRegistrationData.java | 42 - .../java/se/leap/bitmaskclient/StartActivity.java | 231 ----- .../leap/bitmaskclient/TLSCompatSocketFactory.java | 158 ---- .../leap/bitmaskclient/VpnNotificationManager.java | 353 -------- .../se/leap/bitmaskclient/base/BitmaskApp.java | 98 +++ .../bitmaskclient/base/BitmaskTileService.java | 104 +++ .../base/FragmentManagerEnhanced.java | 58 ++ .../se/leap/bitmaskclient/base/MainActivity.java | 372 ++++++++ .../se/leap/bitmaskclient/base/OnBootReceiver.java | 54 ++ .../se/leap/bitmaskclient/base/StartActivity.java | 236 +++++ .../base/drawer/NavigationDrawerFragment.java | 674 +++++++++++++++ .../base/fragments/AboutFragment.java | 67 ++ .../base/fragments/AlwaysOnDialog.java | 76 ++ .../base/fragments/DonationReminderDialog.java | 120 +++ .../bitmaskclient/base/fragments/EipFragment.java | 608 +++++++++++++ .../base/fragments/ExcludeAppsFragment.java | 335 ++++++++ .../bitmaskclient/base/fragments/LogFragment.java | 587 +++++++++++++ .../base/fragments/MainActivityErrorDialog.java | 174 ++++ .../base/fragments/TetheringDialog.java | 258 ++++++ .../leap/bitmaskclient/base/models/Constants.java | 168 ++++ .../bitmaskclient/base/models/DefaultedURL.java | 48 ++ .../base/models/FeatureVersionCode.java | 6 + .../leap/bitmaskclient/base/models/Provider.java | 593 +++++++++++++ .../base/models/ProviderObservable.java | 39 + .../java/se/leap/bitmaskclient/base/utils/Cmd.java | 91 ++ .../bitmaskclient/base/utils/ConfigHelper.java | 230 +++++ .../leap/bitmaskclient/base/utils/DateHelper.java | 29 + .../leap/bitmaskclient/base/utils/FileHelper.java | 46 + .../leap/bitmaskclient/base/utils/IPAddress.java | 102 +++ .../base/utils/InputStreamHelper.java | 21 + .../bitmaskclient/base/utils/KeyStoreHelper.java | 78 ++ .../leap/bitmaskclient/base/utils/PRNGFixes.java | 330 +++++++ .../bitmaskclient/base/utils/PreferenceHelper.java | 273 ++++++ .../leap/bitmaskclient/base/utils/ViewHelper.java | 17 + .../base/views/IconCheckboxEntry.java | 86 ++ .../bitmaskclient/base/views/IconSwitchEntry.java | 116 +++ .../bitmaskclient/base/views/IconTextEntry.java | 106 +++ .../bitmaskclient/base/views/IconTextView.java | 96 +++ .../base/views/ProviderHeaderView.java | 109 +++ .../bitmaskclient/base/views/VpnStateImage.java | 99 +++ .../drawer/NavigationDrawerFragment.java | 674 --------------- .../main/java/se/leap/bitmaskclient/eip/EIP.java | 44 +- .../java/se/leap/bitmaskclient/eip/EipCommand.java | 16 +- .../leap/bitmaskclient/eip/EipResultBroadcast.java | 8 +- .../leap/bitmaskclient/eip/EipSetupListener.java | 12 + .../leap/bitmaskclient/eip/EipSetupObserver.java | 374 ++++++++ .../java/se/leap/bitmaskclient/eip/Gateway.java | 20 +- .../se/leap/bitmaskclient/eip/GatewaySelector.java | 2 +- .../se/leap/bitmaskclient/eip/GatewaysManager.java | 12 +- .../se/leap/bitmaskclient/eip/VoidVpnLauncher.java | 2 +- .../se/leap/bitmaskclient/eip/VoidVpnService.java | 15 +- .../bitmaskclient/eip/VpnCertificateValidator.java | 2 +- .../leap/bitmaskclient/eip/VpnConfigGenerator.java | 22 +- .../bitmaskclient/eip/VpnNotificationManager.java | 355 ++++++++ .../bitmaskclient/firewall/FirewallManager.java | 2 +- .../bitmaskclient/firewall/SetupTetheringTask.java | 2 +- .../firewall/ShutdownIPv6FirewallTask.java | 2 +- .../firewall/ShutdownTetheringTask.java | 2 +- .../firewall/StartIPv6FirewallTask.java | 2 +- .../bitmaskclient/fragments/AboutFragment.java | 67 -- .../bitmaskclient/fragments/AlwaysOnDialog.java | 76 -- .../fragments/DonationReminderDialog.java | 120 --- .../fragments/ExcludeAppsFragment.java | 335 -------- .../leap/bitmaskclient/fragments/LogFragment.java | 587 ------------- .../bitmaskclient/fragments/TetheringDialog.java | 258 ------ .../bitmaskclient/providersetup/ProviderAPI.java | 128 +++ .../providersetup/ProviderAPICommand.java | 86 ++ .../providersetup/ProviderApiConnector.java | 98 +++ .../providersetup/ProviderApiManagerBase.java | 946 +++++++++++++++++++++ .../ProviderApiSetupBroadcastReceiver.java | 84 ++ .../providersetup/ProviderListAdapter.java | 21 + .../providersetup/ProviderManager.java | 272 ++++++ .../providersetup/ProviderRenderer.java | 57 ++ .../providersetup/ProviderRendererBuilder.java | 21 + .../providersetup/ProviderSetupFailedDialog.java | 189 ++++ .../providersetup/ProviderSetupInterface.java | 43 + .../activities/AbstractProviderDetailActivity.java | 109 +++ .../activities/AddProviderBaseActivity.java | 125 +++ .../activities/ButterKnifeActivity.java | 46 + .../activities/ConfigWizardBaseActivity.java | 289 +++++++ .../activities/CustomProviderSetupActivity.java | 121 +++ .../providersetup/activities/LoginActivity.java | 32 + .../ProviderCredentialsBaseActivity.java | 479 +++++++++++ .../activities/ProviderListBaseActivity.java | 193 +++++ .../activities/ProviderSetupBaseActivity.java | 240 ++++++ .../providersetup/activities/SignupActivity.java | 55 ++ .../providersetup/connectivity/DnsResolver.java | 39 + .../connectivity/OkHttpClientGenerator.java | 182 ++++ .../connectivity/TLSCompatSocketFactory.java | 158 ++++ .../providersetup/models/LeapSRPSession.java | 361 ++++++++ .../providersetup/models/SrpCredentials.java | 26 + .../providersetup/models/SrpRegistrationData.java | 42 + .../tethering/TetheringStateManager.java | 9 +- .../main/java/se/leap/bitmaskclient/utils/Cmd.java | 91 -- .../se/leap/bitmaskclient/utils/ConfigHelper.java | 230 ----- .../se/leap/bitmaskclient/utils/DateHelper.java | 29 - .../se/leap/bitmaskclient/utils/FileHelper.java | 46 - .../se/leap/bitmaskclient/utils/IPAddress.java | 102 --- .../bitmaskclient/utils/InputStreamHelper.java | 21 - .../leap/bitmaskclient/utils/KeyStoreHelper.java | 78 -- .../leap/bitmaskclient/utils/PreferenceHelper.java | 273 ------ .../se/leap/bitmaskclient/utils/ViewHelper.java | 17 - .../bitmaskclient/views/IconCheckboxEntry.java | 86 -- .../leap/bitmaskclient/views/IconSwitchEntry.java | 116 --- .../se/leap/bitmaskclient/views/IconTextEntry.java | 106 --- .../se/leap/bitmaskclient/views/IconTextView.java | 96 --- .../bitmaskclient/views/ProviderHeaderView.java | 109 --- .../se/leap/bitmaskclient/views/VpnStateImage.java | 99 --- .../main/res/layout-xlarge-port/a_add_provider.xml | 2 +- .../layout-xlarge-port/a_custom_provider_setup.xml | 2 +- .../layout-xlarge-port/a_provider_credentials.xml | 2 +- .../res/layout-xlarge-port/a_provider_detail.xml | 2 +- .../res/layout-xlarge-port/a_provider_list.xml | 2 +- app/src/main/res/layout-xlarge/a_add_provider.xml | 2 +- .../res/layout-xlarge/a_custom_provider_setup.xml | 2 +- .../res/layout-xlarge/a_provider_credentials.xml | 2 +- .../main/res/layout-xlarge/a_provider_detail.xml | 2 +- app/src/main/res/layout-xlarge/a_provider_list.xml | 2 +- app/src/main/res/layout-xlarge/f_about.xml | 2 +- app/src/main/res/layout-xlarge/f_eip.xml | 2 +- app/src/main/res/layout/a_add_provider.xml | 4 +- .../layout/a_add_provider_tablet_scrollview.xml | 2 +- .../main/res/layout/a_custom_provider_setup.xml | 4 +- ..._custom_provider_setup_tablet_linear_layout.xml | 2 +- app/src/main/res/layout/a_main.xml | 4 +- app/src/main/res/layout/a_provider_credentials.xml | 2 +- ...a_provider_credentials_tablet_linear_layout.xml | 2 +- app/src/main/res/layout/a_provider_detail.xml | 2 +- .../a_provider_detail_tablet_linear_layout.xml | 2 +- app/src/main/res/layout/a_provider_list.xml | 4 +- .../a_provider_list_tablet_linear_layout.xml | 2 +- app/src/main/res/layout/d_checkbox_confirm.xml | 2 +- app/src/main/res/layout/f_about.xml | 2 +- app/src/main/res/layout/f_drawer_main.xml | 24 +- app/src/main/res/layout/f_eip.xml | 2 +- app/src/main/res/values/untranslatable.xml | 3 +- 182 files changed, 11832 insertions(+), 11739 deletions(-) delete mode 100644 app/src/main/java/se/leap/bitmaskclient/AbstractProviderDetailActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/AddProviderBaseActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/BitmaskTileService.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/Constants.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/CustomProviderSetupActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/DnsResolver.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/EipFragment.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/EipSetupListener.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/FragmentManagerEnhanced.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/LoginActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/MainActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/MainActivityErrorDialog.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/Provider.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderAPICommand.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderApiSetupBroadcastReceiver.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderManager.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderObservable.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderRenderer.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderRendererBuilder.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderSetupBaseActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderSetupFailedDialog.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderSetupInterface.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/SignupActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/SrpCredentials.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/SrpRegistrationData.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/StartActivity.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/BitmaskTileService.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/FragmentManagerEnhanced.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/OnBootReceiver.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/drawer/NavigationDrawerFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/models/ProviderObservable.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/views/IconCheckboxEntry.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/views/IconTextView.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/views/ProviderHeaderView.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/base/views/VpnStateImage.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/eip/EipSetupListener.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/fragments/AboutFragment.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/fragments/DonationReminderDialog.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/fragments/ExcludeAppsFragment.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/fragments/LogFragment.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/DateHelper.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/IPAddress.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/InputStreamHelper.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/KeyStoreHelper.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/utils/ViewHelper.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/views/IconTextView.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/views/ProviderHeaderView.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/views/VpnStateImage.java (limited to 'app/src/main') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ec053185..7e964211 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,7 +29,7 @@ android:maxSdkVersion="18"/> @@ -71,7 +71,7 @@ android:name="de.blinkt.openvpn.LaunchVPN" android:theme="@style/invisibleTheme" /> - - + + optionsList = new ArrayList<>(); - if (provider.allowsRegistered()) { - optionsList.add(getString(R.string.login_to_profile)); - optionsList.add(getString(R.string.create_profile)); - if (provider.allowsAnonymous()) { - optionsList.add(getString(R.string.use_anonymously_button)); - } - } else { - onAnonymouslySelected(); - } - - - options.setAdapter(new ArrayAdapter<>( - this, - R.layout.v_single_list_item, - android.R.id.text1, - optionsList.toArray(new String[optionsList.size()]) - )); - options.setOnItemClickListener((parent, view, position, id) -> { - String text = ((TextView) view).getText().toString(); - Intent intent; - if (text.equals(getString(R.string.login_to_profile))) { - Log.d(TAG, "login selected"); - intent = new Intent(getApplicationContext(), LoginActivity.class); - } else if (text.equals(getString(R.string.create_profile))) { - Log.d(TAG, "signup selected"); - intent = new Intent(getApplicationContext(), SignupActivity.class); - } else { - onAnonymouslySelected(); - return; - } - intent.putExtra(PROVIDER_KEY, provider); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivityForResult(intent, REQUEST_CODE_CONFIGURE_LEAP); - }); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - provider = intent.getParcelableExtra(PROVIDER_KEY); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) { - if (resultCode == RESULT_OK) { - setResult(resultCode, data); - finish(); - } - } - } - - private void onAnonymouslySelected() { - Intent intent; - Log.d(TAG, "use anonymously selected"); - intent = new Intent(); - intent.putExtra(Provider.KEY, provider); - setResult(RESULT_OK, intent); - finish(); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/AddProviderBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/AddProviderBaseActivity.java deleted file mode 100644 index e0bb0061..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/AddProviderBaseActivity.java +++ /dev/null @@ -1,124 +0,0 @@ -package se.leap.bitmaskclient; - -import android.content.Intent; -import android.os.Bundle; -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.textfield.TextInputLayout; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.Button; - -import butterknife.InjectView; - -import static se.leap.bitmaskclient.ProviderListBaseActivity.EXTRAS_KEY_INVALID_URL; - -/** - * Created by cyberta on 30.06.18. - */ - -public abstract class AddProviderBaseActivity extends ConfigWizardBaseActivity { - - final public static String EXTRAS_KEY_NEW_URL = "NEW_URL"; - - @InjectView(R.id.text_uri_error) - TextInputLayout urlError; - - @InjectView(R.id.text_uri) - TextInputEditText editUrl; - - @InjectView(R.id.button_cancel) - Button cancelButton; - - @InjectView(R.id.button_save) - Button saveButton; - - - protected void init() { - Bundle extras = this.getIntent().getExtras(); - if (extras != null && extras.containsKey(EXTRAS_KEY_INVALID_URL)) { - editUrl.setText(extras.getString(EXTRAS_KEY_INVALID_URL)); - saveButton.setEnabled(true); - } - - setupSaveButton(); - setupCancelButton(); - setUpListeners(); - setUpInitialUI(); - } - - public abstract void setupSaveButton(); - - private void setupCancelButton() { - cancelButton.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - finish(); - } - }); - } - - private void setUpInitialUI() { - setProviderHeaderText(R.string.add_provider); - hideProgressBar(); - } - - protected void saveProvider() { - String entered_url = getURL(); - if (validURL(entered_url)) { - Intent intent = this.getIntent(); - intent.putExtra(EXTRAS_KEY_NEW_URL, entered_url); - setResult(RESULT_OK, intent); - finish(); - } else { - editUrl.setText(""); - urlError.setError(getString(R.string.not_valid_url_entered)); - } - } - - private void setUpListeners() { - - editUrl.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - if (!validURL(getURL())) { - urlError.setError(getString(R.string.not_valid_url_entered)); - saveButton.setEnabled(false); - - } else { - urlError.setError(null); - saveButton.setEnabled(true); - } - } - }); - } - - private String getURL() { - String entered_url = editUrl.getText().toString().trim(); - if (entered_url.contains("www.")) entered_url = entered_url.replaceFirst("www.", ""); - if (!entered_url.startsWith("https://")) { - if (entered_url.startsWith("http://")) { - entered_url = entered_url.substring("http://".length()); - } - entered_url = "https://".concat(entered_url); - } - return entered_url; - } - - /** - * Checks if the entered url is valid or not. - * - * @param enteredUrl - * @return true if it's not empty nor contains only the protocol. - */ - boolean validURL(String enteredUrl) { - return android.util.Patterns.WEB_URL.matcher(enteredUrl).matches(); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java deleted file mode 100644 index 437998e0..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2020 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.content.Context; -import android.content.IntentFilter; -import android.content.SharedPreferences; - -import androidx.appcompat.app.AppCompatDelegate; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.multidex.MultiDexApplication; - -import com.squareup.leakcanary.LeakCanary; -import com.squareup.leakcanary.RefWatcher; - -import se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver; -import se.leap.bitmaskclient.tethering.TetheringStateManager; - -import static android.content.Intent.CATEGORY_DEFAULT; -import static se.leap.bitmaskclient.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; -import static se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver.ACTION_DOWNLOAD; -import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE; -import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE; -import static se.leap.bitmaskclient.utils.PreferenceHelper.getSavedProviderFromSharedPreferences; - -/** - * Created by cyberta on 24.10.17. - */ - -public class BitmaskApp extends MultiDexApplication { - - private final static String TAG = BitmaskApp.class.getSimpleName(); - private RefWatcher refWatcher; - private ProviderObservable providerObservable; - private DownloadBroadcastReceiver downloadBroadcastReceiver; - - - @Override - public void onCreate() { - super.onCreate(); - if (LeakCanary.isInAnalyzerProcess(this)) { - // This process is dedicated to LeakCanary for heap analysis. - // You should not init your app in this process. - return; - } - refWatcher = LeakCanary.install(this); - // Normal app init code...*/ - PRNGFixes.apply(); - SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - providerObservable = ProviderObservable.getInstance(); - providerObservable.updateProvider(getSavedProviderFromSharedPreferences(preferences)); - EipSetupObserver.init(this, preferences); - AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); - TetheringStateManager.getInstance().init(this); - if (BuildConfig.FLAVOR.contains("Fatweb")) { - downloadBroadcastReceiver = new DownloadBroadcastReceiver(); - IntentFilter intentFilter = new IntentFilter(BROADCAST_DOWNLOAD_SERVICE_EVENT); - intentFilter.addAction(ACTION_DOWNLOAD); - intentFilter.addAction(CHECK_VERSION_FILE); - intentFilter.addAction(DOWNLOAD_UPDATE); - intentFilter.addCategory(CATEGORY_DEFAULT); - LocalBroadcastManager.getInstance(this.getApplicationContext()).registerReceiver(downloadBroadcastReceiver, intentFilter); - } - } - - /** - * Use this method to get a RefWatcher object that checks for memory leaks in the given context. - * Call refWatcher.watch(this) to check if all references get garbage collected. - * @param context - * @return the RefWatcher object - */ - public static RefWatcher getRefWatcher(Context context) { - BitmaskApp application = (BitmaskApp) context.getApplicationContext(); - return application.refWatcher; - } - - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/BitmaskTileService.java b/app/src/main/java/se/leap/bitmaskclient/BitmaskTileService.java deleted file mode 100644 index 4b423624..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/BitmaskTileService.java +++ /dev/null @@ -1,101 +0,0 @@ -package se.leap.bitmaskclient; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Intent; -import android.graphics.drawable.Icon; -import android.os.Build; -import android.service.quicksettings.Tile; -import android.service.quicksettings.TileService; - -import java.util.Observable; -import java.util.Observer; - -import se.leap.bitmaskclient.eip.EipCommand; -import se.leap.bitmaskclient.eip.EipStatus; - - -@TargetApi(Build.VERSION_CODES.N) -public class BitmaskTileService extends TileService implements Observer { - - @SuppressLint("Override") - @TargetApi(Build.VERSION_CODES.N) - @Override - public void onClick() { - super.onClick(); - Provider provider = ProviderObservable.getInstance().getCurrentProvider(); - if (provider.isConfigured()) { - if (!isLocked()) { - onTileTap(); - } else { - unlockAndRun(this::onTileTap); - } - } else { - Intent intent = new Intent(getApplicationContext(), StartActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } - } - - private void onTileTap() { - EipStatus eipStatus = EipStatus.getInstance(); - if (eipStatus.isConnecting() || eipStatus.isBlocking() || eipStatus.isConnected() || eipStatus.isReconnecting()) { - EipCommand.stopVPN(getApplicationContext()); - } else { - EipCommand.startVPN(getApplicationContext(), false); - } - } - - - @TargetApi(Build.VERSION_CODES.N) - @Override - public void onTileAdded() { - } - - @Override - public void onStartListening() { - super.onStartListening(); - EipStatus.getInstance().addObserver(this); - update(EipStatus.getInstance(), null); - } - - @Override - public void onStopListening() { - super.onStopListening(); - EipStatus.getInstance().deleteObserver(this); - } - - @Override - public void update(Observable o, Object arg) { - Tile t = getQsTile(); - - if (o instanceof EipStatus) { - EipStatus status = (EipStatus) o; - Icon icon; - String title; - if (status.isConnecting() || status.isReconnecting()) { - icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_connecting); - title = getResources().getString(R.string.cancel); - t.setState(Tile.STATE_ACTIVE); - } else if (status.isConnected()) { - icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_connected); - title = String.format(getString(R.string.qs_disconnect), getString(R.string.app_name)); - t.setState(Tile.STATE_ACTIVE); - } else if (status.isBlocking()) { - icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_blocking); - title = getString(R.string.vpn_button_turn_off_blocking); - t.setState(Tile.STATE_ACTIVE); - } else { - icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_disconnected); - title = String.format(getString(R.string.qs_enable_vpn), getString(R.string.app_name)); - t.setState(Tile.STATE_INACTIVE); - } - - - t.setIcon(icon); - t.setLabel(title); - - t.updateTile(); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java b/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java deleted file mode 100644 index 4f27f88a..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ButterKnifeActivity.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2020 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 androidx.appcompat.app.AppCompatActivity; -import android.view.View; - -import butterknife.ButterKnife; - -/** - * Automatically inject with ButterKnife after calling setContentView - */ - -public abstract class ButterKnifeActivity extends AppCompatActivity { - - @Override - public void setContentView(View view) { - super.setContentView(view); - ButterKnife.inject(this); - } - - @Override - public void setContentView(int layoutResID) { - super.setContentView(layoutResID); - ButterKnife.inject(this); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java deleted file mode 100644 index 2d163859..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ConfigWizardBaseActivity.java +++ /dev/null @@ -1,287 +0,0 @@ -package se.leap.bitmaskclient; - -import android.content.SharedPreferences; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.os.Build; -import android.os.Bundle; -import androidx.annotation.DrawableRes; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.Guideline; -import androidx.core.content.ContextCompat; -import androidx.appcompat.widget.AppCompatTextView; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.LinearLayout; -import android.widget.ProgressBar; - -import butterknife.InjectView; -import butterknife.Optional; -import se.leap.bitmaskclient.views.ProviderHeaderView; - -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; - -/** - * Base Activity for configuration wizard activities - * - * Created by fupduck on 09.01.18. - */ - -public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity { - - private static final String TAG = ConfigWizardBaseActivity.class.getName(); - public static final float GUIDE_LINE_COMPACT_DELTA = 0.1f; - protected SharedPreferences preferences; - - @InjectView(R.id.header) - ProviderHeaderView providerHeaderView; - - //Add provider screen has no loading screen - @Optional - @InjectView(R.id.loading_screen) - protected LinearLayout loadingScreen; - - @Optional - @InjectView(R.id.progressbar) - protected ProgressBar progressBar; - - @Optional - @InjectView(R.id.progressbar_description) - protected AppCompatTextView progressbarText; - - //Only tablet layouts have guidelines as they are based on a ConstraintLayout - @Optional - @InjectView(R.id.guideline_top) - protected Guideline guideline_top; - - @Optional - @InjectView(R.id.guideline_bottom) - protected Guideline guideline_bottom; - - @InjectView(R.id.content) - protected LinearLayout content; - - protected Provider provider; - - protected boolean isCompactLayout = false; - protected boolean isActivityShowing; - - private float defaultGuidelineTopPercentage; - private float defaultGuidelineBottomPercentage; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - provider = getIntent().getParcelableExtra(PROVIDER_KEY); - } - - @Override - public void setContentView(View view) { - super.setContentView(view); - initContentView(); - } - - @Override - public void setContentView(int layoutResID) { - super.setContentView(layoutResID); - initContentView(); - } - - @Override - public void setContentView(View view, ViewGroup.LayoutParams params) { - super.setContentView(view, params); - initContentView(); - } - - private void initContentView() { - if (provider != null) { - setProviderHeaderText(provider.getName()); - } - setProgressbarColorForPreLollipop(); - setDefaultGuidelineValues(); - setGlobalLayoutChangeListener(); - } - - private void setDefaultGuidelineValues() { - if (isTabletLayout()) { - defaultGuidelineTopPercentage = ((ConstraintLayout.LayoutParams) guideline_top.getLayoutParams()).guidePercent; - defaultGuidelineBottomPercentage = ((ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams()).guidePercent; - } - } - - private void setProgressbarColorForPreLollipop() { - if (progressBar == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return; - } - progressBar.getIndeterminateDrawable().setColorFilter( - ContextCompat.getColor(this, R.color.colorPrimary), - PorterDuff.Mode.SRC_IN); - } - - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (provider != null) { - outState.putParcelable(PROVIDER_KEY, provider); - } - } - - @Override - protected void onPause() { - super.onPause(); - isActivityShowing = false; - } - - @Override - protected void onResume() { - super.onResume(); - isActivityShowing = true; - } - - protected void restoreState(Bundle savedInstanceState) { - if (savedInstanceState != null && savedInstanceState.containsKey(PROVIDER_KEY)) { - provider = savedInstanceState.getParcelable(PROVIDER_KEY); - } - } - - protected void setProviderHeaderLogo(@DrawableRes int providerHeaderLogo) { - providerHeaderView.setLogo(providerHeaderLogo); - } - - protected void setProviderHeaderText(String providerHeaderText) { - providerHeaderView.setTitle(providerHeaderText); - } - - protected void setProviderHeaderText(@StringRes int providerHeaderText) { - providerHeaderView.setTitle(providerHeaderText); - } - - protected void hideProgressBar() { - if (loadingScreen == null) { - return; - } - loadingScreen.setVisibility(GONE); - content.setVisibility(VISIBLE); - } - - protected void showProgressBar() { - if (loadingScreen == null) { - return; - } - content.setVisibility(GONE); - loadingScreen.setVisibility(VISIBLE); - } - - protected void setProgressbarText(@StringRes int progressbarText) { - if (this.progressbarText == null) { - return; - } - this.progressbarText.setText(progressbarText); - } - - - protected void showCompactLayout() { - if (isCompactLayout) { - return; - } - - if (isTabletLayoutInLandscape() || isPhoneLayout()) { - providerHeaderView.showCompactLayout(); - } - - showIncreasedTabletContentArea(); - isCompactLayout = true; - } - - protected void showStandardLayout() { - if (!isCompactLayout) { - return; - } - providerHeaderView.showStandardLayout(); - showStandardTabletContentArea(); - isCompactLayout = false; - } - - private boolean isTabletLayoutInLandscape() { - // TabletLayout is based on a ConstraintLayout and uses Guidelines whereas the phone layout - // has no such elements in it's layout xml file - return guideline_top != null && - guideline_bottom != null && - getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; - } - - protected boolean isPhoneLayout() { - return guideline_top == null && guideline_bottom == null; - } - - protected boolean isTabletLayout() { - return guideline_top != null && guideline_bottom != null; - } - - /** - * Increases the white content area in tablet layouts - */ - private void showIncreasedTabletContentArea() { - if (isPhoneLayout()) { - return; - } - ConstraintLayout.LayoutParams guideLineTopParams = (ConstraintLayout.LayoutParams) guideline_top.getLayoutParams(); - float increasedTopPercentage = defaultGuidelineTopPercentage - GUIDE_LINE_COMPACT_DELTA; - guideLineTopParams.guidePercent = increasedTopPercentage > 0f ? increasedTopPercentage : 0f; - guideline_top.setLayoutParams(guideLineTopParams); - - ConstraintLayout.LayoutParams guideLineBottomParams = (ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams(); - float increasedBottomPercentage = defaultGuidelineBottomPercentage + GUIDE_LINE_COMPACT_DELTA; - guideLineBottomParams.guidePercent = increasedBottomPercentage < 1f ? increasedBottomPercentage : 1f; - guideline_bottom.setLayoutParams(guideLineBottomParams); - } - - /** - * Restores the default size of the white content area in tablet layouts - */ - private void showStandardTabletContentArea() { - if (isPhoneLayout()) { - return; - } - ConstraintLayout.LayoutParams guideLineTopParams = (ConstraintLayout.LayoutParams) guideline_top.getLayoutParams(); - guideLineTopParams.guidePercent = defaultGuidelineTopPercentage; - guideline_top.setLayoutParams(guideLineTopParams); - - ConstraintLayout.LayoutParams guideLineBottomParams = (ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams(); - guideLineBottomParams.guidePercent = defaultGuidelineBottomPercentage; - guideline_bottom.setLayoutParams(guideLineBottomParams); - } - - /** - * Checks if the keyboard is shown and switches between the standard layout and the compact layout - */ - private void setGlobalLayoutChangeListener() { - final View rootView = content.getRootView(); - rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - Rect r = new Rect(); - //r will be populated with the coordinates of your view that area still visible. - rootView.getWindowVisibleDisplayFrame(r); - - float deltaHiddenScreen = 1f - ((float) (r.bottom - r.top) / (float) rootView.getHeight()); - if (deltaHiddenScreen > 0.25f) { - // if more than 1/4 of the screen is hidden - showCompactLayout(); - } else { - showStandardLayout(); - } - } - }); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java deleted file mode 100644 index 1d364074..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/Constants.java +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Copyright (c) 2020 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.text.TextUtils; - -public interface Constants { - - ////////////////////////////////////////////// - // PREFERENCES CONSTANTS - ///////////////////////////////////////////// - - String SHARED_PREFERENCES = "LEAPPreferences"; - String PREFERENCES_APP_VERSION = "bitmask version"; - String ALWAYS_ON_SHOW_DIALOG = "DIALOG.ALWAYS_ON_SHOW_DIALOG"; - String CLEARLOG = "clearlogconnect"; - String LAST_USED_PROFILE = "last_used_profile"; - String EXCLUDED_APPS = "excluded_apps"; - String USE_PLUGGABLE_TRANSPORTS = "usePluggableTransports"; - String ALLOW_TETHERING_BLUETOOTH = "tethering_bluetooth"; - String ALLOW_TETHERING_WIFI = "tethering_wifi"; - String ALLOW_TETHERING_USB = "tethering_usb"; - String SHOW_EXPERIMENTAL = "show_experimental"; - String USE_IPv6_FIREWALL = "use_ipv6_firewall"; - String RESTART_ON_UPDATE = "restart_on_update"; - String LAST_UPDATE_CHECK = "last_update_check"; - - - ////////////////////////////////////////////// - // REQUEST CODE CONSTANTS - ///////////////////////////////////////////// - - String REQUEST_CODE_KEY = "request_code"; - int REQUEST_CODE_CONFIGURE_LEAP = 0; - int REQUEST_CODE_SWITCH_PROVIDER = 1; - int REQUEST_CODE_LOG_IN = 2; - int REQUEST_CODE_ADD_PROVIDER = 3; - int REQUEST_CODE_REQUEST_UPDATE = 4; - - - ////////////////////////////////////////////// - // APP CONSTANTS - ///////////////////////////////////////////// - - String APP_ACTION_QUIT = "quit"; - String APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE = "configure always-on profile"; - String DEFAULT_BITMASK = "normal"; - String CUSTOM_BITMASK = "custom"; - String DANGER_ON = "danger_on"; - - - String ASK_TO_CANCEL_VPN = "ask_to_cancel_vpn"; - - - ////////////////////////////////////////////// - // EIP CONSTANTS - ///////////////////////////////////////////// - - String EIP_ACTION_CHECK_CERT_VALIDITY = "EIP.CHECK_CERT_VALIDITY"; - String EIP_ACTION_START = "se.leap.bitmaskclient.EIP.START"; - String EIP_ACTION_STOP = "se.leap.bitmaskclient.EIP.STOP"; - String EIP_ACTION_IS_RUNNING = "se.leap.bitmaskclient.EIP.IS_RUNNING"; - String EIP_ACTION_START_ALWAYS_ON_VPN = "se.leap.bitmaskclient.START_ALWAYS_ON_VPN"; - 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_ACTION_PREPARE_VPN = "se.leap.bitmaskclient.EIP_ACTION_PREPARE_VPN"; - String EIP_ACTION_CONFIGURE_TETHERING = "se.leap.bitmaskclient.EIP_ACTION_CONFIGURE_TETHERING"; - - 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"; - String EIP_EARLY_ROUTES = "EIP.EARLY_ROUTES"; - String EIP_N_CLOSEST_GATEWAY = "EIP.N_CLOSEST_GATEWAY"; - - - ////////////////////////////////////////////// - // PROVIDER CONSTANTS - ///////////////////////////////////////////// - - String PROVIDER_ALLOW_ANONYMOUS = "allow_anonymous"; - String PROVIDER_ALLOWED_REGISTERED = "allow_registration"; - String PROVIDER_VPN_CERTIFICATE = "cert"; - String PROVIDER_PRIVATE_KEY = "Constants.PROVIDER_PRIVATE_KEY"; - String PROVIDER_KEY = "Constants.PROVIDER_KEY"; - String PROVIDER_CONFIGURED = "Constants.PROVIDER_CONFIGURED"; - String PROVIDER_EIP_DEFINITION = "Constants.EIP_DEFINITION"; - String PROVIDER_PROFILE_UUID = "Constants.PROVIDER_PROFILE_UUID"; - String PROVIDER_PROFILE = "Constants.PROVIDER_PROFILE"; - - ////////////////////////////////////////////// - // CREDENTIAL CONSTANTS - ///////////////////////////////////////////// - - String CREDENTIALS_USERNAME = "username"; - String CREDENTIALS_PASSWORD = "password"; - - enum CREDENTIAL_ERRORS { - USERNAME_MISSING, - PASSWORD_INVALID_LENGTH, - RISEUP_WARNING - } - - ////////////////////////////////////////////// - // BROADCAST CONSTANTS - ///////////////////////////////////////////// - - String BROADCAST_EIP_EVENT = "BROADCAST.EIP_EVENT"; - String BROADCAST_PROVIDER_API_EVENT = "BROADCAST.PROVIDER_API_EVENT"; - String BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT = "BROADCAST.GATEWAY_SETUP_WATCHER_EVENT"; - String BROADCAST_RESULT_CODE = "BROADCAST.RESULT_CODE"; - String BROADCAST_RESULT_KEY = "BROADCAST.RESULT_KEY"; - String BROADCAST_DOWNLOAD_SERVICE_EVENT = "BROADCAST.DOWNLOAD_SERVICE_EVENT"; - - - ////////////////////////////////////////////// - // ICS-OPENVPN CONSTANTS - ///////////////////////////////////////////// - String DEFAULT_SHARED_PREFS_BATTERY_SAVER = "screenoff"; - - ////////////////////////////////////////////// - // CUSTOM CONSTANTS - ///////////////////////////////////////////// - boolean ENABLE_DONATION = BuildConfig.enable_donation; - boolean ENABLE_DONATION_REMINDER = BuildConfig.enable_donation_reminder; - int DONATION_REMINDER_DURATION = BuildConfig.donation_reminder_duration; - String DONATION_URL = TextUtils.isEmpty(BuildConfig.donation_url) ? - BuildConfig.default_donation_url : BuildConfig.donation_url; - String LAST_DONATION_REMINDER_DATE = "last_donation_reminder_date"; - String FIRST_TIME_USER_DATE = "first_time_user_date"; - - - ////////////////////////////////////////////// - // JSON KEYS - ///////////////////////////////////////////// - String IP_ADDRESS = "ip_address"; - String REMOTE = "remote"; - String PORTS = "ports"; - String PROTOCOLS = "protocols"; - String CAPABILITIES = "capabilities"; - String TRANSPORT = "transport"; - String TYPE = "type"; - String OPTIONS = "options"; - String VERSION = "version"; - String NAME = "name"; - String TIMEZONE = "timezone"; - String LOCATIONS = "locations"; - String LOCATION = "location"; - String OPENVPN_CONFIGURATION = "openvpn_configuration"; - String GATEWAYS = "gateways"; - String HOST = "host"; -} diff --git a/app/src/main/java/se/leap/bitmaskclient/CustomProviderSetupActivity.java b/app/src/main/java/se/leap/bitmaskclient/CustomProviderSetupActivity.java deleted file mode 100644 index 0974f427..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/CustomProviderSetupActivity.java +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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.content.Intent; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_CONFIGURE_LEAP; -import static se.leap.bitmaskclient.ProviderAPI.SET_UP_PROVIDER; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER; -import static se.leap.bitmaskclient.utils.ConfigHelper.preferAnonymousUsage; - -/** - * Created by cyberta on 17.08.18. - */ - -public class CustomProviderSetupActivity extends ProviderSetupBaseActivity { - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setUpInitialUI(); - restoreState(savedInstanceState); - setProvider(new Provider(BuildConfig.customProviderUrl, BuildConfig.geoipUrl, BuildConfig.customProviderIp, BuildConfig.customProviderApiIp)); - } - - @Override - protected void onResume() { - super.onResume(); - if (getConfigState() == ProviderConfigState.PROVIDER_NOT_SET) { - showProgressBar(); - setupProvider(); - } - } - - private void setUpInitialUI() { - setContentView(R.layout.a_custom_provider_setup); - setProviderHeaderText(R.string.setup_provider); - hideProgressBar(); - } - - private void setupProvider() { - setProviderConfigState(SETTING_UP_PROVIDER); - ProviderAPICommand.execute(this, SET_UP_PROVIDER, getProvider()); - } - - // ------- ProviderSetupInterface ---v - @Override - public void handleProviderSetUp(Provider provider) { - setProvider(provider); - if (provider.allowsAnonymous()) { - downloadVpnCertificate(); - } else { - showProviderDetails(); - } - } - - @Override - public void handleCorrectlyDownloadedCertificate(Provider provider) { - if (preferAnonymousUsage()) { - finishWithSetupWithProvider(provider); - } else { - this.provider = provider; - showProviderDetails(); - } - } - - // ------- DownloadFailedDialogInterface ---v - @Override - public void retrySetUpProvider(@NonNull Provider provider) { - setupProvider(); - showProgressBar(); - } - - @Override - public void cancelSettingUpProvider() { - super.cancelSettingUpProvider(); - finish(); - } - - @Override - public void addAndSelectNewProvider(String url) { - // ignore - } - - private void finishWithSetupWithProvider(Provider provider) { - Intent intent = new Intent(); - intent.putExtra(Provider.KEY, provider); - setResult(RESULT_OK, intent); - finish(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) { - setResult(resultCode, data); - finish(); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java b/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java deleted file mode 100644 index 0cbb0d72..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java +++ /dev/null @@ -1,48 +0,0 @@ -package se.leap.bitmaskclient; - -import java.net.MalformedURLException; -import java.net.URL; - -public class DefaultedURL { - private URL DEFAULT_URL; - private String default_url = "https://example.net"; - - private URL url; - - DefaultedURL() { - try { - DEFAULT_URL = new URL(default_url); - url = DEFAULT_URL; - } catch (MalformedURLException e) { - e.printStackTrace(); - } - } - - public boolean isDefault() { return url.equals(DEFAULT_URL); } - - public void setUrl(URL url) { - this.url = url; - } - - public String getDomain() { - return url.getHost(); - } - - public URL getUrl() { - return url; - } - - @Override - public String toString() { - return url.toString(); - } - - @Override - public boolean equals(Object o) { - if (o instanceof DefaultedURL) { - return url.equals(((DefaultedURL) o).getUrl()); - } - return false; - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/DnsResolver.java b/app/src/main/java/se/leap/bitmaskclient/DnsResolver.java deleted file mode 100644 index 92f70492..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/DnsResolver.java +++ /dev/null @@ -1,37 +0,0 @@ -package se.leap.bitmaskclient; - -import org.jetbrains.annotations.NotNull; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; - -import okhttp3.Dns; -import se.leap.bitmaskclient.utils.IPAddress; - -class DnsResolver implements Dns { - - @Override - public List lookup(@NotNull String hostname) throws UnknownHostException { - try { - return Dns.SYSTEM.lookup(hostname); - } catch (UnknownHostException e) { - ProviderObservable observable = ProviderObservable.getInstance(); - Provider currentProvider; - if (observable.getProviderForDns() != null) { - currentProvider = observable.getProviderForDns(); - } else { - currentProvider = observable.getCurrentProvider(); - } - String ip = currentProvider.getIpForHostname(hostname); - if (!ip.isEmpty()) { - ArrayList addresses = new ArrayList<>(); - addresses.add(InetAddress.getByAddress(hostname, IPAddress.asBytes(ip))); - return addresses; - } else { - throw new UnknownHostException("Hostname " + hostname + " not found"); - } - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java b/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/se/leap/bitmaskclient/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/EipFragment.java deleted file mode 100644 index fceadd88..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/EipFragment.java +++ /dev/null @@ -1,600 +0,0 @@ -/** - * 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.app.Activity; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; -import android.os.Bundle; -import android.os.IBinder; -import android.os.Vibrator; -import android.text.TextUtils; -import android.util.Log; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.AppCompatButton; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; - -import java.util.Observable; -import java.util.Observer; - -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.VpnStatus; -import se.leap.bitmaskclient.eip.EipCommand; -import se.leap.bitmaskclient.eip.EipStatus; -import se.leap.bitmaskclient.fragments.DonationReminderDialog; -import se.leap.bitmaskclient.views.VpnStateImage; - -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; -import static se.leap.bitmaskclient.Constants.ASK_TO_CANCEL_VPN; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; -import static se.leap.bitmaskclient.Constants.EIP_EARLY_ROUTES; -import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_CONFIGURE_LEAP; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_LOG_IN; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_SWITCH_PROVIDER; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; -import static se.leap.bitmaskclient.EipSetupObserver.connectionRetry; -import static se.leap.bitmaskclient.EipSetupObserver.gatewayOrder; -import static se.leap.bitmaskclient.EipSetupObserver.reconnectingWithDifferentGateway; -import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_GEOIP_JSON; -import static se.leap.bitmaskclient.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE; -import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; -import static se.leap.bitmaskclient.utils.ConfigHelper.isDefaultBitmask; -import static se.leap.bitmaskclient.utils.ViewHelper.convertDimensionToPx; - -public class EipFragment extends Fragment implements Observer { - - public final static String TAG = EipFragment.class.getSimpleName(); - - - private SharedPreferences preferences; - private Provider provider; - - @InjectView(R.id.background) - AppCompatImageView background; - - @InjectView(R.id.vpn_state_image) - VpnStateImage vpnStateImage; - - @InjectView(R.id.vpn_main_button) - AppCompatButton mainButton; - - @InjectView(R.id.routed_text) - AppCompatTextView routedText; - - @InjectView(R.id.vpn_route) - AppCompatTextView vpnRoute; - - - - private EipStatus eipStatus; - - //---saved Instance ------- - private final String KEY_SHOW_PENDING_START_CANCELLATION = "KEY_SHOW_PENDING_START_CANCELLATION"; - private final String KEY_SHOW_ASK_TO_STOP_EIP = "KEY_SHOW_ASK_TO_STOP_EIP"; - private boolean showPendingStartCancellation = false; - private boolean showAskToStopEip = false; - //------------------------ - AlertDialog alertDialog; - - private IOpenVPNServiceInternal mService; - private ServiceConnection openVpnConnection; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - Bundle arguments = getArguments(); - Activity activity = getActivity(); - if (activity != null) { - if (arguments != null) { - provider = arguments.getParcelable(PROVIDER_KEY); - if (provider == null) { - handleNoProvider(activity); - } else { - Log.d(TAG, provider.getName() + " configured as provider"); - } - } else { - handleNoProvider(activity); - } - } - } - - private void handleNoProvider(Activity activity) { - if (isDefaultBitmask()) { - activity.startActivityForResult(new Intent(activity, ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER); - } else { - Log.e(TAG, "no provider given - try to reconfigure custom provider"); - startActivityForResult(new Intent(activity, CustomProviderSetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP); - - } - - } - - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - openVpnConnection = new EipFragmentServiceConnection(); - eipStatus = EipStatus.getInstance(); - Activity activity = getActivity(); - if (activity != null) { - preferences = getActivity().getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); - } else { - Log.e(TAG, "activity is null in onCreate - no preferences set!"); - } - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - eipStatus.addObserver(this); - View view = inflater.inflate(R.layout.f_eip, container, false); - ButterKnife.inject(this, view); - - Bundle arguments = getArguments(); - if (arguments != null && arguments.containsKey(ASK_TO_CANCEL_VPN) && arguments.getBoolean(ASK_TO_CANCEL_VPN)) { - arguments.remove(ASK_TO_CANCEL_VPN); - setArguments(arguments); - askToStopEIP(); - } - restoreFromSavedInstance(savedInstanceState); - return view; - } - - @Override - public void onStart() { - super.onStart(); - if (DonationReminderDialog.isCallable(getContext())) { - showDonationReminderDialog(); - } - } - - @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); - bindOpenVpnService(); - handleNewState(); - } - - @Override - public void onPause() { - super.onPause(); - - Activity activity = getActivity(); - if (activity != null) { - getActivity().unbindService(openVpnConnection); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (showAskToStopEip) { - outState.putBoolean(KEY_SHOW_ASK_TO_STOP_EIP, true); - alertDialog.dismiss(); - } else if (showPendingStartCancellation) { - outState.putBoolean(KEY_SHOW_PENDING_START_CANCELLATION, true); - alertDialog.dismiss(); - } - } - - private void restoreFromSavedInstance(Bundle savedInstanceState) { - if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_PENDING_START_CANCELLATION)) { - showPendingStartCancellation = true; - askPendingStartCancellation(); - } else if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_ASK_TO_STOP_EIP)) { - showAskToStopEip = true; - askToStopEIP(); - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - eipStatus.deleteObserver(this); - } - - private void saveStatus(boolean restartOnBoot) { - preferences.edit().putBoolean(EIP_RESTART_ON_BOOT, restartOnBoot).apply(); - } - - @OnClick(R.id.vpn_main_button) - void onButtonClick() { - handleIcon(); - } - - @OnClick(R.id.vpn_state_image) - void onVpnStateImageClick() { - handleIcon(); - } - - void handleIcon() { - if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnected() || eipStatus.isConnecting()) - handleSwitchOff(); - else - handleSwitchOn(); - } - - private void handleSwitchOn() { - Context context = getContext(); - if (context == null) { - Log.e(TAG, "context is null when switch turning on"); - return; - } - - if (canStartEIP()) { - startEipFromScratch(); - } else if (canLogInToStartEIP()) { - askUserToLogIn(getString(vpn_certificate_user_message)); - } else { - // provider has no VpnCertificate but user is logged in - updateInvalidVpnCertificate(); - } - } - - private boolean canStartEIP() { - boolean certificateExists = provider.hasVpnCertificate(); - boolean isAllowedAnon = provider.allowsAnonymous(); - return (isAllowedAnon || certificateExists) && !eipStatus.isConnected() && !eipStatus.isConnecting(); - } - - private boolean canLogInToStartEIP() { - boolean isAllowedRegistered = provider.allowsRegistered(); - boolean isLoggedIn = LeapSRPSession.loggedIn(); - return isAllowedRegistered && !isLoggedIn && !eipStatus.isConnecting() && !eipStatus.isConnected(); - } - - private void handleSwitchOff() { - if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnecting()) { - askPendingStartCancellation(); - } else if (eipStatus.isConnected()) { - askToStopEIP(); - } - } - - private void setMainButtonEnabled(boolean enabled) { - mainButton.setEnabled(enabled); - vpnStateImage.setEnabled(enabled); - } - - public void startEipFromScratch() { - saveStatus(true); - Context context = getContext(); - if (context == null) { - Log.e(TAG, "context is null when trying to start VPN"); - return; - } - if (!provider.getGeoipUrl().isDefault() && provider.shouldUpdateGeoIpJson()) { - Bundle bundle = new Bundle(); - bundle.putBoolean(EIP_ACTION_START, true); - bundle.putBoolean(EIP_EARLY_ROUTES, false); - ProviderAPICommand.execute(getContext().getApplicationContext(), DOWNLOAD_GEOIP_JSON, bundle, provider); - } else { - EipCommand.startVPN(context.getApplicationContext(), false); - } - vpnStateImage.showProgress(); - routedText.setVisibility(GONE); - vpnRoute.setVisibility(GONE); - colorBackgroundALittle(); - } - - protected void stopEipIfPossible() { - Context context = getContext(); - if (context == null) { - Log.e(TAG, "context is null when trying to stop EIP"); - return; - } - EipCommand.stopVPN(context.getApplicationContext()); - } - - private void askPendingStartCancellation() { - Activity activity = getActivity(); - if (activity == null) { - Log.e(TAG, "activity is null when asking to cancel"); - return; - } - - try { - AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); - showPendingStartCancellation = true; - alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title)) - .setMessage(activity.getString(R.string.eip_cancel_connect_text)) - .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible()) - .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> { - }).setOnDismissListener(dialog -> showPendingStartCancellation = false).show(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - - } - - protected void askToStopEIP() { - Activity activity = getActivity(); - if (activity == null) { - Log.e(TAG, "activity is null when asking to stop EIP"); - return; - } - try { - AlertDialog.Builder alertBuilder = new AlertDialog.Builder(activity); - showAskToStopEip = true; - alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title)) - .setMessage(activity.getString(R.string.eip_warning_browser_inconsistency)) - .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible()) - .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> { - }).setOnDismissListener(dialog -> showAskToStopEip = false).show(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - - } - - @Override - public void update(Observable observable, Object data) { - if (observable instanceof EipStatus) { - eipStatus = (EipStatus) observable; - Activity activity = getActivity(); - if (activity != null) { - activity.runOnUiThread(this::handleNewState); - } else { - Log.e("EipFragment", "activity is null"); - } - } else if (observable instanceof ProviderObservable) { - provider = ((ProviderObservable) observable).getCurrentProvider(); - } - } - - private void handleNewState() { - Activity activity = getActivity(); - if (activity == null) { - Log.e(TAG, "activity is null while trying to handle new state"); - return; - } - - //Log.d(TAG, "eip fragment eipStatus state: " + eipStatus.getState() + " - level: " + eipStatus.getLevel() + " - is reconnecting: " + eipStatus.isReconnecting()); - - - if (eipStatus.isConnecting() ) { - setMainButtonEnabled(true); - showConnectingLayout(activity); - if (eipStatus.isReconnecting()) { - //Log.d(TAG, "eip show reconnecting toast!"); - //showReconnectToast(activity); - } - } else if (eipStatus.isConnected() ) { - mainButton.setText(activity.getString(R.string.vpn_button_turn_off)); - setMainButtonEnabled(true); - vpnStateImage.setStateIcon(R.drawable.vpn_connected); - vpnStateImage.stopProgress(false); - routedText.setText(R.string.vpn_securely_routed); - routedText.setVisibility(VISIBLE); - vpnRoute.setVisibility(VISIBLE); - setVpnRouteText(); - colorBackground(); - } else if(isOpenVpnRunningWithoutNetwork()){ - mainButton.setText(activity.getString(R.string.vpn_button_turn_off)); - setMainButtonEnabled(true); - vpnStateImage.setStateIcon(R.drawable.vpn_disconnected); - vpnStateImage.stopProgress(false); - routedText.setText(R.string.vpn_securely_routed_no_internet); - routedText.setVisibility(VISIBLE); - vpnRoute.setVisibility(VISIBLE); - setVpnRouteText(); - colorBackgroundALittle(); - } else if (eipStatus.isDisconnected() && reconnectingWithDifferentGateway()) { - showConnectingLayout(activity); - // showRetryToast(activity); - } else if (eipStatus.isDisconnecting()) { - setMainButtonEnabled(false); - showDisconnectingLayout(activity); - } else if (eipStatus.isBlocking()) { - setMainButtonEnabled(true); - vpnStateImage.setStateIcon(R.drawable.vpn_blocking); - vpnStateImage.stopProgress(false); - routedText.setText(getString(R.string.void_vpn_establish, getString(R.string.app_name))); - routedText.setVisibility(VISIBLE); - vpnRoute.setVisibility(GONE); - colorBackgroundALittle(); - } else { - mainButton.setText(activity.getString(R.string.vpn_button_turn_on)); - setMainButtonEnabled(true); - vpnStateImage.setStateIcon(R.drawable.vpn_disconnected); - vpnStateImage.stopProgress(false); - routedText.setVisibility(GONE); - vpnRoute.setVisibility(GONE); - greyscaleBackground(); - } - } - - private void showToast(Activity activity, String message, boolean vibrateLong) { - LayoutInflater inflater = getLayoutInflater(); - View layout = inflater.inflate(R.layout.custom_toast, - activity.findViewById(R.id.custom_toast_container)); - - TextView text = layout.findViewById(R.id.text); - text.setText(message); - - Vibrator v = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE); - if (vibrateLong) { - v.vibrate(100); - v.vibrate(200); - } else { - v.vibrate(100); - } - - Toast toast = new Toast(activity.getApplicationContext()); - toast.setGravity(Gravity.BOTTOM, 0, convertDimensionToPx(this.getContext(), R.dimen.stdpadding)); - toast.setDuration(Toast.LENGTH_LONG); - toast.setView(layout); - toast.show(); - } - private void showReconnectToast(Activity activity) { - String message = (String.format("Retry %d of %d before the next closest gateway will be selected.", connectionRetry()+1, 5)); - showToast(activity, message, false); - } - - private void showRetryToast(Activity activity) { - int nClosestGateway = gatewayOrder(); - String message = String.format("Server number " + nClosestGateway + " not reachable. Trying next gateway."); - showToast(activity, message, true ); - } - - private void showConnectingLayout(Context activity) { - showConnectionTransitionLayout(activity, true); - } - - private void showDisconnectingLayout(Activity activity) { - showConnectionTransitionLayout(activity, false); - } - - private void showConnectionTransitionLayout(Context activity, boolean isConnecting) { - mainButton.setText(activity.getString(android.R.string.cancel)); - vpnStateImage.setStateIcon(R.drawable.vpn_connecting); - vpnStateImage.showProgress(); - routedText.setVisibility(GONE); - vpnRoute.setVisibility(GONE); - if (isConnecting) { - colorBackgroundALittle(); - } else { - greyscaleBackground(); - } - } - - private boolean isOpenVpnRunningWithoutNetwork() { - boolean isRunning = false; - try { - isRunning = eipStatus.getLevel() == LEVEL_NONETWORK && - mService.isVpnRunning(); - } catch (Exception e) { - //eat me - e.printStackTrace(); - } - - return isRunning; - } - - private void bindOpenVpnService() { - Activity activity = getActivity(); - if (activity == null) { - Log.e(TAG, "activity is null when binding OpenVpn"); - return; - } - - Intent intent = new Intent(activity, OpenVPNService.class); - intent.setAction(OpenVPNService.START_SERVICE); - activity.bindService(intent, openVpnConnection, Context.BIND_AUTO_CREATE); - - } - - private void greyscaleBackground() { - ColorMatrix matrix = new ColorMatrix(); - matrix.setSaturation(0); - ColorMatrixColorFilter cf = new ColorMatrixColorFilter(matrix); - background.setColorFilter(cf); - background.setImageAlpha(255); - } - - private void colorBackgroundALittle() { - background.setColorFilter(null); - background.setImageAlpha(144); - } - - private void colorBackground() { - background.setColorFilter(null); - background.setImageAlpha(210); - } - - private void updateInvalidVpnCertificate() { - ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider); - } - - private void askUserToLogIn(String userMessage) { - Intent intent = new Intent(getContext(), LoginActivity.class); - intent.putExtra(PROVIDER_KEY, provider); - - if(userMessage != null) { - intent.putExtra(USER_MESSAGE, userMessage); - } - - Activity activity = getActivity(); - if (activity != null) { - activity.startActivityForResult(intent, REQUEST_CODE_LOG_IN); - } - } - - private void setVpnRouteText() { - String vpnRouteString = provider.getName(); - String profileName = VpnStatus.getLastConnectedVpnName(); - if (!TextUtils.isEmpty(profileName)) { - vpnRouteString += " (" + profileName + ")"; - } - vpnRoute.setText(vpnRouteString); - } - - private class EipFragmentServiceConnection implements ServiceConnection { - @Override - public void onServiceConnected(ComponentName className, - IBinder service) { - mService = IOpenVPNServiceInternal.Stub.asInterface(service); - handleNewState(); - } - - @Override - public void onServiceDisconnected(ComponentName arg0) { - mService = null; - } - } - - public void showDonationReminderDialog() { - try { - FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( - getActivity().getSupportFragmentManager()).removePreviousFragment( - DonationReminderDialog.TAG); - DialogFragment newFragment = new DonationReminderDialog(); - newFragment.setCancelable(false); - newFragment.show(fragmentTransaction, DonationReminderDialog.TAG); - } catch (IllegalStateException | NullPointerException e) { - e.printStackTrace(); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/EipSetupListener.java b/app/src/main/java/se/leap/bitmaskclient/EipSetupListener.java deleted file mode 100644 index 71e2fd52..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/EipSetupListener.java +++ /dev/null @@ -1,12 +0,0 @@ -package se.leap.bitmaskclient; - -import android.content.Intent; - -/** - * Created by cyberta on 05.12.18. - */ -public interface EipSetupListener { - void handleEipEvent(Intent intent); - - void handleProviderApiEvent(Intent intent); -} diff --git a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java deleted file mode 100644 index e365c857..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java +++ /dev/null @@ -1,375 +0,0 @@ -/** - * Copyright (c) 2020 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.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.util.Log; - -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import org.json.JSONObject; - -import java.util.Vector; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import de.blinkt.openvpn.LaunchVPN; -import de.blinkt.openvpn.VpnProfile; -import de.blinkt.openvpn.core.ConnectionStatus; -import de.blinkt.openvpn.core.LogItem; -import de.blinkt.openvpn.core.VpnStatus; -import se.leap.bitmaskclient.appUpdate.DownloadServiceCommand; -import se.leap.bitmaskclient.eip.EIP; -import se.leap.bitmaskclient.eip.EipCommand; -import se.leap.bitmaskclient.eip.EipStatus; -import se.leap.bitmaskclient.eip.Gateway; -import se.leap.bitmaskclient.eip.GatewaysManager; -import se.leap.bitmaskclient.utils.PreferenceHelper; - -import static android.app.Activity.RESULT_CANCELED; -import static android.content.Intent.CATEGORY_DEFAULT; -import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET; -import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NOTCONNECTED; -import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT; -import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT; -import static se.leap.bitmaskclient.Constants.BROADCAST_PROVIDER_API_EVENT; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_PREPARE_VPN; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_ALWAYS_ON_VPN; -import static se.leap.bitmaskclient.Constants.EIP_EARLY_ROUTES; -import static se.leap.bitmaskclient.Constants.EIP_REQUEST; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE; -import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE; -import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON; -import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON; -import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE; - -/** - * Created by cyberta on 05.12.18. - */ -class EipSetupObserver extends BroadcastReceiver implements VpnStatus.StateListener, VpnStatus.LogListener { - - private static final String TAG = EipSetupObserver.class.getName(); - - //The real timout is 4*2s + 1*4s + 1*8s + 1*16s + 1*32s + 1*64s = 132 s; - private static final String TIMEOUT = "4"; - private static final int UPDATE_CHECK_TIMEOUT = 1000*60*60*24*7; - private Context context; - private VpnProfile setupVpnProfile; - private String observedProfileFromVpnStatus; - AtomicBoolean changingGateway = new AtomicBoolean(false); - AtomicInteger setupNClosestGateway = new AtomicInteger(); - AtomicInteger reconnectTry = new AtomicInteger(); - private Vector listeners = new Vector<>(); - private SharedPreferences preferences; - private static EipSetupObserver instance; - - private EipSetupObserver(Context context, SharedPreferences preferences) { - this.context = context; - this.preferences = preferences; - IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT); - updateIntentFilter.addAction(BROADCAST_EIP_EVENT); - updateIntentFilter.addAction(BROADCAST_PROVIDER_API_EVENT); - updateIntentFilter.addCategory(CATEGORY_DEFAULT); - LocalBroadcastManager.getInstance(context.getApplicationContext()).registerReceiver(this, updateIntentFilter); - instance = this; - VpnStatus.addLogListener(this); - } - - public static void init(Context context, SharedPreferences preferences) { - if (instance == null) { - instance = new EipSetupObserver(context, preferences); - } - } - - public static boolean reconnectingWithDifferentGateway() { - return instance.setupNClosestGateway.get() > 0; - } - - public static int connectionRetry() { - return instance.reconnectTry.get(); - } - - public static int gatewayOrder() { - return instance.setupNClosestGateway.get(); - } - - public static synchronized void addListener(EipSetupListener listener) { - if (instance.listeners.contains(listener)) { - return; - } - instance.listeners.add(listener); - } - - public static synchronized void removeListener(EipSetupListener listener) { - instance.listeners.remove(listener); - } - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action == null) { - return; - } - - switch (action) { - case BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT: - handleGatewaySetupObserverEvent(intent); - break; - case BROADCAST_EIP_EVENT: - handleEipEvent(intent); - break; - case BROADCAST_PROVIDER_API_EVENT: - handleProviderApiEvent(intent); - break; - default: - break; - } - } - - private void handleProviderApiEvent(Intent intent) { - int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); - Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY); - if (resultData == null) { - resultData = Bundle.EMPTY; - } - - Provider provider; - switch (resultCode) { - case CORRECTLY_DOWNLOADED_EIP_SERVICE: - Log.d(TAG, "correctly updated service json"); - provider = resultData.getParcelable(PROVIDER_KEY); - ProviderObservable.getInstance().updateProvider(provider); - PreferenceHelper.storeProviderInPreferences(preferences, provider); - if (EipStatus.getInstance().isDisconnected()) { - EipCommand.startVPN(context.getApplicationContext(), true); - } - break; - case CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE: - provider = resultData.getParcelable(PROVIDER_KEY); - ProviderObservable.getInstance().updateProvider(provider); - PreferenceHelper.storeProviderInPreferences(preferences, provider); - EipCommand.startVPN(context.getApplicationContext(), true); - break; - case CORRECTLY_DOWNLOADED_GEOIP_JSON: - provider = resultData.getParcelable(PROVIDER_KEY); - ProviderObservable.getInstance().updateProvider(provider); - PreferenceHelper.storeProviderInPreferences(preferences, provider); - maybeStartEipService(resultData); - break; - case INCORRECTLY_DOWNLOADED_GEOIP_JSON: - maybeStartEipService(resultData); - break; - default: - break; - } - - for (EipSetupListener listener : listeners) { - listener.handleProviderApiEvent(intent); - } - } - - private void maybeStartEipService(Bundle resultData) { - if (resultData.getBoolean(EIP_ACTION_START)) { - boolean earlyRoutes = resultData.getBoolean(EIP_EARLY_ROUTES); - EipCommand.startVPN(context.getApplicationContext(), earlyRoutes); - } - } - - - private void handleEipEvent(Intent intent) { - int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); - Bundle result = intent.getBundleExtra(BROADCAST_RESULT_KEY); - String eipRequest = result.getString(EIP_REQUEST); - EIP.EIPErrors error = EIP.EIPErrors.UNKNOWN; - try { - JSONObject jsonObject = new JSONObject(result.getString(EIP.ERRORS)); - error = EIP.EIPErrors.valueOf(jsonObject.getString(EIP.ERRORID)); - } catch (Exception e) { - //ignore - } - if (eipRequest == null) { - return; - } - switch (eipRequest) { - case EIP_ACTION_START: - case EIP_ACTION_START_ALWAYS_ON_VPN: - if (resultCode == RESULT_CANCELED) { - //setup failed - if (error == EIP.EIPErrors.NO_MORE_GATEWAYS) { - finishGatewaySetup(false); - EipCommand.startBlockingVPN(context.getApplicationContext()); - } else { - //FIXME: - finishGatewaySetup(false); - EipCommand.stopVPN(context); - EipStatus.refresh(); - } - } - break; - case EIP_ACTION_PREPARE_VPN: - if (resultCode == RESULT_CANCELED) { - VpnStatus.logError("Error preparing VpnService."); - finishGatewaySetup(false); - EipStatus.refresh(); - } - break; - default: - break; - } - - for (EipSetupListener listener : listeners) { - listener.handleEipEvent(intent); - } - } - - private void handleGatewaySetupObserverEvent(Intent event) { - if (observedProfileFromVpnStatus != null || setupVpnProfile != null) { - //finish last setup observation - Log.d(TAG, "finish last gateway setup"); - finishGatewaySetup(true); - } - - VpnProfile vpnProfile = (VpnProfile) event.getSerializableExtra(PROVIDER_PROFILE); - if (vpnProfile == null) { - Log.e(TAG, "Tried to setup non existing vpn profile."); - return; - } - setupVpnProfile = vpnProfile; - setupNClosestGateway.set(event.getIntExtra(Gateway.KEY_N_CLOSEST_GATEWAY, 0)); - Log.d(TAG, "bitmaskapp add state listener"); - VpnStatus.addStateListener(this); - - launchVPN(setupVpnProfile); - } - - private void launchVPN(VpnProfile vpnProfile) { - Intent intent = new Intent(context.getApplicationContext(), LaunchVPN.class); - intent.setAction(Intent.ACTION_MAIN); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(LaunchVPN.EXTRA_HIDELOG, true); - intent.putExtra(PROVIDER_PROFILE, vpnProfile); - intent.putExtra(Gateway.KEY_N_CLOSEST_GATEWAY, setupNClosestGateway.get()); - context.startActivity(intent); - } - - @Override - public void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level) { - // VpnStatus.updateStateString("NOPROCESS", "No process running.", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); - - Log.d(TAG, "vpn status: " + state + " - " + logmessage + " - " + level); - if (observedProfileFromVpnStatus == null || - setupVpnProfile == null) { - return; - } - if (!observedProfileFromVpnStatus.equals(setupVpnProfile.getUUIDString())) { - Log.d(TAG, "vpn profile to setup and observed profile currently is used differ: " + setupVpnProfile.getUUIDString() + " vs. " + observedProfileFromVpnStatus); - return; - } - - if (ConnectionStatus.LEVEL_STOPPING == level) { - finishGatewaySetup(false); - } else if ("CONNECTRETRY".equals(state) && LEVEL_CONNECTING_NO_SERVER_REPLY_YET.equals(level)) { - Log.d(TAG, "trying gateway: " + setupVpnProfile.getName()); - if (TIMEOUT.equals(logmessage)) { - Log.e(TAG, "Timeout reached! Try next gateway!"); - VpnStatus.logError("Timeout reached! Try next gateway!"); - selectNextGateway(); - return; - } - int current = reconnectTry.get(); - reconnectTry.set(current + 1); - } else if ("NOPROCESS".equals(state) && LEVEL_NOTCONNECTED == level) { - //?? - } else if ("CONNECTED".equals(state)) { - //saveLastProfile(context.getApplicationContext(), setupVpnProfile.getUUIDString()); - Provider provider = ProviderObservable.getInstance().getCurrentProvider(); - if (setupNClosestGateway.get() > 0 || provider.shouldUpdateEipServiceJson()) { - //setupNClostestGateway > 0: at least one failed gateway -> did the provider change it's gateways? - ProviderAPICommand.execute(context, ProviderAPI.DOWNLOAD_SERVICE_JSON, provider); - } - - if (shouldCheckAppUpdate()) { - DownloadServiceCommand.execute(context, CHECK_VERSION_FILE); - } - finishGatewaySetup(false); - } else if ("TCP_CONNECT".equals(state)) { - changingGateway.set(false); - } - } - - private boolean shouldCheckAppUpdate() { - return System.currentTimeMillis() - PreferenceHelper.getLastAppUpdateCheck(context) >= UPDATE_CHECK_TIMEOUT; - } - - private void selectNextGateway() { - changingGateway.set(true); - reconnectTry.set(0); - EipCommand.startVPN(context.getApplicationContext(), false, setupNClosestGateway.get() + 1); - } - - private void finishGatewaySetup(boolean changingGateway) { - VpnStatus.removeStateListener(this); - setupVpnProfile = null; - setupNClosestGateway.set(0); - observedProfileFromVpnStatus = null; - this.changingGateway.set(changingGateway); - this.reconnectTry.set(0); - } - - /** - * gets called as soon as a new VPN is about to launch - * - * @param uuid - */ - @Override - public void setConnectedVPN(String uuid) { - observedProfileFromVpnStatus = uuid; - } - - @Override - public void newLog(LogItem logItem) { - if (logItem.getLogLevel() == VpnStatus.LogLevel.ERROR) { - switch (logItem.getErrorType()) { - case SHAPESHIFTER: - VpnProfile profile = VpnStatus.getLastConnectedVpnProfile(); - if (profile == null) { - EipCommand.startVPN(context.getApplicationContext(), false, 0); - } else { - GatewaysManager gatewaysManager = new GatewaysManager(context.getApplicationContext()); - int position = gatewaysManager.getPosition(profile); - setupNClosestGateway.set(position >= 0 ? position : 0); - selectNextGateway(); - } - break; - default: - break; - - } - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java b/app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java deleted file mode 100644 index 519e4fc2..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/FeatureVersionCode.java +++ /dev/null @@ -1,6 +0,0 @@ -package se.leap.bitmaskclient; - -public interface FeatureVersionCode { - int RENAMED_EIP_IN_PREFERENCES = 132; - int GEOIP_SERVICE = 148; -} diff --git a/app/src/main/java/se/leap/bitmaskclient/FragmentManagerEnhanced.java b/app/src/main/java/se/leap/bitmaskclient/FragmentManagerEnhanced.java deleted file mode 100644 index f4215e89..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/FragmentManagerEnhanced.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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; - -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; - -public class FragmentManagerEnhanced { - - private FragmentManager genericFragmentManager; - - public FragmentManagerEnhanced(FragmentManager genericFragmentManager) { - this.genericFragmentManager = genericFragmentManager; - } - - public FragmentTransaction removePreviousFragment(String tag) { - FragmentTransaction transaction = genericFragmentManager.beginTransaction(); - Fragment previousFragment = genericFragmentManager.findFragmentByTag(tag); - if (previousFragment != null) { - transaction.remove(previousFragment); - } - - return transaction; - } - - public void replace(int containerViewId, Fragment fragment, String tag) { - try { - if (genericFragmentManager.findFragmentByTag(tag) != null) { - FragmentTransaction transaction = genericFragmentManager.beginTransaction(); - transaction.replace(containerViewId, fragment, tag).commit(); - } else { - genericFragmentManager.beginTransaction().add(containerViewId, fragment, tag).commit(); - } - } catch (IllegalStateException e) { - e.printStackTrace(); - } - - } - - public Fragment findFragmentByTag(String tag) { - return genericFragmentManager.findFragmentByTag(tag); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java b/app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java deleted file mode 100644 index d1f1ed21..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java +++ /dev/null @@ -1,361 +0,0 @@ -/** - * 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; - - -import org.jboss.security.srp.SRPParameters; - -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Arrays; - -import se.leap.bitmaskclient.utils.ConfigHelper; - -/** - * Implements all SRP algorithm logic. - *

- * It's derived from JBoss implementation, with adjustments to make it work with LEAP platform. - * - * @author parmegv - */ -public class LeapSRPSession { - - private static String token = ""; - - final public static String SALT = "salt"; - final public static String M1 = "M1"; - final public static String M2 = "M2"; - final public static String TOKEN = "token"; - final public static String AUTHORIZATION_HEADER = "Authorization"; - final public static String TAG = "Leap SRP session class tag"; - - private SRPParameters params; - private String username; - private String password; - private BigInteger N; - private byte[] N_bytes; - private BigInteger g; - private BigInteger x; - private BigInteger v; - private BigInteger a; - private BigInteger A; - private byte[] K; - private SecureRandom pseudoRng; - /** - * The M1 = H(H(N) xor H(g) | H(U) | s | A | B | K) hash - */ - private MessageDigest clientHash; - /** - * The M2 = H(A | M | K) hash - */ - private MessageDigest serverHash; - - private static int A_LEN; - - - /** - * Creates a new SRP server session object from the username, password - * verifier, - * - * @param username, the user ID - * @param password, the user clear text password - */ - public LeapSRPSession(String username, String password) { - this(username, password, null); - } - - /** - * Creates a new SRP server session object from the username, password - * verifier, - * - * @param username, the user ID - * @param password, the user clear text password - * @param abytes, the random exponent used in the A public key - */ - public LeapSRPSession(String username, String password, byte[] abytes) { - - params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), ConfigHelper.G.toByteArray(), BigInteger.ZERO.toByteArray(), "SHA-256"); - this.g = new BigInteger(1, params.g); - N_bytes = ConfigHelper.trim(params.N); - this.N = new BigInteger(1, N_bytes); - this.username = username; - this.password = password; - - try { - pseudoRng = SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - if (abytes != null) { - A_LEN = 8 * abytes.length; - /* TODO Why did they put this condition? - if( 8*abytes.length != A_LEN ) - throw new IllegalArgumentException("The abytes param must be " - +(A_LEN/8)+" in length, abytes.length="+abytes.length); - */ - this.a = new BigInteger(abytes); - } else - A_LEN = 64; - - serverHash = newDigest(); - clientHash = newDigest(); - } - - /** - * Calculates the parameter x of the SRP-6a algorithm. - * - * @param username - * @param password - * @param salt the salt of the user - * @return x - */ - public byte[] calculatePasswordHash(String username, String password, byte[] salt) { - //password = password.replaceAll("\\\\", "\\\\\\\\"); - // Calculate x = H(s | H(U | ':' | password)) - MessageDigest x_digest = newDigest(); - // Try to convert the username to a byte[] using ISO-8859-1 - byte[] user = null; - byte[] password_bytes = null; - byte[] colon = {}; - String encoding = "ISO-8859-1"; - try { - user = ConfigHelper.trim(username.getBytes(encoding)); - colon = ConfigHelper.trim(":".getBytes(encoding)); - password_bytes = ConfigHelper.trim(password.getBytes(encoding)); - } catch (UnsupportedEncodingException e) { - // Use the default platform encoding - user = ConfigHelper.trim(username.getBytes()); - colon = ConfigHelper.trim(":".getBytes()); - password_bytes = ConfigHelper.trim(password.getBytes()); - } - - // Build the hash - x_digest.update(user); - x_digest.update(colon); - x_digest.update(password_bytes); - byte[] h = x_digest.digest(); - - x_digest.reset(); - x_digest.update(salt); - x_digest.update(h); - byte[] x_digest_bytes = x_digest.digest(); - - return x_digest_bytes; - } - - public byte[] calculateNewSalt() { - try { - BigInteger salt = new BigInteger(64, SecureRandom.getInstance("SHA1PRNG")); - return ConfigHelper.trim(salt.toByteArray()); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } - return null; - } - - /** - * Calculates the parameter V of the SRP-6a algorithm. - * - * @return the value of V - */ - public BigInteger calculateV(String username, String password, byte[] salt) { - byte[] x_bytes = calculatePasswordHash(username, password, ConfigHelper.trim(salt)); - x = new BigInteger(1, x_bytes); - BigInteger v = g.modPow(x, N); // g^x % N - return v; - } - - /** - * Calculates the trimmed xor from two BigInteger numbers - * - * @param b1 the positive source to build first BigInteger - * @param b2 the positive source to build second BigInteger - * @return - */ - public byte[] xor(byte[] b1, byte[] b2) { - //TODO Check if length matters in the order, when b2 is smaller than b1 or viceversa - byte[] xor_digest = new BigInteger(1, b1).xor(new BigInteger(1, b2)).toByteArray(); - return ConfigHelper.trim(xor_digest); - } - - /** - * @returns The exponential residue (parameter A) to be sent to the server. - */ - public byte[] exponential() { - byte[] Abytes = null; - if (A == null) { - /* If the random component of A has not been specified use a random - number */ - if (a == null) { - BigInteger one = BigInteger.ONE; - do { - a = new BigInteger(A_LEN, pseudoRng); - } while (a.compareTo(one) <= 0); - } - A = g.modPow(a, N); - Abytes = ConfigHelper.trim(A.toByteArray()); - } - return Abytes; - } - - /** - * Calculates the parameter M1, to be sent to the SRP server. - * It also updates hashes of client and server for further calculations in other methods. - * It uses a predefined k. - * - * @param salt_bytes - * @param Bbytes the parameter received from the server, in bytes - * @return the parameter M1 - * @throws NoSuchAlgorithmException - */ - public byte[] response(byte[] salt_bytes, byte[] Bbytes) { - // Calculate x = H(s | H(U | ':' | password)) - byte[] M1 = null; - if (new BigInteger(1, Bbytes).mod(new BigInteger(1, N_bytes)) != BigInteger.ZERO) { - this.v = calculateV(username, password, salt_bytes); - // H(N) - byte[] digest_of_n = newDigest().digest(N_bytes); - - // H(g) - byte[] digest_of_g = newDigest().digest(params.g); - - // clientHash = H(N) xor H(g) - byte[] xor_digest = xor(digest_of_n, digest_of_g); - clientHash.update(xor_digest); - - // clientHash = H(N) xor H(g) | H(U) - byte[] username_digest = newDigest().digest(ConfigHelper.trim(username.getBytes())); - username_digest = ConfigHelper.trim(username_digest); - clientHash.update(username_digest); - - // clientHash = H(N) xor H(g) | H(U) | s - clientHash.update(ConfigHelper.trim(salt_bytes)); - - K = null; - - // clientHash = H(N) xor H(g) | H(U) | A - byte[] Abytes = ConfigHelper.trim(A.toByteArray()); - clientHash.update(Abytes); - - // clientHash = H(N) xor H(g) | H(U) | s | A | B - Bbytes = ConfigHelper.trim(Bbytes); - clientHash.update(Bbytes); - - // Calculate S = (B - kg^x) ^ (a + u * x) % N - BigInteger S = calculateS(Bbytes); - byte[] S_bytes = ConfigHelper.trim(S.toByteArray()); - - // K = SessionHash(S) - MessageDigest sessionDigest = newDigest(); - K = ConfigHelper.trim(sessionDigest.digest(S_bytes)); - - // clientHash = H(N) xor H(g) | H(U) | A | B | K - clientHash.update(K); - - M1 = ConfigHelper.trim(clientHash.digest()); - - // serverHash = Astr + M + K - serverHash.update(Abytes); - serverHash.update(M1); - serverHash.update(K); - - } - return M1; - } - - /** - * It calculates the parameter S used by response() to obtain session hash K. - * - * @param Bbytes the parameter received from the server, in bytes - * @return the parameter S - */ - private BigInteger calculateS(byte[] Bbytes) { - byte[] Abytes = ConfigHelper.trim(A.toByteArray()); - Bbytes = ConfigHelper.trim(Bbytes); - byte[] u_bytes = getU(Abytes, Bbytes); - - BigInteger B = new BigInteger(1, Bbytes); - BigInteger u = new BigInteger(1, u_bytes); - String k_string = "bf66c44a428916cad64aa7c679f3fd897ad4c375e9bbb4cbf2f5de241d618ef0"; - BigInteger k = new BigInteger(k_string, 16); - BigInteger B_minus_v = B.subtract(k.multiply(v)); - BigInteger a_ux = a.add(u.multiply(x)); - BigInteger S = B_minus_v.modPow(a_ux, N); - return S; - } - - /** - * It calculates the parameter u used by calculateS to obtain S. - * - * @param Abytes the exponential residue sent to the server - * @param Bbytes the parameter received from the server, in bytes - * @return - */ - public byte[] getU(byte[] Abytes, byte[] Bbytes) { - MessageDigest u_digest = newDigest(); - u_digest.update(ConfigHelper.trim(Abytes)); - u_digest.update(ConfigHelper.trim(Bbytes)); - byte[] u_digest_bytes = u_digest.digest(); - return ConfigHelper.trim(new BigInteger(1, u_digest_bytes).toByteArray()); - } - - /** - * @param M2 The server's response to the client's challenge - * @returns True if and only if the server's response was correct. - */ - public boolean verify(byte[] M2) { - // M2 = H(A | M1 | K) - M2 = ConfigHelper.trim(M2); - byte[] myM2 = ConfigHelper.trim(serverHash.digest()); - boolean valid = Arrays.equals(M2, myM2); - return valid; - } - - protected static void setToken(String token) { - LeapSRPSession.token = token; - } - - protected static String getToken() { - return token; - } - - public static boolean loggedIn() { - return !token.isEmpty(); - } - - /** - * @return a new SHA-256 digest. - */ - public MessageDigest newDigest() { - MessageDigest md = null; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } - return md; - } - - public byte[] getK() { - return K; - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/LoginActivity.java b/app/src/main/java/se/leap/bitmaskclient/LoginActivity.java deleted file mode 100644 index 15166c67..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/LoginActivity.java +++ /dev/null @@ -1,31 +0,0 @@ -package se.leap.bitmaskclient; - -import android.os.Bundle; -import androidx.annotation.Nullable; - -import butterknife.OnClick; - -/** - * Activity to login to chosen Provider - * - * Created by fupduck on 09.01.18. - */ - -public class LoginActivity extends ProviderCredentialsBaseActivity { - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setProgressbarText(R.string.logging_in); - setProviderHeaderLogo(R.drawable.logo); - setProviderHeaderText(R.string.login_to_profile); - } - - @Override - @OnClick(R.id.button) - void handleButton() { - super.handleButton(); - login(getUsername(), getPassword()); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java deleted file mode 100644 index 867f66e5..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java +++ /dev/null @@ -1,363 +0,0 @@ -/** - * 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.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import androidx.annotation.StringRes; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Observable; -import java.util.Observer; - -import se.leap.bitmaskclient.drawer.NavigationDrawerFragment; -import se.leap.bitmaskclient.eip.EIP; -import se.leap.bitmaskclient.eip.EipCommand; -import se.leap.bitmaskclient.fragments.ExcludeAppsFragment; -import se.leap.bitmaskclient.fragments.LogFragment; -import se.leap.bitmaskclient.utils.PreferenceHelper; - -import static se.leap.bitmaskclient.Constants.ASK_TO_CANCEL_VPN; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_PREPARE_VPN; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; -import static se.leap.bitmaskclient.Constants.EIP_REQUEST; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_CONFIGURE_LEAP; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_LOG_IN; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_SWITCH_PROVIDER; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; -import static se.leap.bitmaskclient.ProviderAPI.ERRORID; -import static se.leap.bitmaskclient.ProviderAPI.ERRORS; -import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE; -import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE; -import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed; -import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; -import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_VPN_PREPARE; -import static se.leap.bitmaskclient.utils.PreferenceHelper.storeProviderInPreferences; - - -public class MainActivity extends AppCompatActivity implements EipSetupListener, Observer, ExcludeAppsFragment.ExcludedAppsCallback { - - public final static String TAG = MainActivity.class.getSimpleName(); - - private Provider provider; - private SharedPreferences preferences; - private NavigationDrawerFragment navigationDrawerFragment; - - public final static String ACTION_SHOW_VPN_FRAGMENT = "action_show_vpn_fragment"; - public final static String ACTION_SHOW_LOG_FRAGMENT = "action_show_log_fragment"; - - /** - * Fragment managing the behaviors, interactions and presentation of the navigation drawer. - */ - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.a_main); - setSupportActionBar(findViewById(R.id.toolbar)); - - navigationDrawerFragment = (NavigationDrawerFragment) - getSupportFragmentManager().findFragmentById(R.id.navigation_drawer); - - preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - provider = ProviderObservable.getInstance().getCurrentProvider(); - - EipSetupObserver.addListener(this); - // Set up the drawer. - navigationDrawerFragment.setUp(R.id.navigation_drawer, findViewById(R.id.drawer_layout)); - handleIntentAction(getIntent()); - } - - @Override - public void onBackPressed() { - FragmentManagerEnhanced fragmentManagerEnhanced = new FragmentManagerEnhanced(getSupportFragmentManager()); - Fragment fragment = fragmentManagerEnhanced.findFragmentByTag(MainActivity.TAG); - if (fragment == null || !(fragment instanceof EipFragment)) { - Fragment eipFragment = new EipFragment(); - Bundle bundle = new Bundle(); - bundle.putParcelable(PROVIDER_KEY, provider); - eipFragment.setArguments(bundle); - fragmentManagerEnhanced.replace(R.id.main_container, eipFragment, MainActivity.TAG); - hideActionBarSubTitle(); - } else { - super.onBackPressed(); - } - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - handleIntentAction(intent); - } - - private void handleIntentAction(Intent intent) { - if (intent == null || intent.getAction() == null) { - return; - } - - Fragment fragment = null; - switch (intent.getAction()) { - case ACTION_SHOW_VPN_FRAGMENT: - fragment = new EipFragment(); - Bundle bundle = new Bundle(); - if (intent.hasExtra(ASK_TO_CANCEL_VPN)) { - bundle.putBoolean(ASK_TO_CANCEL_VPN, true); - } - bundle.putParcelable(PROVIDER_KEY, provider); - fragment.setArguments(bundle); - hideActionBarSubTitle(); - break; - case ACTION_SHOW_LOG_FRAGMENT: - fragment = new LogFragment(); - setActionBarTitle(R.string.log_fragment_title); - break; - default: - break; - } - // on layout change / recreation of the activity, we don't want create new Fragments - // instead the fragments themselves care about recreation and state restoration - intent.setAction(null); - - if (fragment != null) { - new FragmentManagerEnhanced(getSupportFragmentManager()) - .replace(R.id.main_container, fragment, MainActivity.TAG); - } - } - - private void hideActionBarSubTitle() { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setSubtitle(null); - } - } - private void setActionBarTitle(@StringRes int stringId) { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setSubtitle(stringId); - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (data == null) { - return; - } - - if (resultCode == RESULT_OK && data.hasExtra(Provider.KEY)) { - provider = data.getParcelableExtra(Provider.KEY); - - if (provider == null) { - return; - } - - storeProviderInPreferences(preferences, provider); - ProviderObservable.getInstance().updateProvider(provider); - if (!provider.supportsPluggableTransports()) { - PreferenceHelper.usePluggableTransports(this, false); - } - navigationDrawerFragment.refresh(); - - switch (requestCode) { - case REQUEST_CODE_SWITCH_PROVIDER: - EipCommand.stopVPN(this.getApplicationContext()); - break; - case REQUEST_CODE_CONFIGURE_LEAP: - Log.d(TAG, "REQUEST_CODE_CONFIGURE_LEAP - onActivityResult - MainActivity"); - break; - case REQUEST_CODE_LOG_IN: - EipCommand.startVPN(this.getApplicationContext(), true); - break; - } - } - - // on switch provider we need to set the EIP fragment - Fragment fragment = new EipFragment(); - Bundle arguments = new Bundle(); - arguments.putParcelable(PROVIDER_KEY, provider); - fragment.setArguments(arguments); - new FragmentManagerEnhanced(getSupportFragmentManager()) - .replace(R.id.main_container, fragment, MainActivity.TAG); - hideActionBarSubTitle(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - EipSetupObserver.removeListener(this); - } - - @Override - public void handleEipEvent(Intent intent) { - int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); - Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY); - if (resultData == null) { - resultData = Bundle.EMPTY; - } - String request = resultData.getString(EIP_REQUEST); - - if (request == null) { - return; - } - - switch (request) { - case EIP_ACTION_START: - if (resultCode == RESULT_CANCELED) { - String error = resultData.getString(ERRORS); - if (isInternalErrorHandling(error)) { - return; - } - - if (LeapSRPSession.loggedIn() || provider.allowsAnonymous()) { - showMainActivityErrorDialog(error); - } else if (isInvalidCertificateForLoginOnlyProvider(error)) { - askUserToLogIn(getString(vpn_certificate_user_message)); - } - } - break; - case EIP_ACTION_PREPARE_VPN: - if (resultCode == RESULT_CANCELED) { - showMainActivityErrorDialog(getString(R.string.vpn_error_establish), ERROR_VPN_PREPARE); - } - break; - } - } - - @Override - public void handleProviderApiEvent(Intent intent) { - int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); - - switch (resultCode) { - case INCORRECTLY_DOWNLOADED_EIP_SERVICE: - // TODO CATCH ME IF YOU CAN - WHAT DO WE WANT TO DO? - break; - case INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE: - if (LeapSRPSession.loggedIn() || provider.allowsAnonymous()) { - showMainActivityErrorDialog(getString(downloading_vpn_certificate_failed)); - } else { - askUserToLogIn(getString(vpn_certificate_user_message)); - } - break; - } - } - - @Override - public void update(Observable o, Object arg) { - if (o instanceof ProviderObservable) { - this.provider = ((ProviderObservable) o).getCurrentProvider(); - } - } - - /** - * Shows an error dialog - */ - public void showMainActivityErrorDialog(String reasonToFail) { - try { - - FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( - this.getSupportFragmentManager()).removePreviousFragment( - MainActivityErrorDialog.TAG); - DialogFragment newFragment; - try { - JSONObject errorJson = new JSONObject(reasonToFail); - newFragment = MainActivityErrorDialog.newInstance(provider, errorJson); - } catch (JSONException e) { - e.printStackTrace(); - newFragment = MainActivityErrorDialog.newInstance(provider, reasonToFail); - } - newFragment.show(fragmentTransaction, MainActivityErrorDialog.TAG); - } catch (IllegalStateException | NullPointerException e) { - e.printStackTrace(); - Log.w(TAG, "error dialog leaked!"); - } - } - - /** - * Shows an error dialog - */ - public void showMainActivityErrorDialog(String reasonToFail, EIP.EIPErrors error) { - try { - FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( - this.getSupportFragmentManager()).removePreviousFragment( - MainActivityErrorDialog.TAG); - DialogFragment newFragment = MainActivityErrorDialog.newInstance(provider, reasonToFail, error); - newFragment.show(fragmentTransaction, MainActivityErrorDialog.TAG); - } catch (IllegalStateException | NullPointerException e) { - e.printStackTrace(); - Log.w(TAG, "error dialog leaked!"); - } - } - - /** - * - * @param errorJsonString - * @return true if errorJson is a valid json and contains only ERRORID but - * not an ERRORS field containing an error message - */ - public boolean isInternalErrorHandling(String errorJsonString) { - try { - JSONObject errorJson = new JSONObject(errorJsonString); - return !errorJson.has(ERRORS) && errorJson.has(ERRORID); - } catch (JSONException | NullPointerException e) { - e.printStackTrace(); - } - return false; - } - - public boolean isInvalidCertificateForLoginOnlyProvider(String errorJsonString) { - try { - JSONObject errorJson = new JSONObject(errorJsonString); - return ERROR_INVALID_VPN_CERTIFICATE.toString().equals(errorJson.getString(ERRORID)) && - !LeapSRPSession.loggedIn() && - !provider.allowsAnonymous(); - } catch (JSONException e) { - e.printStackTrace(); - } - return false; - } - - private void askUserToLogIn(String userMessage) { - Intent intent = new Intent(this, LoginActivity.class); - intent.putExtra(PROVIDER_KEY, provider); - if (userMessage != null) { - intent.putExtra(USER_MESSAGE, userMessage); - } - startActivityForResult(intent, REQUEST_CODE_LOG_IN); - } - - - @Override - public void onAppsExcluded(int number) { - navigationDrawerFragment.onAppsExcluded(number); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/MainActivityErrorDialog.java b/app/src/main/java/se/leap/bitmaskclient/MainActivityErrorDialog.java deleted file mode 100644 index 7e9bad22..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/MainActivityErrorDialog.java +++ /dev/null @@ -1,171 +0,0 @@ -/** - * 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.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.appcompat.app.AlertDialog; - -import org.json.JSONObject; - -import se.leap.bitmaskclient.eip.EIP; -import se.leap.bitmaskclient.eip.EipCommand; - -import static se.leap.bitmaskclient.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.R.string.warning_option_try_ovpn; -import static se.leap.bitmaskclient.R.string.warning_option_try_pt; -import static se.leap.bitmaskclient.eip.EIP.EIPErrors.UNKNOWN; -import static se.leap.bitmaskclient.eip.EIP.EIPErrors.valueOf; -import static se.leap.bitmaskclient.eip.EIP.ERRORS; -import static se.leap.bitmaskclient.eip.EIP.ERRORID; -import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports; -import static se.leap.bitmaskclient.utils.PreferenceHelper.usePluggableTransports; - -/** - * Implements an error dialog for the main activity. - * - * @author fupduck - * @author cyberta - */ -public class MainActivityErrorDialog extends DialogFragment { - - final public static String TAG = "downloaded_failed_dialog"; - final private static String KEY_REASON_TO_FAIL = "key reason to fail"; - final private static String KEY_PROVIDER = "key provider"; - private String reasonToFail; - private EIP.EIPErrors downloadError = UNKNOWN; - - private Provider provider; - - /** - * @return a new instance of this DialogFragment. - */ - public static DialogFragment newInstance(Provider provider, String reasonToFail) { - return newInstance(provider, reasonToFail, UNKNOWN); - } - - /** - * @return a new instance of this DialogFragment. - */ - public static DialogFragment newInstance(Provider provider, String reasonToFail, EIP.EIPErrors error) { - MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog(); - dialogFragment.reasonToFail = reasonToFail; - dialogFragment.provider = provider; - dialogFragment.downloadError = error; - return dialogFragment; - } - - /** - * @return a new instance of this DialogFragment. - */ - public static DialogFragment newInstance(Provider provider, JSONObject errorJson) { - MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog(); - dialogFragment.provider = provider; - try { - if (errorJson.has(ERRORS)) { - dialogFragment.reasonToFail = errorJson.getString(ERRORS); - } else { - //default error msg - dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); - } - - if (errorJson.has(ERRORID)) { - dialogFragment.downloadError = valueOf(errorJson.getString(ERRORID)); - } - } catch (Exception e) { - e.printStackTrace(); - dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); - } - return dialogFragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - restoreFromSavedInstance(savedInstanceState); - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - Context applicationContext = getContext().getApplicationContext(); - builder.setMessage(reasonToFail) - .setNegativeButton(R.string.cancel, (dialog, id) -> { - }); - switch (downloadError) { - case ERROR_INVALID_VPN_CERTIFICATE: - builder.setPositiveButton(R.string.update_certificate, (dialog, which) -> - ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider)); - break; - case NO_MORE_GATEWAYS: - if (provider.supportsPluggableTransports()) { - if (getUsePluggableTransports(applicationContext)) { - builder.setPositiveButton(warning_option_try_ovpn, ((dialog, which) -> { - usePluggableTransports(applicationContext, false); - EipCommand.startVPN(applicationContext, false); - })); - } else { - builder.setPositiveButton(warning_option_try_pt, ((dialog, which) -> { - usePluggableTransports(applicationContext, true); - EipCommand.startVPN(applicationContext, false); - })); - } - } else { - builder.setPositiveButton(R.string.retry, (dialog, which) -> { - EipCommand.startVPN(applicationContext, false); - }); - } - break; - case ERROR_VPN_PREPARE: - builder.setPositiveButton(R.string.retry, (dialog, which) -> { - EipCommand.startVPN(applicationContext, false); - }); - break; - default: - break; - } - - // Create the AlertDialog object and return it - return builder.create(); - } - - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString(KEY_REASON_TO_FAIL, reasonToFail); - outState.putParcelable(KEY_PROVIDER, provider); - } - - private void restoreFromSavedInstance(Bundle savedInstanceState) { - if (savedInstanceState == null) { - return; - } - if (savedInstanceState.containsKey(KEY_PROVIDER)) { - this.provider = savedInstanceState.getParcelable(KEY_PROVIDER); - } - if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) { - this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL); - } - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java deleted file mode 100644 index 576e76e0..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java +++ /dev/null @@ -1,182 +0,0 @@ -/** - * 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.content.SharedPreferences; -import android.content.res.Resources; -import android.os.Build; -import androidx.annotation.NonNull; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -import okhttp3.CipherSuite; -import okhttp3.ConnectionSpec; -import okhttp3.Cookie; -import okhttp3.CookieJar; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.TlsVersion; - -import static android.text.TextUtils.isEmpty; -import static se.leap.bitmaskclient.ProviderAPI.ERRORS; -import static se.leap.bitmaskclient.R.string.certificate_error; -import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; -import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message; -import static se.leap.bitmaskclient.R.string.keyChainAccessError; -import static se.leap.bitmaskclient.R.string.server_unreachable_message; -import static se.leap.bitmaskclient.utils.ConfigHelper.getProviderFormattedString; - -/** - * Created by cyberta on 08.01.18. - */ - -public class OkHttpClientGenerator { - - Resources resources; - - public OkHttpClientGenerator(/*SharedPreferences preferences,*/ Resources resources) { - this.resources = resources; - } - - public OkHttpClient initCommercialCAHttpClient(JSONObject initError) { - return initHttpClient(initError, null); - } - - public OkHttpClient initSelfSignedCAHttpClient(String caCert, JSONObject initError) { - return initHttpClient(initError, caCert); - } - - public OkHttpClient init() { - try { - return createClient(null); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - private OkHttpClient initHttpClient(JSONObject initError, String certificate) { - if (resources == null) { - return null; - } - try { - return createClient(certificate); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - // TODO ca cert is invalid - show better error ?! - addErrorMessageToJson(initError, getProviderFormattedString(resources, certificate_error)); - } catch (IllegalStateException | KeyManagementException | KeyStoreException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage())); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, resources.getString(error_no_such_algorithm_exception_user_message)); - } catch (CertificateException e) { - e.printStackTrace(); - // TODO ca cert is invalid - show better error ?! - addErrorMessageToJson(initError, getProviderFormattedString(resources, certificate_error)); - } catch (UnknownHostException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, resources.getString(server_unreachable_message)); - } catch (IOException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message)); - } catch (Exception e) { - e.printStackTrace(); - // unexpected exception, should never happen - // only to shorten the method signature createClient(String certificate) - } - return null; - } - - private OkHttpClient createClient(String certificate) throws Exception { - TLSCompatSocketFactory sslCompatFactory; - ConnectionSpec spec = getConnectionSpec(); - OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); - - if (!isEmpty(certificate)) { - sslCompatFactory = new TLSCompatSocketFactory(certificate); - } else { - sslCompatFactory = new TLSCompatSocketFactory(); - } - sslCompatFactory.initSSLSocketFactory(clientBuilder); - clientBuilder.cookieJar(getCookieJar()) - .connectionSpecs(Collections.singletonList(spec)); - clientBuilder.dns(new DnsResolver()); - return clientBuilder.build(); - } - - - - @NonNull - private ConnectionSpec getConnectionSpec() { - ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3); - //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels - //figure out how to use bcjsse for that purpose - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) - connectionSpecbuilder.cipherSuites( - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 - ); - return connectionSpecbuilder.build(); - } - - @NonNull - private CookieJar getCookieJar() { - return new CookieJar() { - private final HashMap> cookieStore = new HashMap<>(); - - @Override - public void saveFromResponse(HttpUrl url, List cookies) { - cookieStore.put(url.host(), cookies); - } - - @Override - public List loadForRequest(HttpUrl url) { - List cookies = cookieStore.get(url.host()); - return cookies != null ? cookies : new ArrayList(); - } - }; - } - - private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) { - try { - jsonObject.put(ERRORS, errorMessage); - } catch (JSONException e) { - e.printStackTrace(); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java b/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java deleted file mode 100644 index 2efce9e4..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/OnBootReceiver.java +++ /dev/null @@ -1,56 +0,0 @@ -package se.leap.bitmaskclient; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.util.Log; - -import de.blinkt.openvpn.core.VpnStatus; -import se.leap.bitmaskclient.utils.PreferenceHelper; - -import static android.content.Intent.ACTION_BOOT_COMPLETED; -import static se.leap.bitmaskclient.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE; -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; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; - -public class OnBootReceiver extends BroadcastReceiver { - - SharedPreferences preferences; - - // 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(SHARED_PREFERENCES, Context.MODE_PRIVATE); - boolean providerConfigured = !preferences.getString(PROVIDER_VPN_CERTIFICATE, "").isEmpty(); - boolean startOnBoot = preferences.getBoolean(EIP_RESTART_ON_BOOT, false); - boolean isAlwaysOnConfigured = VpnStatus.isAlwaysOn(); - 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) { - Log.d("OpenVpn", "start StartActivity!"); - Intent startActivityIntent = new Intent(context.getApplicationContext(), StartActivity.class); - startActivityIntent.putExtra(EIP_RESTART_ON_BOOT, true); - startActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(startActivityIntent); - } - } else { - if (isAlwaysOnConfigured) { - Intent dashboardIntent = new Intent(context.getApplicationContext(), StartActivity.class); - dashboardIntent.putExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE, true); - dashboardIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(dashboardIntent); - } - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java b/app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java deleted file mode 100644 index 9e523751..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java +++ /dev/null @@ -1,330 +0,0 @@ -package se.leap.bitmaskclient; - -/* - * This software is provided 'as-is', without any express or implied - * warranty. In no event will Google be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, as long as the origin is not misrepresented. - * - * Source: http://android-developers.blogspot.de/2013/08/some-securerandom-thoughts.html - */ - -import android.os.*; -import android.os.Process; -import android.util.*; - -import java.io.*; -import java.security.*; -import java.security.Provider; - -/** - * Fixes for the output of the default PRNG having low entropy. - *

- * The fixes need to be applied via {@link #apply()} before any use of Java - * Cryptography Architecture primitives. A good place to invoke them is in the - * application's {@code onCreate}. - */ -public final class PRNGFixes { - - private static final int VERSION_CODE_JELLY_BEAN = 16; - private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; - private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = - getBuildFingerprintAndDeviceSerial(); - - /** - * Hidden constructor to prevent instantiation. - */ - private PRNGFixes() { - } - - /** - * Applies all fixes. - * - * @throws SecurityException if a fix is needed but could not be applied. - */ - public static void apply() { - applyOpenSSLFix(); - installLinuxPRNGSecureRandom(); - } - - /** - * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the - * fix is not needed. - * - * @throws SecurityException if the fix is needed but could not be applied. - */ - private static void applyOpenSSLFix() throws SecurityException { - if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) - || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { - // No need to apply the fix - return; - } - - try { - // Mix in the device- and invocation-specific seed. - Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") - .getMethod("RAND_seed", byte[].class) - .invoke(null, generateSeed()); - - // Mix output of Linux PRNG into OpenSSL's PRNG - int bytesRead = (Integer) Class.forName( - "org.apache.harmony.xnet.provider.jsse.NativeCrypto") - .getMethod("RAND_load_file", String.class, long.class) - .invoke(null, "/dev/urandom", 1024); - if (bytesRead != 1024) { - throw new IOException( - "Unexpected number of bytes read from Linux PRNG: " - + bytesRead); - } - } catch (Exception e) { - throw new SecurityException("Failed to seed OpenSSL PRNG", e); - } - } - - /** - * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the - * default. Does nothing if the implementation is already the default or if - * there is not need to install the implementation. - * - * @throws SecurityException if the fix is needed but could not be applied. - */ - private static void installLinuxPRNGSecureRandom() - throws SecurityException { - if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { - // No need to apply the fix - return; - } - - // Install a Linux PRNG-based SecureRandom implementation as the - // default, if not yet installed. - Provider[] secureRandomProviders = - Security.getProviders("SecureRandom.SHA1PRNG"); - if ((secureRandomProviders == null) - || (secureRandomProviders.length < 1) - || (!LinuxPRNGSecureRandomProvider.class.equals( - secureRandomProviders[0].getClass()))) { - Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); - } - - // Assert that new SecureRandom() and - // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed - // by the Linux PRNG-based SecureRandom implementation. - SecureRandom rng1 = new SecureRandom(); - if (!LinuxPRNGSecureRandomProvider.class.equals( - rng1.getProvider().getClass())) { - throw new SecurityException( - "new SecureRandom() backed by wrong Provider: " - + rng1.getProvider().getClass()); - } - - SecureRandom rng2; - try { - rng2 = SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - throw new SecurityException("SHA1PRNG not available", e); - } - if (!LinuxPRNGSecureRandomProvider.class.equals( - rng2.getProvider().getClass())) { - throw new SecurityException( - "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" - + " Provider: " + rng2.getProvider().getClass()); - } - } - - /** - * {@code Provider} of {@code SecureRandom} engines which pass through - * all requests to the Linux PRNG. - */ - private static class LinuxPRNGSecureRandomProvider extends Provider { - - public LinuxPRNGSecureRandomProvider() { - super("LinuxPRNG", - 1.0, - "A Linux-specific random number provider that uses" - + " /dev/urandom"); - // Although /dev/urandom is not a SHA-1 PRNG, some apps - // explicitly request a SHA1PRNG SecureRandom and we thus need to - // prevent them from getting the default implementation whose output - // may have low entropy. - put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); - put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); - } - } - - /** - * {@link SecureRandomSpi} which passes all requests to the Linux PRNG - * ({@code /dev/urandom}). - */ - public static class LinuxPRNGSecureRandom extends SecureRandomSpi { - - /* - * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed - * are passed through to the Linux PRNG (/dev/urandom). Instances of - * this class seed themselves by mixing in the current time, PID, UID, - * build fingerprint, and hardware serial number (where available) into - * Linux PRNG. - * - * Concurrency: Read requests to the underlying Linux PRNG are - * serialized (on sLock) to ensure that multiple threads do not get - * duplicated PRNG output. - */ - - private static final File URANDOM_FILE = new File("/dev/urandom"); - - private static final Object sLock = new Object(); - - /** - * Input stream for reading from Linux PRNG or {@code null} if not yet - * opened. - * - * @GuardedBy("sLock") - */ - private static DataInputStream sUrandomIn; - - /** - * Output stream for writing to Linux PRNG or {@code null} if not yet - * opened. - * - * @GuardedBy("sLock") - */ - private static OutputStream sUrandomOut; - - /** - * Whether this engine instance has been seeded. This is needed because - * each instance needs to seed itself if the client does not explicitly - * seed it. - */ - private boolean mSeeded; - - @Override - protected void engineSetSeed(byte[] bytes) { - try { - OutputStream out; - synchronized (sLock) { - out = getUrandomOutputStream(); - } - out.write(bytes); - out.flush(); - } catch (IOException e) { - // On a small fraction of devices /dev/urandom is not writable. - // Log and ignore. - Log.w(PRNGFixes.class.getSimpleName(), - "Failed to mix seed into " + URANDOM_FILE); - } finally { - mSeeded = true; - } - } - - @Override - protected void engineNextBytes(byte[] bytes) { - if (!mSeeded) { - // Mix in the device- and invocation-specific seed. - engineSetSeed(generateSeed()); - } - - try { - DataInputStream in; - synchronized (sLock) { - in = getUrandomInputStream(); - } - synchronized (in) { - in.readFully(bytes); - } - } catch (IOException e) { - throw new SecurityException( - "Failed to read from " + URANDOM_FILE, e); - } - } - - @Override - protected byte[] engineGenerateSeed(int size) { - byte[] seed = new byte[size]; - engineNextBytes(seed); - return seed; - } - - private DataInputStream getUrandomInputStream() { - synchronized (sLock) { - if (sUrandomIn == null) { - // NOTE: Consider inserting a BufferedInputStream between - // DataInputStream and FileInputStream if you need higher - // PRNG output performance and can live with future PRNG - // output being pulled into this process prematurely. - try { - sUrandomIn = new DataInputStream( - new FileInputStream(URANDOM_FILE)); - } catch (IOException e) { - throw new SecurityException("Failed to open " - + URANDOM_FILE + " for reading", e); - } - } - return sUrandomIn; - } - } - - private OutputStream getUrandomOutputStream() throws IOException { - synchronized (sLock) { - if (sUrandomOut == null) { - sUrandomOut = new FileOutputStream(URANDOM_FILE); - } - return sUrandomOut; - } - } - } - - /** - * Generates a device- and invocation-specific seed to be mixed into the - * Linux PRNG. - */ - private static byte[] generateSeed() { - try { - ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); - DataOutputStream seedBufferOut = - new DataOutputStream(seedBuffer); - seedBufferOut.writeLong(System.currentTimeMillis()); - seedBufferOut.writeLong(System.nanoTime()); - seedBufferOut.writeInt(Process.myPid()); - seedBufferOut.writeInt(Process.myUid()); - seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); - seedBufferOut.close(); - return seedBuffer.toByteArray(); - } catch (IOException e) { - throw new SecurityException("Failed to generate seed", e); - } - } - - /** - * Gets the hardware serial number of this device. - * - * @return serial number or {@code null} if not available. - */ - private static String getDeviceSerialNumber() { - // We're using the Reflection API because Build.SERIAL is only available - // since API Level 9 (Gingerbread, Android 2.3). - try { - return (String) Build.class.getField("SERIAL").get(null); - } catch (Exception ignored) { - return null; - } - } - - private static byte[] getBuildFingerprintAndDeviceSerial() { - StringBuilder result = new StringBuilder(); - String fingerprint = Build.FINGERPRINT; - if (fingerprint != null) { - result.append(fingerprint); - } - String serial = getDeviceSerialNumber(); - if (serial != null) { - result.append(serial); - } - try { - return result.toString().getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("UTF-8 encoding not supported"); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java deleted file mode 100644 index ce428a54..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/Provider.java +++ /dev/null @@ -1,593 +0,0 @@ -/** - * 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; - -import android.os.Parcel; -import android.os.Parcelable; - -import com.google.gson.Gson; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Locale; - -import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4; -import static se.leap.bitmaskclient.Constants.CAPABILITIES; -import static se.leap.bitmaskclient.Constants.GATEWAYS; -import static se.leap.bitmaskclient.Constants.PROVIDER_ALLOWED_REGISTERED; -import static se.leap.bitmaskclient.Constants.PROVIDER_ALLOW_ANONYMOUS; -import static se.leap.bitmaskclient.Constants.TRANSPORT; -import static se.leap.bitmaskclient.Constants.TYPE; -import static se.leap.bitmaskclient.ProviderAPI.ERRORS; - -/** - * @author Sean Leonard - * @author Parménides GV - */ -public final class Provider implements Parcelable { - - private static long EIP_SERVICE_TIMEOUT = 1000 * 60 * 60 * 24 * 3; - private static long GEOIP_SERVICE_TIMEOUT = 1000 * 60 * 60; - private JSONObject definition = new JSONObject(); // Represents our Provider's provider.json - private JSONObject eipServiceJson = new JSONObject(); - private JSONObject geoIpJson = new JSONObject(); - private DefaultedURL mainUrl = new DefaultedURL(); - private DefaultedURL apiUrl = new DefaultedURL(); - private DefaultedURL geoipUrl = new DefaultedURL(); - private String providerIp = ""; - private String providerApiIp = ""; - private String certificatePin = ""; - private String certificatePinEncoding = ""; - private String caCert = ""; - private String apiVersion = ""; - private String privateKey = ""; - private String vpnCertificate = ""; - private long lastEipServiceUpdate = 0L; - private long lastGeoIpUpdate = 0L; - - private boolean allowAnonymous; - private boolean allowRegistered; - - final public static String - API_URL = "api_uri", - API_VERSION = "api_version", - ALLOW_REGISTRATION = "allow_registration", - API_RETURN_SERIAL = "serial", - SERVICE = "service", - KEY = "provider", - CA_CERT = "ca_cert", - CA_CERT_URI = "ca_cert_uri", - CA_CERT_FINGERPRINT = "ca_cert_fingerprint", - NAME = "name", - DESCRIPTION = "description", - DOMAIN = "domain", - MAIN_URL = "main_url", - PROVIDER_IP = "provider_ip", - PROVIDER_API_IP = "provider_api_ip", - GEOIP_URL = "geoip_url"; - - private static final String API_TERM_NAME = "name"; - - public Provider() { } - - public Provider(String mainUrl) { - this(mainUrl, null); - } - - public Provider(String mainUrl, String geoipUrl) { - try { - this.mainUrl.setUrl(new URL(mainUrl)); - } catch (MalformedURLException e) { - this.mainUrl = new DefaultedURL(); - } - setGeoipUrl(geoipUrl); - } - - public Provider(String mainUrl, String providerIp, String providerApiIp) { - this(mainUrl, null, providerIp, providerApiIp); - } - - public Provider(String mainUrl, String geoipUrl, String providerIp, String providerApiIp) { - try { - this.mainUrl.setUrl(new URL(mainUrl)); - if (providerIp != null) { - this.providerIp = providerIp; - } - if (providerApiIp != null) { - this.providerApiIp = providerApiIp; - } - } catch (MalformedURLException e) { - e.printStackTrace(); - return; - } - setGeoipUrl(geoipUrl); - } - - - public Provider(String mainUrl, String geoipUrl, String providerIp, String providerApiIp, String caCert, String definition) { - this(mainUrl, geoipUrl, providerIp, providerApiIp); - if (caCert != null) { - this.caCert = caCert; - } - if (definition != null) { - try { - define(new JSONObject(definition)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - public Provider createFromParcel(Parcel in) { - return new Provider(in); - } - - public Provider[] newArray(int size) { - return new Provider[size]; - } - }; - - public boolean isConfigured() { - return !mainUrl.isDefault() && - !apiUrl.isDefault() && - hasCaCert() && - hasDefinition() && - hasVpnCertificate() && - hasEIP() && - hasPrivateKey(); - } - - public boolean supportsPluggableTransports() { - try { - JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS); - for (int i = 0; i < gatewayJsons.length(); i++) { - JSONArray transports = gatewayJsons.getJSONObject(i). - getJSONObject(CAPABILITIES). - getJSONArray(TRANSPORT); - for (int j = 0; j < transports.length(); j++) { - if (OBFS4.toString().equals(transports.getJSONObject(j).getString(TYPE))) { - return true; - } - } - } - } catch (Exception e) { - // ignore - } - return false; - } - - public String getIpForHostname(String host) { - if (host != null) { - if (host.equals(mainUrl.getUrl().getHost())) { - return providerIp; - } else if (host.equals(apiUrl.getUrl().getHost())) { - return providerApiIp; - } - } - return ""; - } - - public String getProviderApiIp() { - return this.providerApiIp; - } - - public void setProviderApiIp(String providerApiIp) { - if (providerApiIp == null) return; - this.providerApiIp = providerApiIp; - } - - public void setProviderIp(String providerIp) { - if (providerIp == null) return; - this.providerIp = providerIp; - } - - public String getProviderIp() { - return this.providerIp; - } - - public void setMainUrl(URL url) { - mainUrl.setUrl(url); - } - - public void setMainUrl(String url) { - try { - mainUrl.setUrl(new URL(url)); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - } - - public boolean define(JSONObject providerJson) { - definition = providerJson; - return parseDefinition(definition); - } - - public JSONObject getDefinition() { - return definition; - } - - public String getDefinitionString() { - return getDefinition().toString(); - } - - public String getDomain() { - return mainUrl.getDomain(); - } - - public String getMainUrlString() { - return getMainUrl().toString(); - } - - public DefaultedURL getMainUrl() { - return mainUrl; - } - - protected DefaultedURL getApiUrl() { - return apiUrl; - } - - public DefaultedURL getGeoipUrl() { - return geoipUrl; - } - - public void setGeoipUrl(String url) { - try { - this.geoipUrl.setUrl(new URL(url)); - } catch (MalformedURLException e) { - this.geoipUrl = new DefaultedURL(); - } - } - - protected String getApiUrlWithVersion() { - return getApiUrlString() + "/" + getApiVersion(); - } - - - public String getApiUrlString() { - return getApiUrl().toString(); - } - - public String getApiVersion() { - return apiVersion; - } - - boolean hasCaCert() { - return caCert != null && !caCert.isEmpty(); - } - - public boolean hasDefinition() { - return definition != null && definition.length() > 0; - } - - public boolean hasGeoIpJson() { - return geoIpJson != null && geoIpJson.length() > 0; - } - - - public String getCaCert() { - return caCert; - } - - public String getName() { - // Should we pass the locale in, or query the system here? - String lang = Locale.getDefault().getLanguage(); - String name = ""; - try { - if (definition != null) - name = definition.getJSONObject(API_TERM_NAME).getString(lang); - else throw new JSONException("Provider not defined"); - } catch (JSONException e) { - try { - name = definition.getJSONObject(API_TERM_NAME).getString("en"); - } catch (JSONException e2) { - if (mainUrl != null) { - String host = mainUrl.getDomain(); - name = host.substring(0, host.indexOf(".")); - } - } - } - - return name; - } - - protected String getDescription() { - String lang = Locale.getDefault().getLanguage(); - String desc = null; - try { - desc = definition.getJSONObject("description").getString(lang); - } catch (JSONException e) { - // TODO: handle exception!! - try { - desc = definition.getJSONObject("description").getString(definition.getString("default_language")); - } catch (JSONException e2) { - // TODO: i can't believe you're doing it again! - } - } - - return desc; - } - - protected boolean hasEIP() { - return getEipServiceJson() != null && getEipServiceJson().length() > 0 - && !getEipServiceJson().has(ERRORS); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int i) { - parcel.writeString(getMainUrlString()); - parcel.writeString(getProviderIp()); - parcel.writeString(getProviderApiIp()); - parcel.writeString(getGeoipUrl().toString()); - parcel.writeString(getDefinitionString()); - parcel.writeString(getCaCert()); - parcel.writeString(getEipServiceJsonString()); - parcel.writeString(getGeoIpJsonString()); - parcel.writeString(getPrivateKey()); - parcel.writeString(getVpnCertificate()); - parcel.writeLong(lastEipServiceUpdate); - parcel.writeLong(lastGeoIpUpdate); - } - - - //TODO: write a test for marshalling! - private Provider(Parcel in) { - try { - mainUrl.setUrl(new URL(in.readString())); - String tmpString = in.readString(); - if (!tmpString.isEmpty()) { - providerIp = tmpString; - } - tmpString = in.readString(); - if (!tmpString.isEmpty()) { - providerApiIp = tmpString; - } - tmpString = in.readString(); - if (!tmpString.isEmpty()) { - geoipUrl.setUrl(new URL(tmpString)); - } - tmpString = in.readString(); - if (!tmpString.isEmpty()) { - definition = new JSONObject((tmpString)); - parseDefinition(definition); - } - tmpString = in.readString(); - if (!tmpString.isEmpty()) { - this.caCert = tmpString; - } - tmpString = in.readString(); - if (!tmpString.isEmpty()) { - this.setEipServiceJson(new JSONObject(tmpString)); - } - tmpString = in.readString(); - if (!tmpString.isEmpty()) { - this.setGeoIpJson(new JSONObject(tmpString)); - } - tmpString = in.readString(); - if (!tmpString.isEmpty()) { - this.setPrivateKey(tmpString); - } - tmpString = in.readString(); - if (!tmpString.isEmpty()) { - this.setVpnCertificate(tmpString); - } - this.lastEipServiceUpdate = in.readLong(); - this.lastGeoIpUpdate = in.readLong(); - } catch (MalformedURLException | JSONException e) { - e.printStackTrace(); - } - } - - - @Override - public boolean equals(Object o) { - if (o instanceof Provider) { - Provider p = (Provider) o; - return p.getDomain().equals(getDomain()) && - definition.toString().equals(p.getDefinition().toString()) && - eipServiceJson.toString().equals(p.getEipServiceJsonString()) && - geoIpJson.toString().equals(p.getGeoIpJsonString()) && - providerIp.equals(p.getProviderIp()) && - providerApiIp.equals(p.getProviderApiIp()) && - apiUrl.equals(p.getApiUrl()) && - geoipUrl.equals(p.getGeoipUrl()) && - certificatePin.equals(p.getCertificatePin()) && - certificatePinEncoding.equals(p.getCertificatePinEncoding()) && - caCert.equals(p.getCaCert()) && - apiVersion.equals(p.getApiVersion()) && - privateKey.equals(p.getPrivateKey()) && - vpnCertificate.equals(p.getVpnCertificate()) && - allowAnonymous == p.allowsAnonymous() && - allowRegistered == p.allowsRegistered(); - } else return false; - } - - - public JSONObject toJson() { - JSONObject json = new JSONObject(); - try { - json.put(Provider.MAIN_URL, mainUrl); - } catch (JSONException e) { - e.printStackTrace(); - } - return json; - } - - @Override - public int hashCode() { - return getDomain().hashCode(); - } - - @Override - public String toString() { - return new Gson().toJson(this); - } - - private boolean parseDefinition(JSONObject definition) { - try { - String pin = definition.getString(CA_CERT_FINGERPRINT); - this.certificatePin = pin.split(":")[1].trim(); - this.certificatePinEncoding = pin.split(":")[0].trim(); - this.apiUrl.setUrl(new URL(definition.getString(API_URL))); - this.allowAnonymous = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOW_ANONYMOUS); - this.allowRegistered = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOWED_REGISTERED); - this.apiVersion = getDefinition().getString(Provider.API_VERSION); - return true; - } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) { - return false; - } - } - - public void setCaCert(String cert) { - this.caCert = cert; - } - - public boolean allowsAnonymous() { - return allowAnonymous; - } - - public boolean allowsRegistered() { - return allowRegistered; - } - - public void setLastEipServiceUpdate(long timestamp) { - lastEipServiceUpdate = timestamp; - } - - public boolean shouldUpdateEipServiceJson() { - return System.currentTimeMillis() - lastEipServiceUpdate >= EIP_SERVICE_TIMEOUT; - } - - - public void setLastGeoIpUpdate(long timestamp) { - lastGeoIpUpdate = timestamp; - } - - public boolean shouldUpdateGeoIpJson() { - return System.currentTimeMillis() - lastGeoIpUpdate >= GEOIP_SERVICE_TIMEOUT; - } - - - public boolean setEipServiceJson(JSONObject eipServiceJson) { - if (eipServiceJson.has(ERRORS)) { - return false; - } - this.eipServiceJson = eipServiceJson; - return true; - } - - public boolean setGeoIpJson(JSONObject geoIpJson) { - if (geoIpJson.has(ERRORS)) { - return false; - } - this.geoIpJson = geoIpJson; - return true; - } - - public JSONObject getEipServiceJson() { - return eipServiceJson; - } - - public JSONObject getGeoIpJson() { - return geoIpJson; - } - - public String getGeoIpJsonString() { - return geoIpJson.toString(); - } - - public String getEipServiceJsonString() { - return getEipServiceJson().toString(); - } - - public boolean isDefault() { - return getMainUrl().isDefault() && - getApiUrl().isDefault() && - getGeoipUrl().isDefault() && - certificatePin.isEmpty() && - certificatePinEncoding.isEmpty() && - caCert.isEmpty(); - } - - public String getPrivateKey() { - return privateKey; - } - - public void setPrivateKey(String privateKey) { - this.privateKey = privateKey; - } - - public boolean hasPrivateKey() { - return privateKey != null && privateKey.length() > 0; - } - - public String getVpnCertificate() { - return vpnCertificate; - } - - public void setVpnCertificate(String vpnCertificate) { - this.vpnCertificate = vpnCertificate; - } - - public boolean hasVpnCertificate() { - return getVpnCertificate() != null && getVpnCertificate().length() >0 ; - } - - public String getCertificatePin() { - return certificatePin; - } - - public String getCertificatePinEncoding() { - return certificatePinEncoding; - } - - public String getCaCertFingerprint() { - return getCertificatePinEncoding() + ":" + getCertificatePin(); - } - - /** - * resets everything except the main url, the providerIp and the geoip - * service url (currently preseeded) - */ - public void reset() { - definition = new JSONObject(); - eipServiceJson = new JSONObject(); - geoIpJson = new JSONObject(); - apiUrl = new DefaultedURL(); - certificatePin = ""; - certificatePinEncoding = ""; - caCert = ""; - apiVersion = ""; - privateKey = ""; - vpnCertificate = ""; - allowRegistered = false; - allowAnonymous = false; - lastGeoIpUpdate = 0L; - lastEipServiceUpdate = 0L; - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java deleted file mode 100644 index bec16139..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) 2017 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.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; - -import androidx.annotation.NonNull; -import androidx.core.app.JobIntentService; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; - -/** - * Implements HTTP api methods (encapsulated in {{@link ProviderApiManager}}) - * used to manage communications with the provider server. - *

- * It's an JobIntentService because it downloads data from the Internet, so it operates in the background. - * - * @author parmegv - * @author MeanderingCode - * @author cyberta - */ - -public class ProviderAPI extends JobIntentService implements ProviderApiManagerBase.ProviderApiServiceCallback { - - /** - * Unique job ID for this service. - */ - static final int JOB_ID = 161375; - - final public static String - TAG = ProviderAPI.class.getSimpleName(), - SET_UP_PROVIDER = "setUpProvider", - UPDATE_PROVIDER_DETAILS = "updateProviderDetails", - DOWNLOAD_GEOIP_JSON = "downloadGeoIpJson", - SIGN_UP = "srpRegister", - LOG_IN = "srpAuth", - LOG_OUT = "logOut", - DOWNLOAD_VPN_CERTIFICATE = "downloadUserAuthedVPNCertificate", - UPDATE_INVALID_VPN_CERTIFICATE = "ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE", - PARAMETERS = "parameters", - RECEIVER_KEY = "receiver", - ERRORS = "errors", - ERRORID = "errorId", - BACKEND_ERROR_KEY = "error", - BACKEND_ERROR_MESSAGE = "message", - USER_MESSAGE = "userMessage", - DOWNLOAD_SERVICE_JSON = "ProviderAPI.DOWNLOAD_SERVICE_JSON"; - - final public static int - SUCCESSFUL_LOGIN = 3, - FAILED_LOGIN = 4, - SUCCESSFUL_SIGNUP = 5, - FAILED_SIGNUP = 6, - SUCCESSFUL_LOGOUT = 7, - LOGOUT_FAILED = 8, - CORRECTLY_DOWNLOADED_VPN_CERTIFICATE = 9, - INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE = 10, - PROVIDER_OK = 11, - PROVIDER_NOK = 12, - CORRECTLY_DOWNLOADED_EIP_SERVICE = 13, - INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14, - CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE = 15, - INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE = 16, - CORRECTLY_DOWNLOADED_GEOIP_JSON = 17, - INCORRECTLY_DOWNLOADED_GEOIP_JSON = 18; - - ProviderApiManager providerApiManager; - - //TODO: refactor me, please! - //used in insecure flavor only - @SuppressLint("unused") - public static boolean lastDangerOn() { - return ProviderApiManager.lastDangerOn(); - } - - @Override - public void onCreate() { - super.onCreate(); - providerApiManager = initApiManager(); - } - - /** - * Convenience method for enqueuing work in to this service. - */ - static void enqueueWork(Context context, Intent work) { - try { - ProviderAPI.enqueueWork(context, ProviderAPI.class, JOB_ID, work); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - - @Override - protected void onHandleWork(@NonNull Intent command) { - providerApiManager.handleIntent(command); - } - - @Override - public void broadcastEvent(Intent intent) { - LocalBroadcastManager.getInstance(this).sendBroadcast(intent); - } - - private ProviderApiManager initApiManager() { - SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources()); - return new ProviderApiManager(preferences, getResources(), clientGenerator, this); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPICommand.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPICommand.java deleted file mode 100644 index 8de90af2..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPICommand.java +++ /dev/null @@ -1,84 +0,0 @@ -package se.leap.bitmaskclient; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.ResultReceiver; -import android.util.Log; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class ProviderAPICommand { - private static final String TAG = ProviderAPICommand.class.getSimpleName(); - private Context context; - - private String action; - private Bundle parameters; - private ResultReceiver resultReceiver; - private Provider provider; - - private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Provider provider, ResultReceiver resultReceiver) { - this(context.getApplicationContext(), action, Bundle.EMPTY, provider, resultReceiver); - } - private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Provider provider) { - this(context.getApplicationContext(), action, Bundle.EMPTY, provider); - } - - private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Bundle parameters, @NotNull Provider provider) { - this(context.getApplicationContext(), action, parameters, provider, null); - } - - private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Bundle parameters, @NotNull Provider provider, @Nullable ResultReceiver resultReceiver) { - super(); - this.context = context; - this.action = action; - this.parameters = parameters; - this.resultReceiver = resultReceiver; - this.provider = provider; - } - - private boolean isInitialized() { - return context != null; - } - - private void execute() { - if (isInitialized()) { - Intent intent = setUpIntent(); - ProviderAPI.enqueueWork(context, intent); - } - } - - private Intent setUpIntent() { - Intent command = new Intent(context, ProviderAPI.class); - - command.setAction(action); - command.putExtra(ProviderAPI.PARAMETERS, parameters); - if (resultReceiver != null) { - command.putExtra(ProviderAPI.RECEIVER_KEY, resultReceiver); - } - command.putExtra(Constants.PROVIDER_KEY, provider); - - return command; - } - - public static void execute(Context context, String action, @NotNull Provider provider) { - ProviderAPICommand command = new ProviderAPICommand(context, action, provider); - command.execute(); - } - - public static void execute(Context context, String action, Bundle parameters, @NotNull Provider provider) { - ProviderAPICommand command = new ProviderAPICommand(context, action, parameters, provider); - command.execute(); - } - - public static void execute(Context context, String action, Bundle parameters, @NotNull Provider provider, ResultReceiver resultReceiver) { - ProviderAPICommand command = new ProviderAPICommand(context, action, parameters, provider, resultReceiver); - command.execute(); - } - - public static void execute(Context context, String action, @NotNull Provider provider, ResultReceiver resultReceiver) { - ProviderAPICommand command = new ProviderAPICommand(context, action, provider, resultReceiver); - command.execute(); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java deleted file mode 100644 index 84f21343..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java +++ /dev/null @@ -1,98 +0,0 @@ -/** - * 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 androidx.annotation.NonNull; -import android.util.Pair; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Locale; -import java.util.Scanner; - -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -/** - * Created by cyberta on 08.01.18. - */ - -public class ProviderApiConnector { - - private static final MediaType JSON - = MediaType.parse("application/json; charset=utf-8"); - - - public static boolean delete(OkHttpClient okHttpClient, String deleteUrl) { - try { - Request.Builder requestBuilder = new Request.Builder() - .url(deleteUrl) - .delete(); - Request request = requestBuilder.build(); - - Response response = okHttpClient.newCall(request).execute(); - //response code 401: already logged out - if (response.isSuccessful() || response.code() == 401) { - return true; - } - } catch (IOException | RuntimeException e) { - return false; - } - - return false; - } - - public static boolean canConnect(@NonNull OkHttpClient okHttpClient, String url) throws RuntimeException, IOException { - Request.Builder requestBuilder = new Request.Builder() - .url(url) - .method("GET", null); - Request request = requestBuilder.build(); - - Response response = okHttpClient.newCall(request).execute(); - return response.isSuccessful(); - - } - - public static String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) throws RuntimeException, IOException { - - RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null; - Request.Builder requestBuilder = new Request.Builder() - .url(url) - .method(request_method, jsonBody); - for (Pair keyValPair : headerArgs) { - requestBuilder.addHeader(keyValPair.first, keyValPair.second); - } - - //TODO: move to getHeaderArgs()? - String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); - requestBuilder.addHeader("Accept-Language", locale); - Request request = requestBuilder.build(); - - Response response = okHttpClient.newCall(request).execute(); - InputStream inputStream = response.body().byteStream(); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - if (scanner.hasNext()) { - return scanner.next(); - } - return null; - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java deleted file mode 100644 index 025e7aab..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java +++ /dev/null @@ -1,939 +0,0 @@ -/** - * 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.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.os.Bundle; -import android.os.ResultReceiver; -import android.util.Base64; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.math.BigInteger; -import java.net.ConnectException; -import java.net.MalformedURLException; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.net.UnknownServiceException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.X509Certificate; -import java.security.interfaces.RSAPrivateKey; -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; - -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; - -import okhttp3.OkHttpClient; -import se.leap.bitmaskclient.Constants.CREDENTIAL_ERRORS; -import se.leap.bitmaskclient.utils.ConfigHelper; - -import static se.leap.bitmaskclient.Constants.BROADCAST_PROVIDER_API_EVENT; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; -import static se.leap.bitmaskclient.Constants.CREDENTIALS_PASSWORD; -import static se.leap.bitmaskclient.Constants.CREDENTIALS_USERNAME; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; -import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.Provider.CA_CERT; -import static se.leap.bitmaskclient.Provider.GEOIP_URL; -import static se.leap.bitmaskclient.Provider.PROVIDER_API_IP; -import static se.leap.bitmaskclient.Provider.PROVIDER_IP; -import static se.leap.bitmaskclient.ProviderAPI.BACKEND_ERROR_KEY; -import static se.leap.bitmaskclient.ProviderAPI.BACKEND_ERROR_MESSAGE; -import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE; -import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON; -import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_GEOIP_JSON; -import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_SERVICE_JSON; -import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.ERRORID; -import static se.leap.bitmaskclient.ProviderAPI.ERRORS; -import static se.leap.bitmaskclient.ProviderAPI.FAILED_LOGIN; -import static se.leap.bitmaskclient.ProviderAPI.FAILED_SIGNUP; -import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE; -import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON; -import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.LOGOUT_FAILED; -import static se.leap.bitmaskclient.ProviderAPI.LOG_IN; -import static se.leap.bitmaskclient.ProviderAPI.LOG_OUT; -import static se.leap.bitmaskclient.ProviderAPI.PARAMETERS; -import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK; -import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK; -import static se.leap.bitmaskclient.ProviderAPI.RECEIVER_KEY; -import static se.leap.bitmaskclient.ProviderAPI.SET_UP_PROVIDER; -import static se.leap.bitmaskclient.ProviderAPI.SIGN_UP; -import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGIN; -import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGOUT; -import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_SIGNUP; -import static se.leap.bitmaskclient.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROVIDER_DETAILS; -import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE; -import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING; -import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; -import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE; -import static se.leap.bitmaskclient.R.string.certificate_error; -import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; -import static se.leap.bitmaskclient.R.string.error_json_exception_user_message; -import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message; -import static se.leap.bitmaskclient.R.string.malformed_url; -import static se.leap.bitmaskclient.R.string.server_unreachable_message; -import static se.leap.bitmaskclient.R.string.service_is_down_error; -import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid; -import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert; -import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details; -import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert; -import static se.leap.bitmaskclient.utils.ConfigHelper.getFingerprintFromCertificate; -import static se.leap.bitmaskclient.utils.ConfigHelper.getProviderFormattedString; -import static se.leap.bitmaskclient.utils.ConfigHelper.parseRsaKeyFromString; -import static se.leap.bitmaskclient.utils.PreferenceHelper.deleteProviderDetailsFromPreferences; -import static se.leap.bitmaskclient.utils.PreferenceHelper.getFromPersistedProvider; - -/** - * Implements the logic of the http api calls. The methods of this class needs to be called from - * a background thread. - */ - -public abstract class ProviderApiManagerBase { - - private final static String TAG = ProviderApiManagerBase.class.getName(); - - public interface ProviderApiServiceCallback { - void broadcastEvent(Intent intent); - } - - private ProviderApiServiceCallback serviceCallback; - - protected SharedPreferences preferences; - protected Resources resources; - OkHttpClientGenerator clientGenerator; - - ProviderApiManagerBase(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) { - this.preferences = preferences; - this.resources = resources; - this.serviceCallback = callback; - this.clientGenerator = clientGenerator; - } - - public void handleIntent(Intent command) { -// Log.d(TAG, "handleIntent was called!"); - ResultReceiver receiver = null; - if (command.getParcelableExtra(RECEIVER_KEY) != null) { - receiver = command.getParcelableExtra(RECEIVER_KEY); - } - String action = command.getAction(); - Bundle parameters = command.getBundleExtra(PARAMETERS); - - Provider provider = command.getParcelableExtra(PROVIDER_KEY); - - if (provider == null) { - //TODO: consider returning error back e.g. NO_PROVIDER - Log.e(TAG, action +" called without provider!"); - return; - } - if (action == null) { - Log.e(TAG, "Intent without action sent!"); - return; - } - - Bundle result = new Bundle(); - switch (action) { - case UPDATE_PROVIDER_DETAILS: - ProviderObservable.getInstance().setProviderForDns(provider); - resetProviderDetails(provider); - Bundle task = new Bundle(); - result = setUpProvider(provider, task); - if (result.getBoolean(BROADCAST_RESULT_KEY)) { - getGeoIPJson(provider); - sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider); - } else { - sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider); - } - ProviderObservable.getInstance().setProviderForDns(null); - break; - case SET_UP_PROVIDER: - ProviderObservable.getInstance().setProviderForDns(provider); - result = setUpProvider(provider, parameters); - if (result.getBoolean(BROADCAST_RESULT_KEY)) { - getGeoIPJson(provider); - sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider); - } else { - sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider); - } - ProviderObservable.getInstance().setProviderForDns(null); - break; - case SIGN_UP: - result = tryToRegister(provider, parameters); - if (result.getBoolean(BROADCAST_RESULT_KEY)) { - sendToReceiverOrBroadcast(receiver, SUCCESSFUL_SIGNUP, result, provider); - } else { - sendToReceiverOrBroadcast(receiver, FAILED_SIGNUP, result, provider); - } - break; - case LOG_IN: - result = tryToAuthenticate(provider, parameters); - if (result.getBoolean(BROADCAST_RESULT_KEY)) { - sendToReceiverOrBroadcast(receiver, SUCCESSFUL_LOGIN, result, provider); - } else { - sendToReceiverOrBroadcast(receiver, FAILED_LOGIN, result, provider); - } - break; - case LOG_OUT: - if (logOut(provider)) { - sendToReceiverOrBroadcast(receiver, SUCCESSFUL_LOGOUT, Bundle.EMPTY, provider); - } else { - sendToReceiverOrBroadcast(receiver, LOGOUT_FAILED, Bundle.EMPTY, provider); - } - break; - case DOWNLOAD_VPN_CERTIFICATE: - ProviderObservable.getInstance().setProviderForDns(provider); - result = updateVpnCertificate(provider); - if (result.getBoolean(BROADCAST_RESULT_KEY)) { - sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider); - } else { - sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider); - } - ProviderObservable.getInstance().setProviderForDns(null); - break; - case UPDATE_INVALID_VPN_CERTIFICATE: - ProviderObservable.getInstance().setProviderForDns(provider); - result = updateVpnCertificate(provider); - if (result.getBoolean(BROADCAST_RESULT_KEY)) { - sendToReceiverOrBroadcast(receiver, CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider); - } else { - sendToReceiverOrBroadcast(receiver, INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider); - } - ProviderObservable.getInstance().setProviderForDns(null); - break; - case DOWNLOAD_SERVICE_JSON: - ProviderObservable.getInstance().setProviderForDns(provider); - Log.d(TAG, "update eip service json"); - result = getAndSetEipServiceJson(provider); - if (result.getBoolean(BROADCAST_RESULT_KEY)) { - sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider); - } else { - sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider); - } - ProviderObservable.getInstance().setProviderForDns(null); - break; - case DOWNLOAD_GEOIP_JSON: - if (!provider.getGeoipUrl().isDefault()) { - boolean startEIP = parameters.getBoolean(EIP_ACTION_START); - ProviderObservable.getInstance().setProviderForDns(provider); - result = getGeoIPJson(provider); - result.putBoolean(EIP_ACTION_START, startEIP); - if (result.getBoolean(BROADCAST_RESULT_KEY)) { - sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider); - } else { - sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider); - } - ProviderObservable.getInstance().setProviderForDns(null); - } - } - } - - void resetProviderDetails(Provider provider) { - provider.reset(); - deleteProviderDetailsFromPreferences(preferences, provider.getDomain()); - } - - String formatErrorMessage(final int errorStringId) { - return formatErrorMessage(getProviderFormattedString(resources, errorStringId)); - } - - private String formatErrorMessage(String errorMessage) { - return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; - } - - private JSONObject getErrorMessageAsJson(final int toastStringId) { - try { - return new JSONObject(formatErrorMessage(toastStringId)); - } catch (JSONException e) { - e.printStackTrace(); - return new JSONObject(); - } - } - - private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) { - try { - jsonObject.put(ERRORS, errorMessage); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) { - try { - jsonObject.put(ERRORS, errorMessage); - jsonObject.put(ERRORID, errorId); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - private Bundle tryToRegister(Provider provider, Bundle task) { - Bundle result = new Bundle(); - - String username = task.getString(CREDENTIALS_USERNAME); - String password = task.getString(CREDENTIALS_PASSWORD); - - if(provider == null) { - result.putBoolean(BROADCAST_RESULT_KEY, false); - Log.e(TAG, "no provider when trying to register"); - return result; - } - - if (validUserLoginData(username, password)) { - result = register(provider, username, password); - } else { - if (!wellFormedPassword(password)) { - result.putBoolean(BROADCAST_RESULT_KEY, false); - result.putString(CREDENTIALS_USERNAME, username); - result.putBoolean(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); - } - if (!validUsername(username)) { - result.putBoolean(BROADCAST_RESULT_KEY, false); - result.putBoolean(CREDENTIAL_ERRORS.USERNAME_MISSING.toString(), true); - } - } - - return result; - } - - private Bundle register(Provider provider, String username, String password) { - JSONObject stepResult = null; - OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult); - if (okHttpClient == null) { - return backendErrorNotification(stepResult, username); - } - - LeapSRPSession client = new LeapSRPSession(username, password); - byte[] salt = client.calculateNewSalt(); - - BigInteger password_verifier = client.calculateV(username, password, salt); - - JSONObject api_result = sendNewUserDataToSRPServer(provider.getApiUrlWithVersion(), username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); - - Bundle result = new Bundle(); - if (api_result.has(ERRORS) || api_result.has(BACKEND_ERROR_KEY)) - result = backendErrorNotification(api_result, username); - else { - result.putString(CREDENTIALS_USERNAME, username); - result.putString(CREDENTIALS_PASSWORD, password); - result.putBoolean(BROADCAST_RESULT_KEY, true); - } - - return result; - } - - /** - * Starts the authentication process using SRP protocol. - * - * @param task containing: username, password and provider - * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if authentication was successful. - */ - private Bundle tryToAuthenticate(Provider provider, Bundle task) { - Bundle result = new Bundle(); - - String username = task.getString(CREDENTIALS_USERNAME); - String password = task.getString(CREDENTIALS_PASSWORD); - - if (validUserLoginData(username, password)) { - result = authenticate(provider, username, password); - } else { - if (!wellFormedPassword(password)) { - result.putBoolean(BROADCAST_RESULT_KEY, false); - result.putString(CREDENTIALS_USERNAME, username); - result.putBoolean(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); - } - if (!validUsername(username)) { - result.putBoolean(BROADCAST_RESULT_KEY, false); - result.putBoolean(CREDENTIAL_ERRORS.USERNAME_MISSING.toString(), true); - } - } - - return result; - } - - private Bundle authenticate(Provider provider, String username, String password) { - Bundle result = new Bundle(); - JSONObject stepResult = new JSONObject(); - - String providerApiUrl = provider.getApiUrlWithVersion(); - - OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult); - if (okHttpClient == null) { - return backendErrorNotification(stepResult, username); - } - - LeapSRPSession client = new LeapSRPSession(username, password); - byte[] A = client.exponential(); - - JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient); - try { - String salt = step_result.getString(LeapSRPSession.SALT); - byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray(); - byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes); - if (M1 != null) { - step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient); - setTokenIfAvailable(step_result); - byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); - if (client.verify(M2)) { - result.putBoolean(BROADCAST_RESULT_KEY, true); - } else { - backendErrorNotification(step_result, username); - } - } else { - result.putBoolean(BROADCAST_RESULT_KEY, false); - result.putString(CREDENTIALS_USERNAME, username); - result.putString(USER_MESSAGE, resources.getString(R.string.error_srp_math_error_user_message)); - } - } catch (JSONException e) { - result = backendErrorNotification(step_result, username); - e.printStackTrace(); - } - - return result; - } - - private boolean setTokenIfAvailable(JSONObject authentication_step_result) { - try { - LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); - } catch (JSONException e) { - return false; - } - return true; - } - - private Bundle backendErrorNotification(JSONObject result, String username) { - Bundle userNotificationBundle = new Bundle(); - if (result.has(ERRORS)) { - Object baseErrorMessage = result.opt(ERRORS); - if (baseErrorMessage instanceof JSONObject) { - try { - JSONObject errorMessage = result.getJSONObject(ERRORS); - String errorType = errorMessage.keys().next().toString(); - String message = errorMessage.get(errorType).toString(); - userNotificationBundle.putString(USER_MESSAGE, message); - } catch (JSONException | NoSuchElementException | NullPointerException e) { - e.printStackTrace(); - } - } else if (baseErrorMessage instanceof String) { - try { - String errorMessage = result.getString(ERRORS); - userNotificationBundle.putString(USER_MESSAGE, errorMessage); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } else if (result.has(BACKEND_ERROR_KEY)) { - try { - String backendErrorMessage = resources.getString(R.string.error_json_exception_user_message); - if (result.has(BACKEND_ERROR_MESSAGE)) { - backendErrorMessage = resources.getString(R.string.error) + result.getString(BACKEND_ERROR_MESSAGE); - } - userNotificationBundle.putString(USER_MESSAGE, backendErrorMessage); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - if (!username.isEmpty()) - userNotificationBundle.putString(CREDENTIALS_USERNAME, username); - userNotificationBundle.putBoolean(BROADCAST_RESULT_KEY, false); - - return userNotificationBundle; - } - - private void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) { - if (resultData == null || resultData == Bundle.EMPTY) { - resultData = new Bundle(); - } - resultData.putParcelable(PROVIDER_KEY, provider); - if (receiver != null) { - receiver.send(resultCode, resultData); - } else { - broadcastEvent(resultCode, resultData); - } - } - - private void broadcastEvent(int resultCode , Bundle resultData) { - Intent intentUpdate = new Intent(BROADCAST_PROVIDER_API_EVENT); - intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); - intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode); - intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData); - serviceCallback.broadcastEvent(intentUpdate); - } - - - /** - * Validates parameters entered by the user to log in - * - * @param username - * @param password - * @return true if both parameters are present and the entered password length is greater or equal to eight (8). - */ - private boolean validUserLoginData(String username, String password) { - return validUsername(username) && wellFormedPassword(password); - } - - private boolean validUsername(String username) { - return username != null && !username.isEmpty(); - } - - /** - * Validates a password - * - * @param password - * @return true if the entered password length is greater or equal to eight (8). - */ - private boolean wellFormedPassword(String password) { - return password != null && password.length() >= 8; - } - - /** - * Sends an HTTP POST request to the authentication server with the SRP Parameter A. - * - * @param server_url - * @param username - * @param clientA First SRP parameter sent - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { - SrpCredentials srpCredentials = new SrpCredentials(username, clientA); - return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); - } - - /** - * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M). - * - * @param server_url - * @param username - * @param m1 Second SRP parameter sent - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { - String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; - return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); - } - - /** - * Sends an HTTP POST request to the api server to register a new user. - * - * @param server_url - * @param username - * @param salt - * @param password_verifier - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { - return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); - } - - /** - * Executes an HTTP request expecting a JSON response. - * - * @param url - * @param request_method - * @return response from authentication server - */ - private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { - return requestJsonFromServer(url, request_method, jsonString, new ArrayList>(), okHttpClient); - } - - protected String sendGetStringToServer(@NonNull String url, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { - return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); - } - - - - private JSONObject requestJsonFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { - JSONObject responseJson; - String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); - - try { - responseJson = new JSONObject(plain_response); - } catch (NullPointerException | JSONException e) { - e.printStackTrace(); - responseJson = getErrorMessageAsJson(error_json_exception_user_message); - } - return responseJson; - - } - - private String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { - String plainResponseBody; - - try { - - plainResponseBody = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); - - } catch (NullPointerException npe) { - plainResponseBody = formatErrorMessage(error_json_exception_user_message); - } catch (UnknownHostException | SocketTimeoutException e) { - plainResponseBody = formatErrorMessage(server_unreachable_message); - } catch (MalformedURLException e) { - plainResponseBody = formatErrorMessage(malformed_url); - } catch (SSLHandshakeException | SSLPeerUnverifiedException e) { - plainResponseBody = formatErrorMessage(certificate_error); - } catch (ConnectException e) { - plainResponseBody = formatErrorMessage(service_is_down_error); - } catch (IllegalArgumentException e) { - plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); - } catch (UnknownServiceException e) { - //unable to find acceptable protocols - tlsv1.2 not enabled? - plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); - } catch (IOException e) { - plainResponseBody = formatErrorMessage(error_io_exception_user_message); - } - - return plainResponseBody; - } - - private boolean canConnect(Provider provider, Bundle result) { - JSONObject errorJson = new JSONObject(); - String providerUrl = provider.getApiUrlString() + "/provider.json"; - - OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), errorJson); - if (okHttpClient == null) { - result.putString(ERRORS, errorJson.toString()); - return false; - } - - try { - - return ProviderApiConnector.canConnect(okHttpClient, providerUrl); - - } catch (UnknownHostException | SocketTimeoutException e) { - setErrorResult(result, server_unreachable_message, null); - } catch (MalformedURLException e) { - setErrorResult(result, malformed_url, null); - } catch (SSLHandshakeException e) { - setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); - } catch (ConnectException e) { - setErrorResult(result, service_is_down_error, null); - } catch (IllegalArgumentException e) { - setErrorResult(result, error_no_such_algorithm_exception_user_message, null); - } catch (UnknownServiceException e) { - //unable to find acceptable protocols - tlsv1.2 not enabled? - setErrorResult(result, error_no_such_algorithm_exception_user_message, null); - } catch (IOException e) { - setErrorResult(result, error_io_exception_user_message, null); - } - return false; - } - - /** - * Downloads a provider.json from a given URL, adding a new provider using the given name. - * - * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider - * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the update was successful. - */ - protected abstract Bundle setUpProvider(Provider provider, Bundle task); - - /** - * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways - * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the download was successful. - */ - protected abstract Bundle getAndSetEipServiceJson(Provider provider); - - /** - * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. - * - * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. - */ - protected abstract Bundle updateVpnCertificate(Provider provider); - - - /** - * Fetches the Geo ip Json, containing a list of gateways sorted by distance from the users current location - * - * @param provider - * @return - */ - protected abstract Bundle getGeoIPJson(Provider provider); - - - protected boolean isValidJson(String jsonString) { - try { - new JSONObject(jsonString); - return true; - } catch(JSONException e) { - return false; - } catch(NullPointerException e) { - e.printStackTrace(); - return false; - } - } - - protected boolean validCertificate(Provider provider, String certString) { - boolean result = false; - if (!ConfigHelper.checkErroneousDownload(certString)) { - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certString); - try { - if (certificate != null) { - JSONObject providerJson = provider.getDefinition(); - String fingerprint = providerJson.getString(Provider.CA_CERT_FINGERPRINT); - String encoding = fingerprint.split(":")[0]; - String expectedFingerprint = fingerprint.split(":")[1]; - String realFingerprint = getFingerprintFromCertificate(certificate, encoding); - - result = realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim()); - } else - result = false; - } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) { - result = false; - } - } - - return result; - } - - protected void getPersistedProviderUpdates(Provider provider) { - String providerDomain = getDomainFromMainURL(provider.getMainUrlString()); - if (hasUpdatedProviderDetails(providerDomain)) { - provider.setCaCert(getPersistedProviderCA(providerDomain)); - provider.define(getPersistedProviderDefinition(providerDomain)); - provider.setPrivateKey(getPersistedPrivateKey(providerDomain)); - provider.setVpnCertificate(getPersistedVPNCertificate(providerDomain)); - provider.setProviderApiIp(getPersistedProviderApiIp(providerDomain)); - provider.setProviderIp(getPersistedProviderIp(providerDomain)); - provider.setGeoipUrl(getPersistedGeoIp(providerDomain)); - } - } - - Bundle validateProviderDetails(Provider provider) { - Bundle result = new Bundle(); - result.putBoolean(BROADCAST_RESULT_KEY, false); - - if (!provider.hasDefinition()) { - return result; - } - - result = validateCertificateForProvider(result, provider); - - //invalid certificate or no certificate - if (result.containsKey(ERRORS) || (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) ) { - return result; - } - - result.putBoolean(BROADCAST_RESULT_KEY, true); - - return result; - } - - protected Bundle validateCertificateForProvider(Bundle result, Provider provider) { - String caCert = provider.getCaCert(); - - if (ConfigHelper.checkErroneousDownload(caCert)) { - return result; - } - - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(caCert); - if (certificate == null) { - return setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); - } - try { - certificate.checkValidity(); - String encoding = provider.getCertificatePinEncoding(); - String expectedFingerprint = provider.getCertificatePin(); - - String realFingerprint = getFingerprintFromCertificate(certificate, encoding); - if (!realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim())) { - return setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString()); - } - - if (!canConnect(provider, result)) { - return result; - } - } catch (NoSuchAlgorithmException e ) { - return setErrorResult(result, error_no_such_algorithm_exception_user_message, null); - } catch (ArrayIndexOutOfBoundsException e) { - return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString()); - } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) { - return setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); - } - - result.putBoolean(BROADCAST_RESULT_KEY, true); - return result; - } - - protected Bundle setErrorResult(Bundle result, String stringJsonErrorMessage) { - String reasonToFail = pickErrorMessage(stringJsonErrorMessage); - result.putString(ERRORS, reasonToFail); - result.putBoolean(BROADCAST_RESULT_KEY, false); - return result; - } - - Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) { - JSONObject errorJson = new JSONObject(); - String errorMessage = getProviderFormattedString(resources, errorMessageId); - if (errorId != null) { - addErrorMessageToJson(errorJson, errorMessage, errorId); - } else { - addErrorMessageToJson(errorJson, errorMessage); - } - result.putString(ERRORS, errorJson.toString()); - result.putBoolean(BROADCAST_RESULT_KEY, false); - return result; - } - - protected String getPersistedPrivateKey(String providerDomain) { - return getFromPersistedProvider(PROVIDER_PRIVATE_KEY, providerDomain, preferences); - } - - protected String getPersistedVPNCertificate(String providerDomain) { - return getFromPersistedProvider(PROVIDER_VPN_CERTIFICATE, providerDomain, preferences); - } - - protected JSONObject getPersistedProviderDefinition(String providerDomain) { - try { - return new JSONObject(getFromPersistedProvider(Provider.KEY, providerDomain, preferences)); - } catch (JSONException e) { - e.printStackTrace(); - return new JSONObject(); - } - } - - protected String getPersistedProviderCA(String providerDomain) { - return getFromPersistedProvider(CA_CERT, providerDomain, preferences); - } - - protected String getPersistedProviderApiIp(String providerDomain) { - return getFromPersistedProvider(PROVIDER_API_IP, providerDomain, preferences); - } - - protected String getPersistedProviderIp(String providerDomain) { - return getFromPersistedProvider(PROVIDER_IP, providerDomain, preferences); - } - - protected String getPersistedGeoIp(String providerDomain) { - return getFromPersistedProvider(GEOIP_URL, providerDomain, preferences); - } - - protected boolean hasUpdatedProviderDetails(String domain) { - return preferences.contains(Provider.KEY + "." + domain) && preferences.contains(CA_CERT + "." + domain); - } - - protected String getDomainFromMainURL(String mainUrl) { - return mainUrl.replaceFirst("http[s]?://", "").replaceFirst("/.*", ""); - - } - - /** - * Interprets the error message as a JSON object and extract the "errors" keyword pair. - * If the error message is not a JSON object, then it is returned untouched. - * - * @param stringJsonErrorMessage - * @return final error message - */ - protected String pickErrorMessage(String stringJsonErrorMessage) { - String errorMessage = ""; - try { - JSONObject jsonErrorMessage = new JSONObject(stringJsonErrorMessage); - errorMessage = jsonErrorMessage.getString(ERRORS); - } catch (JSONException e) { - // TODO Auto-generated catch block - errorMessage = stringJsonErrorMessage; - } catch (NullPointerException e) { - //do nothing - } - - return errorMessage; - } - - @NonNull - protected List> getAuthorizationHeader() { - List> headerArgs = new ArrayList<>(); - if (!LeapSRPSession.getToken().isEmpty()) { - Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); - headerArgs.add(authorizationHeaderPair); - } - return headerArgs; - } - - private boolean logOut(Provider provider) { - OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), new JSONObject()); - if (okHttpClient == null) { - return false; - } - - String deleteUrl = provider.getApiUrlWithVersion() + "/logout"; - - if (ProviderApiConnector.delete(okHttpClient, deleteUrl)) { - LeapSRPSession.setToken(""); - return true; - } - return false; - } - - protected Bundle loadCertificate(Provider provider, String certString) { - Bundle result = new Bundle(); - if (certString == null) { - setErrorResult(result, vpn_certificate_is_invalid, null); - return result; - } - - try { - // API returns concatenated cert & key. Split them for OpenVPN options - String certificateString = null, keyString = null; - String[] certAndKey = certString.split("(?<=-\n)"); - for (int i = 0; i < certAndKey.length - 1; i++) { - if (certAndKey[i].contains("KEY")) { - keyString = certAndKey[i++] + certAndKey[i]; - } else if (certAndKey[i].contains("CERTIFICATE")) { - certificateString = certAndKey[i++] + certAndKey[i]; - } - } - - RSAPrivateKey key = parseRsaKeyFromString(keyString); - keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); - provider.setPrivateKey( "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----"); - - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certificateString); - certificate.checkValidity(); - certificateString = Base64.encodeToString(certificate.getEncoded(), Base64.DEFAULT); - provider.setVpnCertificate( "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----"); - result.putBoolean(BROADCAST_RESULT_KEY, true); - } catch (CertificateException | NullPointerException e) { - e.printStackTrace(); - setErrorResult(result, vpn_certificate_is_invalid, null); - } - return result; - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiSetupBroadcastReceiver.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiSetupBroadcastReceiver.java deleted file mode 100644 index 890d3b67..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiSetupBroadcastReceiver.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * 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.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import java.lang.ref.WeakReference; - -import se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState; - -/** - * Broadcast receiver that handles callback intents of ProviderApi during provider setup. - * It is used by CustomProviderSetupActivity for custom branded apps and ProviderListActivity - * for 'normal' Bitmask. - * - * Created by cyberta on 17.08.18. - */ - -public class ProviderApiSetupBroadcastReceiver extends BroadcastReceiver { - private WeakReference setupInterfaceRef; - - public ProviderApiSetupBroadcastReceiver(ProviderSetupInterface setupInterface) { - this.setupInterfaceRef = new WeakReference<>(setupInterface); - } - - @Override - public void onReceive(Context context, Intent intent) { - Log.d(ProviderListBaseActivity.TAG, "received Broadcast"); - ProviderSetupInterface setupInterface = setupInterfaceRef.get(); - String action = intent.getAction(); - if (action == null || !action.equalsIgnoreCase(Constants.BROADCAST_PROVIDER_API_EVENT) || setupInterface == null) { - return; - } - - if (setupInterface.getConfigState() != null && - setupInterface.getConfigState() == ProviderConfigState.SETTING_UP_PROVIDER) { - int resultCode = intent.getIntExtra(Constants.BROADCAST_RESULT_CODE, ProviderListBaseActivity.RESULT_CANCELED); - Log.d(ProviderListBaseActivity.TAG, "Broadcast resultCode: " + resultCode); - - Bundle resultData = intent.getParcelableExtra(Constants.BROADCAST_RESULT_KEY); - Provider handledProvider = resultData.getParcelable(Constants.PROVIDER_KEY); - - if (handledProvider != null && setupInterface.getProvider() != null && - handledProvider.getDomain().equalsIgnoreCase(setupInterface.getProvider().getDomain())) { - switch (resultCode) { - case ProviderAPI.PROVIDER_OK: - setupInterface.handleProviderSetUp(handledProvider); - break; - case ProviderAPI.PROVIDER_NOK: - setupInterface.handleProviderSetupFailed(resultData); - break; - case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE: - setupInterface.handleCorrectlyDownloadedCertificate(handledProvider); - break; - case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE: - setupInterface.handleIncorrectlyDownloadedCertificate(); - break; - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java deleted file mode 100644 index c61caead..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java +++ /dev/null @@ -1,475 +0,0 @@ -/** - * 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.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.textfield.TextInputLayout; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.appcompat.widget.AppCompatButton; -import androidx.appcompat.widget.AppCompatTextView; -import android.text.Editable; -import android.text.Html; -import android.text.TextWatcher; -import android.text.method.LinkMovementMethod; -import android.text.util.Linkify; -import android.util.Log; -import android.view.KeyEvent; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; - -import org.json.JSONArray; -import org.json.JSONException; - -import butterknife.InjectView; -import butterknife.OnClick; -import se.leap.bitmaskclient.Constants.CREDENTIAL_ERRORS; - -import static android.text.TextUtils.isEmpty; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static se.leap.bitmaskclient.Constants.BROADCAST_PROVIDER_API_EVENT; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; -import static se.leap.bitmaskclient.Constants.CREDENTIALS_PASSWORD; -import static se.leap.bitmaskclient.Constants.CREDENTIALS_USERNAME; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.LOG_IN; -import static se.leap.bitmaskclient.ProviderAPI.SIGN_UP; -import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE; - -/** - * Base Activity for activities concerning a provider interaction - * - * Created by fupduck on 09.01.18. - */ - -public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseActivity { - - final protected static String TAG = ProviderCredentialsBaseActivity.class.getName(); - - final private static String ACTIVITY_STATE = "ACTIVITY STATE"; - - final private static String SHOWING_FORM = "SHOWING_FORM"; - final private static String PERFORMING_ACTION = "PERFORMING_ACTION"; - final private static String USERNAME_ERROR = "USERNAME_ERROR"; - final private static String PASSWORD_ERROR = "PASSWORD_ERROR"; - final private static String PASSWORD_VERIFICATION_ERROR = "PASSWORD_VERIFICATION_ERROR"; - - protected Intent mConfigState = new Intent(SHOWING_FORM); - protected ProviderAPIBroadcastReceiver providerAPIBroadcastReceiver; - - @InjectView(R.id.provider_credentials_user_message) - AppCompatTextView userMessage; - - @InjectView(R.id.provider_credentials_username) - TextInputEditText usernameField; - - @InjectView(R.id.provider_credentials_password) - TextInputEditText passwordField; - - @InjectView(R.id.provider_credentials_password_verification) - TextInputEditText passwordVerificationField; - - @InjectView(R.id.provider_credentials_username_error) - TextInputLayout usernameError; - - @InjectView(R.id.provider_credentials_password_error) - TextInputLayout passwordError; - - @InjectView(R.id.provider_credentials_password_verification_error) - TextInputLayout passwordVerificationError; - - @InjectView(R.id.button) - AppCompatButton button; - - private boolean isUsernameError = false; - private boolean isPasswordError = false; - private boolean isVerificationError = false; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.a_provider_credentials); - providerAPIBroadcastReceiver = new ProviderAPIBroadcastReceiver(); - - IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_PROVIDER_API_EVENT); - updateIntentFilter.addCategory(Intent.CATEGORY_DEFAULT); - LocalBroadcastManager.getInstance(this).registerReceiver(providerAPIBroadcastReceiver, updateIntentFilter); - - setUpListeners(); - restoreState(savedInstanceState); - - String userMessageString = getIntent().getStringExtra(USER_MESSAGE); - if (userMessageString != null) { - userMessage.setText(userMessageString); - userMessage.setVisibility(VISIBLE); - } - } - - @Override - protected void onResume() { - super.onResume(); - - String action = mConfigState.getAction(); - if (action == null) { - return; - } - - if(action.equalsIgnoreCase(PERFORMING_ACTION)) { - showProgressBar(); - } - } - - protected void restoreState(Bundle savedInstance) { - super.restoreState(savedInstance); - if (savedInstance == null) { - return; - } - if (savedInstance.getString(USER_MESSAGE) != null) { - userMessage.setText(savedInstance.getString(USER_MESSAGE)); - userMessage.setVisibility(VISIBLE); - } - updateUsernameError(savedInstance.getString(USERNAME_ERROR)); - updatePasswordError(savedInstance.getString(PASSWORD_ERROR)); - updateVerificationError(savedInstance.getString(PASSWORD_VERIFICATION_ERROR)); - if (savedInstance.getString(ACTIVITY_STATE) != null) { - mConfigState.setAction(savedInstance.getString(ACTIVITY_STATE)); - } - } - - private void updateUsernameError(String usernameErrorString) { - usernameError.setError(usernameErrorString); - isUsernameError = usernameErrorString != null; - updateButton(); - } - - private void updatePasswordError(String passwordErrorString) { - passwordError.setError(passwordErrorString); - isPasswordError = passwordErrorString != null; - updateButton(); - } - - private void updateVerificationError(String verificationErrorString) { - passwordVerificationError.setError(verificationErrorString); - isVerificationError = verificationErrorString != null; - updateButton(); - } - - private void updateButton() { - button.setEnabled(!isPasswordError && - !isUsernameError && - !isVerificationError && - !isEmpty(passwordField.getText()) && - !isEmpty(usernameField.getText()) && - !(passwordVerificationField.getVisibility() == VISIBLE && - getPasswordVerification().length() == 0)); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putString(ACTIVITY_STATE, mConfigState.getAction()); - if (userMessage.getText() != null && userMessage.getVisibility() == VISIBLE) { - outState.putString(USER_MESSAGE, userMessage.getText().toString()); - } - if (usernameError.getError() != null) { - outState.putString(USERNAME_ERROR, usernameError.getError().toString()); - } - if (passwordError.getError() != null) { - outState.putString(PASSWORD_ERROR, passwordError.getError().toString()); - } - if (passwordVerificationError.getError() != null) { - outState.putString(PASSWORD_VERIFICATION_ERROR, passwordVerificationError.getError().toString()); - } - - super.onSaveInstanceState(outState); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (providerAPIBroadcastReceiver != null) - LocalBroadcastManager.getInstance(this).unregisterReceiver(providerAPIBroadcastReceiver); - } - - @OnClick(R.id.button) - void handleButton() { - mConfigState.setAction(PERFORMING_ACTION); - hideKeyboard(); - showProgressBar(); - } - - protected void setButtonText(@StringRes int buttonText) { - button.setText(buttonText); - } - - String getUsername() { - String username = usernameField.getText().toString(); - String providerDomain = provider.getDomain(); - if (username.endsWith(providerDomain)) { - try { - return username.split("@" + providerDomain)[0]; - } catch (ArrayIndexOutOfBoundsException e) { - return ""; - } - } - return username; - } - - String getPassword() { - return passwordField.getText().toString(); - } - - String getPasswordVerification() { - return passwordVerificationField.getText().toString(); - } - - void login(String username, String password) { - - Bundle parameters = bundleUsernameAndPassword(username, password); - ProviderAPICommand.execute(this, LOG_IN, parameters, provider); - } - - public void signUp(String username, String password) { - - Bundle parameters = bundleUsernameAndPassword(username, password); - ProviderAPICommand.execute(this, SIGN_UP, parameters, provider); - } - - void downloadVpnCertificate(Provider handledProvider) { - provider = handledProvider; - ProviderAPICommand.execute(this, DOWNLOAD_VPN_CERTIFICATE, provider); - } - - protected Bundle bundleUsernameAndPassword(String username, String password) { - Bundle parameters = new Bundle(); - if (!username.isEmpty()) - parameters.putString(CREDENTIALS_USERNAME, username); - if (!password.isEmpty()) - parameters.putString(CREDENTIALS_PASSWORD, password); - return parameters; - } - - private void setUpListeners() { - usernameField.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - if (getUsername().equalsIgnoreCase("")) { - s.clear(); - updateUsernameError(getString(R.string.username_ask)); - } else { - updateUsernameError(null); - String suffix = "@" + provider.getDomain(); - if (!usernameField.getText().toString().endsWith(suffix)) { - s.append(suffix); - usernameField.setSelection(usernameField.getText().toString().indexOf('@')); - } - } - } - }); - usernameField.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == IME_ACTION_DONE - || event != null && event.getAction() == KeyEvent.ACTION_DOWN - && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { - passwordField.requestFocus(); - return true; - } - return false; - } - }); - - passwordField.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - if(getPassword().length() < 8) { - updatePasswordError(getString(R.string.error_not_valid_password_user_message)); - } else { - updatePasswordError(null); - } - } - }); - passwordField.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == IME_ACTION_DONE - || event != null && event.getAction() == KeyEvent.ACTION_DOWN - && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { - if (passwordVerificationField.getVisibility() == VISIBLE) { - passwordVerificationField.requestFocus(); - } else { - button.performClick(); - } - return true; - } - return false; - } - }); - - passwordVerificationField.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - if(getPassword().equals(getPasswordVerification())) { - updateVerificationError(null); - } else { - updateVerificationError(getString(R.string.password_mismatch)); - } - } - }); - passwordVerificationField.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == IME_ACTION_DONE - || event != null && event.getAction() == KeyEvent.ACTION_DOWN - && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { - button.performClick(); - return true; - } - return false; - } - }); - } - - private void hideKeyboard() { - InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.hideSoftInputFromWindow(passwordField.getWindowToken(), 0); - } - } - - private void handleReceivedErrors(Bundle arguments) { - if (arguments.containsKey(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString())) { - updatePasswordError(getString(R.string.error_not_valid_password_user_message)); - } else if (arguments.containsKey(CREDENTIAL_ERRORS.RISEUP_WARNING.toString())) { - userMessage.setVisibility(VISIBLE); - userMessage.setText(R.string.login_riseup_warning); - } - if (arguments.containsKey(CREDENTIALS_USERNAME)) { - String username = arguments.getString(CREDENTIALS_USERNAME); - usernameField.setText(username); - } - if (arguments.containsKey(CREDENTIAL_ERRORS.USERNAME_MISSING.toString())) { - updateUsernameError(getString(R.string.username_ask)); - } - if (arguments.containsKey(USER_MESSAGE)) { - String userMessageString = arguments.getString(USER_MESSAGE); - try { - userMessageString = new JSONArray(userMessageString).getString(0); - } catch (JSONException e) { - e.printStackTrace(); - } - - if (Build.VERSION.SDK_INT >= VERSION_CODES.N) { - userMessage.setText(Html.fromHtml(userMessageString, Html.FROM_HTML_MODE_LEGACY)); - } else { - userMessage.setText(Html.fromHtml(userMessageString)); - } - Linkify.addLinks(userMessage, Linkify.ALL); - userMessage.setMovementMethod(LinkMovementMethod.getInstance()); - userMessage.setVisibility(VISIBLE); - } else if (userMessage.getVisibility() != GONE) { - userMessage.setVisibility(GONE); - } - - if (!usernameField.getText().toString().isEmpty() && passwordField.isFocusable()) - passwordField.requestFocus(); - - mConfigState.setAction(SHOWING_FORM); - hideProgressBar(); - } - - private void successfullyFinished(Provider handledProvider) { - provider = handledProvider; - Intent resultData = new Intent(); - resultData.putExtra(Provider.KEY, provider); - setResult(RESULT_OK, resultData); - finish(); - } - - //TODO: replace with EipSetupObserver - public class ProviderAPIBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - Log.d(TAG, "received Broadcast"); - - String action = intent.getAction(); - if (action == null || !action.equalsIgnoreCase(BROADCAST_PROVIDER_API_EVENT)) { - return; - } - - int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); - Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY); - Provider handledProvider = resultData.getParcelable(PROVIDER_KEY); - - switch (resultCode) { - case ProviderAPI.SUCCESSFUL_SIGNUP: - String password = resultData.getString(CREDENTIALS_PASSWORD); - String username = resultData.getString(CREDENTIALS_USERNAME); - login(username, password); - break; - case ProviderAPI.SUCCESSFUL_LOGIN: - downloadVpnCertificate(handledProvider); - break; - case ProviderAPI.FAILED_LOGIN: - case ProviderAPI.FAILED_SIGNUP: - handleReceivedErrors((Bundle) intent.getParcelableExtra(BROADCAST_RESULT_KEY)); - break; - - case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE: - // error handling takes place in MainActivity - case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE: - successfullyFinished(handledProvider); - break; - } - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java b/app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java deleted file mode 100644 index 6672c575..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java +++ /dev/null @@ -1,19 +0,0 @@ -package se.leap.bitmaskclient; - -import android.view.LayoutInflater; - -import com.pedrogomez.renderers.AdapteeCollection; -import com.pedrogomez.renderers.RendererAdapter; -import com.pedrogomez.renderers.RendererBuilder; - -public class ProviderListAdapter extends RendererAdapter { - public ProviderListAdapter(LayoutInflater layoutInflater, RendererBuilder rendererBuilder, - AdapteeCollection collection) { - super(layoutInflater, rendererBuilder, collection); - } - - public void saveProviders() { - ProviderManager provider_manager = (ProviderManager) getCollection(); - provider_manager.saveCustomProvidersToFile(); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java deleted file mode 100644 index 44258a7b..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright (c) 2017 LEAP Encryption Access Project and contributors - *

- * 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.content.Intent; -import android.os.Bundle; -import android.widget.ListView; - -import androidx.annotation.NonNull; - -import com.pedrogomez.renderers.Renderer; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -import butterknife.InjectView; -import butterknife.OnItemClick; - -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_ADD_PROVIDER; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_CONFIGURE_LEAP; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.SHOW_FAILED_DIALOG; - -/** - * abstract base Activity that builds and shows the list of known available providers. - * The implementation of ProviderListBaseActivity differ in that they may or may not allow to bypass - * secure download mechanisms including certificate validation. - *

- * It also allows the user to enter custom providers with a button. - * - * @author parmegv - * @author cyberta - */ - -public abstract class ProviderListBaseActivity extends ProviderSetupBaseActivity { - - @InjectView(R.id.provider_list) - protected ListView providerListView; - @Inject - protected ProviderListAdapter adapter; - - final public static String TAG = ProviderListActivity.class.getSimpleName(); - final protected static String EXTRAS_KEY_INVALID_URL = "INVALID_URL"; - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setUpInitialUI(); - initProviderList(); - restoreState(savedInstanceState); - } - - public abstract void retrySetUpProvider(@NonNull Provider provider); - - protected abstract void onItemSelectedLogic(); - - private void initProviderList() { - List> prototypes = new ArrayList<>(); - prototypes.add(new ProviderRenderer(this)); - ProviderRendererBuilder providerRendererBuilder = new ProviderRendererBuilder(prototypes); - adapter = new ProviderListAdapter(getLayoutInflater(), providerRendererBuilder, getProviderManager()); - providerListView.setAdapter(adapter); - } - - private void setUpInitialUI() { - setContentView(R.layout.a_provider_list); - setProviderHeaderText(R.string.setup_provider); - hideProgressBar(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) { - if (resultCode == RESULT_OK) { - setResult(resultCode, data); - finish(); - } - } else if (requestCode == REQUEST_CODE_ADD_PROVIDER) { - if (resultCode == RESULT_OK) { - testNewURL = true; - String newUrl = data.getStringExtra(AddProviderActivity.EXTRAS_KEY_NEW_URL); - this.provider.setMainUrl(newUrl); - showAndSelectProvider(newUrl); - } else { - cancelSettingUpProvider(); - } - } - } - - public void showAndSelectProvider(String newURL) { - provider = new Provider(newURL, null, null); - autoSelectProvider(); - } - - private void autoSelectProvider() { - onItemSelectedLogic(); - showProgressBar(); - } - - // ------- ProviderSetupInterface ---v - @Override - public void handleProviderSetUp(Provider handledProvider) { - this.provider = handledProvider; - adapter.add(provider); - adapter.saveProviders(); - if (provider.allowsAnonymous()) { - //FIXME: providerApiBroadcastReceiver.getConfigState().putExtra(SERVICES_RETRIEVED, true); DEAD CODE??? - downloadVpnCertificate(); - } else { - showProviderDetails(); - } - } - - @Override - public void handleCorrectlyDownloadedCertificate(Provider handledProvider) { - this.provider = handledProvider; - showProviderDetails(); - } - - @OnItemClick(R.id.provider_list) - void onItemSelected(int position) { - if (SETTING_UP_PROVIDER == getConfigState() || - SHOW_FAILED_DIALOG == getConfigState()) { - return; - } - - //TODO Code 2 pane view - provider = adapter.getItem(position); - if (provider != null && !provider.isDefault()) { - //TODO Code 2 pane view - providerConfigState = SETTING_UP_PROVIDER; - showProgressBar(); - onItemSelectedLogic(); - } else { - addAndSelectNewProvider(); - } - } - - @Override - public void onBackPressed() { - if (SETTING_UP_PROVIDER == providerConfigState || - SHOW_FAILED_DIALOG == providerConfigState) { - cancelSettingUpProvider(); - } else { - super.onBackPressed(); - } - } - - /** - * Open the new provider dialog - */ - public void addAndSelectNewProvider() { - Intent intent = new Intent(this, AddProviderActivity.class); - startActivityForResult(intent, REQUEST_CODE_ADD_PROVIDER); - } - - /** - * Open the new provider dialog - */ - @Override - public void addAndSelectNewProvider(String url) { - testNewURL = false; - Intent intent = new Intent(this, AddProviderActivity.class); - intent.putExtra(EXTRAS_KEY_INVALID_URL, url); - startActivityForResult(intent, REQUEST_CODE_ADD_PROVIDER); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java deleted file mode 100644 index c23ad270..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java +++ /dev/null @@ -1,270 +0,0 @@ -package se.leap.bitmaskclient; - -import android.content.res.AssetManager; -import androidx.annotation.VisibleForTesting; - -import com.pedrogomez.renderers.AdapteeCollection; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import static se.leap.bitmaskclient.Provider.GEOIP_URL; -import static se.leap.bitmaskclient.Provider.MAIN_URL; -import static se.leap.bitmaskclient.Provider.PROVIDER_API_IP; -import static se.leap.bitmaskclient.Provider.PROVIDER_IP; -import static se.leap.bitmaskclient.utils.FileHelper.createFile; -import static se.leap.bitmaskclient.utils.FileHelper.persistFile; -import static se.leap.bitmaskclient.utils.InputStreamHelper.getInputStreamFrom; -import static se.leap.bitmaskclient.utils.InputStreamHelper.loadInputStreamAsString; - -/** - * Created by parmegv on 4/12/14. - */ -public class ProviderManager implements AdapteeCollection { - - private AssetManager assetsManager; - private File externalFilesDir; - private Set defaultProviders; - private Set customProviders; - private Set defaultProviderURLs; - private Set customProviderURLs; - - private static ProviderManager instance; - - final private static String URLS = "urls"; - final private static String EXT_JSON = ".json"; - final private static String EXT_PEM = ".pem"; - - public static ProviderManager getInstance(AssetManager assetsManager, File externalFilesDir) { - if (instance == null) - instance = new ProviderManager(assetsManager, externalFilesDir); - - return instance; - } - - @VisibleForTesting - static void reset() { - instance = null; - } - - private ProviderManager(AssetManager assetManager, File externalFilesDir) { - this.assetsManager = assetManager; - addDefaultProviders(assetManager); - addCustomProviders(externalFilesDir); - } - - private void addDefaultProviders(AssetManager assets_manager) { - try { - defaultProviders = providersFromAssets(URLS, assets_manager.list(URLS)); - defaultProviderURLs = getProviderUrlSetFromProviderSet(defaultProviders); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private Set getProviderUrlSetFromProviderSet(Set providers) { - HashSet providerUrls = new HashSet<>(); - for (Provider provider : providers) { - providerUrls.add(provider.getMainUrl().getUrl()); - } - return providerUrls; - } - - private Set providersFromAssets(String directory, String[] relativeFilePaths) { - Set providers = new HashSet<>(); - - for (String file : relativeFilePaths) { - String mainUrl = null; - String providerIp = null; - String providerApiIp = null; - String certificate = null; - String providerDefinition = null; - String geoipUrl = null; - try { - String provider = file.substring(0, file.length() - ".url".length()); - InputStream providerFile = assetsManager.open(directory + "/" + file); - mainUrl = extractKeyFromInputStream(providerFile, MAIN_URL); - providerIp = extractKeyFromInputStream(providerFile, PROVIDER_IP); - providerApiIp = extractKeyFromInputStream(providerFile, PROVIDER_API_IP); - geoipUrl = extractKeyFromInputStream(providerFile, GEOIP_URL); - certificate = loadInputStreamAsString(assetsManager.open(provider + EXT_PEM)); - providerDefinition = loadInputStreamAsString(assetsManager.open(provider + EXT_JSON)); - } catch (IOException e) { - e.printStackTrace(); - } - providers.add(new Provider(mainUrl, geoipUrl, providerIp, providerApiIp, certificate, providerDefinition)); - } - - return providers; - } - - - private void addCustomProviders(File externalFilesDir) { - this.externalFilesDir = externalFilesDir; - customProviders = externalFilesDir != null && externalFilesDir.isDirectory() ? - providersFromFiles(externalFilesDir.list()) : - new HashSet<>(); - customProviderURLs = getProviderUrlSetFromProviderSet(customProviders); - } - - private Set providersFromFiles(String[] files) { - Set providers = new HashSet<>(); - try { - for (String file : files) { - InputStream inputStream = getInputStreamFrom(externalFilesDir.getAbsolutePath() + "/" + file); - String mainUrl = extractKeyFromInputStream(inputStream, MAIN_URL); - String providerIp = extractKeyFromInputStream(inputStream, PROVIDER_IP); - String providerApiIp = extractKeyFromInputStream(inputStream, PROVIDER_API_IP); - providers.add(new Provider(mainUrl, providerIp, providerApiIp)); - } - } catch (FileNotFoundException | NullPointerException e) { - e.printStackTrace(); - } - - return providers; - } - - private String extractKeyFromInputStream(InputStream inputStream, String key) { - String value = ""; - - JSONObject fileContents = inputStreamToJson(inputStream); - if (fileContents != null) - value = fileContents.optString(key); - return value; - } - - private JSONObject inputStreamToJson(InputStream inputStream) { - JSONObject json = null; - try { - byte[] bytes = new byte[inputStream.available()]; - if (inputStream.read(bytes) > 0) - json = new JSONObject(new String(bytes)); - inputStream.reset(); - } catch (IOException | JSONException e) { - e.printStackTrace(); - } - return json; - } - - public List providers() { - List allProviders = new ArrayList<>(); - allProviders.addAll(defaultProviders); - if(customProviders != null) - allProviders.addAll(customProviders); - //add an option to add a custom provider - //TODO: refactor me? - allProviders.add(new Provider()); - return allProviders; - } - - @Override - public int size() { - return providers().size(); - } - - @Override - public Provider get(int index) { - Iterator iterator = providers().iterator(); - while (iterator.hasNext() && index > 0) { - iterator.next(); - index--; - } - return iterator.next(); - } - - @Override - public boolean add(Provider element) { - return element != null && - !defaultProviderURLs.contains(element.getMainUrl().getUrl()) && - customProviders.add(element) && - customProviderURLs.add(element.getMainUrl().getUrl()); - } - - @Override - public boolean remove(Object element) { - return element instanceof Provider && - customProviders.remove(element) && - customProviderURLs.remove(((Provider) element).getMainUrl().getUrl()); - } - - @Override - public boolean addAll(Collection elements) { - Iterator iterator = elements.iterator(); - boolean addedAll = true; - while (iterator.hasNext()) { - Provider p = (Provider) iterator.next(); - addedAll = customProviders.add(p) && - customProviderURLs.add(p.getMainUrl().getUrl()) && - addedAll; - } - return addedAll; - } - - @Override - public boolean removeAll(Collection elements) { - Iterator iterator = elements.iterator(); - boolean removedAll = true; - try { - while (iterator.hasNext()) { - Provider p = (Provider) iterator.next(); - removedAll = ((defaultProviders.remove(p) && defaultProviderURLs.remove(p.getMainUrl().getUrl())) || - (customProviders.remove(p) && customProviderURLs.remove(p.getMainUrl().getUrl()))) && - removedAll; - } - } catch (ClassCastException e) { - return false; - } - - return removedAll; - } - - @Override - public void clear() { - defaultProviders.clear(); - customProviders.clear(); - customProviderURLs.clear(); - defaultProviderURLs.clear(); - } - - void saveCustomProvidersToFile() { - try { - deleteLegacyCustomProviders(); - - for (Provider provider : customProviders) { - File providerFile = createFile(externalFilesDir, provider.getName() + EXT_JSON); - if (!providerFile.exists()) { - persistFile(providerFile, provider.toJson().toString()); - } - } - } catch (IOException | SecurityException e) { - e.printStackTrace(); - } - } - - /** - * Deletes persisted custom providers from from internal storage that are not in customProviders list anymore - */ - private void deleteLegacyCustomProviders() throws IOException, SecurityException { - Set persistedCustomProviders = externalFilesDir != null && externalFilesDir.isDirectory() ? - providersFromFiles(externalFilesDir.list()) : new HashSet(); - persistedCustomProviders.removeAll(customProviders); - for (Provider providerToDelete : persistedCustomProviders) { - File providerFile = createFile(externalFilesDir, providerToDelete.getName() + EXT_JSON); - if (providerFile.exists()) { - providerFile.delete(); - } - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderObservable.java b/app/src/main/java/se/leap/bitmaskclient/ProviderObservable.java deleted file mode 100644 index 50cc37d4..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderObservable.java +++ /dev/null @@ -1,39 +0,0 @@ -package se.leap.bitmaskclient; - -import java.util.Observable; - -/** - * Created by cyberta on 05.12.18. - */ -public class ProviderObservable extends Observable { - private static ProviderObservable instance; - private Provider currentProvider; - private Provider providerForDns; - - public static ProviderObservable getInstance() { - if (instance == null) { - instance = new ProviderObservable(); - } - return instance; - } - - public synchronized void updateProvider(Provider provider) { - instance.currentProvider = provider; - instance.providerForDns = null; - instance.setChanged(); - instance.notifyObservers(); - } - - public Provider getCurrentProvider() { - return instance.currentProvider; - } - - public void setProviderForDns(Provider provider) { - this.providerForDns = provider; - } - - public Provider getProviderForDns() { - return instance.providerForDns; - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderRenderer.java b/app/src/main/java/se/leap/bitmaskclient/ProviderRenderer.java deleted file mode 100644 index 795c8e82..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderRenderer.java +++ /dev/null @@ -1,55 +0,0 @@ -package se.leap.bitmaskclient; - -import android.content.*; -import android.view.*; -import android.widget.*; - -import com.pedrogomez.renderers.*; - -import butterknife.*; - -/** - * Created by parmegv on 4/12/14. - */ -public class ProviderRenderer extends Renderer { - private final Context context; - - @InjectView(R.id.provider_name) - TextView name; - @InjectView(R.id.provider_domain) - TextView domain; - - public ProviderRenderer(Context context) { - this.context = context; - } - - @Override - protected View inflate(LayoutInflater inflater, ViewGroup parent) { - View view = inflater.inflate(R.layout.v_provider_list_item, parent, false); - ButterKnife.inject(this, view); - return view; - } - - @Override - protected void setUpView(View rootView) { - /* - * Empty implementation substituted with the usage of ButterKnife library by Jake Wharton. - */ - } - - @Override - protected void hookListeners(View rootView) { - //Empty - } - - @Override - public void render() { - Provider provider = getContent(); - if (!provider.isDefault()) { - name.setText(provider.getName()); - domain.setText(provider.getDomain()); - } else { - domain.setText(R.string.add_provider); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderRendererBuilder.java b/app/src/main/java/se/leap/bitmaskclient/ProviderRendererBuilder.java deleted file mode 100644 index 5a6e857d..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderRendererBuilder.java +++ /dev/null @@ -1,19 +0,0 @@ -package se.leap.bitmaskclient; - -import com.pedrogomez.renderers.*; - -import java.util.*; - -/** - * Created by parmegv on 4/12/14. - */ -public class ProviderRendererBuilder extends RendererBuilder { - public ProviderRendererBuilder(Collection> prototypes) { - super(prototypes); - } - - @Override - protected Class getPrototypeClass(Provider content) { - return ProviderRenderer.class; - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderSetupBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ProviderSetupBaseActivity.java deleted file mode 100644 index 0994927f..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderSetupBaseActivity.java +++ /dev/null @@ -1,231 +0,0 @@ -/** - * 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.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import org.jetbrains.annotations.NotNull; -import org.json.JSONException; -import org.json.JSONObject; - -import static se.leap.bitmaskclient.Constants.BROADCAST_PROVIDER_API_EVENT; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_CONFIGURE_LEAP; -import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.ProviderAPI.ERRORS; -import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROVIDER_DETAILS; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.PENDING_SHOW_FAILED_DIALOG; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.PENDING_SHOW_PROVIDER_DETAILS; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.PROVIDER_NOT_SET; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.SHOWING_PROVIDER_DETAILS; -import static se.leap.bitmaskclient.ProviderSetupInterface.ProviderConfigState.SHOW_FAILED_DIALOG; - -/** - * Created by cyberta on 19.08.18. - */ - -public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity implements ProviderSetupInterface, ProviderSetupFailedDialog.DownloadFailedDialogInterface { - final public static String TAG = "PoviderSetupActivity"; - final private static String ACTIVITY_STATE = "ACTIVITY STATE"; - final private static String REASON_TO_FAIL = "REASON TO FAIL"; - - protected ProviderSetupInterface.ProviderConfigState providerConfigState = PROVIDER_NOT_SET; - private ProviderManager providerManager; - private FragmentManagerEnhanced fragmentManager; - - private String reasonToFail; - protected boolean testNewURL; - - private ProviderApiSetupBroadcastReceiver providerAPIBroadcastReceiver; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - fragmentManager = new FragmentManagerEnhanced(getSupportFragmentManager()); - providerManager = ProviderManager.getInstance(getAssets(), getExternalFilesDir(null)); - setUpProviderAPIResultReceiver(); - } - - @Override - protected void onResume() { - super.onResume(); - Log.d(TAG, "resuming with ConfigState: " + providerConfigState.toString()); - if (SETTING_UP_PROVIDER == providerConfigState) { - showProgressBar(); - } else if (PENDING_SHOW_FAILED_DIALOG == providerConfigState) { - showProgressBar(); - showDownloadFailedDialog(); - } else if (SHOW_FAILED_DIALOG == providerConfigState) { - showProgressBar(); - } else if (SHOWING_PROVIDER_DETAILS == providerConfigState) { - cancelSettingUpProvider(); - } else if (PENDING_SHOW_PROVIDER_DETAILS == providerConfigState) { - showProviderDetails(); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (providerAPIBroadcastReceiver != null) { - LocalBroadcastManager.getInstance(this).unregisterReceiver(providerAPIBroadcastReceiver); - } - providerAPIBroadcastReceiver = null; - } - - - @Override - public void onSaveInstanceState(@NotNull Bundle outState) { - outState.putString(ACTIVITY_STATE, providerConfigState.toString()); - outState.putString(REASON_TO_FAIL, reasonToFail); - - super.onSaveInstanceState(outState); - } - - protected FragmentManagerEnhanced getFragmentManagerEnhanced() { - return fragmentManager; - } - - protected ProviderManager getProviderManager() { - return providerManager; - } - - protected void setProviderConfigState(ProviderConfigState state) { - this.providerConfigState = state; - } - - protected void setProvider(Provider provider) { - this.provider = provider; - } - - // --------- ProviderSetupInterface ---v - @Override - public Provider getProvider() { - return provider; - } - - @Override - public ProviderConfigState getConfigState() { - return providerConfigState; - } - - @Override - public void handleProviderSetupFailed(Bundle resultData) { - reasonToFail = resultData.getString(ERRORS); - showDownloadFailedDialog(); - } - - @Override - public void handleIncorrectlyDownloadedCertificate() { - cancelSettingUpProvider(); - setResult(RESULT_CANCELED, new Intent(getConfigState().toString())); - } - - // -------- DownloadFailedDialogInterface ---v - @Override - public void cancelSettingUpProvider() { - providerConfigState = PROVIDER_NOT_SET; - provider = null; - hideProgressBar(); - } - - @Override - public void updateProviderDetails() { - providerConfigState = SETTING_UP_PROVIDER; - ProviderAPICommand.execute(this, UPDATE_PROVIDER_DETAILS, provider); - } - - protected void restoreState(Bundle savedInstanceState) { - super.restoreState(savedInstanceState); - if (savedInstanceState == null) { - return; - } - this.providerConfigState = ProviderSetupInterface.ProviderConfigState.valueOf(savedInstanceState.getString(ACTIVITY_STATE, PROVIDER_NOT_SET.toString())); - if (savedInstanceState.containsKey(REASON_TO_FAIL)) { - reasonToFail = savedInstanceState.getString(REASON_TO_FAIL); - } - } - - private void setUpProviderAPIResultReceiver() { - providerAPIBroadcastReceiver = new ProviderApiSetupBroadcastReceiver(this); - - IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_PROVIDER_API_EVENT); - updateIntentFilter.addCategory(Intent.CATEGORY_DEFAULT); - LocalBroadcastManager.getInstance(this).registerReceiver(providerAPIBroadcastReceiver, updateIntentFilter); - } - - /** - * Asks ProviderApiService to download an anonymous (anon) VPN certificate. - */ - protected void downloadVpnCertificate() { - ProviderAPICommand.execute(this, DOWNLOAD_VPN_CERTIFICATE, provider); - } - - /** - * Once selected a provider, this fragment offers the user to log in, - * use it anonymously (if possible) - * or cancel his/her election pressing the back button. - */ - public void showProviderDetails() { - // show only if current activity is shown - if (isActivityShowing && - providerConfigState != SHOWING_PROVIDER_DETAILS) { - providerConfigState = SHOWING_PROVIDER_DETAILS; - Intent intent = new Intent(this, ProviderDetailActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - intent.putExtra(PROVIDER_KEY, provider); - startActivityForResult(intent, REQUEST_CODE_CONFIGURE_LEAP); - } else { - providerConfigState = PENDING_SHOW_PROVIDER_DETAILS; - } - } - - /** - * Shows an error dialog, if configuring of a provider failed. - */ - public void showDownloadFailedDialog() { - try { - providerConfigState = SHOW_FAILED_DIALOG; - FragmentTransaction fragmentTransaction = fragmentManager.removePreviousFragment(ProviderSetupFailedDialog.TAG); - DialogFragment newFragment; - try { - JSONObject errorJson = new JSONObject(reasonToFail); - newFragment = ProviderSetupFailedDialog.newInstance(provider, errorJson, testNewURL); - } catch (JSONException e) { - e.printStackTrace(); - newFragment = ProviderSetupFailedDialog.newInstance(provider, reasonToFail); - } catch (NullPointerException e) { - //reasonToFail was null - return; - } - newFragment.show(fragmentTransaction, ProviderSetupFailedDialog.TAG); - } catch (IllegalStateException e) { - e.printStackTrace(); - providerConfigState = PENDING_SHOW_FAILED_DIALOG; - } - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderSetupFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/ProviderSetupFailedDialog.java deleted file mode 100644 index d64f34d8..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderSetupFailedDialog.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * 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; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; - -import org.json.JSONObject; - -import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.DEFAULT; -import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.valueOf; -import static se.leap.bitmaskclient.ProviderAPI.ERRORID; -import static se.leap.bitmaskclient.ProviderAPI.ERRORS; - -/** - * Implements a dialog to show why a download failed. - * - * @author parmegv - */ -public class ProviderSetupFailedDialog extends DialogFragment { - - public static String TAG = "downloaded_failed_dialog"; - private final static String KEY_PROVIDER = "key provider"; - private final static String KEY_REASON_TO_FAIL = "key reason to fail"; - private final static String KEY_DOWNLOAD_ERROR = "key download error"; - private String reasonToFail; - private DOWNLOAD_ERRORS downloadError = DEFAULT; - - private Provider provider; - - /** - * Represent error types that need different error handling actions - */ - public enum DOWNLOAD_ERRORS { - DEFAULT, - ERROR_CORRUPTED_PROVIDER_JSON, - ERROR_INVALID_CERTIFICATE, - ERROR_CERTIFICATE_PINNING, - ERROR_NEW_URL_NO_VPN_PROVIDER - } - - /** - * @return a new instance of this DialogFragment. - */ - public static DialogFragment newInstance(Provider provider, String reasonToFail) { - ProviderSetupFailedDialog dialogFragment = new ProviderSetupFailedDialog(); - dialogFragment.reasonToFail = reasonToFail; - dialogFragment.provider = provider; - return dialogFragment; - } - - /** - * @return a new instance of this DialogFragment. - */ - public static DialogFragment newInstance(Provider provider, JSONObject errorJson, boolean testNewURL) { - ProviderSetupFailedDialog dialogFragment = new ProviderSetupFailedDialog(); - dialogFragment.provider = provider; - try { - if (errorJson.has(ERRORS)) { - dialogFragment.reasonToFail = errorJson.getString(ERRORS); - } else { - //default error msg - dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); - } - - if (errorJson.has(ERRORID)) { - dialogFragment.downloadError = valueOf(errorJson.getString(ERRORID)); - } else if (testNewURL) { - dialogFragment.downloadError = DOWNLOAD_ERRORS.ERROR_NEW_URL_NO_VPN_PROVIDER; - } - } catch (Exception e) { - e.printStackTrace(); - dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); - } - return dialogFragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - restoreFromSavedInstance(savedInstanceState); - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(reasonToFail) - .setNegativeButton(R.string.cancel, (dialog, id) - -> interfaceWithConfigurationWizard.cancelSettingUpProvider()); - switch (downloadError) { - case ERROR_CORRUPTED_PROVIDER_JSON: - builder.setPositiveButton(R.string.update_provider_details, (dialog, which) - -> interfaceWithConfigurationWizard.updateProviderDetails()); - break; - case ERROR_CERTIFICATE_PINNING: - case ERROR_INVALID_CERTIFICATE: - builder.setPositiveButton(R.string.update_certificate, (dialog, which) - -> interfaceWithConfigurationWizard.updateProviderDetails()); - break; - case ERROR_NEW_URL_NO_VPN_PROVIDER: - builder.setPositiveButton(R.string.retry, (dialog, id) - -> interfaceWithConfigurationWizard.addAndSelectNewProvider(provider.getMainUrlString())); - break; - default: - builder.setPositiveButton(R.string.retry, (dialog, id) - -> interfaceWithConfigurationWizard.retrySetUpProvider(provider)); - break; - } - - // Create the AlertDialog object and return it - return builder.create(); - } - - public interface DownloadFailedDialogInterface { - void retrySetUpProvider(@NonNull Provider provider); - - void cancelSettingUpProvider(); - - void updateProviderDetails(); - - void addAndSelectNewProvider(String url); - } - - DownloadFailedDialogInterface interfaceWithConfigurationWizard; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - try { - interfaceWithConfigurationWizard = (DownloadFailedDialogInterface) context; - } catch (ClassCastException e) { - throw new ClassCastException(context.toString() - + " must implement NoticeDialogListener"); - } - } - - @Override - public void onCancel(DialogInterface dialog) { - dialog.dismiss(); - interfaceWithConfigurationWizard.cancelSettingUpProvider(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putParcelable(KEY_PROVIDER, provider); - outState.putString(KEY_REASON_TO_FAIL, reasonToFail); - outState.putString(KEY_DOWNLOAD_ERROR, downloadError.toString()); - } - - private void restoreFromSavedInstance(Bundle savedInstanceState) { - if (savedInstanceState == null) { - return; - } - if (savedInstanceState.containsKey(KEY_PROVIDER)) { - this.provider = savedInstanceState.getParcelable(KEY_PROVIDER); - } - if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) { - this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL); - } - if (savedInstanceState.containsKey(KEY_DOWNLOAD_ERROR)) { - this.downloadError = valueOf(savedInstanceState.getString(KEY_DOWNLOAD_ERROR)); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderSetupInterface.java b/app/src/main/java/se/leap/bitmaskclient/ProviderSetupInterface.java deleted file mode 100644 index 9cf4dc33..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderSetupInterface.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 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.os.Bundle; - -/** - * Created by cyberta on 17.08.18. - */ - -public interface ProviderSetupInterface { - enum ProviderConfigState { - PROVIDER_NOT_SET, - SETTING_UP_PROVIDER, - SHOWING_PROVIDER_DETAILS, - PENDING_SHOW_PROVIDER_DETAILS, - PENDING_SHOW_FAILED_DIALOG, - SHOW_FAILED_DIALOG, - } - - void handleProviderSetUp(Provider provider); - void handleProviderSetupFailed(Bundle resultData); - void handleCorrectlyDownloadedCertificate(Provider provider); - void handleIncorrectlyDownloadedCertificate(); - Provider getProvider(); - ProviderConfigState getConfigState(); -} diff --git a/app/src/main/java/se/leap/bitmaskclient/SignupActivity.java b/app/src/main/java/se/leap/bitmaskclient/SignupActivity.java deleted file mode 100644 index abd3d224..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/SignupActivity.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * 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.os.Bundle; -import androidx.annotation.Nullable; - -import butterknife.OnClick; - -import static android.view.View.VISIBLE; - -/** - * Create an account with a provider - */ - -public class SignupActivity extends ProviderCredentialsBaseActivity { - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setProviderHeaderLogo(R.drawable.logo); - setProviderHeaderText(R.string.create_profile); - - setProgressbarText(R.string.signing_up); - setButtonText(R.string.signup_button); - - passwordVerificationField.setVisibility(VISIBLE); - passwordVerificationError.setVisibility(VISIBLE); - } - - @Override - @OnClick(R.id.button) - void handleButton() { - super.handleButton(); - if (getPassword().equals(getPasswordVerification())) { - signUp(getUsername(), getPassword()); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/SrpCredentials.java b/app/src/main/java/se/leap/bitmaskclient/SrpCredentials.java deleted file mode 100644 index c1815ac5..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/SrpCredentials.java +++ /dev/null @@ -1,26 +0,0 @@ -package se.leap.bitmaskclient; - -import com.google.gson.Gson; - -/** - * Created by cyberta on 23.10.17. - */ - -public class SrpCredentials { - - /** - * Parameter A of SRP authentication - */ - private String A; - private String login; - - public SrpCredentials(String username, String A) { - this.login = username; - this.A = A; - } - - @Override - public String toString() { - return new Gson().toJson(this); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/SrpRegistrationData.java b/app/src/main/java/se/leap/bitmaskclient/SrpRegistrationData.java deleted file mode 100644 index d4e00308..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/SrpRegistrationData.java +++ /dev/null @@ -1,42 +0,0 @@ -package se.leap.bitmaskclient; - -import com.google.gson.Gson; - -/** - * Created by cyberta on 23.10.17. - */ - -public class SrpRegistrationData { - - - private User user; - - public SrpRegistrationData(String username, String passwordSalt, String passwordVerifier) { - user = new User(username, passwordSalt, passwordVerifier); - } - - - @Override - public String toString() { - return new Gson().toJson(this); - } - - - public class User { - - String login; - String password_salt; - String password_verifier; - - public User(String login, String password_salt, String password_verifier) { - this.login = login; - this.password_salt = password_salt; - this.password_verifier = password_verifier; - } - - @Override - public String toString() { - return new Gson().toJson(this); - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java deleted file mode 100644 index 1a679b1c..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java +++ /dev/null @@ -1,231 +0,0 @@ -/** - * 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.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import de.blinkt.openvpn.core.VpnStatus; -import se.leap.bitmaskclient.eip.EipCommand; -import se.leap.bitmaskclient.utils.PreferenceHelper; - -import static se.leap.bitmaskclient.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE; -import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; -import static se.leap.bitmaskclient.Constants.PREFERENCES_APP_VERSION; -import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_CONFIGURE_LEAP; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; -import static se.leap.bitmaskclient.MainActivity.ACTION_SHOW_VPN_FRAGMENT; -import static se.leap.bitmaskclient.utils.ConfigHelper.isDefaultBitmask; -import static se.leap.bitmaskclient.utils.PreferenceHelper.storeProviderInPreferences; - -/** - * Activity shown at startup. Evaluates if App is started for the first time or has been upgraded - * and acts and calls another activity accordingly. - * - */ -public class StartActivity extends Activity{ - public static final String TAG = StartActivity.class.getSimpleName(); - - @Retention(RetentionPolicy.SOURCE) - @IntDef({FIRST, NORMAL, UPGRADE}) - private @interface StartupMode {} - private static final int FIRST = 0; - private static final int NORMAL = 1; - private static final int UPGRADE = 2; - - private int versionCode; - private int previousVersionCode; - - private SharedPreferences preferences; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - - Log.d(TAG, "Started"); - - switch (checkAppStart()) { - case NORMAL: - break; - - case FIRST: - storeAppVersion(); - // TODO start ProfileCreation & replace below code - // (new Intent(getActivity(), ProviderListActivity.class), Constants.REQUEST_CODE_SWITCH_PROVIDER); - break; - - case UPGRADE: - executeUpgrade(); - // TODO show donation dialog - break; - } - - // initialize app necessities - VpnStatus.initLogCache(getApplicationContext().getCacheDir()); - - prepareEIP(); - - } - - /** - * check if normal start, first run, up or downgrade - * @return @StartupMode - */ - @StartupMode - private int checkAppStart() { - try { - versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode; - previousVersionCode = preferences.getInt(PREFERENCES_APP_VERSION, -1); - - // versions do match -> normal start - if (versionCode == previousVersionCode) { - Log.d(TAG, "App start was: NORMAL START"); - return NORMAL; - } - - // no previous app version -> first start - if (previousVersionCode == -1 ) { - Log.d(TAG, "FIRST START"); - return FIRST; - } - - // version has increased -> upgrade - if (versionCode > previousVersionCode) { - Log.d(TAG, "UPGRADE"); - return UPGRADE; - } - - } catch (PackageManager.NameNotFoundException e) { - Log.d(TAG, "Splash screen didn't find any " + getPackageName() + " package"); - } - - return NORMAL; - } - - /** - * execute necessary upgrades for version change - */ - private void executeUpgrade() { - if (hasNewFeature(FeatureVersionCode.RENAMED_EIP_IN_PREFERENCES)) { - String eipJson = preferences.getString(PROVIDER_KEY, null); - if (eipJson != null) { - preferences.edit().putString(PROVIDER_EIP_DEFINITION, eipJson). - remove(PROVIDER_KEY).apply(); - } - } - - if (hasNewFeature(FeatureVersionCode.GEOIP_SERVICE)) { - // deletion of current configured provider so that the geoip url will picked out - // from the preseeded *.url file / geoipUrl buildconfigfield (build.gradle) during - // next setup - Provider provider = ProviderObservable.getInstance().getCurrentProvider(); - if (provider != null && !provider.isDefault()) { - PreferenceHelper.deleteProviderDetailsFromPreferences(preferences, provider.getDomain()); - ProviderObservable.getInstance().updateProvider(null); - } - } - - // ensure all upgrades have passed before storing new information - storeAppVersion(); - } - - /** - * check if an upgrade passed or moved to given milestone - * @param featureVersionCode Version code of the Milestone FeatureVersionCode.MILE_STONE - * @return true if milestone is reached - false otherwise - */ - private boolean hasNewFeature(int featureVersionCode) { - return previousVersionCode < featureVersionCode && versionCode >= featureVersionCode; - } - - private void storeAppVersion() { - preferences.edit().putInt(PREFERENCES_APP_VERSION, versionCode).apply(); - } - - private void prepareEIP() { - boolean providerExists = ProviderObservable.getInstance().getCurrentProvider() != null; - if (providerExists) { - Provider provider = ProviderObservable.getInstance().getCurrentProvider(); - if(!provider.isConfigured()) { - configureLeapProvider(); - } else { - Log.d(TAG, "vpn provider is configured"); - if (getIntent() != null && getIntent().getBooleanExtra(EIP_RESTART_ON_BOOT, false)) { - EipCommand.startVPN(this.getApplicationContext(), true); - finish(); - } else if (PreferenceHelper.getRestartOnUpdate(this.getApplicationContext())) { - PreferenceHelper.restartOnUpdate(this.getApplicationContext(), false); - EipCommand.startVPN(this.getApplicationContext(), false); - showMainActivity(); - finish(); - } else { - showMainActivity(); - } - } - } else { - configureLeapProvider(); - } - } - - private void configureLeapProvider() { - if (getIntent().hasExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE)) { - getIntent().removeExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE); - } - if (isDefaultBitmask()) { - startActivityForResult(new Intent(this, ProviderListActivity.class), REQUEST_CODE_CONFIGURE_LEAP); - } else { // custom branded app - startActivityForResult(new Intent(this, CustomProviderSetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP); - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - - if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) { - if (resultCode == RESULT_OK && data != null && data.hasExtra(Provider.KEY)) { - Provider provider = data.getParcelableExtra(Provider.KEY); - storeProviderInPreferences(preferences, provider); - ProviderObservable.getInstance().updateProvider(provider); - EipCommand.startVPN(this.getApplicationContext(), false); - showMainActivity(); - } else if (resultCode == RESULT_CANCELED) { - finish(); - } - } - } - - private void showMainActivity() { - Intent intent = new Intent(this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.setAction(ACTION_SHOW_VPN_FRAGMENT); - startActivity(intent); - finish(); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java b/app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java deleted file mode 100644 index 32652964..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java +++ /dev/null @@ -1,158 +0,0 @@ -package se.leap.bitmaskclient; - -import android.text.TextUtils; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateException; -import java.util.Arrays; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import okhttp3.OkHttpClient; -import se.leap.bitmaskclient.utils.ConfigHelper; - -/** - * Created by cyberta on 24.10.17. - * This class ensures that modern TLS algorithms will also be used on old devices (Android 4.1 - Android 4.4.4) in order to avoid - * attacks like POODLE. - */ - -public class TLSCompatSocketFactory extends SSLSocketFactory { - - private static final String TAG = TLSCompatSocketFactory.class.getName(); - private SSLSocketFactory internalSSLSocketFactory; - private TrustManager trustManager; - - public TLSCompatSocketFactory(String trustedCaCert) throws KeyManagementException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, NoSuchProviderException { - initForSelfSignedCAs(trustedCaCert); - } - - public TLSCompatSocketFactory() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, NoSuchProviderException, IOException { - initForCommercialCAs(); - } - - public void initSSLSocketFactory(OkHttpClient.Builder builder) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, IllegalStateException { - builder.sslSocketFactory(this, (X509TrustManager) trustManager); - } - - - private void initForSelfSignedCAs(String trustedSelfSignedCaCert) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, IllegalStateException, KeyManagementException, NoSuchProviderException { - // Create a KeyStore containing our trusted CAs - String defaultType = KeyStore.getDefaultType(); - KeyStore keyStore = KeyStore.getInstance(defaultType); - keyStore.load(null, null); - if (!TextUtils.isEmpty(trustedSelfSignedCaCert)) { - java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(trustedSelfSignedCaCert); - keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate); - } - - // Create a TrustManager that trusts the CAs in our KeyStore - String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); - tmf.init(keyStore); - - // Check if there's only 1 X509Trustmanager -> from okttp3 source code example - TrustManager[] trustManagers = tmf.getTrustManagers(); - if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { - throw new IllegalStateException("Unexpected default trust managers:" - + Arrays.toString(trustManagers)); - } - - trustManager = trustManagers[0]; - - // Create a SSLContext that uses our TrustManager - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, tmf.getTrustManagers(), null); - internalSSLSocketFactory = sslContext.getSocketFactory(); - - } - - - private void initForCommercialCAs() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { - - // Create a TrustManager that trusts the CAs in our KeyStore - String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); - tmf.init((KeyStore) null); - - // Check if there's only 1 X509Trustmanager -> from okttp3 source code example - TrustManager[] trustManagers = tmf.getTrustManagers(); - if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { - throw new IllegalStateException("Unexpected default trust managers:" - + Arrays.toString(trustManagers)); - } - - trustManager = trustManagers[0]; - - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, null, null); - internalSSLSocketFactory = context.getSocketFactory(); - } - - - @Override - public String[] getDefaultCipherSuites() { - return internalSSLSocketFactory.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return internalSSLSocketFactory.getSupportedCipherSuites(); - } - - @Override - public Socket createSocket() throws IOException, IllegalArgumentException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); - } - - @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException, IllegalArgumentException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); - } - - @Override - public Socket createSocket(String host, int port) throws IOException, UnknownHostException, IllegalArgumentException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException, IllegalArgumentException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException, IllegalArgumentException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); - } - - @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException, IllegalArgumentException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); - } - - private Socket enableTLSOnSocket(Socket socket) throws IllegalArgumentException { - if(socket != null && (socket instanceof SSLSocket)) { - ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"}); - //TODO: add a android version check as soon as a new Android API or bcjsse supports TLSv1.3 - } - return socket; - - - } - - - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java b/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java deleted file mode 100644 index 290a9aa9..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java +++ /dev/null @@ -1,353 +0,0 @@ -/** - * 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.graphics.Typeface; -import android.os.Build; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.StyleSpan; -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 androidx.core.app.NotificationCompat.PRIORITY_HIGH; -import static androidx.core.app.NotificationCompat.PRIORITY_MAX; -import static androidx.core.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.ASK_TO_CANCEL_VPN; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN; -import static se.leap.bitmaskclient.MainActivity.ACTION_SHOW_VPN_FRAGMENT; - -/** - * 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, - null, - tickerText, - status, - VoidVpnService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, - PRIORITY_MAX, - 0, - getMainActivityIntent(), - 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, boolean isObfuscated, String msg, String tickerText, ConnectionStatus status, long when, String notificationChannelNewstatusId) { - String cancelString; - CharSequence bigmessage = null; - String ghostIcon = new String(Character.toChars(0x1f309)); - - switch (status) { - // show cancel if no connection - case LEVEL_START: - case LEVEL_NONETWORK: - case LEVEL_CONNECTING_SERVER_REPLIED: - case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: - cancelString = context.getString(R.string.cancel); - if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection_try)); - spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg); - } - break; - - // show disconnect if connection exists - case LEVEL_CONNECTED: - if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection)); - spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg); - } - default: - cancelString = context.getString(R.string.cancel_connection); - } - - if (isObfuscated) { - msg = ghostIcon + " " + msg; - } - - NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action. - Builder(R.drawable.ic_menu_close_clear_cancel, cancelString, getDisconnectIntent()); - String title; - String appName = context.getString(R.string.app_name); - if (isEmpty(profileName)) { - title = appName; - } else { - title = context.getString(R.string.notifcation_title_bitmask, appName, profileName); - } - - PendingIntent contentIntent; - if (status == LEVEL_WAITING_FOR_USER_INPUT) - contentIntent = getUserInputIntent(msg); - else - contentIntent = getMainActivityIntent(); - - int priority; - if (OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID.equals(notificationChannelNewstatusId)) { - priority = PRIORITY_HIGH; - } else { - // background channel - priority = PRIORITY_MIN; - } - - buildVpnNotification( - title, - msg, - bigmessage, - 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); - mChannel.setSound(null, null); - 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); - mChannel.setSound(null, null); - 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.v_custom_notification); - 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, String message, CharSequence bigMessage, 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)) { - if (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, message)); - } - } else { - nCompatBuilder.setStyle(new NotificationCompat.BigTextStyle(). - setBigContentTitle(title). - bigText(bigMessage)); - } - nCompatBuilder.addAction(notificationAction); - nCompatBuilder.setContentTitle(title); - nCompatBuilder.setCategory(NotificationCompat.CATEGORY_SERVICE); - nCompatBuilder.setLocalOnly(true); - nCompatBuilder.setContentText(message); - 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 getMainActivityIntent() { - Intent startActivity = new Intent(context, StartActivity.class); - return PendingIntent.getActivity(context, 0, startActivity, 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, MainActivity.class); - disconnectVPN.setAction(ACTION_SHOW_VPN_FRAGMENT); - disconnectVPN.putExtra(ASK_TO_CANCEL_VPN, true); - disconnectVPN.addFlags(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/base/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java new file mode 100644 index 00000000..4b6fea72 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2020 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.base; + +import android.content.Context; +import android.content.IntentFilter; +import android.content.SharedPreferences; + +import androidx.appcompat.app.AppCompatDelegate; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.multidex.MultiDexApplication; + +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; + +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver; +import se.leap.bitmaskclient.eip.EipSetupObserver; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.tethering.TetheringStateManager; +import se.leap.bitmaskclient.base.utils.PRNGFixes; + +import static android.content.Intent.CATEGORY_DEFAULT; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.appUpdate.DownloadBroadcastReceiver.ACTION_DOWNLOAD; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.DOWNLOAD_UPDATE; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSavedProviderFromSharedPreferences; + +/** + * Created by cyberta on 24.10.17. + */ + +public class BitmaskApp extends MultiDexApplication { + + private final static String TAG = BitmaskApp.class.getSimpleName(); + private RefWatcher refWatcher; + private ProviderObservable providerObservable; + private DownloadBroadcastReceiver downloadBroadcastReceiver; + + + @Override + public void onCreate() { + super.onCreate(); + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + refWatcher = LeakCanary.install(this); + // Normal app init code...*/ + PRNGFixes.apply(); + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + providerObservable = ProviderObservable.getInstance(); + providerObservable.updateProvider(getSavedProviderFromSharedPreferences(preferences)); + EipSetupObserver.init(this, preferences); + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); + TetheringStateManager.getInstance().init(this); + if (BuildConfig.FLAVOR.contains("Fatweb")) { + downloadBroadcastReceiver = new DownloadBroadcastReceiver(); + IntentFilter intentFilter = new IntentFilter(BROADCAST_DOWNLOAD_SERVICE_EVENT); + intentFilter.addAction(ACTION_DOWNLOAD); + intentFilter.addAction(CHECK_VERSION_FILE); + intentFilter.addAction(DOWNLOAD_UPDATE); + intentFilter.addCategory(CATEGORY_DEFAULT); + LocalBroadcastManager.getInstance(this.getApplicationContext()).registerReceiver(downloadBroadcastReceiver, intentFilter); + } + } + + /** + * Use this method to get a RefWatcher object that checks for memory leaks in the given context. + * Call refWatcher.watch(this) to check if all references get garbage collected. + * @param context + * @return the RefWatcher object + */ + public static RefWatcher getRefWatcher(Context context) { + BitmaskApp application = (BitmaskApp) context.getApplicationContext(); + return application.refWatcher; + } + + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskTileService.java b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskTileService.java new file mode 100644 index 00000000..4a8b1236 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskTileService.java @@ -0,0 +1,104 @@ +package se.leap.bitmaskclient.base; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; + +import java.util.Observable; +import java.util.Observer; + +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.eip.EipStatus; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.base.models.ProviderObservable; + + +@TargetApi(Build.VERSION_CODES.N) +public class BitmaskTileService extends TileService implements Observer { + + @SuppressLint("Override") + @TargetApi(Build.VERSION_CODES.N) + @Override + public void onClick() { + super.onClick(); + Provider provider = ProviderObservable.getInstance().getCurrentProvider(); + if (provider.isConfigured()) { + if (!isLocked()) { + onTileTap(); + } else { + unlockAndRun(this::onTileTap); + } + } else { + Intent intent = new Intent(getApplicationContext(), StartActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + } + + private void onTileTap() { + EipStatus eipStatus = EipStatus.getInstance(); + if (eipStatus.isConnecting() || eipStatus.isBlocking() || eipStatus.isConnected() || eipStatus.isReconnecting()) { + EipCommand.stopVPN(getApplicationContext()); + } else { + EipCommand.startVPN(getApplicationContext(), false); + } + } + + + @TargetApi(Build.VERSION_CODES.N) + @Override + public void onTileAdded() { + } + + @Override + public void onStartListening() { + super.onStartListening(); + EipStatus.getInstance().addObserver(this); + update(EipStatus.getInstance(), null); + } + + @Override + public void onStopListening() { + super.onStopListening(); + EipStatus.getInstance().deleteObserver(this); + } + + @Override + public void update(Observable o, Object arg) { + Tile t = getQsTile(); + + if (o instanceof EipStatus) { + EipStatus status = (EipStatus) o; + Icon icon; + String title; + if (status.isConnecting() || status.isReconnecting()) { + icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_connecting); + title = getResources().getString(R.string.cancel); + t.setState(Tile.STATE_ACTIVE); + } else if (status.isConnected()) { + icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_connected); + title = String.format(getString(R.string.qs_disconnect), getString(R.string.app_name)); + t.setState(Tile.STATE_ACTIVE); + } else if (status.isBlocking()) { + icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_blocking); + title = getString(R.string.vpn_button_turn_off_blocking); + t.setState(Tile.STATE_ACTIVE); + } else { + icon = Icon.createWithResource(getApplicationContext(), R.drawable.vpn_disconnected); + title = String.format(getString(R.string.qs_enable_vpn), getString(R.string.app_name)); + t.setState(Tile.STATE_INACTIVE); + } + + + t.setIcon(icon); + t.setLabel(title); + + t.updateTile(); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/FragmentManagerEnhanced.java b/app/src/main/java/se/leap/bitmaskclient/base/FragmentManagerEnhanced.java new file mode 100644 index 00000000..bc01dcec --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/FragmentManagerEnhanced.java @@ -0,0 +1,58 @@ +/** + * 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.base; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +public class FragmentManagerEnhanced { + + private FragmentManager genericFragmentManager; + + public FragmentManagerEnhanced(FragmentManager genericFragmentManager) { + this.genericFragmentManager = genericFragmentManager; + } + + public FragmentTransaction removePreviousFragment(String tag) { + FragmentTransaction transaction = genericFragmentManager.beginTransaction(); + Fragment previousFragment = genericFragmentManager.findFragmentByTag(tag); + if (previousFragment != null) { + transaction.remove(previousFragment); + } + + return transaction; + } + + public void replace(int containerViewId, Fragment fragment, String tag) { + try { + if (genericFragmentManager.findFragmentByTag(tag) != null) { + FragmentTransaction transaction = genericFragmentManager.beginTransaction(); + transaction.replace(containerViewId, fragment, tag).commit(); + } else { + genericFragmentManager.beginTransaction().add(containerViewId, fragment, tag).commit(); + } + } catch (IllegalStateException e) { + e.printStackTrace(); + } + + } + + public Fragment findFragmentByTag(String tag) { + return genericFragmentManager.findFragmentByTag(tag); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java new file mode 100644 index 00000000..1b7de10e --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java @@ -0,0 +1,372 @@ +/** + * 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.base; + + +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import androidx.annotation.StringRes; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Observable; +import java.util.Observer; + +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.drawer.NavigationDrawerFragment; +import se.leap.bitmaskclient.eip.EIP; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.eip.EipSetupListener; +import se.leap.bitmaskclient.eip.EipSetupObserver; +import se.leap.bitmaskclient.base.fragments.EipFragment; +import se.leap.bitmaskclient.base.fragments.ExcludeAppsFragment; +import se.leap.bitmaskclient.base.fragments.LogFragment; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.providersetup.models.LeapSRPSession; +import se.leap.bitmaskclient.providersetup.activities.LoginActivity; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; +import se.leap.bitmaskclient.base.fragments.MainActivityErrorDialog; + +import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_PREPARE_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.base.models.Constants.EIP_REQUEST; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE; +import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed; +import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; +import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_VPN_PREPARE; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.storeProviderInPreferences; + + +public class MainActivity extends AppCompatActivity implements EipSetupListener, Observer, ExcludeAppsFragment.ExcludedAppsCallback { + + public final static String TAG = MainActivity.class.getSimpleName(); + + private Provider provider; + private SharedPreferences preferences; + private NavigationDrawerFragment navigationDrawerFragment; + + public final static String ACTION_SHOW_VPN_FRAGMENT = "action_show_vpn_fragment"; + public final static String ACTION_SHOW_LOG_FRAGMENT = "action_show_log_fragment"; + + /** + * Fragment managing the behaviors, interactions and presentation of the navigation drawer. + */ + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.a_main); + setSupportActionBar(findViewById(R.id.toolbar)); + + navigationDrawerFragment = (NavigationDrawerFragment) + getSupportFragmentManager().findFragmentById(R.id.navigation_drawer); + + preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + provider = ProviderObservable.getInstance().getCurrentProvider(); + + EipSetupObserver.addListener(this); + // Set up the drawer. + navigationDrawerFragment.setUp(R.id.navigation_drawer, findViewById(R.id.drawer_layout)); + handleIntentAction(getIntent()); + } + + @Override + public void onBackPressed() { + FragmentManagerEnhanced fragmentManagerEnhanced = new FragmentManagerEnhanced(getSupportFragmentManager()); + Fragment fragment = fragmentManagerEnhanced.findFragmentByTag(MainActivity.TAG); + if (fragment == null || !(fragment instanceof EipFragment)) { + Fragment eipFragment = new EipFragment(); + Bundle bundle = new Bundle(); + bundle.putParcelable(PROVIDER_KEY, provider); + eipFragment.setArguments(bundle); + fragmentManagerEnhanced.replace(R.id.main_container, eipFragment, MainActivity.TAG); + hideActionBarSubTitle(); + } else { + super.onBackPressed(); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + handleIntentAction(intent); + } + + private void handleIntentAction(Intent intent) { + if (intent == null || intent.getAction() == null) { + return; + } + + Fragment fragment = null; + switch (intent.getAction()) { + case ACTION_SHOW_VPN_FRAGMENT: + fragment = new EipFragment(); + Bundle bundle = new Bundle(); + if (intent.hasExtra(ASK_TO_CANCEL_VPN)) { + bundle.putBoolean(ASK_TO_CANCEL_VPN, true); + } + bundle.putParcelable(PROVIDER_KEY, provider); + fragment.setArguments(bundle); + hideActionBarSubTitle(); + break; + case ACTION_SHOW_LOG_FRAGMENT: + fragment = new LogFragment(); + setActionBarTitle(R.string.log_fragment_title); + break; + default: + break; + } + // on layout change / recreation of the activity, we don't want create new Fragments + // instead the fragments themselves care about recreation and state restoration + intent.setAction(null); + + if (fragment != null) { + new FragmentManagerEnhanced(getSupportFragmentManager()) + .replace(R.id.main_container, fragment, MainActivity.TAG); + } + } + + private void hideActionBarSubTitle() { + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setSubtitle(null); + } + } + private void setActionBarTitle(@StringRes int stringId) { + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setSubtitle(stringId); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (data == null) { + return; + } + + if (resultCode == RESULT_OK && data.hasExtra(Provider.KEY)) { + provider = data.getParcelableExtra(Provider.KEY); + + if (provider == null) { + return; + } + + storeProviderInPreferences(preferences, provider); + ProviderObservable.getInstance().updateProvider(provider); + if (!provider.supportsPluggableTransports()) { + PreferenceHelper.usePluggableTransports(this, false); + } + navigationDrawerFragment.refresh(); + + switch (requestCode) { + case REQUEST_CODE_SWITCH_PROVIDER: + EipCommand.stopVPN(this.getApplicationContext()); + break; + case REQUEST_CODE_CONFIGURE_LEAP: + Log.d(TAG, "REQUEST_CODE_CONFIGURE_LEAP - onActivityResult - MainActivity"); + break; + case REQUEST_CODE_LOG_IN: + EipCommand.startVPN(this.getApplicationContext(), true); + break; + } + } + + // on switch provider we need to set the EIP fragment + Fragment fragment = new EipFragment(); + Bundle arguments = new Bundle(); + arguments.putParcelable(PROVIDER_KEY, provider); + fragment.setArguments(arguments); + new FragmentManagerEnhanced(getSupportFragmentManager()) + .replace(R.id.main_container, fragment, MainActivity.TAG); + hideActionBarSubTitle(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + EipSetupObserver.removeListener(this); + } + + @Override + public void handleEipEvent(Intent intent) { + int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); + Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY); + if (resultData == null) { + resultData = Bundle.EMPTY; + } + String request = resultData.getString(EIP_REQUEST); + + if (request == null) { + return; + } + + switch (request) { + case EIP_ACTION_START: + if (resultCode == RESULT_CANCELED) { + String error = resultData.getString(ERRORS); + if (isInternalErrorHandling(error)) { + return; + } + + if (LeapSRPSession.loggedIn() || provider.allowsAnonymous()) { + showMainActivityErrorDialog(error); + } else if (isInvalidCertificateForLoginOnlyProvider(error)) { + askUserToLogIn(getString(vpn_certificate_user_message)); + } + } + break; + case EIP_ACTION_PREPARE_VPN: + if (resultCode == RESULT_CANCELED) { + showMainActivityErrorDialog(getString(R.string.vpn_error_establish), ERROR_VPN_PREPARE); + } + break; + } + } + + @Override + public void handleProviderApiEvent(Intent intent) { + int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); + + switch (resultCode) { + case INCORRECTLY_DOWNLOADED_EIP_SERVICE: + // TODO CATCH ME IF YOU CAN - WHAT DO WE WANT TO DO? + break; + case INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE: + if (LeapSRPSession.loggedIn() || provider.allowsAnonymous()) { + showMainActivityErrorDialog(getString(downloading_vpn_certificate_failed)); + } else { + askUserToLogIn(getString(vpn_certificate_user_message)); + } + break; + } + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof ProviderObservable) { + this.provider = ((ProviderObservable) o).getCurrentProvider(); + } + } + + /** + * Shows an error dialog + */ + public void showMainActivityErrorDialog(String reasonToFail) { + try { + + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + this.getSupportFragmentManager()).removePreviousFragment( + MainActivityErrorDialog.TAG); + DialogFragment newFragment; + try { + JSONObject errorJson = new JSONObject(reasonToFail); + newFragment = MainActivityErrorDialog.newInstance(provider, errorJson); + } catch (JSONException e) { + e.printStackTrace(); + newFragment = MainActivityErrorDialog.newInstance(provider, reasonToFail); + } + newFragment.show(fragmentTransaction, MainActivityErrorDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + Log.w(TAG, "error dialog leaked!"); + } + } + + /** + * Shows an error dialog + */ + public void showMainActivityErrorDialog(String reasonToFail, EIP.EIPErrors error) { + try { + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + this.getSupportFragmentManager()).removePreviousFragment( + MainActivityErrorDialog.TAG); + DialogFragment newFragment = MainActivityErrorDialog.newInstance(provider, reasonToFail, error); + newFragment.show(fragmentTransaction, MainActivityErrorDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + Log.w(TAG, "error dialog leaked!"); + } + } + + /** + * + * @param errorJsonString + * @return true if errorJson is a valid json and contains only ERRORID but + * not an ERRORS field containing an error message + */ + public boolean isInternalErrorHandling(String errorJsonString) { + try { + JSONObject errorJson = new JSONObject(errorJsonString); + return !errorJson.has(ERRORS) && errorJson.has(ERRORID); + } catch (JSONException | NullPointerException e) { + e.printStackTrace(); + } + return false; + } + + public boolean isInvalidCertificateForLoginOnlyProvider(String errorJsonString) { + try { + JSONObject errorJson = new JSONObject(errorJsonString); + return ERROR_INVALID_VPN_CERTIFICATE.toString().equals(errorJson.getString(ERRORID)) && + !LeapSRPSession.loggedIn() && + !provider.allowsAnonymous(); + } catch (JSONException e) { + e.printStackTrace(); + } + return false; + } + + private void askUserToLogIn(String userMessage) { + Intent intent = new Intent(this, LoginActivity.class); + intent.putExtra(PROVIDER_KEY, provider); + if (userMessage != null) { + intent.putExtra(USER_MESSAGE, userMessage); + } + startActivityForResult(intent, REQUEST_CODE_LOG_IN); + } + + + @Override + public void onAppsExcluded(int number) { + navigationDrawerFragment.onAppsExcluded(number); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/OnBootReceiver.java b/app/src/main/java/se/leap/bitmaskclient/base/OnBootReceiver.java new file mode 100644 index 00000000..df1d3e5a --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/OnBootReceiver.java @@ -0,0 +1,54 @@ +package se.leap.bitmaskclient.base; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; + +import de.blinkt.openvpn.core.VpnStatus; + +import static android.content.Intent.ACTION_BOOT_COMPLETED; +import static se.leap.bitmaskclient.base.models.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE; +import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; + +public class OnBootReceiver extends BroadcastReceiver { + + SharedPreferences preferences; + + // 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(SHARED_PREFERENCES, Context.MODE_PRIVATE); + boolean providerConfigured = !preferences.getString(PROVIDER_VPN_CERTIFICATE, "").isEmpty(); + boolean startOnBoot = preferences.getBoolean(EIP_RESTART_ON_BOOT, false); + boolean isAlwaysOnConfigured = VpnStatus.isAlwaysOn(); + 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) { + Log.d("OpenVpn", "start StartActivity!"); + Intent startActivityIntent = new Intent(context.getApplicationContext(), StartActivity.class); + startActivityIntent.putExtra(EIP_RESTART_ON_BOOT, true); + startActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(startActivityIntent); + } + } else { + if (isAlwaysOnConfigured) { + Intent dashboardIntent = new Intent(context.getApplicationContext(), StartActivity.class); + dashboardIntent.putExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE, true); + dashboardIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(dashboardIntent); + } + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java new file mode 100644 index 00000000..cf64905a --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java @@ -0,0 +1,236 @@ +/** + * 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.base; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.providersetup.ProviderListActivity; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.base.models.FeatureVersionCode; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; + +import static se.leap.bitmaskclient.base.models.Constants.APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE; +import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT; +import static se.leap.bitmaskclient.base.models.Constants.PREFERENCES_APP_VERSION; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.storeProviderInPreferences; + +/** + * Activity shown at startup. Evaluates if App is started for the first time or has been upgraded + * and acts and calls another activity accordingly. + * + */ +public class StartActivity extends Activity{ + public static final String TAG = StartActivity.class.getSimpleName(); + + @Retention(RetentionPolicy.SOURCE) + @IntDef({FIRST, NORMAL, UPGRADE}) + private @interface StartupMode {} + private static final int FIRST = 0; + private static final int NORMAL = 1; + private static final int UPGRADE = 2; + + private int versionCode; + private int previousVersionCode; + + private SharedPreferences preferences; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + + Log.d(TAG, "Started"); + + switch (checkAppStart()) { + case NORMAL: + break; + + case FIRST: + storeAppVersion(); + // TODO start ProfileCreation & replace below code + // (new Intent(getActivity(), ProviderListActivity.class), Constants.REQUEST_CODE_SWITCH_PROVIDER); + break; + + case UPGRADE: + executeUpgrade(); + // TODO show donation dialog + break; + } + + // initialize app necessities + VpnStatus.initLogCache(getApplicationContext().getCacheDir()); + + prepareEIP(); + + } + + /** + * check if normal start, first run, up or downgrade + * @return @StartupMode + */ + @StartupMode + private int checkAppStart() { + try { + versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode; + previousVersionCode = preferences.getInt(PREFERENCES_APP_VERSION, -1); + + // versions do match -> normal start + if (versionCode == previousVersionCode) { + Log.d(TAG, "App start was: NORMAL START"); + return NORMAL; + } + + // no previous app version -> first start + if (previousVersionCode == -1 ) { + Log.d(TAG, "FIRST START"); + return FIRST; + } + + // version has increased -> upgrade + if (versionCode > previousVersionCode) { + Log.d(TAG, "UPGRADE"); + return UPGRADE; + } + + } catch (PackageManager.NameNotFoundException e) { + Log.d(TAG, "Splash screen didn't find any " + getPackageName() + " package"); + } + + return NORMAL; + } + + /** + * execute necessary upgrades for version change + */ + private void executeUpgrade() { + if (hasNewFeature(FeatureVersionCode.RENAMED_EIP_IN_PREFERENCES)) { + String eipJson = preferences.getString(PROVIDER_KEY, null); + if (eipJson != null) { + preferences.edit().putString(PROVIDER_EIP_DEFINITION, eipJson). + remove(PROVIDER_KEY).apply(); + } + } + + if (hasNewFeature(FeatureVersionCode.GEOIP_SERVICE)) { + // deletion of current configured provider so that the geoip url will picked out + // from the preseeded *.url file / geoipUrl buildconfigfield (build.gradle) during + // next setup + Provider provider = ProviderObservable.getInstance().getCurrentProvider(); + if (provider != null && !provider.isDefault()) { + PreferenceHelper.deleteProviderDetailsFromPreferences(preferences, provider.getDomain()); + ProviderObservable.getInstance().updateProvider(null); + } + } + + // ensure all upgrades have passed before storing new information + storeAppVersion(); + } + + /** + * check if an upgrade passed or moved to given milestone + * @param featureVersionCode Version code of the Milestone FeatureVersionCode.MILE_STONE + * @return true if milestone is reached - false otherwise + */ + private boolean hasNewFeature(int featureVersionCode) { + return previousVersionCode < featureVersionCode && versionCode >= featureVersionCode; + } + + private void storeAppVersion() { + preferences.edit().putInt(PREFERENCES_APP_VERSION, versionCode).apply(); + } + + private void prepareEIP() { + boolean providerExists = ProviderObservable.getInstance().getCurrentProvider() != null; + if (providerExists) { + Provider provider = ProviderObservable.getInstance().getCurrentProvider(); + if(!provider.isConfigured()) { + configureLeapProvider(); + } else { + Log.d(TAG, "vpn provider is configured"); + if (getIntent() != null && getIntent().getBooleanExtra(EIP_RESTART_ON_BOOT, false)) { + EipCommand.startVPN(this.getApplicationContext(), true); + finish(); + } else if (PreferenceHelper.getRestartOnUpdate(this.getApplicationContext())) { + PreferenceHelper.restartOnUpdate(this.getApplicationContext(), false); + EipCommand.startVPN(this.getApplicationContext(), false); + showMainActivity(); + finish(); + } else { + showMainActivity(); + } + } + } else { + configureLeapProvider(); + } + } + + private void configureLeapProvider() { + if (getIntent().hasExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE)) { + getIntent().removeExtra(APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE); + } + if (isDefaultBitmask()) { + startActivityForResult(new Intent(this, ProviderListActivity.class), REQUEST_CODE_CONFIGURE_LEAP); + } else { // custom branded app + startActivityForResult(new Intent(this, CustomProviderSetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + + if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) { + if (resultCode == RESULT_OK && data != null && data.hasExtra(Provider.KEY)) { + Provider provider = data.getParcelableExtra(Provider.KEY); + storeProviderInPreferences(preferences, provider); + ProviderObservable.getInstance().updateProvider(provider); + EipCommand.startVPN(this.getApplicationContext(), false); + showMainActivity(); + } else if (resultCode == RESULT_CANCELED) { + finish(); + } + } + } + + private void showMainActivity() { + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.setAction(ACTION_SHOW_VPN_FRAGMENT); + startActivity(intent); + finish(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/drawer/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/drawer/NavigationDrawerFragment.java new file mode 100644 index 00000000..e70c87f9 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/drawer/NavigationDrawerFragment.java @@ -0,0 +1,674 @@ +/** + * Copyright (c) 2019 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.base.drawer; + + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.Observable; +import java.util.Observer; +import java.util.Set; + +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.base.fragments.EipFragment; +import se.leap.bitmaskclient.base.FragmentManagerEnhanced; +import se.leap.bitmaskclient.base.MainActivity; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderListActivity; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.eip.EipStatus; +import se.leap.bitmaskclient.firewall.FirewallManager; +import se.leap.bitmaskclient.base.fragments.AboutFragment; +import se.leap.bitmaskclient.base.fragments.AlwaysOnDialog; +import se.leap.bitmaskclient.base.fragments.ExcludeAppsFragment; +import se.leap.bitmaskclient.base.fragments.LogFragment; +import se.leap.bitmaskclient.base.fragments.TetheringDialog; +import se.leap.bitmaskclient.tethering.TetheringObservable; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; +import se.leap.bitmaskclient.base.views.IconSwitchEntry; +import se.leap.bitmaskclient.base.views.IconTextEntry; + +import static android.content.Context.MODE_PRIVATE; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static se.leap.bitmaskclient.base.BitmaskApp.getRefWatcher; +import static se.leap.bitmaskclient.base.models.Constants.DONATION_URL; +import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL; +import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS; +import static se.leap.bitmaskclient.R.string.about_fragment_title; +import static se.leap.bitmaskclient.R.string.exclude_apps_fragment_title; +import static se.leap.bitmaskclient.R.string.log_fragment_title; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSaveBattery; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getShowAlwaysOnDialog; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.saveBattery; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.showExperimentalFeatures; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.usePluggableTransports; + +/** + * Fragment used for managing interactions for and presentation of a navigation drawer. + * See the + * design guidelines for a complete explanation of the behaviors implemented here. + */ +public class NavigationDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener, Observer { + + /** + * Per the design guidelines, you should show the drawer on launch until the user manually + * expands it. This shared preference tracks this. + */ + private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned"; + private static final String TAG = NavigationDrawerFragment.class.getName(); + public static final int TWO_SECONDS = 2000; + + /** + * Helper component that ties the action bar to the navigation drawer. + */ + private ActionBarDrawerToggle drawerToggle; + + private DrawerLayout drawerLayout; + private View drawerView; + private View fragmentContainerView; + private Toolbar toolbar; + private IconTextEntry account; + private IconSwitchEntry saveBattery; + private IconTextEntry tethering; + private IconSwitchEntry firewall; + private View experimentalFeatureFooter; + + private boolean userLearnedDrawer; + private volatile boolean wasPaused; + private volatile boolean shouldCloseOnResume; + + private SharedPreferences preferences; + + private final static String KEY_SHOW_SAVE_BATTERY_ALERT = "KEY_SHOW_SAVE_BATTERY_ALERT"; + private volatile boolean showSaveBattery = false; + AlertDialog alertDialog; + private FirewallManager firewallManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Reads in the flag indicating whether or not the user has demonstrated awareness of the + // drawer. See PREF_USER_LEARNED_DRAWER for details. + preferences = getContext().getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + userLearnedDrawer = preferences.getBoolean(PREF_USER_LEARNED_DRAWER, false); + preferences.registerOnSharedPreferenceChangeListener(this); + firewallManager = new FirewallManager(getContext().getApplicationContext(), false); + + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Indicates that this fragment would like to influence the set of actions in the action bar. + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + drawerView = inflater.inflate(R.layout.f_drawer_main, container, false); + restoreFromSavedInstance(savedInstanceState); + TetheringObservable.getInstance().addObserver(this); + EipStatus.getInstance().addObserver(this); + return drawerView; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + TetheringObservable.getInstance().deleteObserver(this); + EipStatus.getInstance().deleteObserver(this); + } + + public boolean isDrawerOpen() { + return drawerLayout != null && drawerLayout.isDrawerOpen(fragmentContainerView); + } + + @Override + public void onResume() { + super.onResume(); + wasPaused = false; + if (shouldCloseOnResume) { + closeDrawerWithDelay(); + } + } + + @Override + public void onPause() { + super.onPause(); + wasPaused = true; + } + + + + /** + * Users of this fragment must call this method to set up the navigation drawer interactions. + * + * @param fragmentId The android:id of this fragment in its activity's layout. + * @param drawerLayout The DrawerLayout containing this fragment's UI. + */ + public void setUp(int fragmentId, DrawerLayout drawerLayout) { + final AppCompatActivity activity = (AppCompatActivity) getActivity(); + fragmentContainerView = activity.findViewById(fragmentId); + this.drawerLayout = drawerLayout; + // set a custom shadow that overlays the main content when the drawer opens + this.drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + toolbar = this.drawerLayout.findViewById(R.id.toolbar); + + setupActionBar(); + setupEntries(); + setupActionBarDrawerToggle(activity); + + if (!userLearnedDrawer) { + openNavigationDrawerForFirstTimeUsers(); + } + + // Defer code dependent on restoration of previous instance state. + this.drawerLayout.post(() -> drawerToggle.syncState()); + this.drawerLayout.addDrawerListener(drawerToggle); + } + + private void setupActionBarDrawerToggle(final AppCompatActivity activity) { + // ActionBarDrawerToggle ties together the the proper interactions + // between the navigation drawer and the action bar app icon. + drawerToggle = new ActionBarDrawerToggle( + activity, + drawerLayout, + toolbar, + R.string.navigation_drawer_open, + R.string.navigation_drawer_close + ) { + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + if (!isAdded()) { + return; + } + activity.invalidateOptionsMenu(); + } + + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + if (!isAdded()) { + return; + } + + if (!userLearnedDrawer) { + // The user manually opened the drawer; store this flag to prevent auto-showing + // the navigation drawer automatically in the future. + userLearnedDrawer = true; + preferences.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).apply(); + } + activity.invalidateOptionsMenu(); + } + }; + } + + private void setupEntries() { + initAccountEntry(); + initSwitchProviderEntry(); + initUseBridgesEntry(); + initSaveBatteryEntry(); + initAlwaysOnVpnEntry(); + initExcludeAppsEntry(); + initShowExperimentalHint(); + initTetheringEntry(); + initFirewallEntry(); + initExperimentalFeatureFooter(); + initDonateEntry(); + initLogEntry(); + initAboutEntry(); + } + + private void initAccountEntry() { + account = drawerView.findViewById(R.id.account); + FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); + Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider(); + account.setText(currentProvider.getName()); + account.setOnClickListener((buttonView) -> { + Fragment fragment = new EipFragment(); + Bundle arguments = new Bundle(); + arguments.putParcelable(PROVIDER_KEY, currentProvider); + fragment.setArguments(arguments); + hideActionBarSubTitle(); + fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); + closeDrawer(); + }); + } + + private void initSwitchProviderEntry() { + if (isDefaultBitmask()) { + IconTextEntry switchProvider = drawerView.findViewById(R.id.switch_provider); + switchProvider.setVisibility(VISIBLE); + switchProvider.setOnClickListener(v -> + getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER)); + } + } + + private void initUseBridgesEntry() { + IconSwitchEntry useBridges = drawerView.findViewById(R.id.bridges_switch); + if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) { + useBridges.setVisibility(VISIBLE); + useBridges.setChecked(getUsePluggableTransports(getContext())); + useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (!buttonView.isPressed()) { + return; + } + usePluggableTransports(getContext(), isChecked); + if (VpnStatus.isVPNActive()) { + EipCommand.startVPN(getContext(), false); + closeDrawer(); + } + }); + + + } else { + useBridges.setVisibility(GONE); + } + } + + private void initSaveBatteryEntry() { + saveBattery = drawerView.findViewById(R.id.battery_switch); + saveBattery.showSubtitle(false); + saveBattery.setChecked(getSaveBattery(getContext())); + saveBattery.setOnCheckedChangeListener(((buttonView, isChecked) -> { + if (!buttonView.isPressed()) { + return; + } + if (isChecked) { + showSaveBatteryAlert(); + } else { + saveBattery(getContext(), false); + } + })); + boolean enableEntry = !TetheringObservable.getInstance().getTetheringState().isVpnTetheringRunning(); + enableSaveBatteryEntry(enableEntry); + } + + private void enableSaveBatteryEntry(boolean enabled) { + if (saveBattery.isEnabled() == enabled) { + return; + } + saveBattery.setEnabled(enabled); + saveBattery.showSubtitle(!enabled); + } + + private void initAlwaysOnVpnEntry() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + IconTextEntry alwaysOnVpn = drawerView.findViewById(R.id.always_on_vpn); + alwaysOnVpn.setVisibility(VISIBLE); + alwaysOnVpn.setOnClickListener((buttonView) -> { + closeDrawer(); + if (getShowAlwaysOnDialog(getContext())) { + showAlwaysOnDialog(); + } else { + Intent intent = new Intent("android.net.vpn.SETTINGS"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + }); + } + } + + private void initExcludeAppsEntry() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps); + excludeApps.setVisibility(VISIBLE); + Set apps = PreferenceHelper.getExcludedApps(this.getContext()); + if (apps != null) { + updateExcludeAppsSubtitle(excludeApps, apps.size()); + } + FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); + excludeApps.setOnClickListener((buttonView) -> { + closeDrawer(); + Fragment fragment = new ExcludeAppsFragment(); + setActionBarTitle(exclude_apps_fragment_title); + fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); + }); + } + } + + private void initShowExperimentalHint() { + TextView textView = drawerLayout.findViewById(R.id.show_experimental_features); + textView.setText(showExperimentalFeatures(getContext()) ? R.string.hide_experimental : R.string.show_experimental); + textView.setOnClickListener(v -> { + boolean shown = showExperimentalFeatures(getContext()); + if (shown) { + tethering.setVisibility(GONE); + firewall.setVisibility(GONE); + experimentalFeatureFooter.setVisibility(GONE); + ((TextView) v).setText(R.string.show_experimental); + } else { + tethering.setVisibility(VISIBLE); + firewall.setVisibility(VISIBLE); + experimentalFeatureFooter.setVisibility(VISIBLE); + ((TextView) v).setText(R.string.hide_experimental); + } + PreferenceHelper.setShowExperimentalFeatures(getContext(), !shown); + }); + } + + private void initFirewallEntry() { + firewall = drawerView.findViewById(R.id.enableIPv6Firewall); + boolean show = showExperimentalFeatures(getContext()); + firewall.setVisibility(show ? VISIBLE : GONE); + firewall.setChecked(PreferenceHelper.useIpv6Firewall(getContext())); + firewall.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (!buttonView.isPressed()) { + return; + } + PreferenceHelper.setUseIPv6Firewall(getContext(), isChecked); + if (VpnStatus.isVPNActive()) { + if (isChecked) { + firewallManager.startIPv6Firewall(); + } else { + firewallManager.stopIPv6Firewall(); + } + } + }); + } + + private void initTetheringEntry() { + tethering = drawerView.findViewById(R.id.tethering); + boolean show = showExperimentalFeatures(getContext()); + tethering.setVisibility(show ? VISIBLE : GONE); + tethering.setOnClickListener((buttonView) -> { + showTetheringAlert(); + }); + } + + private void initExperimentalFeatureFooter() { + experimentalFeatureFooter = drawerView.findViewById(R.id.experimental_features_footer); + boolean show = showExperimentalFeatures(getContext()); + experimentalFeatureFooter.setVisibility(show ? VISIBLE : GONE); + } + + private void initDonateEntry() { + if (ENABLE_DONATION) { + IconTextEntry donate = drawerView.findViewById(R.id.donate); + donate.setVisibility(VISIBLE); + donate.setOnClickListener((buttonView) -> { + closeDrawer(); + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL)); + startActivity(browserIntent); + + }); + } + } + + private void initLogEntry() { + IconTextEntry log = drawerView.findViewById(R.id.log); + FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); + log.setOnClickListener((buttonView) -> { + closeDrawer(); + Fragment fragment = new LogFragment(); + setActionBarTitle(log_fragment_title); + fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); + }); + } + + private void initAboutEntry() { + IconTextEntry about = drawerView.findViewById(R.id.about); + FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); + about.setOnClickListener((buttonView) -> { + closeDrawer(); + Fragment fragment = new AboutFragment(); + setActionBarTitle(about_fragment_title); + fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); + }); + } + + private void closeDrawer() { + if (drawerLayout != null) { + drawerLayout.closeDrawer(fragmentContainerView); + } + } + + private ActionBar setupActionBar() { + AppCompatActivity activity = (AppCompatActivity) getActivity(); + activity.setSupportActionBar(toolbar); + final ActionBar actionBar = activity.getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + return actionBar; + } + + private void openNavigationDrawerForFirstTimeUsers() { + if (userLearnedDrawer) { + return; + } + + drawerLayout.openDrawer(fragmentContainerView, false); + closeDrawerWithDelay(); + } + + @NonNull + private void closeDrawerWithDelay() { + final Handler navigationDrawerHandler = new Handler(); + navigationDrawerHandler.postDelayed(() -> { + if (!wasPaused) { + drawerLayout.closeDrawer(fragmentContainerView, true); + } else { + shouldCloseOnResume = true; + } + + }, TWO_SECONDS); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (showSaveBattery) { + outState.putBoolean(KEY_SHOW_SAVE_BATTERY_ALERT, true); + alertDialog.dismiss(); + } + } + + private void restoreFromSavedInstance(Bundle savedInstanceState) { + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_SAVE_BATTERY_ALERT)) { + showSaveBatteryAlert(); + } + } + + private void showSaveBatteryAlert() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + try { + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); + showSaveBattery = true; + alertDialog = alertBuilder + .setTitle(activity.getString(R.string.save_battery)) + .setMessage(activity.getString(R.string.save_battery_message)) + .setPositiveButton((android.R.string.yes), (dialog, which) -> { + saveBattery(getContext(), true); + }) + .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> saveBattery.setCheckedQuietly(false)) + .setOnDismissListener(dialog -> showSaveBattery = false) + .setOnCancelListener(dialog -> saveBattery.setCheckedQuietly(false)).show(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + + public void showTetheringAlert() { + try { + + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + getActivity().getSupportFragmentManager()).removePreviousFragment( + TetheringDialog.TAG); + DialogFragment newFragment = new TetheringDialog(); + newFragment.show(fragmentTransaction, TetheringDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + } + + public void showAlwaysOnDialog() { + try { + + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + getActivity().getSupportFragmentManager()).removePreviousFragment( + AlwaysOnDialog.TAG); + DialogFragment newFragment = new AlwaysOnDialog(); + newFragment.show(fragmentTransaction, AlwaysOnDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // Forward the new configuration the drawer toggle component. + drawerToggle.onConfigurationChanged(newConfig); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (drawerLayout != null && isDrawerOpen()) { + showGlobalContextActionBar(); + } + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (drawerToggle.onOptionsItemSelected(item)) { + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onDestroy() { + super.onDestroy(); + getRefWatcher(getActivity()).watch(this); + preferences.unregisterOnSharedPreferenceChangeListener(this); + } + + /** + * Per the navigation drawer design guidelines, updates the action bar to show the global app + * 'context', rather than just what's in the current screen. + */ + private void showGlobalContextActionBar() { + ActionBar actionBar = getActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setTitle(R.string.app_name); + } + + private ActionBar getActionBar() { + return ((AppCompatActivity) getActivity()).getSupportActionBar(); + } + + private void setActionBarTitle(@StringRes int resId) { + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setSubtitle(resId); + } + } + + private void hideActionBarSubTitle() { + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setSubtitle(null); + } + } + + public void refresh() { + Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider(); + account.setText(currentProvider.getName()); + initUseBridgesEntry(); + } + + private void updateExcludeAppsSubtitle(IconTextEntry excludeApps, int number) { + if (number > 0) { + excludeApps.setSubtitle(getContext().getResources().getQuantityString(R.plurals.subtitle_exclude_apps, number, number)); + excludeApps.setSubtitleColor(R.color.colorError); + } else { + excludeApps.hideSubtitle(); + } + } + + public void onAppsExcluded(int number) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps); + updateExcludeAppsSubtitle(excludeApps, number); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(USE_PLUGGABLE_TRANSPORTS)) { + initUseBridgesEntry(); + } else if (key.equals(USE_IPv6_FIREWALL)) { + initFirewallEntry(); + } + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof TetheringObservable || o instanceof EipStatus) { + try { + getActivity().runOnUiThread(() -> + enableSaveBatteryEntry(!TetheringObservable.getInstance().getTetheringState().isVpnTetheringRunning())); + } catch (NullPointerException npe) { + // eat me + } + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java new file mode 100644 index 00000000..d901ba68 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AboutFragment.java @@ -0,0 +1,67 @@ +package se.leap.bitmaskclient.base.fragments; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.R; + +import static android.view.View.VISIBLE; + +public class AboutFragment extends Fragment { + + final public static String TAG = AboutFragment.class.getSimpleName(); + final public static int VIEWED = 0; + + @InjectView(R.id.version) + TextView versionTextView; + + @InjectView(R.id.terms_of_service) + TextView termsOfService; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.f_about, container, false); + ButterKnife.inject(this, view); + return view; + } + + @Override + public void onStart() { + super.onStart(); + String version; + String name = "Bitmask"; + try { + PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo( + getActivity().getPackageName(), 0); + version = packageinfo.versionName; + name = getString(R.string.app_name); + } catch (NameNotFoundException e) { + version = "error fetching version"; + } + + versionTextView.setText(getString(R.string.version_info, name, version)); + + if (BuildConfig.FLAVOR_branding.equals("custom") && hasTermsOfServiceResource()) { + termsOfService.setText(getString(getTermsOfServiceResource())); + termsOfService.setVisibility(VISIBLE); + } + } + + private boolean hasTermsOfServiceResource() { + return getTermsOfServiceResource() != 0; + } + + private int getTermsOfServiceResource() { + return this.getContext().getResources().getIdentifier("terms_of_service", "string", this.getContext().getPackageName()); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java new file mode 100644 index 00000000..a8034e1a --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/AlwaysOnDialog.java @@ -0,0 +1,76 @@ +package se.leap.bitmaskclient.base.fragments; + +import android.app.Dialog; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.appcompat.widget.AppCompatTextView; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.views.IconTextView; + +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.saveShowAlwaysOnDialog; + + +/** + * Created by cyberta on 25.02.18. + */ + + + +public class AlwaysOnDialog extends AppCompatDialogFragment { + + public final static String TAG = AlwaysOnDialog.class.getName(); + + @InjectView(R.id.do_not_show_again) + CheckBox doNotShowAgainCheckBox; + + @InjectView(R.id.user_message) + IconTextView userMessage; + + @InjectView(R.id.block_vpn_user_message) + AppCompatTextView blockVpnUserMessage; + + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.d_checkbox_confirm, null); + ButterKnife.inject(this, view); + + userMessage.setIcon(R.drawable.ic_settings); + userMessage.setText(getString(R.string.always_on_vpn_user_message)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + blockVpnUserMessage.setVisibility(View.VISIBLE); + } + + builder.setView(view) + .setPositiveButton(android.R.string.ok, (dialog, id) -> { + if (doNotShowAgainCheckBox.isChecked()) { + saveShowAlwaysOnDialog(getContext(), false); + } + Intent intent = new Intent("android.net.vpn.SETTINGS"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + }) + .setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()); + return builder.create(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java new file mode 100644 index 00000000..0277933c --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/DonationReminderDialog.java @@ -0,0 +1,120 @@ +package se.leap.bitmaskclient.base.fragments; + +import android.app.Dialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; + +import java.text.ParseException; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.utils.DateHelper; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; + +import static se.leap.bitmaskclient.base.models.Constants.DONATION_REMINDER_DURATION; +import static se.leap.bitmaskclient.base.models.Constants.DONATION_URL; +import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION; +import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION_REMINDER; +import static se.leap.bitmaskclient.base.models.Constants.FIRST_TIME_USER_DATE; +import static se.leap.bitmaskclient.base.models.Constants.LAST_DONATION_REMINDER_DATE; + +public class DonationReminderDialog extends AppCompatDialogFragment { + + public final static String TAG = DonationReminderDialog.class.getName(); + private static boolean isShown = false; + + @InjectView(R.id.btnDonate) + Button btnDonate; + + @InjectView(R.id.btnLater) + Button btnLater; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.donation_reminder_dialog, null); + ButterKnife.inject(this, view); + isShown = true; + + builder.setView(view); + btnDonate.setOnClickListener(v -> { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL)); + try { + startActivity(browserIntent); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + } + PreferenceHelper.putString(getContext(), LAST_DONATION_REMINDER_DATE, + DateHelper.getCurrentDateString()); + dismiss(); + }); + btnLater.setOnClickListener(v -> { + PreferenceHelper.putString(getContext(), LAST_DONATION_REMINDER_DATE, + DateHelper.getCurrentDateString()); + dismiss(); + }); + + return builder.create(); + } + + public static boolean isCallable(Context context) { + if (isShown) { + return false; + } + + if (!ENABLE_DONATION || !ENABLE_DONATION_REMINDER) { + return false; + } + + if (context == null) { + Log.e(TAG, "context is null!"); + return false; + } + + String firstTimeUserDate = PreferenceHelper.getString(context, FIRST_TIME_USER_DATE, null); + if (firstTimeUserDate == null) { + PreferenceHelper.putString(context, FIRST_TIME_USER_DATE, DateHelper.getCurrentDateString()); + return false; + } + + try { + long diffDays; + + diffDays = DateHelper.getDateDiffToCurrentDateInDays(firstTimeUserDate); + if (diffDays < 1) { + return false; + } + + String lastDonationReminderDate = PreferenceHelper.getString(context, LAST_DONATION_REMINDER_DATE, null); + if (lastDonationReminderDate == null) { + return true; + } + diffDays = DateHelper.getDateDiffToCurrentDateInDays(lastDonationReminderDate); + return diffDays >= DONATION_REMINDER_DURATION; + + } catch (ParseException e) { + e.printStackTrace(); + Log.e(TAG, e.getMessage()); + return false; + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java new file mode 100644 index 00000000..9544fb1e --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java @@ -0,0 +1,608 @@ +/** + * 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.base.fragments; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Vibrator; +import android.text.TextUtils; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; + +import java.util.Observable; +import java.util.Observer; + +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.VpnStatus; +import se.leap.bitmaskclient.providersetup.ProviderListActivity; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.FragmentManagerEnhanced; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.base.views.VpnStateImage; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.eip.EipStatus; +import se.leap.bitmaskclient.providersetup.ProviderAPICommand; +import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity; +import se.leap.bitmaskclient.providersetup.activities.LoginActivity; +import se.leap.bitmaskclient.providersetup.models.LeapSRPSession; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; +import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; +import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES; +import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask; +import static se.leap.bitmaskclient.base.utils.ViewHelper.convertDimensionToPx; +import static se.leap.bitmaskclient.eip.EipSetupObserver.connectionRetry; +import static se.leap.bitmaskclient.eip.EipSetupObserver.gatewayOrder; +import static se.leap.bitmaskclient.eip.EipSetupObserver.reconnectingWithDifferentGateway; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE; + +public class EipFragment extends Fragment implements Observer { + + public final static String TAG = EipFragment.class.getSimpleName(); + + + private SharedPreferences preferences; + private Provider provider; + + @InjectView(R.id.background) + AppCompatImageView background; + + @InjectView(R.id.vpn_state_image) + VpnStateImage vpnStateImage; + + @InjectView(R.id.vpn_main_button) + AppCompatButton mainButton; + + @InjectView(R.id.routed_text) + AppCompatTextView routedText; + + @InjectView(R.id.vpn_route) + AppCompatTextView vpnRoute; + + + + private EipStatus eipStatus; + + //---saved Instance ------- + private final String KEY_SHOW_PENDING_START_CANCELLATION = "KEY_SHOW_PENDING_START_CANCELLATION"; + private final String KEY_SHOW_ASK_TO_STOP_EIP = "KEY_SHOW_ASK_TO_STOP_EIP"; + private boolean showPendingStartCancellation = false; + private boolean showAskToStopEip = false; + //------------------------ + AlertDialog alertDialog; + + private IOpenVPNServiceInternal mService; + private ServiceConnection openVpnConnection; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Bundle arguments = getArguments(); + Activity activity = getActivity(); + if (activity != null) { + if (arguments != null) { + provider = arguments.getParcelable(PROVIDER_KEY); + if (provider == null) { + handleNoProvider(activity); + } else { + Log.d(TAG, provider.getName() + " configured as provider"); + } + } else { + handleNoProvider(activity); + } + } + } + + private void handleNoProvider(Activity activity) { + if (isDefaultBitmask()) { + activity.startActivityForResult(new Intent(activity, ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER); + } else { + Log.e(TAG, "no provider given - try to reconfigure custom provider"); + startActivityForResult(new Intent(activity, CustomProviderSetupActivity.class), REQUEST_CODE_CONFIGURE_LEAP); + + } + + } + + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + openVpnConnection = new EipFragmentServiceConnection(); + eipStatus = EipStatus.getInstance(); + Activity activity = getActivity(); + if (activity != null) { + preferences = getActivity().getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); + } else { + Log.e(TAG, "activity is null in onCreate - no preferences set!"); + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + eipStatus.addObserver(this); + View view = inflater.inflate(R.layout.f_eip, container, false); + ButterKnife.inject(this, view); + + Bundle arguments = getArguments(); + if (arguments != null && arguments.containsKey(ASK_TO_CANCEL_VPN) && arguments.getBoolean(ASK_TO_CANCEL_VPN)) { + arguments.remove(ASK_TO_CANCEL_VPN); + setArguments(arguments); + askToStopEIP(); + } + restoreFromSavedInstance(savedInstanceState); + return view; + } + + @Override + public void onStart() { + super.onStart(); + if (DonationReminderDialog.isCallable(getContext())) { + showDonationReminderDialog(); + } + } + + @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); + bindOpenVpnService(); + handleNewState(); + } + + @Override + public void onPause() { + super.onPause(); + + Activity activity = getActivity(); + if (activity != null) { + getActivity().unbindService(openVpnConnection); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (showAskToStopEip) { + outState.putBoolean(KEY_SHOW_ASK_TO_STOP_EIP, true); + alertDialog.dismiss(); + } else if (showPendingStartCancellation) { + outState.putBoolean(KEY_SHOW_PENDING_START_CANCELLATION, true); + alertDialog.dismiss(); + } + } + + private void restoreFromSavedInstance(Bundle savedInstanceState) { + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_PENDING_START_CANCELLATION)) { + showPendingStartCancellation = true; + askPendingStartCancellation(); + } else if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_ASK_TO_STOP_EIP)) { + showAskToStopEip = true; + askToStopEIP(); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + eipStatus.deleteObserver(this); + } + + private void saveStatus(boolean restartOnBoot) { + preferences.edit().putBoolean(EIP_RESTART_ON_BOOT, restartOnBoot).apply(); + } + + @OnClick(R.id.vpn_main_button) + void onButtonClick() { + handleIcon(); + } + + @OnClick(R.id.vpn_state_image) + void onVpnStateImageClick() { + handleIcon(); + } + + void handleIcon() { + if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnected() || eipStatus.isConnecting()) + handleSwitchOff(); + else + handleSwitchOn(); + } + + private void handleSwitchOn() { + Context context = getContext(); + if (context == null) { + Log.e(TAG, "context is null when switch turning on"); + return; + } + + if (canStartEIP()) { + startEipFromScratch(); + } else if (canLogInToStartEIP()) { + askUserToLogIn(getString(vpn_certificate_user_message)); + } else { + // provider has no VpnCertificate but user is logged in + updateInvalidVpnCertificate(); + } + } + + private boolean canStartEIP() { + boolean certificateExists = provider.hasVpnCertificate(); + boolean isAllowedAnon = provider.allowsAnonymous(); + return (isAllowedAnon || certificateExists) && !eipStatus.isConnected() && !eipStatus.isConnecting(); + } + + private boolean canLogInToStartEIP() { + boolean isAllowedRegistered = provider.allowsRegistered(); + boolean isLoggedIn = LeapSRPSession.loggedIn(); + return isAllowedRegistered && !isLoggedIn && !eipStatus.isConnecting() && !eipStatus.isConnected(); + } + + private void handleSwitchOff() { + if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnecting()) { + askPendingStartCancellation(); + } else if (eipStatus.isConnected()) { + askToStopEIP(); + } + } + + private void setMainButtonEnabled(boolean enabled) { + mainButton.setEnabled(enabled); + vpnStateImage.setEnabled(enabled); + } + + public void startEipFromScratch() { + saveStatus(true); + Context context = getContext(); + if (context == null) { + Log.e(TAG, "context is null when trying to start VPN"); + return; + } + if (!provider.getGeoipUrl().isDefault() && provider.shouldUpdateGeoIpJson()) { + Bundle bundle = new Bundle(); + bundle.putBoolean(EIP_ACTION_START, true); + bundle.putBoolean(EIP_EARLY_ROUTES, false); + ProviderAPICommand.execute(getContext().getApplicationContext(), DOWNLOAD_GEOIP_JSON, bundle, provider); + } else { + EipCommand.startVPN(context.getApplicationContext(), false); + } + vpnStateImage.showProgress(); + routedText.setVisibility(GONE); + vpnRoute.setVisibility(GONE); + colorBackgroundALittle(); + } + + protected void stopEipIfPossible() { + Context context = getContext(); + if (context == null) { + Log.e(TAG, "context is null when trying to stop EIP"); + return; + } + EipCommand.stopVPN(context.getApplicationContext()); + } + + private void askPendingStartCancellation() { + Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "activity is null when asking to cancel"); + return; + } + + try { + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); + showPendingStartCancellation = true; + alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title)) + .setMessage(activity.getString(R.string.eip_cancel_connect_text)) + .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible()) + .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> { + }).setOnDismissListener(dialog -> showPendingStartCancellation = false).show(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + + } + + protected void askToStopEIP() { + Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "activity is null when asking to stop EIP"); + return; + } + try { + AlertDialog.Builder alertBuilder = new AlertDialog.Builder(activity); + showAskToStopEip = true; + alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title)) + .setMessage(activity.getString(R.string.eip_warning_browser_inconsistency)) + .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible()) + .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> { + }).setOnDismissListener(dialog -> showAskToStopEip = false).show(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + + } + + @Override + public void update(Observable observable, Object data) { + if (observable instanceof EipStatus) { + eipStatus = (EipStatus) observable; + Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread(this::handleNewState); + } else { + Log.e("EipFragment", "activity is null"); + } + } else if (observable instanceof ProviderObservable) { + provider = ((ProviderObservable) observable).getCurrentProvider(); + } + } + + private void handleNewState() { + Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "activity is null while trying to handle new state"); + return; + } + + //Log.d(TAG, "eip fragment eipStatus state: " + eipStatus.getState() + " - level: " + eipStatus.getLevel() + " - is reconnecting: " + eipStatus.isReconnecting()); + + + if (eipStatus.isConnecting() ) { + setMainButtonEnabled(true); + showConnectingLayout(activity); + if (eipStatus.isReconnecting()) { + //Log.d(TAG, "eip show reconnecting toast!"); + //showReconnectToast(activity); + } + } else if (eipStatus.isConnected() ) { + mainButton.setText(activity.getString(R.string.vpn_button_turn_off)); + setMainButtonEnabled(true); + vpnStateImage.setStateIcon(R.drawable.vpn_connected); + vpnStateImage.stopProgress(false); + routedText.setText(R.string.vpn_securely_routed); + routedText.setVisibility(VISIBLE); + vpnRoute.setVisibility(VISIBLE); + setVpnRouteText(); + colorBackground(); + } else if(isOpenVpnRunningWithoutNetwork()){ + mainButton.setText(activity.getString(R.string.vpn_button_turn_off)); + setMainButtonEnabled(true); + vpnStateImage.setStateIcon(R.drawable.vpn_disconnected); + vpnStateImage.stopProgress(false); + routedText.setText(R.string.vpn_securely_routed_no_internet); + routedText.setVisibility(VISIBLE); + vpnRoute.setVisibility(VISIBLE); + setVpnRouteText(); + colorBackgroundALittle(); + } else if (eipStatus.isDisconnected() && reconnectingWithDifferentGateway()) { + showConnectingLayout(activity); + // showRetryToast(activity); + } else if (eipStatus.isDisconnecting()) { + setMainButtonEnabled(false); + showDisconnectingLayout(activity); + } else if (eipStatus.isBlocking()) { + setMainButtonEnabled(true); + vpnStateImage.setStateIcon(R.drawable.vpn_blocking); + vpnStateImage.stopProgress(false); + routedText.setText(getString(R.string.void_vpn_establish, getString(R.string.app_name))); + routedText.setVisibility(VISIBLE); + vpnRoute.setVisibility(GONE); + colorBackgroundALittle(); + } else { + mainButton.setText(activity.getString(R.string.vpn_button_turn_on)); + setMainButtonEnabled(true); + vpnStateImage.setStateIcon(R.drawable.vpn_disconnected); + vpnStateImage.stopProgress(false); + routedText.setVisibility(GONE); + vpnRoute.setVisibility(GONE); + greyscaleBackground(); + } + } + + private void showToast(Activity activity, String message, boolean vibrateLong) { + LayoutInflater inflater = getLayoutInflater(); + View layout = inflater.inflate(R.layout.custom_toast, + activity.findViewById(R.id.custom_toast_container)); + + TextView text = layout.findViewById(R.id.text); + text.setText(message); + + Vibrator v = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE); + if (vibrateLong) { + v.vibrate(100); + v.vibrate(200); + } else { + v.vibrate(100); + } + + Toast toast = new Toast(activity.getApplicationContext()); + toast.setGravity(Gravity.BOTTOM, 0, convertDimensionToPx(this.getContext(), R.dimen.stdpadding)); + toast.setDuration(Toast.LENGTH_LONG); + toast.setView(layout); + toast.show(); + } + private void showReconnectToast(Activity activity) { + String message = (String.format("Retry %d of %d before the next closest gateway will be selected.", connectionRetry()+1, 5)); + showToast(activity, message, false); + } + + private void showRetryToast(Activity activity) { + int nClosestGateway = gatewayOrder(); + String message = String.format("Server number " + nClosestGateway + " not reachable. Trying next gateway."); + showToast(activity, message, true ); + } + + private void showConnectingLayout(Context activity) { + showConnectionTransitionLayout(activity, true); + } + + private void showDisconnectingLayout(Activity activity) { + showConnectionTransitionLayout(activity, false); + } + + private void showConnectionTransitionLayout(Context activity, boolean isConnecting) { + mainButton.setText(activity.getString(android.R.string.cancel)); + vpnStateImage.setStateIcon(R.drawable.vpn_connecting); + vpnStateImage.showProgress(); + routedText.setVisibility(GONE); + vpnRoute.setVisibility(GONE); + if (isConnecting) { + colorBackgroundALittle(); + } else { + greyscaleBackground(); + } + } + + private boolean isOpenVpnRunningWithoutNetwork() { + boolean isRunning = false; + try { + isRunning = eipStatus.getLevel() == LEVEL_NONETWORK && + mService.isVpnRunning(); + } catch (Exception e) { + //eat me + e.printStackTrace(); + } + + return isRunning; + } + + private void bindOpenVpnService() { + Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "activity is null when binding OpenVpn"); + return; + } + + Intent intent = new Intent(activity, OpenVPNService.class); + intent.setAction(OpenVPNService.START_SERVICE); + activity.bindService(intent, openVpnConnection, Context.BIND_AUTO_CREATE); + + } + + private void greyscaleBackground() { + ColorMatrix matrix = new ColorMatrix(); + matrix.setSaturation(0); + ColorMatrixColorFilter cf = new ColorMatrixColorFilter(matrix); + background.setColorFilter(cf); + background.setImageAlpha(255); + } + + private void colorBackgroundALittle() { + background.setColorFilter(null); + background.setImageAlpha(144); + } + + private void colorBackground() { + background.setColorFilter(null); + background.setImageAlpha(210); + } + + private void updateInvalidVpnCertificate() { + ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider); + } + + private void askUserToLogIn(String userMessage) { + Intent intent = new Intent(getContext(), LoginActivity.class); + intent.putExtra(PROVIDER_KEY, provider); + + if(userMessage != null) { + intent.putExtra(USER_MESSAGE, userMessage); + } + + Activity activity = getActivity(); + if (activity != null) { + activity.startActivityForResult(intent, REQUEST_CODE_LOG_IN); + } + } + + private void setVpnRouteText() { + String vpnRouteString = provider.getName(); + String profileName = VpnStatus.getLastConnectedVpnName(); + if (!TextUtils.isEmpty(profileName)) { + vpnRouteString += " (" + profileName + ")"; + } + vpnRoute.setText(vpnRouteString); + } + + private class EipFragmentServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName className, + IBinder service) { + mService = IOpenVPNServiceInternal.Stub.asInterface(service); + handleNewState(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + mService = null; + } + } + + public void showDonationReminderDialog() { + try { + FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( + getActivity().getSupportFragmentManager()).removePreviousFragment( + DonationReminderDialog.TAG); + DialogFragment newFragment = new DonationReminderDialog(); + newFragment.setCancelable(false); + newFragment.show(fragmentTransaction, DonationReminderDialog.TAG); + } catch (IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java new file mode 100644 index 00000000..18000171 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ExcludeAppsFragment.java @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package se.leap.bitmaskclient.base.fragments; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.CompoundButton; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SearchView; +import android.widget.TextView; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.Vector; + +import de.blinkt.openvpn.VpnProfile; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; + +/** + * Created by arne on 16.11.14. + */ +public class ExcludeAppsFragment extends Fragment implements AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener { + private ListView mListView; + private VpnProfile mProfile; + private PackageAdapter mListAdapter; + + private Set apps; + + public interface ExcludedAppsCallback { + void onAppsExcluded(int number); + } + + private ExcludedAppsCallback callback; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof ExcludedAppsCallback) { + callback = (ExcludedAppsCallback) context; + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + AppViewHolder avh = (AppViewHolder) view.getTag(); + avh.checkBox.toggle(); + } + + @Override + public void onClick(View v) { + + } + + static class AppViewHolder { + public ApplicationInfo mInfo; + public View rootView; + public TextView appName; + public ImageView appIcon; + //public TextView appSize; + //public TextView disabled; + public CompoundButton checkBox; + + static public AppViewHolder createOrRecycle(LayoutInflater inflater, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.allowed_application_layout, parent, false); + + // Creates a ViewHolder and store references to the two children views + // we want to bind data to. + AppViewHolder holder = new AppViewHolder(); + holder.rootView = convertView; + holder.appName = convertView.findViewById(R.id.app_name); + holder.appIcon = convertView.findViewById(R.id.app_icon); + holder.checkBox = convertView.findViewById(R.id.app_selected); + convertView.setTag(holder); + + return holder; + } else { + // Get the ViewHolder back to get fast access to the TextView + // and the ImageView. + return (AppViewHolder) convertView.getTag(); + } + } + + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + String packageName = (String) buttonView.getTag(); + + if (isChecked) { + Log.d("openvpn", "adding to allowed apps" + packageName); + apps.add(packageName); + + } else { + Log.d("openvpn", "removing from allowed apps" + packageName); + apps.remove(packageName); + } + + if (callback != null) { + callback.onAppsExcluded(apps.size()); + } + } + + class PackageAdapter extends BaseAdapter implements Filterable { + private Vector mPackages; + private final LayoutInflater mInflater; + private final PackageManager mPm; + private ItemFilter mFilter = new ItemFilter(); + private Vector mFilteredData; + + + private class ItemFilter extends Filter { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + + String filterString = constraint.toString().toLowerCase(Locale.getDefault()); + + FilterResults results = new FilterResults(); + + + int count = mPackages.size(); + final Vector nlist = new Vector<>(count); + + for (int i = 0; i < count; i++) { + ApplicationInfo pInfo = mPackages.get(i); + CharSequence appName = pInfo.loadLabel(mPm); + + if (TextUtils.isEmpty(appName)) + appName = pInfo.packageName; + + if (appName instanceof String) { + if (((String) appName).toLowerCase(Locale.getDefault()).contains(filterString)) + nlist.add(pInfo); + } else { + if (appName.toString().toLowerCase(Locale.getDefault()).contains(filterString)) + nlist.add(pInfo); + } + } + results.values = nlist; + results.count = nlist.size(); + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + mFilteredData = (Vector) results.values; + notifyDataSetChanged(); + } + + } + + + PackageAdapter(Context c, VpnProfile vp) { + mPm = c.getPackageManager(); + mProfile = vp; + mInflater = LayoutInflater.from(c); + + mPackages = new Vector<>(); + mFilteredData = mPackages; + } + + private void populateList(Activity c) { + List installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA); + + // Remove apps not using Internet + + int androidSystemUid = 0; + ApplicationInfo system = null; + Vector apps = new Vector(); + + try { + system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA); + androidSystemUid = system.uid; + apps.add(system); + } catch (PackageManager.NameNotFoundException e) { + } + + + for (ApplicationInfo app : installedPackages) { + + if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED && + app.uid != androidSystemUid) { + + apps.add(app); + } + } + + Collections.sort(apps, new ApplicationInfo.DisplayNameComparator(mPm)); + mPackages = apps; + mFilteredData = apps; + c.runOnUiThread(new Runnable() { + @Override + public void run() { + notifyDataSetChanged(); + } + }); + } + + @Override + public int getCount() { + return mFilteredData.size(); + } + + @Override + public Object getItem(int position) { + return mFilteredData.get(position); + } + + @Override + public long getItemId(int position) { + return mFilteredData.get(position).packageName.hashCode(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + AppViewHolder viewHolder = AppViewHolder.createOrRecycle(mInflater, convertView, parent); + + viewHolder.mInfo = mFilteredData.get(position); + final ApplicationInfo mInfo = mFilteredData.get(position); + + + CharSequence appName = mInfo.loadLabel(mPm); + + if (TextUtils.isEmpty(appName)) + appName = mInfo.packageName; + viewHolder.appName.setText(appName); + viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm)); + viewHolder.checkBox.setTag(mInfo.packageName); + viewHolder.checkBox.setOnCheckedChangeListener(ExcludeAppsFragment.this); + viewHolder.checkBox.setChecked(apps.contains(mInfo.packageName)); + + return viewHolder.rootView; + } + + @Override + public Filter getFilter() { + return mFilter; + } + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onDestroy() { + PreferenceHelper.setExcludedApps(this.getActivity().getApplicationContext(), apps); + super.onDestroy(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + apps = PreferenceHelper.getExcludedApps(this.getContext()); + + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.allowed_apps, menu); + + SearchView searchView = (SearchView) menu.findItem( R.id.app_search_widget ).getActionView(); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + mListView.setFilterText(query); + mListView.setTextFilterEnabled(true); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + mListView.setFilterText(newText); + if (TextUtils.isEmpty(newText)) + mListView.setTextFilterEnabled(false); + else + mListView.setTextFilterEnabled(true); + + return true; + } + }); + searchView.setOnCloseListener(() -> { + mListView.clearTextFilter(); + mListAdapter.getFilter().filter(""); + return false; + }); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.allowed_vpn_apps, container, false); + + mListView = v.findViewById(android.R.id.list); + + mListAdapter = new PackageAdapter(getActivity(), mProfile); + mListView.setAdapter(mListAdapter); + mListView.setOnItemClickListener(this); + + mListView.setEmptyView(v.findViewById(R.id.loading_container)); + + new Thread(() -> mListAdapter.populateList(getActivity())).start(); + + return v; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java new file mode 100644 index 00000000..d788b9e6 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java @@ -0,0 +1,587 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package se.leap.bitmaskclient.base.fragments; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.DataSetObserver; +import android.os.Bundle; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Message; +import android.preference.PreferenceManager; +import androidx.annotation.Nullable; +import androidx.fragment.app.ListFragment; +import android.text.SpannableString; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.RadioGroup; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; +import java.util.Vector; + +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.LogItem; +import de.blinkt.openvpn.core.OpenVPNManagement; +import de.blinkt.openvpn.core.OpenVPNService; +import de.blinkt.openvpn.core.Preferences; +import de.blinkt.openvpn.core.VpnStatus; +import de.blinkt.openvpn.core.VpnStatus.LogListener; +import de.blinkt.openvpn.core.VpnStatus.StateListener; +import se.leap.bitmaskclient.base.models.Constants; +import se.leap.bitmaskclient.R; + +import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount; + +public class LogFragment extends ListFragment implements StateListener, SeekBar.OnSeekBarChangeListener, RadioGroup.OnCheckedChangeListener, VpnStatus.ByteCountListener { + public static final String TAG = LogFragment.class.getSimpleName(); + private static final String LOGTIMEFORMAT = "logtimeformat"; + private static final String VERBOSITYLEVEL = "verbositylevel"; + + + + private SeekBar mLogLevelSlider; + private LinearLayout mOptionsLayout; + private RadioGroup mTimeRadioGroup; + private TextView mUpStatus; + private TextView mDownStatus; + private TextView mConnectStatus; + private boolean mShowOptionsLayout; + private CheckBox mClearLogCheckBox; + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + ladapter.setLogLevel(progress + 1); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + switch (checkedId) { + case R.id.radioISO: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO); + break; + case R.id.radioNone: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE); + break; + case R.id.radioShort: + ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT); + break; + + } + } + + @Override + public void updateByteCount(long in, long out, long diffIn, long diffOut) { + //%2$s/s %1$s - ↑%4$s/s %3$s + Resources res = getActivity().getResources(); + final String down = String.format("%2$s %1$s", humanReadableByteCount(in, false, res), humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res)); + final String up = String.format("%2$s %1$s", humanReadableByteCount(out, false, res), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res)); + + if (mUpStatus != null && mDownStatus != null) { + if (getActivity() != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mUpStatus.setText(up); + mDownStatus.setText(down); + } + }); + } + } + + } + + + class LogWindowListAdapter implements ListAdapter, LogListener, Callback { + + private static final int MESSAGE_NEWLOG = 0; + + private static final int MESSAGE_CLEARLOG = 1; + + private static final int MESSAGE_NEWTS = 2; + private static final int MESSAGE_NEWLOGLEVEL = 3; + + public static final int TIME_FORMAT_NONE = 0; + public static final int TIME_FORMAT_SHORT = 1; + public static final int TIME_FORMAT_ISO = 2; + private static final int MAX_STORED_LOG_ENTRIES = 1000; + + private Vector allEntries = new Vector<>(); + + private Vector currentLevelEntries = new Vector(); + + private Handler mHandler; + + private Vector observers = new Vector(); + + private int mTimeFormat = 0; + private int mLogLevel = 3; + + + public LogWindowListAdapter() { + initLogBuffer(); + if (mHandler == null) { + mHandler = new Handler(this); + } + + VpnStatus.addLogListener(this); + } + + + private void initLogBuffer() { + allEntries.clear(); + Collections.addAll(allEntries, VpnStatus.getlogbuffer()); + initCurrentMessages(); + } + + String getLogStr() { + String str = ""; + for (LogItem entry : allEntries) { + str += getTime(entry, TIME_FORMAT_ISO) + entry.getString(getActivity()) + '\n'; + } + return str; + } + + + private void shareLog() { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr()); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.ics_openvpn_log_file)); + shareIntent.setType("text/plain"); + startActivity(Intent.createChooser(shareIntent, "Send Logfile")); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + observers.add(observer); + + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + observers.remove(observer); + } + + @Override + public int getCount() { + return currentLevelEntries.size(); + } + + @Override + public Object getItem(int position) { + return currentLevelEntries.get(position); + } + + @Override + public long getItemId(int position) { + return ((Object) currentLevelEntries.get(position)).hashCode(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView v; + if (convertView == null) + v = new TextView(getActivity()); + else + v = (TextView) convertView; + + LogItem le = currentLevelEntries.get(position); + String msg = le.getString(getActivity()); + String time = getTime(le, mTimeFormat); + msg = time + msg; + + int spanStart = time.length(); + + SpannableString t = new SpannableString(msg); + + v.setText(t); + return v; + } + + private String getTime(LogItem le, int time) { + if (time != TIME_FORMAT_NONE) { + Date d = new Date(le.getLogtime()); + java.text.DateFormat timeformat; + if (time == TIME_FORMAT_ISO) + timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + else + timeformat = DateFormat.getTimeFormat(getActivity()); + + return timeformat.format(d) + " "; + + } else { + return ""; + } + + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return currentLevelEntries.isEmpty(); + + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public void newLog(LogItem logMessage) { + Message msg = Message.obtain(); + assert (msg != null); + msg.what = MESSAGE_NEWLOG; + Bundle bundle = new Bundle(); + bundle.putParcelable("logmessage", logMessage); + msg.setData(bundle); + mHandler.sendMessage(msg); + } + + @Override + public boolean handleMessage(Message msg) { + // We have been called + if (msg.what == MESSAGE_NEWLOG) { + + LogItem logMessage = msg.getData().getParcelable("logmessage"); + if (addLogMessage(logMessage)) + for (DataSetObserver observer : observers) { + observer.onChanged(); + } + } else if (msg.what == MESSAGE_CLEARLOG) { + for (DataSetObserver observer : observers) { + observer.onInvalidated(); + } + initLogBuffer(); + } else if (msg.what == MESSAGE_NEWTS) { + for (DataSetObserver observer : observers) { + observer.onInvalidated(); + } + } else if (msg.what == MESSAGE_NEWLOGLEVEL) { + initCurrentMessages(); + + for (DataSetObserver observer : observers) { + observer.onChanged(); + } + + } + + return true; + } + + private void initCurrentMessages() { + currentLevelEntries.clear(); + for (LogItem li : allEntries) { + if (li.getVerbosityLevel() <= mLogLevel || + mLogLevel == VpnProfile.MAXLOGLEVEL) + currentLevelEntries.add(li); + } + } + + /** + * @param logmessage + * @return True if the current entries have changed + */ + private boolean addLogMessage(LogItem logmessage) { + allEntries.add(logmessage); + + if (allEntries.size() > MAX_STORED_LOG_ENTRIES) { + Vector oldAllEntries = allEntries; + allEntries = new Vector(allEntries.size()); + for (int i = 50; i < oldAllEntries.size(); i++) { + allEntries.add(oldAllEntries.elementAt(i)); + } + initCurrentMessages(); + return true; + } else { + if (logmessage.getVerbosityLevel() <= mLogLevel) { + currentLevelEntries.add(logmessage); + return true; + } else { + return false; + } + } + } + + void clearLog() { + // Actually is probably called from GUI Thread as result of the user + // pressing a button. But better safe than sorry + VpnStatus.clearLog(); + VpnStatus.logInfo(R.string.logCleared); + mHandler.sendEmptyMessage(MESSAGE_CLEARLOG); + } + + + public void setTimeFormat(int newTimeFormat) { + mTimeFormat = newTimeFormat; + mHandler.sendEmptyMessage(MESSAGE_NEWTS); + } + + public void setLogLevel(int logLevel) { + mLogLevel = logLevel; + mHandler.sendEmptyMessage(MESSAGE_NEWLOGLEVEL); + } + + } + + + private LogWindowListAdapter ladapter; + private TextView mSpeedView; + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.clearlog) { + ladapter.clearLog(); + return true; + } else if (item.getItemId() == R.id.send) { + ladapter.shareLog(); + } else if (item.getItemId() == R.id.toggle_time) { + showHideOptionsPanel(); + } + return super.onOptionsItemSelected(item); + + } + + private void showHideOptionsPanel() { + boolean optionsVisible = (mOptionsLayout.getVisibility() != View.GONE); + + ObjectAnimator anim; + if (optionsVisible) { + anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 1.0f, 0f); + anim.addListener(collapseListener); + + } else { + mOptionsLayout.setVisibility(View.VISIBLE); + anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 0f, 1.0f); + //anim = new TranslateAnimation(0.0f, 0.0f, mOptionsLayout.getHeight(), 0.0f); + + } + + //anim.setInterpolator(new AccelerateInterpolator(1.0f)); + //anim.setDuration(300); + //mOptionsLayout.startAnimation(anim); + anim.start(); + + } + + AnimatorListenerAdapter collapseListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mOptionsLayout.setVisibility(View.GONE); + } + + }; + + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.f_log, menu); + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) + menu.removeItem(R.id.toggle_time); + } + + + @Override + public void onResume() { + super.onResume(); + Intent intent = new Intent(getActivity(), OpenVPNService.class); + intent.setAction(OpenVPNService.START_SERVICE); + } + + @Override + public void onStart() { + super.onStart(); + VpnStatus.addStateListener(this); + VpnStatus.addByteCountListener(this); + } + + @Override + public void onStop() { + super.onStop(); + VpnStatus.removeStateListener(this); + VpnStatus.removeByteCountListener(this); + + getActivity().getPreferences(0).edit().putInt(LOGTIMEFORMAT, ladapter.mTimeFormat) + .putInt(VERBOSITYLEVEL, ladapter.mLogLevel).apply(); + + } + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ListView lv = getListView(); + + lv.setOnItemLongClickListener(new OnItemLongClickListener() { + + @Override + public boolean onItemLongClick(AdapterView parent, View view, + int position, long id) { + ClipboardManager clipboard = (ClipboardManager) + getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Log Entry", ((TextView) view).getText()); + clipboard.setPrimaryClip(clip); + Toast.makeText(getActivity(), R.string.copied_entry, Toast.LENGTH_SHORT).show(); + return true; + } + }); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.f_log, container, false); + + setHasOptionsMenu(true); + + ladapter = new LogWindowListAdapter(); + ladapter.mTimeFormat = getActivity().getPreferences(0).getInt(LOGTIMEFORMAT, 1); + int logLevel = getActivity().getPreferences(0).getInt(VERBOSITYLEVEL, 1); + ladapter.setLogLevel(logLevel); + + setListAdapter(ladapter); + + mTimeRadioGroup = v.findViewById(R.id.timeFormatRadioGroup); + mTimeRadioGroup.setOnCheckedChangeListener(this); + + if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_ISO) { + mTimeRadioGroup.check(R.id.radioISO); + } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_NONE) { + mTimeRadioGroup.check(R.id.radioNone); + } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_SHORT) { + mTimeRadioGroup.check(R.id.radioShort); + } + + mClearLogCheckBox = v.findViewById(R.id.clearlogconnect); + mClearLogCheckBox.setChecked(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(Constants.CLEARLOG, true)); + mClearLogCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> + Preferences.getDefaultSharedPreferences(getActivity()).edit().putBoolean(Constants.CLEARLOG, isChecked).apply()); + + mSpeedView = v.findViewById(R.id.speed); + + mOptionsLayout = v.findViewById(R.id.logOptionsLayout); + mLogLevelSlider = v.findViewById(R.id.LogLevelSlider); + mLogLevelSlider.setMax(VpnProfile.MAXLOGLEVEL - 1); + mLogLevelSlider.setProgress(logLevel - 1); + + mLogLevelSlider.setOnSeekBarChangeListener(this); + + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) + mOptionsLayout.setVisibility(View.VISIBLE); + + mUpStatus = v.findViewById(R.id.speedUp); + mDownStatus = v.findViewById(R.id.speedDown); + mConnectStatus = v.findViewById(R.id.speedStatus); + if (mShowOptionsLayout) + mOptionsLayout.setVisibility(View.VISIBLE); + return v; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // Scroll to the end of the list end + //getListView().setSelection(getListView().getAdapter().getCount()-1); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) { + mShowOptionsLayout = true; + if (mOptionsLayout != null) + mOptionsLayout.setVisibility(View.VISIBLE); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + } + + + @Override + public void updateState(final String status, final String logMessage, final int resId, final ConnectionStatus level) { + if (isAdded()) { + final String cleanLogMessage = VpnStatus.getLastCleanLogMessage(getActivity()); + + getActivity().runOnUiThread(() -> { + if (isAdded()) { + if (mSpeedView != null) { + mSpeedView.setText(cleanLogMessage); + } + if (mConnectStatus != null) + mConnectStatus.setText(cleanLogMessage); + } + }); + } + } + + @Override + public void setConnectedVPN(String uuid) { + } + + + @Override + public void onDestroy() { + VpnStatus.removeLogListener(ladapter); + super.onDestroy(); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java new file mode 100644 index 00000000..4b307f23 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java @@ -0,0 +1,174 @@ +/** + * 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.base.fragments; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.appcompat.app.AlertDialog; + +import org.json.JSONObject; + +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.eip.EIP; +import se.leap.bitmaskclient.eip.EipCommand; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderAPICommand; + +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.R.string.warning_option_try_ovpn; +import static se.leap.bitmaskclient.R.string.warning_option_try_pt; +import static se.leap.bitmaskclient.eip.EIP.EIPErrors.UNKNOWN; +import static se.leap.bitmaskclient.eip.EIP.EIPErrors.valueOf; +import static se.leap.bitmaskclient.eip.EIP.ERRORS; +import static se.leap.bitmaskclient.eip.EIP.ERRORID; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.usePluggableTransports; + +/** + * Implements an error dialog for the main activity. + * + * @author fupduck + * @author cyberta + */ +public class MainActivityErrorDialog extends DialogFragment { + + final public static String TAG = "downloaded_failed_dialog"; + final private static String KEY_REASON_TO_FAIL = "key reason to fail"; + final private static String KEY_PROVIDER = "key provider"; + private String reasonToFail; + private EIP.EIPErrors downloadError = UNKNOWN; + + private Provider provider; + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, String reasonToFail) { + return newInstance(provider, reasonToFail, UNKNOWN); + } + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, String reasonToFail, EIP.EIPErrors error) { + MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog(); + dialogFragment.reasonToFail = reasonToFail; + dialogFragment.provider = provider; + dialogFragment.downloadError = error; + return dialogFragment; + } + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, JSONObject errorJson) { + MainActivityErrorDialog dialogFragment = new MainActivityErrorDialog(); + dialogFragment.provider = provider; + try { + if (errorJson.has(ERRORS)) { + dialogFragment.reasonToFail = errorJson.getString(ERRORS); + } else { + //default error msg + dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); + } + + if (errorJson.has(ERRORID)) { + dialogFragment.downloadError = valueOf(errorJson.getString(ERRORID)); + } + } catch (Exception e) { + e.printStackTrace(); + dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); + } + return dialogFragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + restoreFromSavedInstance(savedInstanceState); + } + + @Override + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + Context applicationContext = getContext().getApplicationContext(); + builder.setMessage(reasonToFail) + .setNegativeButton(R.string.cancel, (dialog, id) -> { + }); + switch (downloadError) { + case ERROR_INVALID_VPN_CERTIFICATE: + builder.setPositiveButton(R.string.update_certificate, (dialog, which) -> + ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider)); + break; + case NO_MORE_GATEWAYS: + if (provider.supportsPluggableTransports()) { + if (getUsePluggableTransports(applicationContext)) { + builder.setPositiveButton(warning_option_try_ovpn, ((dialog, which) -> { + usePluggableTransports(applicationContext, false); + EipCommand.startVPN(applicationContext, false); + })); + } else { + builder.setPositiveButton(warning_option_try_pt, ((dialog, which) -> { + usePluggableTransports(applicationContext, true); + EipCommand.startVPN(applicationContext, false); + })); + } + } else { + builder.setPositiveButton(R.string.retry, (dialog, which) -> { + EipCommand.startVPN(applicationContext, false); + }); + } + break; + case ERROR_VPN_PREPARE: + builder.setPositiveButton(R.string.retry, (dialog, which) -> { + EipCommand.startVPN(applicationContext, false); + }); + break; + default: + break; + } + + // Create the AlertDialog object and return it + return builder.create(); + } + + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(KEY_REASON_TO_FAIL, reasonToFail); + outState.putParcelable(KEY_PROVIDER, provider); + } + + private void restoreFromSavedInstance(Bundle savedInstanceState) { + if (savedInstanceState == null) { + return; + } + if (savedInstanceState.containsKey(KEY_PROVIDER)) { + this.provider = savedInstanceState.getParcelable(KEY_PROVIDER); + } + if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) { + this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL); + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java new file mode 100644 index 00000000..8593e25c --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/TetheringDialog.java @@ -0,0 +1,258 @@ +package se.leap.bitmaskclient.base.fragments; + +import android.app.Dialog; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.provider.Settings; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Observable; +import java.util.Observer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.firewall.FirewallManager; +import se.leap.bitmaskclient.tethering.TetheringObservable; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; +import se.leap.bitmaskclient.base.views.IconCheckboxEntry; + +/** + * Copyright (c) 2020 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 . + */ + +public class TetheringDialog extends AppCompatDialogFragment implements Observer { + + public final static String TAG = TetheringDialog.class.getName(); + + @InjectView(R.id.tvTitle) + AppCompatTextView title; + + @InjectView(R.id.user_message) + AppCompatTextView userMessage; + + @InjectView(R.id.selection_list_view) + RecyclerView selectionListView; + DialogListAdapter adapter; + private DialogListAdapter.ViewModel[] dataset; + + public static class DialogListAdapter extends RecyclerView.Adapter { + + interface OnItemClickListener { + void onItemClick(ViewModel item); + } + + private ViewModel[] dataSet; + private OnItemClickListener clickListener; + + DialogListAdapter(ViewModel[] dataSet, OnItemClickListener clickListener) { + this.dataSet = dataSet; + this.clickListener = clickListener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + IconCheckboxEntry v = new IconCheckboxEntry(viewGroup.getContext()); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { + viewHolder.bind(dataSet[i], clickListener); + } + + @Override + public int getItemCount() { + return dataSet.length; + } + + public static class ViewModel { + + public Drawable image; + public String text; + public boolean checked; + public boolean enabled; + + ViewModel(Drawable image, String text, boolean checked, boolean enabled) { + this.image = image; + this.text = text; + this.checked = checked; + this.enabled = enabled; + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + ViewHolder(IconCheckboxEntry v) { + super(v); + } + + public void bind(ViewModel model, OnItemClickListener onClickListener) { + ((IconCheckboxEntry) this.itemView).bind(model); + this.itemView.setOnClickListener(v -> { + model.checked = !model.checked; + ((IconCheckboxEntry) itemView).setChecked(model.checked); + onClickListener.onItemClick(model); + }); + } + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.d_list_selection, null); + ButterKnife.inject(this, view); + + title.setText(R.string.tethering); + userMessage.setMovementMethod(LinkMovementMethod.getInstance()); + userMessage.setLinkTextColor(getContext().getResources().getColor(R.color.colorPrimary)); + userMessage.setText(createUserMessage()); + + initDataset(); + adapter = new DialogListAdapter(dataset, this::onItemClick); + selectionListView.setAdapter(adapter); + selectionListView.setLayoutManager(new LinearLayoutManager(getActivity())); + + + builder.setView(view) + .setPositiveButton(android.R.string.ok, (dialog, id) -> { + PreferenceHelper.allowWifiTethering(getContext(), dataset[0].checked); + PreferenceHelper.allowUsbTethering(getContext(), dataset[1].checked); + PreferenceHelper.allowBluetoothTethering(getContext(), dataset[2].checked); + TetheringObservable.allowVpnWifiTethering(dataset[0].checked); + TetheringObservable.allowVpnUsbTethering(dataset[1].checked); + TetheringObservable.allowVpnBluetoothTethering(dataset[2].checked); + FirewallManager firewallManager = new FirewallManager(getContext().getApplicationContext(), false); + if (VpnStatus.isVPNActive()) { + if (TetheringObservable.getInstance().getTetheringState().hasAnyDeviceTetheringEnabled() && + TetheringObservable.getInstance().getTetheringState().hasAnyVpnTetheringAllowed()) { + firewallManager.startTethering(); + } else { + firewallManager.stopTethering(); + } + } + }).setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()); + return builder.create(); + } + + @Override + public void onResume() { + super.onResume(); + dataset[0].enabled = TetheringObservable.getInstance().isWifiTetheringEnabled(); + dataset[1].enabled = TetheringObservable.getInstance().isUsbTetheringEnabled(); + dataset[2].enabled = TetheringObservable.getInstance().isBluetoothTetheringEnabled(); + adapter.notifyDataSetChanged(); + TetheringObservable.getInstance().addObserver(this); + } + + @Override + public void onPause() { + super.onPause(); + TetheringObservable.getInstance().deleteObserver(this); + } + + public void onItemClick(DialogListAdapter.ViewModel item) { + + } + + private CharSequence createUserMessage() { + String tetheringMessage = getString(R.string.tethering_message); + String systemSettingsMessage = getString(R.string.tethering_enabled_message); + Pattern pattern = Pattern.compile("([\\w .]*)()+([\\w ]*)()([\\w .]*)"); + Matcher matcher = pattern.matcher(systemSettingsMessage); + int startIndex = 0; + int endIndex = 0; + if (matcher.matches()) { + startIndex = matcher.start(2); + endIndex = startIndex + matcher.group(3).length(); + } + systemSettingsMessage = systemSettingsMessage.replace("", "").replace("", ""); + String wholeMessage = systemSettingsMessage + "\n\n" + tetheringMessage; + Spannable spannable = new SpannableString(wholeMessage); + spannable.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + try { + final Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + final ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.TetherSettings"); + intent.setComponent(cn); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } catch (ActivityNotFoundException e) { + Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); + startActivity(intent); + } + + } + }, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spannable; + } + + private void initDataset() { + dataset = new DialogListAdapter.ViewModel[] { + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_wifi), + getContext().getString(R.string.tethering_wifi), + PreferenceHelper.isWifiTetheringAllowed(getContext()), + TetheringObservable.getInstance().isWifiTetheringEnabled()), + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_usb), + getContext().getString(R.string.tethering_usb), + PreferenceHelper.isUsbTetheringAllowed(getContext()), + TetheringObservable.getInstance().isUsbTetheringEnabled()), + new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_bluetooth), + getContext().getString(R.string.tethering_bluetooth), + PreferenceHelper.isBluetoothTetheringAllowed(getContext()), + TetheringObservable.getInstance().isUsbTetheringEnabled()) + }; + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof TetheringObservable) { + TetheringObservable observable = (TetheringObservable) o; + Log.d(TAG, "TetheringObservable is updated"); + dataset[0].enabled = observable.isWifiTetheringEnabled(); + dataset[1].enabled = observable.isUsbTetheringEnabled(); + dataset[2].enabled = observable.isBluetoothTetheringEnabled(); + adapter.notifyDataSetChanged(); + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java new file mode 100644 index 00000000..d649aaf5 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2020 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.base.models; + +import android.text.TextUtils; + +import se.leap.bitmaskclient.BuildConfig; + +public interface Constants { + + ////////////////////////////////////////////// + // PREFERENCES CONSTANTS + ///////////////////////////////////////////// + + String SHARED_PREFERENCES = "LEAPPreferences"; + String PREFERENCES_APP_VERSION = "bitmask version"; + String ALWAYS_ON_SHOW_DIALOG = "DIALOG.ALWAYS_ON_SHOW_DIALOG"; + String CLEARLOG = "clearlogconnect"; + String LAST_USED_PROFILE = "last_used_profile"; + String EXCLUDED_APPS = "excluded_apps"; + String USE_PLUGGABLE_TRANSPORTS = "usePluggableTransports"; + String ALLOW_TETHERING_BLUETOOTH = "tethering_bluetooth"; + String ALLOW_TETHERING_WIFI = "tethering_wifi"; + String ALLOW_TETHERING_USB = "tethering_usb"; + String SHOW_EXPERIMENTAL = "show_experimental"; + String USE_IPv6_FIREWALL = "use_ipv6_firewall"; + String RESTART_ON_UPDATE = "restart_on_update"; + String LAST_UPDATE_CHECK = "last_update_check"; + + + ////////////////////////////////////////////// + // REQUEST CODE CONSTANTS + ///////////////////////////////////////////// + + String REQUEST_CODE_KEY = "request_code"; + int REQUEST_CODE_CONFIGURE_LEAP = 0; + int REQUEST_CODE_SWITCH_PROVIDER = 1; + int REQUEST_CODE_LOG_IN = 2; + int REQUEST_CODE_ADD_PROVIDER = 3; + int REQUEST_CODE_REQUEST_UPDATE = 4; + + + ////////////////////////////////////////////// + // APP CONSTANTS + ///////////////////////////////////////////// + + String APP_ACTION_QUIT = "quit"; + String APP_ACTION_CONFIGURE_ALWAYS_ON_PROFILE = "configure always-on profile"; + String DEFAULT_BITMASK = "normal"; + String CUSTOM_BITMASK = "custom"; + String DANGER_ON = "danger_on"; + + + String ASK_TO_CANCEL_VPN = "ask_to_cancel_vpn"; + + + ////////////////////////////////////////////// + // EIP CONSTANTS + ///////////////////////////////////////////// + + String EIP_ACTION_CHECK_CERT_VALIDITY = "EIP.CHECK_CERT_VALIDITY"; + String EIP_ACTION_START = "se.leap.bitmaskclient.EIP.START"; + String EIP_ACTION_STOP = "se.leap.bitmaskclient.EIP.STOP"; + String EIP_ACTION_IS_RUNNING = "se.leap.bitmaskclient.EIP.IS_RUNNING"; + String EIP_ACTION_START_ALWAYS_ON_VPN = "se.leap.bitmaskclient.START_ALWAYS_ON_VPN"; + 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_ACTION_PREPARE_VPN = "se.leap.bitmaskclient.EIP_ACTION_PREPARE_VPN"; + String EIP_ACTION_CONFIGURE_TETHERING = "se.leap.bitmaskclient.EIP_ACTION_CONFIGURE_TETHERING"; + + 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"; + String EIP_EARLY_ROUTES = "EIP.EARLY_ROUTES"; + String EIP_N_CLOSEST_GATEWAY = "EIP.N_CLOSEST_GATEWAY"; + + + ////////////////////////////////////////////// + // PROVIDER CONSTANTS + ///////////////////////////////////////////// + + String PROVIDER_ALLOW_ANONYMOUS = "allow_anonymous"; + String PROVIDER_ALLOWED_REGISTERED = "allow_registration"; + String PROVIDER_VPN_CERTIFICATE = "cert"; + String PROVIDER_PRIVATE_KEY = "Constants.PROVIDER_PRIVATE_KEY"; + String PROVIDER_KEY = "Constants.PROVIDER_KEY"; + String PROVIDER_CONFIGURED = "Constants.PROVIDER_CONFIGURED"; + String PROVIDER_EIP_DEFINITION = "Constants.EIP_DEFINITION"; + String PROVIDER_PROFILE_UUID = "Constants.PROVIDER_PROFILE_UUID"; + String PROVIDER_PROFILE = "Constants.PROVIDER_PROFILE"; + + ////////////////////////////////////////////// + // CREDENTIAL CONSTANTS + ///////////////////////////////////////////// + + String CREDENTIALS_USERNAME = "username"; + String CREDENTIALS_PASSWORD = "password"; + + enum CREDENTIAL_ERRORS { + USERNAME_MISSING, + PASSWORD_INVALID_LENGTH, + RISEUP_WARNING + } + + ////////////////////////////////////////////// + // BROADCAST CONSTANTS + ///////////////////////////////////////////// + + String BROADCAST_EIP_EVENT = "BROADCAST.EIP_EVENT"; + String BROADCAST_PROVIDER_API_EVENT = "BROADCAST.PROVIDER_API_EVENT"; + String BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT = "BROADCAST.GATEWAY_SETUP_WATCHER_EVENT"; + String BROADCAST_RESULT_CODE = "BROADCAST.RESULT_CODE"; + String BROADCAST_RESULT_KEY = "BROADCAST.RESULT_KEY"; + String BROADCAST_DOWNLOAD_SERVICE_EVENT = "BROADCAST.DOWNLOAD_SERVICE_EVENT"; + + + ////////////////////////////////////////////// + // ICS-OPENVPN CONSTANTS + ///////////////////////////////////////////// + String DEFAULT_SHARED_PREFS_BATTERY_SAVER = "screenoff"; + + ////////////////////////////////////////////// + // CUSTOM CONSTANTS + ///////////////////////////////////////////// + boolean ENABLE_DONATION = BuildConfig.enable_donation; + boolean ENABLE_DONATION_REMINDER = BuildConfig.enable_donation_reminder; + int DONATION_REMINDER_DURATION = BuildConfig.donation_reminder_duration; + String DONATION_URL = TextUtils.isEmpty(BuildConfig.donation_url) ? + BuildConfig.default_donation_url : BuildConfig.donation_url; + String LAST_DONATION_REMINDER_DATE = "last_donation_reminder_date"; + String FIRST_TIME_USER_DATE = "first_time_user_date"; + + + ////////////////////////////////////////////// + // JSON KEYS + ///////////////////////////////////////////// + String IP_ADDRESS = "ip_address"; + String REMOTE = "remote"; + String PORTS = "ports"; + String PROTOCOLS = "protocols"; + String CAPABILITIES = "capabilities"; + String TRANSPORT = "transport"; + String TYPE = "type"; + String OPTIONS = "options"; + String VERSION = "version"; + String NAME = "name"; + String TIMEZONE = "timezone"; + String LOCATIONS = "locations"; + String LOCATION = "location"; + String OPENVPN_CONFIGURATION = "openvpn_configuration"; + String GATEWAYS = "gateways"; + String HOST = "host"; +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java b/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java new file mode 100644 index 00000000..4bb7e4ee --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java @@ -0,0 +1,48 @@ +package se.leap.bitmaskclient.base.models; + +import java.net.MalformedURLException; +import java.net.URL; + +public class DefaultedURL { + private URL DEFAULT_URL; + private String default_url = "https://example.net"; + + private URL url; + + DefaultedURL() { + try { + DEFAULT_URL = new URL(default_url); + url = DEFAULT_URL; + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + + public boolean isDefault() { return url.equals(DEFAULT_URL); } + + public void setUrl(URL url) { + this.url = url; + } + + public String getDomain() { + return url.getHost(); + } + + public URL getUrl() { + return url; + } + + @Override + public String toString() { + return url.toString(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof DefaultedURL) { + return url.equals(((DefaultedURL) o).getUrl()); + } + return false; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java new file mode 100644 index 00000000..7b3f1888 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java @@ -0,0 +1,6 @@ +package se.leap.bitmaskclient.base.models; + +public interface FeatureVersionCode { + int RENAMED_EIP_IN_PREFERENCES = 132; + int GEOIP_SERVICE = 148; +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java new file mode 100644 index 00000000..97f1019b --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java @@ -0,0 +1,593 @@ +/** + * 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.base.models; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.Gson; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Locale; + +import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4; +import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES; +import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGISTERED; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS; +import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT; +import static se.leap.bitmaskclient.base.models.Constants.TYPE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS; + +/** + * @author Sean Leonard + * @author Parménides GV + */ +public final class Provider implements Parcelable { + + private static long EIP_SERVICE_TIMEOUT = 1000 * 60 * 60 * 24 * 3; + private static long GEOIP_SERVICE_TIMEOUT = 1000 * 60 * 60; + private JSONObject definition = new JSONObject(); // Represents our Provider's provider.json + private JSONObject eipServiceJson = new JSONObject(); + private JSONObject geoIpJson = new JSONObject(); + private DefaultedURL mainUrl = new DefaultedURL(); + private DefaultedURL apiUrl = new DefaultedURL(); + private DefaultedURL geoipUrl = new DefaultedURL(); + private String providerIp = ""; + private String providerApiIp = ""; + private String certificatePin = ""; + private String certificatePinEncoding = ""; + private String caCert = ""; + private String apiVersion = ""; + private String privateKey = ""; + private String vpnCertificate = ""; + private long lastEipServiceUpdate = 0L; + private long lastGeoIpUpdate = 0L; + + private boolean allowAnonymous; + private boolean allowRegistered; + + final public static String + API_URL = "api_uri", + API_VERSION = "api_version", + ALLOW_REGISTRATION = "allow_registration", + API_RETURN_SERIAL = "serial", + SERVICE = "service", + KEY = "provider", + CA_CERT = "ca_cert", + CA_CERT_URI = "ca_cert_uri", + CA_CERT_FINGERPRINT = "ca_cert_fingerprint", + NAME = "name", + DESCRIPTION = "description", + DOMAIN = "domain", + MAIN_URL = "main_url", + PROVIDER_IP = "provider_ip", + PROVIDER_API_IP = "provider_api_ip", + GEOIP_URL = "geoip_url"; + + private static final String API_TERM_NAME = "name"; + + public Provider() { } + + public Provider(String mainUrl) { + this(mainUrl, null); + } + + public Provider(String mainUrl, String geoipUrl) { + try { + this.mainUrl.setUrl(new URL(mainUrl)); + } catch (MalformedURLException e) { + this.mainUrl = new DefaultedURL(); + } + setGeoipUrl(geoipUrl); + } + + public Provider(String mainUrl, String providerIp, String providerApiIp) { + this(mainUrl, null, providerIp, providerApiIp); + } + + public Provider(String mainUrl, String geoipUrl, String providerIp, String providerApiIp) { + try { + this.mainUrl.setUrl(new URL(mainUrl)); + if (providerIp != null) { + this.providerIp = providerIp; + } + if (providerApiIp != null) { + this.providerApiIp = providerApiIp; + } + } catch (MalformedURLException e) { + e.printStackTrace(); + return; + } + setGeoipUrl(geoipUrl); + } + + + public Provider(String mainUrl, String geoipUrl, String providerIp, String providerApiIp, String caCert, String definition) { + this(mainUrl, geoipUrl, providerIp, providerApiIp); + if (caCert != null) { + this.caCert = caCert; + } + if (definition != null) { + try { + define(new JSONObject(definition)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public Provider createFromParcel(Parcel in) { + return new Provider(in); + } + + public Provider[] newArray(int size) { + return new Provider[size]; + } + }; + + public boolean isConfigured() { + return !mainUrl.isDefault() && + !apiUrl.isDefault() && + hasCaCert() && + hasDefinition() && + hasVpnCertificate() && + hasEIP() && + hasPrivateKey(); + } + + public boolean supportsPluggableTransports() { + try { + JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS); + for (int i = 0; i < gatewayJsons.length(); i++) { + JSONArray transports = gatewayJsons.getJSONObject(i). + getJSONObject(CAPABILITIES). + getJSONArray(TRANSPORT); + for (int j = 0; j < transports.length(); j++) { + if (OBFS4.toString().equals(transports.getJSONObject(j).getString(TYPE))) { + return true; + } + } + } + } catch (Exception e) { + // ignore + } + return false; + } + + public String getIpForHostname(String host) { + if (host != null) { + if (host.equals(mainUrl.getUrl().getHost())) { + return providerIp; + } else if (host.equals(apiUrl.getUrl().getHost())) { + return providerApiIp; + } + } + return ""; + } + + public String getProviderApiIp() { + return this.providerApiIp; + } + + public void setProviderApiIp(String providerApiIp) { + if (providerApiIp == null) return; + this.providerApiIp = providerApiIp; + } + + public void setProviderIp(String providerIp) { + if (providerIp == null) return; + this.providerIp = providerIp; + } + + public String getProviderIp() { + return this.providerIp; + } + + public void setMainUrl(URL url) { + mainUrl.setUrl(url); + } + + public void setMainUrl(String url) { + try { + mainUrl.setUrl(new URL(url)); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + + public boolean define(JSONObject providerJson) { + definition = providerJson; + return parseDefinition(definition); + } + + public JSONObject getDefinition() { + return definition; + } + + public String getDefinitionString() { + return getDefinition().toString(); + } + + public String getDomain() { + return mainUrl.getDomain(); + } + + public String getMainUrlString() { + return getMainUrl().toString(); + } + + public DefaultedURL getMainUrl() { + return mainUrl; + } + + protected DefaultedURL getApiUrl() { + return apiUrl; + } + + public DefaultedURL getGeoipUrl() { + return geoipUrl; + } + + public void setGeoipUrl(String url) { + try { + this.geoipUrl.setUrl(new URL(url)); + } catch (MalformedURLException e) { + this.geoipUrl = new DefaultedURL(); + } + } + + public String getApiUrlWithVersion() { + return getApiUrlString() + "/" + getApiVersion(); + } + + + public String getApiUrlString() { + return getApiUrl().toString(); + } + + public String getApiVersion() { + return apiVersion; + } + + public boolean hasCaCert() { + return caCert != null && !caCert.isEmpty(); + } + + public boolean hasDefinition() { + return definition != null && definition.length() > 0; + } + + public boolean hasGeoIpJson() { + return geoIpJson != null && geoIpJson.length() > 0; + } + + + public String getCaCert() { + return caCert; + } + + public String getName() { + // Should we pass the locale in, or query the system here? + String lang = Locale.getDefault().getLanguage(); + String name = ""; + try { + if (definition != null) + name = definition.getJSONObject(API_TERM_NAME).getString(lang); + else throw new JSONException("Provider not defined"); + } catch (JSONException e) { + try { + name = definition.getJSONObject(API_TERM_NAME).getString("en"); + } catch (JSONException e2) { + if (mainUrl != null) { + String host = mainUrl.getDomain(); + name = host.substring(0, host.indexOf(".")); + } + } + } + + return name; + } + + public String getDescription() { + String lang = Locale.getDefault().getLanguage(); + String desc = null; + try { + desc = definition.getJSONObject("description").getString(lang); + } catch (JSONException e) { + // TODO: handle exception!! + try { + desc = definition.getJSONObject("description").getString(definition.getString("default_language")); + } catch (JSONException e2) { + // TODO: i can't believe you're doing it again! + } + } + + return desc; + } + + public boolean hasEIP() { + return getEipServiceJson() != null && getEipServiceJson().length() > 0 + && !getEipServiceJson().has(ERRORS); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(getMainUrlString()); + parcel.writeString(getProviderIp()); + parcel.writeString(getProviderApiIp()); + parcel.writeString(getGeoipUrl().toString()); + parcel.writeString(getDefinitionString()); + parcel.writeString(getCaCert()); + parcel.writeString(getEipServiceJsonString()); + parcel.writeString(getGeoIpJsonString()); + parcel.writeString(getPrivateKey()); + parcel.writeString(getVpnCertificate()); + parcel.writeLong(lastEipServiceUpdate); + parcel.writeLong(lastGeoIpUpdate); + } + + + //TODO: write a test for marshalling! + private Provider(Parcel in) { + try { + mainUrl.setUrl(new URL(in.readString())); + String tmpString = in.readString(); + if (!tmpString.isEmpty()) { + providerIp = tmpString; + } + tmpString = in.readString(); + if (!tmpString.isEmpty()) { + providerApiIp = tmpString; + } + tmpString = in.readString(); + if (!tmpString.isEmpty()) { + geoipUrl.setUrl(new URL(tmpString)); + } + tmpString = in.readString(); + if (!tmpString.isEmpty()) { + definition = new JSONObject((tmpString)); + parseDefinition(definition); + } + tmpString = in.readString(); + if (!tmpString.isEmpty()) { + this.caCert = tmpString; + } + tmpString = in.readString(); + if (!tmpString.isEmpty()) { + this.setEipServiceJson(new JSONObject(tmpString)); + } + tmpString = in.readString(); + if (!tmpString.isEmpty()) { + this.setGeoIpJson(new JSONObject(tmpString)); + } + tmpString = in.readString(); + if (!tmpString.isEmpty()) { + this.setPrivateKey(tmpString); + } + tmpString = in.readString(); + if (!tmpString.isEmpty()) { + this.setVpnCertificate(tmpString); + } + this.lastEipServiceUpdate = in.readLong(); + this.lastGeoIpUpdate = in.readLong(); + } catch (MalformedURLException | JSONException e) { + e.printStackTrace(); + } + } + + + @Override + public boolean equals(Object o) { + if (o instanceof Provider) { + Provider p = (Provider) o; + return p.getDomain().equals(getDomain()) && + definition.toString().equals(p.getDefinition().toString()) && + eipServiceJson.toString().equals(p.getEipServiceJsonString()) && + geoIpJson.toString().equals(p.getGeoIpJsonString()) && + providerIp.equals(p.getProviderIp()) && + providerApiIp.equals(p.getProviderApiIp()) && + apiUrl.equals(p.getApiUrl()) && + geoipUrl.equals(p.getGeoipUrl()) && + certificatePin.equals(p.getCertificatePin()) && + certificatePinEncoding.equals(p.getCertificatePinEncoding()) && + caCert.equals(p.getCaCert()) && + apiVersion.equals(p.getApiVersion()) && + privateKey.equals(p.getPrivateKey()) && + vpnCertificate.equals(p.getVpnCertificate()) && + allowAnonymous == p.allowsAnonymous() && + allowRegistered == p.allowsRegistered(); + } else return false; + } + + + public JSONObject toJson() { + JSONObject json = new JSONObject(); + try { + json.put(Provider.MAIN_URL, mainUrl); + } catch (JSONException e) { + e.printStackTrace(); + } + return json; + } + + @Override + public int hashCode() { + return getDomain().hashCode(); + } + + @Override + public String toString() { + return new Gson().toJson(this); + } + + private boolean parseDefinition(JSONObject definition) { + try { + String pin = definition.getString(CA_CERT_FINGERPRINT); + this.certificatePin = pin.split(":")[1].trim(); + this.certificatePinEncoding = pin.split(":")[0].trim(); + this.apiUrl.setUrl(new URL(definition.getString(API_URL))); + this.allowAnonymous = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOW_ANONYMOUS); + this.allowRegistered = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOWED_REGISTERED); + this.apiVersion = getDefinition().getString(Provider.API_VERSION); + return true; + } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) { + return false; + } + } + + public void setCaCert(String cert) { + this.caCert = cert; + } + + public boolean allowsAnonymous() { + return allowAnonymous; + } + + public boolean allowsRegistered() { + return allowRegistered; + } + + public void setLastEipServiceUpdate(long timestamp) { + lastEipServiceUpdate = timestamp; + } + + public boolean shouldUpdateEipServiceJson() { + return System.currentTimeMillis() - lastEipServiceUpdate >= EIP_SERVICE_TIMEOUT; + } + + + public void setLastGeoIpUpdate(long timestamp) { + lastGeoIpUpdate = timestamp; + } + + public boolean shouldUpdateGeoIpJson() { + return System.currentTimeMillis() - lastGeoIpUpdate >= GEOIP_SERVICE_TIMEOUT; + } + + + public boolean setEipServiceJson(JSONObject eipServiceJson) { + if (eipServiceJson.has(ERRORS)) { + return false; + } + this.eipServiceJson = eipServiceJson; + return true; + } + + public boolean setGeoIpJson(JSONObject geoIpJson) { + if (geoIpJson.has(ERRORS)) { + return false; + } + this.geoIpJson = geoIpJson; + return true; + } + + public JSONObject getEipServiceJson() { + return eipServiceJson; + } + + public JSONObject getGeoIpJson() { + return geoIpJson; + } + + public String getGeoIpJsonString() { + return geoIpJson.toString(); + } + + public String getEipServiceJsonString() { + return getEipServiceJson().toString(); + } + + public boolean isDefault() { + return getMainUrl().isDefault() && + getApiUrl().isDefault() && + getGeoipUrl().isDefault() && + certificatePin.isEmpty() && + certificatePinEncoding.isEmpty() && + caCert.isEmpty(); + } + + public String getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public boolean hasPrivateKey() { + return privateKey != null && privateKey.length() > 0; + } + + public String getVpnCertificate() { + return vpnCertificate; + } + + public void setVpnCertificate(String vpnCertificate) { + this.vpnCertificate = vpnCertificate; + } + + public boolean hasVpnCertificate() { + return getVpnCertificate() != null && getVpnCertificate().length() >0 ; + } + + public String getCertificatePin() { + return certificatePin; + } + + public String getCertificatePinEncoding() { + return certificatePinEncoding; + } + + public String getCaCertFingerprint() { + return getCertificatePinEncoding() + ":" + getCertificatePin(); + } + + /** + * resets everything except the main url, the providerIp and the geoip + * service url (currently preseeded) + */ + public void reset() { + definition = new JSONObject(); + eipServiceJson = new JSONObject(); + geoIpJson = new JSONObject(); + apiUrl = new DefaultedURL(); + certificatePin = ""; + certificatePinEncoding = ""; + caCert = ""; + apiVersion = ""; + privateKey = ""; + vpnCertificate = ""; + allowRegistered = false; + allowAnonymous = false; + lastGeoIpUpdate = 0L; + lastEipServiceUpdate = 0L; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/ProviderObservable.java b/app/src/main/java/se/leap/bitmaskclient/base/models/ProviderObservable.java new file mode 100644 index 00000000..19555504 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/ProviderObservable.java @@ -0,0 +1,39 @@ +package se.leap.bitmaskclient.base.models; + +import java.util.Observable; + +/** + * Created by cyberta on 05.12.18. + */ +public class ProviderObservable extends Observable { + private static ProviderObservable instance; + private Provider currentProvider; + private Provider providerForDns; + + public static ProviderObservable getInstance() { + if (instance == null) { + instance = new ProviderObservable(); + } + return instance; + } + + public synchronized void updateProvider(Provider provider) { + instance.currentProvider = provider; + instance.providerForDns = null; + instance.setChanged(); + instance.notifyObservers(); + } + + public Provider getCurrentProvider() { + return instance.currentProvider; + } + + public void setProviderForDns(Provider provider) { + this.providerForDns = provider; + } + + public Provider getProviderForDns() { + return instance.providerForDns; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java new file mode 100644 index 00000000..affceacf --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/Cmd.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2019 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.base.utils; + +import androidx.annotation.WorkerThread; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; + +public class Cmd { + + private static final String TAG = Cmd.class.getSimpleName(); + + @WorkerThread + public static int runBlockingCmd(String[] cmds, StringBuilder log) throws Exception { + return runCmd(cmds, log, true); + } + + @WorkerThread + private static int runCmd(String[] cmds, StringBuilder log, + boolean waitFor) throws Exception { + + int exitCode = -1; + Process proc = Runtime.getRuntime().exec("sh"); + OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream()); + + try { + for (String cmd : cmds) { + out.write(cmd); + out.write("\n"); + } + + out.flush(); + out.write("exit\n"); + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + out.close(); + } + + if (waitFor) { + // Consume the "stdout" + InputStreamReader reader = new InputStreamReader(proc.getInputStream()); + readToLogString(reader, log); + + // Consume the "stderr" + reader = new InputStreamReader(proc.getErrorStream()); + readToLogString(reader, log); + + try { + exitCode = proc.waitFor(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + return exitCode; + } + + private static void readToLogString(InputStreamReader reader, StringBuilder log) throws IOException { + final char buf[] = new char[10]; + int read = 0; + try { + while ((read = reader.read(buf)) != -1) { + if (log != null) + log.append(buf, 0, read); + } + } catch (IOException e) { + reader.close(); + throw new IOException(e); + } + reader.close(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java new file mode 100644 index 00000000..4248072a --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java @@ -0,0 +1,230 @@ +/** + * 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.base.utils; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import org.json.JSONException; +import org.json.JSONObject; +import org.spongycastle.util.encoders.Base64; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Calendar; + +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.providersetup.ProviderAPI; +import se.leap.bitmaskclient.R; + +import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_BITMASK; + +/** + * Stores constants, and implements auxiliary methods used across all Bitmask Android classes. + * Wraps BuildConfigFields for to support easier unit testing + * + * @author parmegv + * @author MeanderingCode + */ +public class ConfigHelper { + final public static String NG_1024 = + "eeaf0ab9adb38dd69c33f80afa8fc5e86072618775ff3c0b9ea2314c9c256576d674df7496ea81d3383b4813d692c6e0e0d5d8e250b98be48e495c1d6089dad15dc7d7b46154d6b6ce8ef4ad69b15d4982559b297bcf1885c529f566660e57ec68edbc3c05726cc02fd4cbf4976eaa9afd5138fe8376435b9fc61d2fc0eb06e3"; + final public static BigInteger G = new BigInteger("2"); + + public static boolean checkErroneousDownload(String downloadedString) { + try { + if (downloadedString == null || downloadedString.isEmpty() || new JSONObject(downloadedString).has(ProviderAPI.ERRORS) || new JSONObject(downloadedString).has(ProviderAPI.BACKEND_ERROR_KEY)) { + return true; + } else { + return false; + } + } catch (NullPointerException | JSONException e) { + return false; + } + } + + /** + * Treat the input as the MSB representation of a number, + * and lop off leading zero elements. For efficiency, the + * input is simply returned if no leading zeroes are found. + * + * @param in array to be trimmed + */ + public static byte[] trim(byte[] in) { + if (in.length == 0 || in[0] != 0) + return in; + + int len = in.length; + int i = 1; + while (in[i] == 0 && i < len) + ++i; + byte[] ret = new byte[len - i]; + System.arraycopy(in, i, ret, 0, len - i); + return ret; + } + + public static X509Certificate parseX509CertificateFromString(String certificateString) { + java.security.cert.Certificate certificate = null; + CertificateFactory cf; + try { + cf = CertificateFactory.getInstance("X.509"); + + certificateString = certificateString.replaceFirst("-----BEGIN CERTIFICATE-----", "").replaceFirst("-----END CERTIFICATE-----", "").trim(); + byte[] cert_bytes = Base64.decode(certificateString); + InputStream caInput = new ByteArrayInputStream(cert_bytes); + try { + certificate = cf.generateCertificate(caInput); + System.out.println("ca=" + ((X509Certificate) certificate).getSubjectDN()); + } finally { + caInput.close(); + } + } catch (NullPointerException | CertificateException | IOException | IllegalArgumentException e) { + return null; + } + return (X509Certificate) certificate; + } + + public static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) { + RSAPrivateKey key; + try { + KeyFactory kf; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + kf = KeyFactory.getInstance("RSA", "BC"); + } else { + kf = KeyFactory.getInstance("RSA"); + } + rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", ""); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString)); + key = (RSAPrivateKey) kf.generatePrivate(keySpec); + } catch (InvalidKeySpecException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } catch (NullPointerException e) { + e.printStackTrace(); + return null; + } catch (NoSuchProviderException e) { + e.printStackTrace(); + return null; + } + + return key; + } + + private static String byteArrayToHex(byte[] input) { + int readBytes = input.length; + StringBuffer hexData = new StringBuffer(); + int onebyte; + for (int i = 0; i < readBytes; i++) { + onebyte = ((0x000000ff & input[i]) | 0xffffff00); + hexData.append(Integer.toHexString(onebyte).substring(6)); + } + return hexData.toString(); + } + + /** + * Calculates the hexadecimal representation of a sha256/sha1 fingerprint of a certificate + * + * @param certificate + * @param encoding + * @return + * @throws NoSuchAlgorithmException + * @throws CertificateEncodingException + */ + @NonNull + public static String getFingerprintFromCertificate(X509Certificate certificate, String encoding) throws NoSuchAlgorithmException, CertificateEncodingException /*, UnsupportedEncodingException*/ { + byte[] byteArray = MessageDigest.getInstance(encoding).digest(certificate.getEncoded()); + return byteArrayToHex(byteArray); + } + + public static void ensureNotOnMainThread(@NonNull Context context) throws IllegalStateException{ + Looper looper = Looper.myLooper(); + if (looper != null && looper == context.getMainLooper()) { + throw new IllegalStateException( + "calling this from your main thread can lead to deadlock"); + } + } + + public static boolean isDefaultBitmask() { + return BuildConfig.FLAVOR_branding.equals(DEFAULT_BITMASK); + } + + public static boolean preferAnonymousUsage() { + return BuildConfig.priotize_anonymous_usage; + } + + public static int getCurrentTimezone() { + return Calendar.getInstance().get(Calendar.ZONE_OFFSET) / 3600000; + } + + public static String getProviderFormattedString(Resources resources, @StringRes int resourceId) { + String appName = resources.getString(R.string.app_name); + return resources.getString(resourceId, appName); + } + + public static boolean stringEqual(@Nullable String string1, @Nullable String string2) { + return (string1 == null && string2 == null) || + (string1 != null && string1.equals(string2)); + } + + public static String getApkFileName() { + try { + return BuildConfig.update_apk_url.substring(BuildConfig.update_apk_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + + public static String getVersionFileName() { + try { + return BuildConfig.version_file_url.substring(BuildConfig.version_file_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + + public static String getSignatureFileName() { + try { + return BuildConfig.signature_url.substring(BuildConfig.signature_url.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java new file mode 100644 index 00000000..0476bf12 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/DateHelper.java @@ -0,0 +1,29 @@ +package se.leap.bitmaskclient.base.utils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Contains helper methods related to date manipulation. + * + * @author Janak + */ +public class DateHelper { + private static final String DATE_PATTERN = "dd/MM/yyyy"; + private static final int ONE_DAY = 86400000; //1000*60*60*24 + + public static long getDateDiffToCurrentDateInDays(String startDate) throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN, Locale.US); + Date lastDate = sdf.parse(startDate); + Date currentDate = new Date(); + return (currentDate.getTime() - lastDate.getTime()) / ONE_DAY; + } + + public static String getCurrentDateString() { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN, Locale.US); + Date lastDate = new Date(); + return sdf.format(lastDate); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java new file mode 100644 index 00000000..eb1c255c --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java @@ -0,0 +1,46 @@ +package se.leap.bitmaskclient.base.utils; + +import android.content.Context; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Created by cyberta on 18.03.18. + */ + +public class FileHelper { + public static File createFile(File dir, String fileName) { + return new File(dir, fileName); + } + + public static void persistFile(File file, String content) throws IOException { + FileWriter writer = new FileWriter(file); + writer.write(content); + writer.close(); + } + + public static String readPublicKey(Context context) { + { + InputStream inputStream; + try { + inputStream = context.getAssets().open("public.pgp"); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } catch (IOException errabi) { + return null; + } + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java new file mode 100644 index 00000000..377617a4 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/IPAddress.java @@ -0,0 +1,102 @@ +package se.leap.bitmaskclient.base.utils; + +/* + * Copyright (C) 2006-2008 Alfresco Software Limited. + * + * 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. + + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +import java.util.StringTokenizer; + +/** + * TCP/IP Address Utility Class + * + * @author gkspencer + */ +public class IPAddress { + + + /** + * Convert a TCP/IP address string into a byte array + * + * @param addr String + * @return byte[] + */ + public static byte[] asBytes(String addr) { + + // Convert the TCP/IP address string to an integer value + int ipInt = parseNumericAddress(addr); + if (ipInt == 0) + return null; + + // Convert to bytes + byte[] ipByts = new byte[4]; + + ipByts[3] = (byte) (ipInt & 0xFF); + ipByts[2] = (byte) ((ipInt >> 8) & 0xFF); + ipByts[1] = (byte) ((ipInt >> 16) & 0xFF); + ipByts[0] = (byte) ((ipInt >> 24) & 0xFF); + + // Return the TCP/IP bytes + return ipByts; + } + /** + * Check if the specified address is a valid numeric TCP/IP address and return as an integer value + * + * @param ipaddr String + * @return int + */ + private static int parseNumericAddress(String ipaddr) { + + // Check if the string is valid + if (ipaddr == null || ipaddr.length() < 7 || ipaddr.length() > 15) + return 0; + + // Check the address string, should be n.n.n.n format + StringTokenizer token = new StringTokenizer(ipaddr,"."); + if (token.countTokens() != 4) + return 0; + + int ipInt = 0; + while (token.hasMoreTokens()) { + + // Get the current token and convert to an integer value + String ipNum = token.nextToken(); + + try { + // Validate the current address part + int ipVal = Integer.valueOf(ipNum).intValue(); + if (ipVal < 0 || ipVal > 255) + return 0; + + // Add to the integer address + ipInt = (ipInt << 8) + ipVal; + } + catch (NumberFormatException ex) { + return 0; + } + } + + // Return the integer address + return ipInt; + } +} \ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java new file mode 100644 index 00000000..77189dff --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/InputStreamHelper.java @@ -0,0 +1,21 @@ +package se.leap.bitmaskclient.base.utils; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +/** + * Created by cyberta on 18.03.18. + */ + +public class InputStreamHelper { + //allows us to mock FileInputStream + public static InputStream getInputStreamFrom(String filePath) throws FileNotFoundException { + return new FileInputStream(filePath); + } + + public static String loadInputStreamAsString(InputStream is) { + java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java new file mode 100644 index 00000000..b0b28993 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/KeyStoreHelper.java @@ -0,0 +1,78 @@ +package se.leap.bitmaskclient.base.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * Created by cyberta on 18.03.18. + */ + +public class KeyStoreHelper { + private static KeyStore trustedKeystore; + + /** + * Adds a new X509 certificate given its input stream and its provider name + * + * @param provider used to store the certificate in the keystore + * @param inputStream from which X509 certificate must be generated. + */ + public static void addTrustedCertificate(String provider, InputStream inputStream) { + CertificateFactory cf; + try { + cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = + (X509Certificate) cf.generateCertificate(inputStream); + trustedKeystore.setCertificateEntry(provider, cert); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Adds a new X509 certificate given in its string from and using its provider name + * + * @param provider used to store the certificate in the keystore + * @param certificate + */ + public static void addTrustedCertificate(String provider, String certificate) { + + try { + X509Certificate cert = ConfigHelper.parseX509CertificateFromString(certificate); + if (trustedKeystore == null) { + trustedKeystore = KeyStore.getInstance("BKS"); + trustedKeystore.load(null); + } + trustedKeystore.setCertificateEntry(provider, cert); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * @return class wide keystore + */ + public static KeyStore getKeystore() { + return trustedKeystore; + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java new file mode 100644 index 00000000..41b8cf35 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PRNGFixes.java @@ -0,0 +1,330 @@ +package se.leap.bitmaskclient.base.utils; + +/* + * This software is provided 'as-is', without any express or implied + * warranty. In no event will Google be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, as long as the origin is not misrepresented. + * + * Source: http://android-developers.blogspot.de/2013/08/some-securerandom-thoughts.html + */ + +import android.os.*; +import android.os.Process; +import android.util.*; + +import java.io.*; +import java.security.*; +import java.security.Provider; + +/** + * Fixes for the output of the default PRNG having low entropy. + *

+ * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** + * Hidden constructor to prevent instantiation. + */ + private PRNGFixes() { + } + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java new file mode 100644 index 00000000..d31c7a20 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java @@ -0,0 +1,273 @@ +package se.leap.bitmaskclient.base.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +import de.blinkt.openvpn.VpnProfile; +import se.leap.bitmaskclient.base.models.Provider; + +import static android.content.Context.MODE_PRIVATE; +import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_BLUETOOTH; +import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_USB; +import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_WIFI; +import static se.leap.bitmaskclient.base.models.Constants.ALWAYS_ON_SHOW_DIALOG; +import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER; +import static se.leap.bitmaskclient.base.models.Constants.EXCLUDED_APPS; +import static se.leap.bitmaskclient.base.models.Constants.LAST_UPDATE_CHECK; +import static se.leap.bitmaskclient.base.models.Constants.LAST_USED_PROFILE; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_CONFIGURED; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.base.models.Constants.RESTART_ON_UPDATE; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.models.Constants.SHOW_EXPERIMENTAL; +import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL; +import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS; + +/** + * Created by cyberta on 18.03.18. + */ + +public class PreferenceHelper { + + public static Provider getSavedProviderFromSharedPreferences(@NonNull SharedPreferences preferences) { + Provider provider = new Provider(); + try { + provider.setMainUrl(new URL(preferences.getString(Provider.MAIN_URL, ""))); + provider.setProviderIp(preferences.getString(Provider.PROVIDER_IP, "")); + provider.setProviderApiIp(preferences.getString(Provider.PROVIDER_API_IP, "")); + provider.setGeoipUrl(preferences.getString(Provider.GEOIP_URL, "")); + provider.define(new JSONObject(preferences.getString(Provider.KEY, ""))); + provider.setCaCert(preferences.getString(Provider.CA_CERT, "")); + provider.setVpnCertificate(preferences.getString(PROVIDER_VPN_CERTIFICATE, "")); + provider.setPrivateKey(preferences.getString(PROVIDER_PRIVATE_KEY, "")); + provider.setEipServiceJson(new JSONObject(preferences.getString(PROVIDER_EIP_DEFINITION, ""))); + } catch (MalformedURLException | JSONException e) { + e.printStackTrace(); + } + + return provider; + } + + public static String getFromPersistedProvider(String toFetch, String providerDomain, SharedPreferences preferences) { + return preferences.getString(toFetch + "." + providerDomain, ""); + } + + // TODO: replace commit with apply after refactoring EIP + //FIXME: don't save private keys in shared preferences! use the keystore + public static void storeProviderInPreferences(SharedPreferences preferences, Provider provider) { + preferences.edit().putBoolean(PROVIDER_CONFIGURED, true). + putString(Provider.PROVIDER_IP, provider.getProviderIp()). + putString(Provider.GEOIP_URL, provider.getGeoipUrl().toString()). + putString(Provider.PROVIDER_API_IP, provider.getProviderApiIp()). + putString(Provider.MAIN_URL, provider.getMainUrlString()). + putString(Provider.KEY, provider.getDefinitionString()). + putString(Provider.CA_CERT, provider.getCaCert()). + putString(PROVIDER_EIP_DEFINITION, provider.getEipServiceJsonString()). + putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKey()). + putString(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate()). + commit(); + + String providerDomain = provider.getDomain(); + preferences.edit().putBoolean(PROVIDER_CONFIGURED, true). + putString(Provider.PROVIDER_IP + "." + providerDomain, provider.getProviderIp()). + putString(Provider.PROVIDER_API_IP + "." + providerDomain, provider.getProviderApiIp()). + putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString()). + putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl().toString()). + putString(Provider.KEY + "." + providerDomain, provider.getDefinitionString()). + putString(Provider.CA_CERT + "." + providerDomain, provider.getCaCert()). + putString(PROVIDER_EIP_DEFINITION + "." + providerDomain, provider.getEipServiceJsonString()). + apply(); + } + + /** + * Sets the profile that is connected (to connect if the service restarts) + */ + public static void setLastUsedVpnProfile(Context context, VpnProfile connectedProfile) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + SharedPreferences.Editor prefsedit = prefs.edit(); + prefsedit.putString(LAST_USED_PROFILE, connectedProfile.toJson()); + prefsedit.apply(); + } + + /** + * Returns the profile that was last connected (to connect if the service restarts) + */ + public static VpnProfile getLastConnectedVpnProfile(Context context) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + String lastConnectedProfileJson = preferences.getString(LAST_USED_PROFILE, null); + return VpnProfile.fromJson(lastConnectedProfileJson); + } + + public static void deleteProviderDetailsFromPreferences(@NonNull SharedPreferences preferences, String providerDomain) { + preferences.edit(). + remove(Provider.KEY + "." + providerDomain). + remove(Provider.CA_CERT + "." + providerDomain). + remove(Provider.PROVIDER_IP + "." + providerDomain). + remove(Provider.PROVIDER_API_IP + "." + providerDomain). + remove(Provider.MAIN_URL + "." + providerDomain). + remove(Provider.GEOIP_URL + "." + providerDomain). + remove(PROVIDER_EIP_DEFINITION + "." + providerDomain). + remove(PROVIDER_PRIVATE_KEY + "." + providerDomain). + remove(PROVIDER_VPN_CERTIFICATE + "." + providerDomain). + apply(); + } + + public static void setLastAppUpdateCheck(Context context) { + putLong(context, LAST_UPDATE_CHECK, System.currentTimeMillis()); + } + + public static long getLastAppUpdateCheck(Context context) { + return getLong(context, LAST_UPDATE_CHECK, 0); + } + + public static void restartOnUpdate(Context context, boolean isEnabled) { + putBoolean(context, RESTART_ON_UPDATE, isEnabled); + } + + public static boolean getRestartOnUpdate(Context context) { + return getBoolean(context, RESTART_ON_UPDATE, false); + } + + public static boolean getUsePluggableTransports(Context context) { + return getBoolean(context, USE_PLUGGABLE_TRANSPORTS, false); + } + + public static void usePluggableTransports(Context context, boolean isEnabled) { + putBoolean(context, USE_PLUGGABLE_TRANSPORTS, isEnabled); + } + + public static void saveBattery(Context context, boolean isEnabled) { + putBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, isEnabled); + } + + public static boolean getSaveBattery(Context context) { + return getBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, false); + } + + public static void allowUsbTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_USB, isEnabled); + } + + public static boolean isUsbTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_USB, false); + } + + public static void allowWifiTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_WIFI, isEnabled); + } + + public static boolean isWifiTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_WIFI, false); + } + + public static void allowBluetoothTethering(Context context, boolean isEnabled) { + putBoolean(context, ALLOW_TETHERING_BLUETOOTH, isEnabled); + } + + public static boolean isBluetoothTetheringAllowed(Context context) { + return getBoolean(context, ALLOW_TETHERING_BLUETOOTH, false); + } + + public static void setShowExperimentalFeatures(Context context, boolean show) { + putBoolean(context, SHOW_EXPERIMENTAL, show); + } + + public static boolean showExperimentalFeatures(Context context) { + return getBoolean(context, SHOW_EXPERIMENTAL, false); + } + + public static void setUseIPv6Firewall(Context context, boolean useFirewall) { + putBoolean(context, USE_IPv6_FIREWALL, useFirewall); + } + + public static boolean useIpv6Firewall(Context context) { + return getBoolean(context, USE_IPv6_FIREWALL, false); + } + + public static void saveShowAlwaysOnDialog(Context context, boolean showAlwaysOnDialog) { + putBoolean(context, ALWAYS_ON_SHOW_DIALOG, showAlwaysOnDialog); + } + + public static boolean getShowAlwaysOnDialog(Context context) { + return getBoolean(context, ALWAYS_ON_SHOW_DIALOG, true); + } + + public static JSONObject getEipDefinitionFromPreferences(SharedPreferences preferences) { + JSONObject result = new JSONObject(); + try { + String eipDefinitionString = preferences.getString(PROVIDER_EIP_DEFINITION, ""); + if (!eipDefinitionString.isEmpty()) { + result = new JSONObject(eipDefinitionString); + } + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return result; + } + + public static void setExcludedApps(Context context, Set apps) { + SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + SharedPreferences.Editor prefsedit = prefs.edit(); + prefsedit.putStringSet(EXCLUDED_APPS, apps); + prefsedit.apply(); + } + + public static Set getExcludedApps(Context context) { + if (context == null) { + return null; + } + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + return preferences.getStringSet(EXCLUDED_APPS, new HashSet<>()); + } + + public static long getLong(Context context, String key, long defValue) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + return preferences.getLong(key, defValue); + } + + public static void putLong(Context context, String key, long value) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putLong(key, value).apply(); + } + + public static String getString(Context context, String key, String defValue) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + return preferences.getString(key, defValue); + } + + public static void putString(Context context, String key, String value) { + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putString(key, value).apply(); + } + + public static Boolean getBoolean(Context context, String key, Boolean defValue) { + if (context == null) { + return false; + } + + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + return preferences.getBoolean(key, defValue); + } + + public static void putBoolean(Context context, String key, Boolean value) { + if (context == null) { + return; + } + + SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().putBoolean(key, value).apply(); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java new file mode 100644 index 00000000..23ca40e5 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ViewHelper.java @@ -0,0 +1,17 @@ +package se.leap.bitmaskclient.base.utils; + +import android.content.Context; + +import androidx.annotation.DimenRes; + +/** + * Created by cyberta on 29.06.18. + */ + +public class ViewHelper { + + public static int convertDimensionToPx(Context context, @DimenRes int dimension) { + return context.getResources().getDimensionPixelSize(dimension); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconCheckboxEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconCheckboxEntry.java new file mode 100644 index 00000000..fdbd7dbd --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconCheckboxEntry.java @@ -0,0 +1,86 @@ +package se.leap.bitmaskclient.base.views; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.drawable.Drawable; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.appcompat.widget.AppCompatImageView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.fragments.TetheringDialog; + + +public class IconCheckboxEntry extends LinearLayout { + + @InjectView(android.R.id.text1) + TextView textView; + + @InjectView(R.id.material_icon) + AppCompatImageView iconView; + + @InjectView(R.id.checked_icon) + AppCompatImageView checkedIcon; + + public IconCheckboxEntry(Context context) { + super(context); + initLayout(context, null); + } + + public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initLayout(context, attrs); + } + + public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initLayout(context, attrs); + } + + @TargetApi(21) + public IconCheckboxEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initLayout(context, attrs); + } + + void initLayout(Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rootview = inflater.inflate(R.layout.v_icon_select_text_list_item, this, true); + ButterKnife.inject(this, rootview); + + + + } + + public void bind(TetheringDialog.DialogListAdapter.ViewModel model) { + this.setEnabled(model.enabled); + textView.setText(model.text); + textView.setEnabled(model.enabled); + + Drawable checkIcon = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_check_bold)).mutate(); + if (model.enabled) { + DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorSuccess)); + } else { + DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorDisabled)); + } + + iconView.setImageDrawable(model.image); + checkedIcon.setImageDrawable(checkIcon); + setChecked(model.checked); + } + + public void setChecked(boolean checked) { + checkedIcon.setVisibility(checked ? VISIBLE : GONE); + checkedIcon.setContentDescription(checked ? "selected" : "unselected"); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java new file mode 100644 index 00000000..1160986e --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconSwitchEntry.java @@ -0,0 +1,116 @@ +package se.leap.bitmaskclient.base.views; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.SwitchCompat; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import se.leap.bitmaskclient.R; + +public class IconSwitchEntry extends LinearLayout { + + private TextView textView; + private TextView subtitleView; + private AppCompatImageView iconView; + private SwitchCompat switchView; + private CompoundButton.OnCheckedChangeListener checkedChangeListener; + + public IconSwitchEntry(Context context) { + super(context); + initLayout(context, null); + } + + public IconSwitchEntry(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initLayout(context, attrs); + } + + public IconSwitchEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initLayout(context, attrs); + } + + @TargetApi(21) + public IconSwitchEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initLayout(context, attrs); + } + + void initLayout(Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rootview = inflater.inflate(R.layout.v_switch_list_item, this, true); + textView = rootview.findViewById(android.R.id.text1); + subtitleView = rootview.findViewById(R.id.subtitle); + iconView = rootview.findViewById(R.id.material_icon); + switchView = rootview.findViewById(R.id.option_switch); + + if (attrs != null) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconSwitchEntry); + + String entryText = typedArray.getString(R.styleable.IconTextEntry_text); + if (entryText != null) { + textView.setText(entryText); + } + + String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle); + if (subtitle != null) { + subtitleView.setText(subtitle); + subtitleView.setVisibility(VISIBLE); + } + + Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon); + if (drawable != null) { + iconView.setImageDrawable(drawable); + } + + typedArray.recycle(); + } + } + + public void setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener listener) { + checkedChangeListener = listener; + switchView.setOnCheckedChangeListener(checkedChangeListener); + } + + public void setText(@StringRes int id) { + textView.setText(id); + } + + public void showSubtitle(boolean show) { + subtitleView.setVisibility(show ? VISIBLE : GONE); + } + + public void setIcon(@DrawableRes int id) { + iconView.setImageResource(id); + } + + public void setChecked(boolean isChecked) { + switchView.setChecked(isChecked); + } + + public void setCheckedQuietly(boolean isChecked) { + switchView.setOnCheckedChangeListener(null); + switchView.setChecked(isChecked); + switchView.setOnCheckedChangeListener(checkedChangeListener); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + switchView.setVisibility(enabled ? VISIBLE : GONE); + textView.setTextColor(getResources().getColor(enabled ? android.R.color.black : R.color.colorDisabled)); + iconView.setImageAlpha(enabled ? 255 : 128); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java new file mode 100644 index 00000000..6b9bd760 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java @@ -0,0 +1,106 @@ +package se.leap.bitmaskclient.base.views; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import se.leap.bitmaskclient.R; + + +public class IconTextEntry extends LinearLayout { + + private TextView textView; + private ImageView iconView; + private TextView subtitleView; + + public IconTextEntry(Context context) { + super(context); + initLayout(context, null); + } + + public IconTextEntry(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initLayout(context, attrs); + } + + public IconTextEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initLayout(context, attrs); + } + + @TargetApi(21) + public IconTextEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initLayout(context, attrs); + } + + void initLayout(Context context, AttributeSet attrs) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rootview = inflater.inflate(R.layout.v_icon_text_list_item, this, true); + textView = rootview.findViewById(android.R.id.text1); + subtitleView = rootview.findViewById(R.id.subtitle); + iconView = rootview.findViewById(R.id.material_icon); + + if (attrs != null) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconTextEntry); + + String entryText = typedArray.getString(R.styleable.IconTextEntry_text); + if (entryText != null) { + textView.setText(entryText); + } + + String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle); + if (subtitle != null) { + subtitleView.setText(subtitle); + subtitleView.setVisibility(VISIBLE); + } + + Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon); + if (drawable != null) { + iconView.setImageDrawable(drawable); + } + + typedArray.recycle(); + } + + + } + + public void setText(@StringRes int id) { + textView.setText(id); + } + + public void setSubtitle(String text) { + subtitleView.setText(text); + subtitleView.setVisibility(VISIBLE); + } + + public void hideSubtitle() { + subtitleView.setVisibility(GONE); + } + + public void setSubtitleColor(@ColorRes int color) { + subtitleView.setTextColor(getContext().getResources().getColor(color)); + } + + public void setText(CharSequence text) { + textView.setText(text); + } + + public void setIcon(@DrawableRes int id) { + iconView.setImageResource(id); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextView.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextView.java new file mode 100644 index 00000000..1f64e483 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextView.java @@ -0,0 +1,96 @@ +package se.leap.bitmaskclient.base.views; + + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import androidx.appcompat.widget.AppCompatTextView; +import android.text.Spannable; +import android.text.style.ImageSpan; +import android.util.AttributeSet; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class IconTextView extends AppCompatTextView { + + private int imageResource = 0; + /** + * Regex pattern that looks for embedded images of the format: [img src=imageName/] + */ + public static final String PATTERN = "\\Q[img src]\\E"; + + public IconTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public IconTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public IconTextView(Context context) { + super(context); + } + + @Override + public void setText(CharSequence text, BufferType type) { + final Spannable spannable = getTextWithImages(getContext(), text, getLineHeight(), getCurrentTextColor()); + super.setText(spannable, BufferType.SPANNABLE); + } + + public void setIcon(int imageResource) { + this.imageResource = imageResource; + } + + private Spannable getTextWithImages(Context context, CharSequence text, int lineHeight, int colour) { + final Spannable spannable = Spannable.Factory.getInstance().newSpannable(text); + addImages(context, spannable, lineHeight, colour); + return spannable; + } + + private void addImages(Context context, Spannable spannable, int lineHeight, int colour) { + final Pattern refImg = Pattern.compile(PATTERN); + + final Matcher matcher = refImg.matcher(spannable); + while (matcher.find()) { + boolean set = true; + for (ImageSpan span : spannable.getSpans(matcher.start(), matcher.end(), ImageSpan.class)) { + if (spannable.getSpanStart(span) >= matcher.start() + && spannable.getSpanEnd(span) <= matcher.end()) { + spannable.removeSpan(span); + } else { + set = false; + break; + } + } + if (set && imageResource != 0) { + spannable.setSpan(makeImageSpan(context, imageResource, lineHeight, colour), + matcher.start(), + matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + } + } + + /** + * Create an ImageSpan for the given icon drawable. This also sets the image size and colour. + * Works best with a white, square icon because of the colouring and resizing. + * + * @param context The Android Context. + * @param drawableResId A drawable resource Id. + * @param size The desired size (i.e. width and height) of the image icon in pixels. + * Use the lineHeight of the TextView to make the image inline with the + * surrounding text. + * @param colour The colour (careful: NOT a resource Id) to apply to the image. + * @return An ImageSpan, aligned with the bottom of the text. + */ + private ImageSpan makeImageSpan(Context context, int drawableResId, int size, int colour) { + final Drawable drawable = context.getResources().getDrawable(drawableResId); + drawable.mutate(); + drawable.setColorFilter(colour, PorterDuff.Mode.MULTIPLY); + drawable.setBounds(0, 0, size, size); + return new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); + } + +} \ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/ProviderHeaderView.java b/app/src/main/java/se/leap/bitmaskclient/base/views/ProviderHeaderView.java new file mode 100644 index 00000000..811a54a2 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/views/ProviderHeaderView.java @@ -0,0 +1,109 @@ +package se.leap.bitmaskclient.base.views; + +import android.content.Context; +import androidx.annotation.DrawableRes; +import androidx.annotation.RequiresApi; +import androidx.annotation.StringRes; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.AppCompatTextView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RelativeLayout; + +import se.leap.bitmaskclient.R; + +import static se.leap.bitmaskclient.base.utils.ViewHelper.convertDimensionToPx; + +/** + * Created by cyberta on 29.06.18. + */ + +public class ProviderHeaderView extends RelativeLayout { + private int stdPadding; + private int compactPadding; + private int stdImageSize; + private int compactImageSize; + + AppCompatImageView providerHeaderLogo; + AppCompatTextView providerHeaderText; + + public ProviderHeaderView(Context context) { + super(context); + initLayout(context); + } + + public ProviderHeaderView(Context context, AttributeSet attrs) { + super(context, attrs); + initLayout(context); + } + + public ProviderHeaderView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initLayout(context); + } + + @RequiresApi(21) + public ProviderHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initLayout(context); + } + + + void initLayout(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rootview = inflater.inflate(R.layout.v_provider_header, this, true); + providerHeaderLogo = rootview.findViewById(R.id.provider_header_logo); + providerHeaderText = rootview.findViewById(R.id.provider_header_text); + + stdPadding = convertDimensionToPx(context, R.dimen.stdpadding); + compactPadding = convertDimensionToPx(context, R.dimen.compact_padding); + stdImageSize = convertDimensionToPx(context, R.dimen.bitmask_logo); + compactImageSize = convertDimensionToPx(context, R.dimen.bitmask_logo_compact); + } + + public void setTitle(String title) { + providerHeaderText.setText(title); + } + + public void setTitle(@StringRes int stringRes) { + providerHeaderText.setText(stringRes); + } + + public void setLogo(@DrawableRes int drawableRes) { + providerHeaderLogo.setImageResource(drawableRes); + } + + public void showCompactLayout() { + LayoutParams logoLayoutParams = (LayoutParams) providerHeaderLogo.getLayoutParams(); + logoLayoutParams.width = compactImageSize; + logoLayoutParams.height = compactImageSize; + providerHeaderLogo.setLayoutParams(logoLayoutParams); + + LayoutParams textLayoutParams = (LayoutParams) providerHeaderText.getLayoutParams(); + textLayoutParams.addRule(RIGHT_OF, R.id.provider_header_logo); + textLayoutParams.addRule(BELOW, 0); + textLayoutParams.addRule(ALIGN_TOP, R.id.provider_header_logo); + textLayoutParams.setMargins(compactPadding, compactPadding, compactPadding, compactPadding); + + providerHeaderText.setLayoutParams(textLayoutParams); + providerHeaderText.setMaxLines(2); + } + + public void showStandardLayout() { + LayoutParams logoLayoutParams = (LayoutParams) providerHeaderLogo.getLayoutParams(); + logoLayoutParams.width = stdImageSize; + logoLayoutParams.height = stdImageSize; + providerHeaderLogo.setLayoutParams(logoLayoutParams); + + LayoutParams textLayoutParams = (LayoutParams) providerHeaderText.getLayoutParams(); + textLayoutParams.addRule(RIGHT_OF, 0); + textLayoutParams.addRule(BELOW, R.id.provider_header_logo); + textLayoutParams.addRule(ALIGN_TOP, 0); + textLayoutParams.setMargins(stdPadding, stdPadding, stdPadding, stdPadding); + providerHeaderText.setLayoutParams(textLayoutParams); + providerHeaderText.setMaxLines(1); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/VpnStateImage.java b/app/src/main/java/se/leap/bitmaskclient/base/views/VpnStateImage.java new file mode 100644 index 00000000..2f8a4448 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/views/VpnStateImage.java @@ -0,0 +1,99 @@ +/** + * 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.base.views; + +import android.content.Context; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.appcompat.widget.AppCompatImageView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.ProgressBar; + +import se.leap.bitmaskclient.R; + +/** + * Created by cyberta on 12.02.18. + */ + + +public class VpnStateImage extends ConstraintLayout { + + ProgressBar progressBar; + AppCompatImageView stateIcon; + + public VpnStateImage(Context context) { + super(context); + initLayout(context); + } + + public VpnStateImage(Context context, AttributeSet attrs) { + super(context, attrs); + initLayout(context); + } + + public VpnStateImage(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initLayout(context); + } + + void initLayout(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rootview = inflater.inflate(R.layout.v_main_button, this, true); + stateIcon = rootview.findViewById(R.id.vpn_state_key); + progressBar = rootview.findViewById(R.id.progressBar); + progressBar.setIndeterminate(true); + } + + public void showProgress() { + progressBar.setVisibility(VISIBLE); + } + + + public void stopProgress(boolean animated) { + if (!animated) { + progressBar.setVisibility(GONE); + return; + } + + AlphaAnimation fadeOutAnimation = new AlphaAnimation(1.0f, 0.0f); + fadeOutAnimation.setDuration(1000); + fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + progressBar.setVisibility(GONE); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + + progressBar.startAnimation(fadeOutAnimation); + } + + public void setStateIcon(int resource) { + stateIcon.setImageResource(resource); + } + + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java deleted file mode 100644 index fabaa7c3..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java +++ /dev/null @@ -1,674 +0,0 @@ -/** - * Copyright (c) 2019 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.drawer; - - -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.core.view.GravityCompat; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import java.util.Observable; -import java.util.Observer; -import java.util.Set; - -import de.blinkt.openvpn.core.VpnStatus; -import se.leap.bitmaskclient.EipFragment; -import se.leap.bitmaskclient.FragmentManagerEnhanced; -import se.leap.bitmaskclient.MainActivity; -import se.leap.bitmaskclient.Provider; -import se.leap.bitmaskclient.ProviderListActivity; -import se.leap.bitmaskclient.ProviderObservable; -import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.eip.EipCommand; -import se.leap.bitmaskclient.eip.EipStatus; -import se.leap.bitmaskclient.firewall.FirewallManager; -import se.leap.bitmaskclient.fragments.AboutFragment; -import se.leap.bitmaskclient.fragments.AlwaysOnDialog; -import se.leap.bitmaskclient.fragments.ExcludeAppsFragment; -import se.leap.bitmaskclient.fragments.LogFragment; -import se.leap.bitmaskclient.fragments.TetheringDialog; -import se.leap.bitmaskclient.tethering.TetheringObservable; -import se.leap.bitmaskclient.utils.PreferenceHelper; -import se.leap.bitmaskclient.views.IconSwitchEntry; -import se.leap.bitmaskclient.views.IconTextEntry; - -import static android.content.Context.MODE_PRIVATE; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static se.leap.bitmaskclient.BitmaskApp.getRefWatcher; -import static se.leap.bitmaskclient.Constants.DONATION_URL; -import static se.leap.bitmaskclient.Constants.ENABLE_DONATION; -import static se.leap.bitmaskclient.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.Constants.REQUEST_CODE_SWITCH_PROVIDER; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; -import static se.leap.bitmaskclient.Constants.USE_IPv6_FIREWALL; -import static se.leap.bitmaskclient.Constants.USE_PLUGGABLE_TRANSPORTS; -import static se.leap.bitmaskclient.R.string.about_fragment_title; -import static se.leap.bitmaskclient.R.string.exclude_apps_fragment_title; -import static se.leap.bitmaskclient.R.string.log_fragment_title; -import static se.leap.bitmaskclient.utils.ConfigHelper.isDefaultBitmask; -import static se.leap.bitmaskclient.utils.PreferenceHelper.getSaveBattery; -import static se.leap.bitmaskclient.utils.PreferenceHelper.getShowAlwaysOnDialog; -import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports; -import static se.leap.bitmaskclient.utils.PreferenceHelper.saveBattery; -import static se.leap.bitmaskclient.utils.PreferenceHelper.showExperimentalFeatures; -import static se.leap.bitmaskclient.utils.PreferenceHelper.usePluggableTransports; - -/** - * Fragment used for managing interactions for and presentation of a navigation drawer. - * See the - * design guidelines for a complete explanation of the behaviors implemented here. - */ -public class NavigationDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener, Observer { - - /** - * Per the design guidelines, you should show the drawer on launch until the user manually - * expands it. This shared preference tracks this. - */ - private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned"; - private static final String TAG = NavigationDrawerFragment.class.getName(); - public static final int TWO_SECONDS = 2000; - - /** - * Helper component that ties the action bar to the navigation drawer. - */ - private ActionBarDrawerToggle drawerToggle; - - private DrawerLayout drawerLayout; - private View drawerView; - private View fragmentContainerView; - private Toolbar toolbar; - private IconTextEntry account; - private IconSwitchEntry saveBattery; - private IconTextEntry tethering; - private IconSwitchEntry firewall; - private View experimentalFeatureFooter; - - private boolean userLearnedDrawer; - private volatile boolean wasPaused; - private volatile boolean shouldCloseOnResume; - - private SharedPreferences preferences; - - private final static String KEY_SHOW_SAVE_BATTERY_ALERT = "KEY_SHOW_SAVE_BATTERY_ALERT"; - private volatile boolean showSaveBattery = false; - AlertDialog alertDialog; - private FirewallManager firewallManager; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Reads in the flag indicating whether or not the user has demonstrated awareness of the - // drawer. See PREF_USER_LEARNED_DRAWER for details. - preferences = getContext().getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - userLearnedDrawer = preferences.getBoolean(PREF_USER_LEARNED_DRAWER, false); - preferences.registerOnSharedPreferenceChangeListener(this); - firewallManager = new FirewallManager(getContext().getApplicationContext(), false); - - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - // Indicates that this fragment would like to influence the set of actions in the action bar. - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - drawerView = inflater.inflate(R.layout.f_drawer_main, container, false); - restoreFromSavedInstance(savedInstanceState); - TetheringObservable.getInstance().addObserver(this); - EipStatus.getInstance().addObserver(this); - return drawerView; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - TetheringObservable.getInstance().deleteObserver(this); - EipStatus.getInstance().deleteObserver(this); - } - - public boolean isDrawerOpen() { - return drawerLayout != null && drawerLayout.isDrawerOpen(fragmentContainerView); - } - - @Override - public void onResume() { - super.onResume(); - wasPaused = false; - if (shouldCloseOnResume) { - closeDrawerWithDelay(); - } - } - - @Override - public void onPause() { - super.onPause(); - wasPaused = true; - } - - - - /** - * Users of this fragment must call this method to set up the navigation drawer interactions. - * - * @param fragmentId The android:id of this fragment in its activity's layout. - * @param drawerLayout The DrawerLayout containing this fragment's UI. - */ - public void setUp(int fragmentId, DrawerLayout drawerLayout) { - final AppCompatActivity activity = (AppCompatActivity) getActivity(); - fragmentContainerView = activity.findViewById(fragmentId); - this.drawerLayout = drawerLayout; - // set a custom shadow that overlays the main content when the drawer opens - this.drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); - toolbar = this.drawerLayout.findViewById(R.id.toolbar); - - setupActionBar(); - setupEntries(); - setupActionBarDrawerToggle(activity); - - if (!userLearnedDrawer) { - openNavigationDrawerForFirstTimeUsers(); - } - - // Defer code dependent on restoration of previous instance state. - this.drawerLayout.post(() -> drawerToggle.syncState()); - this.drawerLayout.addDrawerListener(drawerToggle); - } - - private void setupActionBarDrawerToggle(final AppCompatActivity activity) { - // ActionBarDrawerToggle ties together the the proper interactions - // between the navigation drawer and the action bar app icon. - drawerToggle = new ActionBarDrawerToggle( - activity, - drawerLayout, - toolbar, - R.string.navigation_drawer_open, - R.string.navigation_drawer_close - ) { - @Override - public void onDrawerClosed(View drawerView) { - super.onDrawerClosed(drawerView); - if (!isAdded()) { - return; - } - activity.invalidateOptionsMenu(); - } - - @Override - public void onDrawerOpened(View drawerView) { - super.onDrawerOpened(drawerView); - if (!isAdded()) { - return; - } - - if (!userLearnedDrawer) { - // The user manually opened the drawer; store this flag to prevent auto-showing - // the navigation drawer automatically in the future. - userLearnedDrawer = true; - preferences.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).apply(); - } - activity.invalidateOptionsMenu(); - } - }; - } - - private void setupEntries() { - initAccountEntry(); - initSwitchProviderEntry(); - initUseBridgesEntry(); - initSaveBatteryEntry(); - initAlwaysOnVpnEntry(); - initExcludeAppsEntry(); - initShowExperimentalHint(); - initTetheringEntry(); - initFirewallEntry(); - initExperimentalFeatureFooter(); - initDonateEntry(); - initLogEntry(); - initAboutEntry(); - } - - private void initAccountEntry() { - account = drawerView.findViewById(R.id.account); - FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); - Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider(); - account.setText(currentProvider.getName()); - account.setOnClickListener((buttonView) -> { - Fragment fragment = new EipFragment(); - Bundle arguments = new Bundle(); - arguments.putParcelable(PROVIDER_KEY, currentProvider); - fragment.setArguments(arguments); - hideActionBarSubTitle(); - fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); - closeDrawer(); - }); - } - - private void initSwitchProviderEntry() { - if (isDefaultBitmask()) { - IconTextEntry switchProvider = drawerView.findViewById(R.id.switch_provider); - switchProvider.setVisibility(VISIBLE); - switchProvider.setOnClickListener(v -> - getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER)); - } - } - - private void initUseBridgesEntry() { - IconSwitchEntry useBridges = drawerView.findViewById(R.id.bridges_switch); - if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) { - useBridges.setVisibility(VISIBLE); - useBridges.setChecked(getUsePluggableTransports(getContext())); - useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (!buttonView.isPressed()) { - return; - } - usePluggableTransports(getContext(), isChecked); - if (VpnStatus.isVPNActive()) { - EipCommand.startVPN(getContext(), false); - closeDrawer(); - } - }); - - - } else { - useBridges.setVisibility(GONE); - } - } - - private void initSaveBatteryEntry() { - saveBattery = drawerView.findViewById(R.id.battery_switch); - saveBattery.showSubtitle(false); - saveBattery.setChecked(getSaveBattery(getContext())); - saveBattery.setOnCheckedChangeListener(((buttonView, isChecked) -> { - if (!buttonView.isPressed()) { - return; - } - if (isChecked) { - showSaveBatteryAlert(); - } else { - saveBattery(getContext(), false); - } - })); - boolean enableEntry = !TetheringObservable.getInstance().getTetheringState().isVpnTetheringRunning(); - enableSaveBatteryEntry(enableEntry); - } - - private void enableSaveBatteryEntry(boolean enabled) { - if (saveBattery.isEnabled() == enabled) { - return; - } - saveBattery.setEnabled(enabled); - saveBattery.showSubtitle(!enabled); - } - - private void initAlwaysOnVpnEntry() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - IconTextEntry alwaysOnVpn = drawerView.findViewById(R.id.always_on_vpn); - alwaysOnVpn.setVisibility(VISIBLE); - alwaysOnVpn.setOnClickListener((buttonView) -> { - closeDrawer(); - if (getShowAlwaysOnDialog(getContext())) { - showAlwaysOnDialog(); - } else { - Intent intent = new Intent("android.net.vpn.SETTINGS"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } - }); - } - } - - private void initExcludeAppsEntry() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps); - excludeApps.setVisibility(VISIBLE); - Set apps = PreferenceHelper.getExcludedApps(this.getContext()); - if (apps != null) { - updateExcludeAppsSubtitle(excludeApps, apps.size()); - } - FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); - excludeApps.setOnClickListener((buttonView) -> { - closeDrawer(); - Fragment fragment = new ExcludeAppsFragment(); - setActionBarTitle(exclude_apps_fragment_title); - fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); - }); - } - } - - private void initShowExperimentalHint() { - TextView textView = drawerLayout.findViewById(R.id.show_experimental_features); - textView.setText(showExperimentalFeatures(getContext()) ? R.string.hide_experimental : R.string.show_experimental); - textView.setOnClickListener(v -> { - boolean shown = showExperimentalFeatures(getContext()); - if (shown) { - tethering.setVisibility(GONE); - firewall.setVisibility(GONE); - experimentalFeatureFooter.setVisibility(GONE); - ((TextView) v).setText(R.string.show_experimental); - } else { - tethering.setVisibility(VISIBLE); - firewall.setVisibility(VISIBLE); - experimentalFeatureFooter.setVisibility(VISIBLE); - ((TextView) v).setText(R.string.hide_experimental); - } - PreferenceHelper.setShowExperimentalFeatures(getContext(), !shown); - }); - } - - private void initFirewallEntry() { - firewall = drawerView.findViewById(R.id.enableIPv6Firewall); - boolean show = showExperimentalFeatures(getContext()); - firewall.setVisibility(show ? VISIBLE : GONE); - firewall.setChecked(PreferenceHelper.useIpv6Firewall(getContext())); - firewall.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (!buttonView.isPressed()) { - return; - } - PreferenceHelper.setUseIPv6Firewall(getContext(), isChecked); - if (VpnStatus.isVPNActive()) { - if (isChecked) { - firewallManager.startIPv6Firewall(); - } else { - firewallManager.stopIPv6Firewall(); - } - } - }); - } - - private void initTetheringEntry() { - tethering = drawerView.findViewById(R.id.tethering); - boolean show = showExperimentalFeatures(getContext()); - tethering.setVisibility(show ? VISIBLE : GONE); - tethering.setOnClickListener((buttonView) -> { - showTetheringAlert(); - }); - } - - private void initExperimentalFeatureFooter() { - experimentalFeatureFooter = drawerView.findViewById(R.id.experimental_features_footer); - boolean show = showExperimentalFeatures(getContext()); - experimentalFeatureFooter.setVisibility(show ? VISIBLE : GONE); - } - - private void initDonateEntry() { - if (ENABLE_DONATION) { - IconTextEntry donate = drawerView.findViewById(R.id.donate); - donate.setVisibility(VISIBLE); - donate.setOnClickListener((buttonView) -> { - closeDrawer(); - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL)); - startActivity(browserIntent); - - }); - } - } - - private void initLogEntry() { - IconTextEntry log = drawerView.findViewById(R.id.log); - FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); - log.setOnClickListener((buttonView) -> { - closeDrawer(); - Fragment fragment = new LogFragment(); - setActionBarTitle(log_fragment_title); - fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); - }); - } - - private void initAboutEntry() { - IconTextEntry about = drawerView.findViewById(R.id.about); - FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); - about.setOnClickListener((buttonView) -> { - closeDrawer(); - Fragment fragment = new AboutFragment(); - setActionBarTitle(about_fragment_title); - fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG); - }); - } - - private void closeDrawer() { - if (drawerLayout != null) { - drawerLayout.closeDrawer(fragmentContainerView); - } - } - - private ActionBar setupActionBar() { - AppCompatActivity activity = (AppCompatActivity) getActivity(); - activity.setSupportActionBar(toolbar); - final ActionBar actionBar = activity.getSupportActionBar(); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeButtonEnabled(true); - actionBar.setDisplayShowTitleEnabled(true); - return actionBar; - } - - private void openNavigationDrawerForFirstTimeUsers() { - if (userLearnedDrawer) { - return; - } - - drawerLayout.openDrawer(fragmentContainerView, false); - closeDrawerWithDelay(); - } - - @NonNull - private void closeDrawerWithDelay() { - final Handler navigationDrawerHandler = new Handler(); - navigationDrawerHandler.postDelayed(() -> { - if (!wasPaused) { - drawerLayout.closeDrawer(fragmentContainerView, true); - } else { - shouldCloseOnResume = true; - } - - }, TWO_SECONDS); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (showSaveBattery) { - outState.putBoolean(KEY_SHOW_SAVE_BATTERY_ALERT, true); - alertDialog.dismiss(); - } - } - - private void restoreFromSavedInstance(Bundle savedInstanceState) { - if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SHOW_SAVE_BATTERY_ALERT)) { - showSaveBatteryAlert(); - } - } - - private void showSaveBatteryAlert() { - Activity activity = getActivity(); - if (activity == null) { - return; - } - - try { - AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); - showSaveBattery = true; - alertDialog = alertBuilder - .setTitle(activity.getString(R.string.save_battery)) - .setMessage(activity.getString(R.string.save_battery_message)) - .setPositiveButton((android.R.string.yes), (dialog, which) -> { - saveBattery(getContext(), true); - }) - .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> saveBattery.setCheckedQuietly(false)) - .setOnDismissListener(dialog -> showSaveBattery = false) - .setOnCancelListener(dialog -> saveBattery.setCheckedQuietly(false)).show(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - - public void showTetheringAlert() { - try { - - FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( - getActivity().getSupportFragmentManager()).removePreviousFragment( - TetheringDialog.TAG); - DialogFragment newFragment = new TetheringDialog(); - newFragment.show(fragmentTransaction, TetheringDialog.TAG); - } catch (IllegalStateException | NullPointerException e) { - e.printStackTrace(); - } - } - - public void showAlwaysOnDialog() { - try { - - FragmentTransaction fragmentTransaction = new FragmentManagerEnhanced( - getActivity().getSupportFragmentManager()).removePreviousFragment( - AlwaysOnDialog.TAG); - DialogFragment newFragment = new AlwaysOnDialog(); - newFragment.show(fragmentTransaction, AlwaysOnDialog.TAG); - } catch (IllegalStateException | NullPointerException e) { - e.printStackTrace(); - } - - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - // Forward the new configuration the drawer toggle component. - drawerToggle.onConfigurationChanged(newConfig); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (drawerLayout != null && isDrawerOpen()) { - showGlobalContextActionBar(); - } - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (drawerToggle.onOptionsItemSelected(item)) { - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onDestroy() { - super.onDestroy(); - getRefWatcher(getActivity()).watch(this); - preferences.unregisterOnSharedPreferenceChangeListener(this); - } - - /** - * Per the navigation drawer design guidelines, updates the action bar to show the global app - * 'context', rather than just what's in the current screen. - */ - private void showGlobalContextActionBar() { - ActionBar actionBar = getActionBar(); - actionBar.setDisplayShowTitleEnabled(true); - actionBar.setTitle(R.string.app_name); - } - - private ActionBar getActionBar() { - return ((AppCompatActivity) getActivity()).getSupportActionBar(); - } - - private void setActionBarTitle(@StringRes int resId) { - ActionBar actionBar = getActionBar(); - if (actionBar != null) { - actionBar.setSubtitle(resId); - } - } - - private void hideActionBarSubTitle() { - ActionBar actionBar = getActionBar(); - if (actionBar != null) { - actionBar.setSubtitle(null); - } - } - - public void refresh() { - Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider(); - account.setText(currentProvider.getName()); - initUseBridgesEntry(); - } - - private void updateExcludeAppsSubtitle(IconTextEntry excludeApps, int number) { - if (number > 0) { - excludeApps.setSubtitle(getContext().getResources().getQuantityString(R.plurals.subtitle_exclude_apps, number, number)); - excludeApps.setSubtitleColor(R.color.colorError); - } else { - excludeApps.hideSubtitle(); - } - } - - public void onAppsExcluded(int number) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps); - updateExcludeAppsSubtitle(excludeApps, number); - } - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(USE_PLUGGABLE_TRANSPORTS)) { - initUseBridgesEntry(); - } else if (key.equals(USE_IPv6_FIREWALL)) { - initFirewallEntry(); - } - } - - @Override - public void update(Observable o, Object arg) { - if (o instanceof TetheringObservable || o instanceof EipStatus) { - try { - getActivity().runOnUiThread(() -> - enableSaveBatteryEntry(!TetheringObservable.getInstance().getTetheringState().isVpnTetheringRunning())); - } catch (NullPointerException npe) { - // eat me - } - } - } -} 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 e0c96ebb..e5cf70be 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java @@ -49,39 +49,39 @@ import de.blinkt.openvpn.core.IOpenVPNServiceInternal; import de.blinkt.openvpn.core.OpenVPNService; import de.blinkt.openvpn.core.VpnStatus; import de.blinkt.openvpn.core.connection.Connection; -import se.leap.bitmaskclient.OnBootReceiver; -import se.leap.bitmaskclient.ProviderObservable; +import se.leap.bitmaskclient.base.OnBootReceiver; +import se.leap.bitmaskclient.base.models.ProviderObservable; import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.utils.PreferenceHelper; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_OK; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN; -import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_CONFIGURE_TETHERING; -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_VPN; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_BLOCKING_VPN; -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_EARLY_ROUTES; -import static se.leap.bitmaskclient.Constants.EIP_N_CLOSEST_GATEWAY; -import static se.leap.bitmaskclient.Constants.EIP_RECEIVER; -import static se.leap.bitmaskclient.Constants.EIP_RESTART_ON_BOOT; -import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE; -import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_CONFIGURE_TETHERING; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_IS_RUNNING; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START_ALWAYS_ON_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START_BLOCKING_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_STOP; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_STOP_BLOCKING_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES; +import static se.leap.bitmaskclient.base.models.Constants.EIP_N_CLOSEST_GATEWAY; +import static se.leap.bitmaskclient.base.models.Constants.EIP_RECEIVER; +import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid; import static se.leap.bitmaskclient.R.string.warning_client_parsing_error_gateways; import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE; import static se.leap.bitmaskclient.eip.EIP.EIPErrors.NO_MORE_GATEWAYS; import static se.leap.bitmaskclient.eip.EipResultBroadcast.tellToReceiverOrBroadcast; -import static se.leap.bitmaskclient.utils.ConfigHelper.ensureNotOnMainThread; -import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.ensureNotOnMainThread; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports; /** * EIP is the abstract base class for interacting with and managing the Encrypted diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java index 25450f56..39d4e33e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipCommand.java @@ -11,14 +11,14 @@ import androidx.annotation.VisibleForTesting; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_CONFIGURE_TETHERING; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_BLOCKING_VPN; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP; -import static se.leap.bitmaskclient.Constants.EIP_EARLY_ROUTES; -import static se.leap.bitmaskclient.Constants.EIP_N_CLOSEST_GATEWAY; -import static se.leap.bitmaskclient.Constants.EIP_RECEIVER; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_CHECK_CERT_VALIDITY; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_CONFIGURE_TETHERING; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START_BLOCKING_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_STOP; +import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES; +import static se.leap.bitmaskclient.base.models.Constants.EIP_N_CLOSEST_GATEWAY; +import static se.leap.bitmaskclient.base.models.Constants.EIP_RECEIVER; /** * Use this class to send commands to EIP diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipResultBroadcast.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipResultBroadcast.java index 92d1338c..68d9c8ad 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EipResultBroadcast.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipResultBroadcast.java @@ -8,10 +8,10 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; import static android.content.Intent.CATEGORY_DEFAULT; -import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE; -import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY; -import static se.leap.bitmaskclient.Constants.EIP_REQUEST; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_EIP_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.base.models.Constants.EIP_REQUEST; public class EipResultBroadcast { private static final String TAG = EipResultBroadcast.class.getSimpleName(); diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupListener.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupListener.java new file mode 100644 index 00000000..13d9bdec --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupListener.java @@ -0,0 +1,12 @@ +package se.leap.bitmaskclient.eip; + +import android.content.Intent; + +/** + * Created by cyberta on 05.12.18. + */ +public interface EipSetupListener { + void handleEipEvent(Intent intent); + + void handleProviderApiEvent(Intent intent); +} diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java new file mode 100644 index 00000000..1c101e2d --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java @@ -0,0 +1,374 @@ +/** + * Copyright (c) 2020 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.json.JSONObject; + +import java.util.Vector; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import de.blinkt.openvpn.LaunchVPN; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ConnectionStatus; +import de.blinkt.openvpn.core.LogItem; +import de.blinkt.openvpn.core.VpnStatus; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderAPI; +import se.leap.bitmaskclient.providersetup.ProviderAPICommand; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.appUpdate.DownloadServiceCommand; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; + +import static android.app.Activity.RESULT_CANCELED; +import static android.content.Intent.CATEGORY_DEFAULT; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NOTCONNECTED; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_EIP_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_PREPARE_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START_ALWAYS_ON_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES; +import static se.leap.bitmaskclient.base.models.Constants.EIP_REQUEST; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON; +import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE; + +/** + * Created by cyberta on 05.12.18. + */ +public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.StateListener, VpnStatus.LogListener { + + private static final String TAG = EipSetupObserver.class.getName(); + + //The real timout is 4*2s + 1*4s + 1*8s + 1*16s + 1*32s + 1*64s = 132 s; + private static final String TIMEOUT = "4"; + private static final int UPDATE_CHECK_TIMEOUT = 1000*60*60*24*7; + private Context context; + private VpnProfile setupVpnProfile; + private String observedProfileFromVpnStatus; + AtomicBoolean changingGateway = new AtomicBoolean(false); + AtomicInteger setupNClosestGateway = new AtomicInteger(); + AtomicInteger reconnectTry = new AtomicInteger(); + private Vector listeners = new Vector<>(); + private SharedPreferences preferences; + private static EipSetupObserver instance; + + private EipSetupObserver(Context context, SharedPreferences preferences) { + this.context = context; + this.preferences = preferences; + IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT); + updateIntentFilter.addAction(BROADCAST_EIP_EVENT); + updateIntentFilter.addAction(BROADCAST_PROVIDER_API_EVENT); + updateIntentFilter.addCategory(CATEGORY_DEFAULT); + LocalBroadcastManager.getInstance(context.getApplicationContext()).registerReceiver(this, updateIntentFilter); + instance = this; + VpnStatus.addLogListener(this); + } + + public static void init(Context context, SharedPreferences preferences) { + if (instance == null) { + instance = new EipSetupObserver(context, preferences); + } + } + + public static boolean reconnectingWithDifferentGateway() { + return instance.setupNClosestGateway.get() > 0; + } + + public static int connectionRetry() { + return instance.reconnectTry.get(); + } + + public static int gatewayOrder() { + return instance.setupNClosestGateway.get(); + } + + public static synchronized void addListener(EipSetupListener listener) { + if (instance.listeners.contains(listener)) { + return; + } + instance.listeners.add(listener); + } + + public static synchronized void removeListener(EipSetupListener listener) { + instance.listeners.remove(listener); + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) { + return; + } + + switch (action) { + case BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT: + handleGatewaySetupObserverEvent(intent); + break; + case BROADCAST_EIP_EVENT: + handleEipEvent(intent); + break; + case BROADCAST_PROVIDER_API_EVENT: + handleProviderApiEvent(intent); + break; + default: + break; + } + } + + private void handleProviderApiEvent(Intent intent) { + int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); + Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY); + if (resultData == null) { + resultData = Bundle.EMPTY; + } + + Provider provider; + switch (resultCode) { + case CORRECTLY_DOWNLOADED_EIP_SERVICE: + Log.d(TAG, "correctly updated service json"); + provider = resultData.getParcelable(PROVIDER_KEY); + ProviderObservable.getInstance().updateProvider(provider); + PreferenceHelper.storeProviderInPreferences(preferences, provider); + if (EipStatus.getInstance().isDisconnected()) { + EipCommand.startVPN(context.getApplicationContext(), true); + } + break; + case CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE: + provider = resultData.getParcelable(PROVIDER_KEY); + ProviderObservable.getInstance().updateProvider(provider); + PreferenceHelper.storeProviderInPreferences(preferences, provider); + EipCommand.startVPN(context.getApplicationContext(), true); + break; + case CORRECTLY_DOWNLOADED_GEOIP_JSON: + provider = resultData.getParcelable(PROVIDER_KEY); + ProviderObservable.getInstance().updateProvider(provider); + PreferenceHelper.storeProviderInPreferences(preferences, provider); + maybeStartEipService(resultData); + break; + case INCORRECTLY_DOWNLOADED_GEOIP_JSON: + maybeStartEipService(resultData); + break; + default: + break; + } + + for (EipSetupListener listener : listeners) { + listener.handleProviderApiEvent(intent); + } + } + + private void maybeStartEipService(Bundle resultData) { + if (resultData.getBoolean(EIP_ACTION_START)) { + boolean earlyRoutes = resultData.getBoolean(EIP_EARLY_ROUTES); + EipCommand.startVPN(context.getApplicationContext(), earlyRoutes); + } + } + + + private void handleEipEvent(Intent intent) { + int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); + Bundle result = intent.getBundleExtra(BROADCAST_RESULT_KEY); + String eipRequest = result.getString(EIP_REQUEST); + EIP.EIPErrors error = EIP.EIPErrors.UNKNOWN; + try { + JSONObject jsonObject = new JSONObject(result.getString(EIP.ERRORS)); + error = EIP.EIPErrors.valueOf(jsonObject.getString(EIP.ERRORID)); + } catch (Exception e) { + //ignore + } + if (eipRequest == null) { + return; + } + switch (eipRequest) { + case EIP_ACTION_START: + case EIP_ACTION_START_ALWAYS_ON_VPN: + if (resultCode == RESULT_CANCELED) { + //setup failed + if (error == EIP.EIPErrors.NO_MORE_GATEWAYS) { + finishGatewaySetup(false); + EipCommand.startBlockingVPN(context.getApplicationContext()); + } else { + //FIXME: + finishGatewaySetup(false); + EipCommand.stopVPN(context); + EipStatus.refresh(); + } + } + break; + case EIP_ACTION_PREPARE_VPN: + if (resultCode == RESULT_CANCELED) { + VpnStatus.logError("Error preparing VpnService."); + finishGatewaySetup(false); + EipStatus.refresh(); + } + break; + default: + break; + } + + for (EipSetupListener listener : listeners) { + listener.handleEipEvent(intent); + } + } + + private void handleGatewaySetupObserverEvent(Intent event) { + if (observedProfileFromVpnStatus != null || setupVpnProfile != null) { + //finish last setup observation + Log.d(TAG, "finish last gateway setup"); + finishGatewaySetup(true); + } + + VpnProfile vpnProfile = (VpnProfile) event.getSerializableExtra(PROVIDER_PROFILE); + if (vpnProfile == null) { + Log.e(TAG, "Tried to setup non existing vpn profile."); + return; + } + setupVpnProfile = vpnProfile; + setupNClosestGateway.set(event.getIntExtra(Gateway.KEY_N_CLOSEST_GATEWAY, 0)); + Log.d(TAG, "bitmaskapp add state listener"); + VpnStatus.addStateListener(this); + + launchVPN(setupVpnProfile); + } + + private void launchVPN(VpnProfile vpnProfile) { + Intent intent = new Intent(context.getApplicationContext(), LaunchVPN.class); + intent.setAction(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(LaunchVPN.EXTRA_HIDELOG, true); + intent.putExtra(PROVIDER_PROFILE, vpnProfile); + intent.putExtra(Gateway.KEY_N_CLOSEST_GATEWAY, setupNClosestGateway.get()); + context.startActivity(intent); + } + + @Override + public void updateState(String state, String logmessage, int localizedResId, ConnectionStatus level) { + // VpnStatus.updateStateString("NOPROCESS", "No process running.", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED); + + Log.d(TAG, "vpn status: " + state + " - " + logmessage + " - " + level); + if (observedProfileFromVpnStatus == null || + setupVpnProfile == null) { + return; + } + if (!observedProfileFromVpnStatus.equals(setupVpnProfile.getUUIDString())) { + Log.d(TAG, "vpn profile to setup and observed profile currently is used differ: " + setupVpnProfile.getUUIDString() + " vs. " + observedProfileFromVpnStatus); + return; + } + + if (ConnectionStatus.LEVEL_STOPPING == level) { + finishGatewaySetup(false); + } else if ("CONNECTRETRY".equals(state) && LEVEL_CONNECTING_NO_SERVER_REPLY_YET.equals(level)) { + Log.d(TAG, "trying gateway: " + setupVpnProfile.getName()); + if (TIMEOUT.equals(logmessage)) { + Log.e(TAG, "Timeout reached! Try next gateway!"); + VpnStatus.logError("Timeout reached! Try next gateway!"); + selectNextGateway(); + return; + } + int current = reconnectTry.get(); + reconnectTry.set(current + 1); + } else if ("NOPROCESS".equals(state) && LEVEL_NOTCONNECTED == level) { + //?? + } else if ("CONNECTED".equals(state)) { + //saveLastProfile(context.getApplicationContext(), setupVpnProfile.getUUIDString()); + Provider provider = ProviderObservable.getInstance().getCurrentProvider(); + if (setupNClosestGateway.get() > 0 || provider.shouldUpdateEipServiceJson()) { + //setupNClostestGateway > 0: at least one failed gateway -> did the provider change it's gateways? + ProviderAPICommand.execute(context, ProviderAPI.DOWNLOAD_SERVICE_JSON, provider); + } + + if (shouldCheckAppUpdate()) { + DownloadServiceCommand.execute(context, CHECK_VERSION_FILE); + } + finishGatewaySetup(false); + } else if ("TCP_CONNECT".equals(state)) { + changingGateway.set(false); + } + } + + private boolean shouldCheckAppUpdate() { + return System.currentTimeMillis() - PreferenceHelper.getLastAppUpdateCheck(context) >= UPDATE_CHECK_TIMEOUT; + } + + private void selectNextGateway() { + changingGateway.set(true); + reconnectTry.set(0); + EipCommand.startVPN(context.getApplicationContext(), false, setupNClosestGateway.get() + 1); + } + + private void finishGatewaySetup(boolean changingGateway) { + VpnStatus.removeStateListener(this); + setupVpnProfile = null; + setupNClosestGateway.set(0); + observedProfileFromVpnStatus = null; + this.changingGateway.set(changingGateway); + this.reconnectTry.set(0); + } + + /** + * gets called as soon as a new VPN is about to launch + * + * @param uuid + */ + @Override + public void setConnectedVPN(String uuid) { + observedProfileFromVpnStatus = uuid; + } + + @Override + public void newLog(LogItem logItem) { + if (logItem.getLogLevel() == VpnStatus.LogLevel.ERROR) { + switch (logItem.getErrorType()) { + case SHAPESHIFTER: + VpnProfile profile = VpnStatus.getLastConnectedVpnProfile(); + if (profile == null) { + EipCommand.startVPN(context.getApplicationContext(), false, 0); + } else { + GatewaysManager gatewaysManager = new GatewaysManager(context.getApplicationContext()); + int position = gatewaysManager.getPosition(profile); + setupNClosestGateway.set(position >= 0 ? position : 0); + selectNextGateway(); + } + break; + default: + break; + + } + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java index f3eea415..1df54e6e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java @@ -32,16 +32,16 @@ import java.util.Set; import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConfigParser; import de.blinkt.openvpn.core.connection.Connection; -import se.leap.bitmaskclient.utils.PreferenceHelper; - -import static se.leap.bitmaskclient.Constants.HOST; -import static se.leap.bitmaskclient.Constants.IP_ADDRESS; -import static se.leap.bitmaskclient.Constants.LOCATION; -import static se.leap.bitmaskclient.Constants.LOCATIONS; -import static se.leap.bitmaskclient.Constants.NAME; -import static se.leap.bitmaskclient.Constants.OPENVPN_CONFIGURATION; -import static se.leap.bitmaskclient.Constants.TIMEZONE; -import static se.leap.bitmaskclient.Constants.VERSION; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; + +import static se.leap.bitmaskclient.base.models.Constants.HOST; +import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS; +import static se.leap.bitmaskclient.base.models.Constants.LOCATION; +import static se.leap.bitmaskclient.base.models.Constants.LOCATIONS; +import static se.leap.bitmaskclient.base.models.Constants.NAME; +import static se.leap.bitmaskclient.base.models.Constants.OPENVPN_CONFIGURATION; +import static se.leap.bitmaskclient.base.models.Constants.TIMEZONE; +import static se.leap.bitmaskclient.base.models.Constants.VERSION; /** * Gateway provides objects defining gateways and their metadata. diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java index 0ba0f207..33fd3c21 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java @@ -8,7 +8,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; -import static se.leap.bitmaskclient.utils.ConfigHelper.getCurrentTimezone; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.getCurrentTimezone; public class GatewaySelector { private final static String TAG = GatewaySelector.class.getSimpleName(); diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java index 354fd9a3..a5d4c416 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java @@ -35,15 +35,15 @@ import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConfigParser; import de.blinkt.openvpn.core.VpnStatus; import de.blinkt.openvpn.core.connection.Connection; -import se.leap.bitmaskclient.Provider; -import se.leap.bitmaskclient.ProviderObservable; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.base.models.ProviderObservable; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN; -import static se.leap.bitmaskclient.Constants.GATEWAYS; -import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; -import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports; +import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports; /** * @author parmegv 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 9a3c8f85..e6905448 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnLauncher.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnLauncher.java @@ -6,7 +6,7 @@ import android.net.VpnService; import android.os.Build; import android.os.Bundle; -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_BLOCKING_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START_BLOCKING_VPN; public class VoidVpnLauncher extends Activity { 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 78deea0b..77038492 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VoidVpnService.java @@ -33,14 +33,13 @@ import java.util.Observer; import de.blinkt.openvpn.core.ConnectionStatus; import de.blinkt.openvpn.core.VpnStatus; import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.VpnNotificationManager; - -import static se.leap.bitmaskclient.Constants.EIP_ACTION_START_ALWAYS_ON_VPN; -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; -import static se.leap.bitmaskclient.utils.ConfigHelper.getProviderFormattedString; + +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START_ALWAYS_ON_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START_BLOCKING_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_STOP_BLOCKING_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_IS_ALWAYS_ON; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString; public class VoidVpnService extends VpnService implements Observer, VpnNotificationManager.VpnServiceCallback { diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnCertificateValidator.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnCertificateValidator.java index 83904729..c747b731 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnCertificateValidator.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnCertificateValidator.java @@ -22,7 +22,7 @@ import java.security.cert.X509Certificate; import java.util.Calendar; import java.util.Date; -import se.leap.bitmaskclient.utils.ConfigHelper; +import se.leap.bitmaskclient.base.utils.ConfigHelper; public class VpnCertificateValidator { public final static String TAG = VpnCertificateValidator.class.getSimpleName(); diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java index 08e219c7..51069d6d 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java @@ -30,21 +30,21 @@ import java.util.Iterator; import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.ConfigParser; import de.blinkt.openvpn.core.connection.Connection; -import se.leap.bitmaskclient.Provider; +import se.leap.bitmaskclient.base.models.Provider; import se.leap.bitmaskclient.pluggableTransports.Obfs4Options; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4; import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN; -import static se.leap.bitmaskclient.Constants.CAPABILITIES; -import static se.leap.bitmaskclient.Constants.IP_ADDRESS; -import static se.leap.bitmaskclient.Constants.OPTIONS; -import static se.leap.bitmaskclient.Constants.PORTS; -import static se.leap.bitmaskclient.Constants.PROTOCOLS; -import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; -import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.Constants.REMOTE; -import static se.leap.bitmaskclient.Constants.TRANSPORT; -import static se.leap.bitmaskclient.Constants.TYPE; +import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES; +import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS; +import static se.leap.bitmaskclient.base.models.Constants.OPTIONS; +import static se.leap.bitmaskclient.base.models.Constants.PORTS; +import static se.leap.bitmaskclient.base.models.Constants.PROTOCOLS; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.base.models.Constants.REMOTE; +import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT; +import static se.leap.bitmaskclient.base.models.Constants.TYPE; import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_IP; import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_PORT; diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java new file mode 100644 index 00000000..b3ed5394 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java @@ -0,0 +1,355 @@ +/** + * 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.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.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.StyleSpan; +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.base.MainActivity; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.StartActivity; + +import static android.os.Build.VERSION_CODES.O; +import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; +import static androidx.core.app.NotificationCompat.PRIORITY_MAX; +import static androidx.core.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.base.models.Constants.ASK_TO_CANCEL_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_STOP_BLOCKING_VPN; +import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT; + +/** + * 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, + null, + tickerText, + status, + VoidVpnService.NOTIFICATION_CHANNEL_NEWSTATUS_ID, + PRIORITY_MAX, + 0, + getMainActivityIntent(), + 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, boolean isObfuscated, String msg, String tickerText, ConnectionStatus status, long when, String notificationChannelNewstatusId) { + String cancelString; + CharSequence bigmessage = null; + String ghostIcon = new String(Character.toChars(0x1f309)); + + switch (status) { + // show cancel if no connection + case LEVEL_START: + case LEVEL_NONETWORK: + case LEVEL_CONNECTING_SERVER_REPLIED: + case LEVEL_CONNECTING_NO_SERVER_REPLY_YET: + cancelString = context.getString(R.string.cancel); + if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection_try)); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg); + } + break; + + // show disconnect if connection exists + case LEVEL_CONNECTED: + if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { + Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection)); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg); + } + default: + cancelString = context.getString(R.string.cancel_connection); + } + + if (isObfuscated) { + msg = ghostIcon + " " + msg; + } + + NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action. + Builder(R.drawable.ic_menu_close_clear_cancel, cancelString, getDisconnectIntent()); + String title; + String appName = context.getString(R.string.app_name); + if (isEmpty(profileName)) { + title = appName; + } else { + title = context.getString(R.string.notifcation_title_bitmask, appName, profileName); + } + + PendingIntent contentIntent; + if (status == LEVEL_WAITING_FOR_USER_INPUT) + contentIntent = getUserInputIntent(msg); + else + contentIntent = getMainActivityIntent(); + + int priority; + if (OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID.equals(notificationChannelNewstatusId)) { + priority = PRIORITY_HIGH; + } else { + // background channel + priority = PRIORITY_MIN; + } + + buildVpnNotification( + title, + msg, + bigmessage, + 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); + mChannel.setSound(null, null); + 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); + mChannel.setSound(null, null); + 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.v_custom_notification); + 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, String message, CharSequence bigMessage, 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)) { + if (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, message)); + } + } else { + nCompatBuilder.setStyle(new NotificationCompat.BigTextStyle(). + setBigContentTitle(title). + bigText(bigMessage)); + } + nCompatBuilder.addAction(notificationAction); + nCompatBuilder.setContentTitle(title); + nCompatBuilder.setCategory(NotificationCompat.CATEGORY_SERVICE); + nCompatBuilder.setLocalOnly(true); + nCompatBuilder.setContentText(message); + 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 getMainActivityIntent() { + Intent startActivity = new Intent(context, StartActivity.class); + return PendingIntent.getActivity(context, 0, startActivity, 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, MainActivity.class); + disconnectVPN.setAction(ACTION_SHOW_VPN_FRAGMENT); + disconnectVPN.putExtra(ASK_TO_CANCEL_VPN, true); + disconnectVPN.addFlags(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/firewall/FirewallManager.java b/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallManager.java index ace8a298..dcb4a743 100644 --- a/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/FirewallManager.java @@ -28,7 +28,7 @@ import de.blinkt.openvpn.core.VpnStatus; import se.leap.bitmaskclient.R; import se.leap.bitmaskclient.tethering.TetheringObservable; import se.leap.bitmaskclient.tethering.TetheringState; -import se.leap.bitmaskclient.utils.PreferenceHelper; +import se.leap.bitmaskclient.base.utils.PreferenceHelper; public class FirewallManager implements FirewallCallback, Observer { public static String BITMASK_CHAIN = "bitmask_fw"; diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/SetupTetheringTask.java b/app/src/main/java/se/leap/bitmaskclient/firewall/SetupTetheringTask.java index edf79add..53118cae 100644 --- a/app/src/main/java/se/leap/bitmaskclient/firewall/SetupTetheringTask.java +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/SetupTetheringTask.java @@ -29,7 +29,7 @@ import se.leap.bitmaskclient.tethering.TetheringState; import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_FORWARD; import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_POSTROUTING; -import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; +import static se.leap.bitmaskclient.base.utils.Cmd.runBlockingCmd; public class SetupTetheringTask extends AsyncTask { diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownIPv6FirewallTask.java b/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownIPv6FirewallTask.java index 63d6074d..c14c579e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownIPv6FirewallTask.java +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownIPv6FirewallTask.java @@ -22,7 +22,7 @@ import android.util.Log; import java.lang.ref.WeakReference; import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_CHAIN; -import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; +import static se.leap.bitmaskclient.base.utils.Cmd.runBlockingCmd; class ShutdownIPv6FirewallTask extends AsyncTask { diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownTetheringTask.java b/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownTetheringTask.java index dcb3ccba..d867009a 100644 --- a/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownTetheringTask.java +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/ShutdownTetheringTask.java @@ -27,7 +27,7 @@ import se.leap.bitmaskclient.tethering.TetheringState; import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_FORWARD; import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_POSTROUTING; -import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; +import static se.leap.bitmaskclient.base.utils.Cmd.runBlockingCmd; public class ShutdownTetheringTask extends AsyncTask { diff --git a/app/src/main/java/se/leap/bitmaskclient/firewall/StartIPv6FirewallTask.java b/app/src/main/java/se/leap/bitmaskclient/firewall/StartIPv6FirewallTask.java index b01270e0..f2c8b739 100644 --- a/app/src/main/java/se/leap/bitmaskclient/firewall/StartIPv6FirewallTask.java +++ b/app/src/main/java/se/leap/bitmaskclient/firewall/StartIPv6FirewallTask.java @@ -22,7 +22,7 @@ import android.util.Log; import java.lang.ref.WeakReference; import static se.leap.bitmaskclient.firewall.FirewallManager.BITMASK_CHAIN; -import static se.leap.bitmaskclient.utils.Cmd.runBlockingCmd; +import static se.leap.bitmaskclient.base.utils.Cmd.runBlockingCmd; class StartIPv6FirewallTask extends AsyncTask { diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/AboutFragment.java b/app/src/main/java/se/leap/bitmaskclient/fragments/AboutFragment.java deleted file mode 100644 index b7743a75..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/fragments/AboutFragment.java +++ /dev/null @@ -1,67 +0,0 @@ -package se.leap.bitmaskclient.fragments; - -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Bundle; -import androidx.fragment.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import butterknife.ButterKnife; -import butterknife.InjectView; -import se.leap.bitmaskclient.BuildConfig; -import se.leap.bitmaskclient.R; - -import static android.view.View.VISIBLE; - -public class AboutFragment extends Fragment { - - final public static String TAG = AboutFragment.class.getSimpleName(); - final public static int VIEWED = 0; - - @InjectView(R.id.version) - TextView versionTextView; - - @InjectView(R.id.terms_of_service) - TextView termsOfService; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.f_about, container, false); - ButterKnife.inject(this, view); - return view; - } - - @Override - public void onStart() { - super.onStart(); - String version; - String name = "Bitmask"; - try { - PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo( - getActivity().getPackageName(), 0); - version = packageinfo.versionName; - name = getString(R.string.app_name); - } catch (NameNotFoundException e) { - version = "error fetching version"; - } - - versionTextView.setText(getString(R.string.version_info, name, version)); - - if (BuildConfig.FLAVOR_branding.equals("custom") && hasTermsOfServiceResource()) { - termsOfService.setText(getString(getTermsOfServiceResource())); - termsOfService.setVisibility(VISIBLE); - } - } - - private boolean hasTermsOfServiceResource() { - return getTermsOfServiceResource() != 0; - } - - private int getTermsOfServiceResource() { - return this.getContext().getResources().getIdentifier("terms_of_service", "string", this.getContext().getPackageName()); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java b/app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java deleted file mode 100644 index cb26e685..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/fragments/AlwaysOnDialog.java +++ /dev/null @@ -1,76 +0,0 @@ -package se.leap.bitmaskclient.fragments; - -import android.app.Dialog; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDialogFragment; -import androidx.appcompat.widget.AppCompatTextView; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.CheckBox; - -import butterknife.ButterKnife; -import butterknife.InjectView; -import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.views.IconTextView; - -import static se.leap.bitmaskclient.utils.PreferenceHelper.saveShowAlwaysOnDialog; - - -/** - * Created by cyberta on 25.02.18. - */ - - - -public class AlwaysOnDialog extends AppCompatDialogFragment { - - public final static String TAG = AlwaysOnDialog.class.getName(); - - @InjectView(R.id.do_not_show_again) - CheckBox doNotShowAgainCheckBox; - - @InjectView(R.id.user_message) - IconTextView userMessage; - - @InjectView(R.id.block_vpn_user_message) - AppCompatTextView blockVpnUserMessage; - - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - LayoutInflater inflater = getActivity().getLayoutInflater(); - View view = inflater.inflate(R.layout.d_checkbox_confirm, null); - ButterKnife.inject(this, view); - - userMessage.setIcon(R.drawable.ic_settings); - userMessage.setText(getString(R.string.always_on_vpn_user_message)); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - blockVpnUserMessage.setVisibility(View.VISIBLE); - } - - builder.setView(view) - .setPositiveButton(android.R.string.ok, (dialog, id) -> { - if (doNotShowAgainCheckBox.isChecked()) { - saveShowAlwaysOnDialog(getContext(), false); - } - Intent intent = new Intent("android.net.vpn.SETTINGS"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - }) - .setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()); - return builder.create(); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/DonationReminderDialog.java b/app/src/main/java/se/leap/bitmaskclient/fragments/DonationReminderDialog.java deleted file mode 100644 index 046acad4..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/fragments/DonationReminderDialog.java +++ /dev/null @@ -1,120 +0,0 @@ -package se.leap.bitmaskclient.fragments; - -import android.app.Dialog; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDialogFragment; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; - -import java.text.ParseException; - -import butterknife.ButterKnife; -import butterknife.InjectView; -import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.utils.DateHelper; -import se.leap.bitmaskclient.utils.PreferenceHelper; - -import static se.leap.bitmaskclient.Constants.DONATION_REMINDER_DURATION; -import static se.leap.bitmaskclient.Constants.DONATION_URL; -import static se.leap.bitmaskclient.Constants.ENABLE_DONATION; -import static se.leap.bitmaskclient.Constants.ENABLE_DONATION_REMINDER; -import static se.leap.bitmaskclient.Constants.FIRST_TIME_USER_DATE; -import static se.leap.bitmaskclient.Constants.LAST_DONATION_REMINDER_DATE; - -public class DonationReminderDialog extends AppCompatDialogFragment { - - public final static String TAG = DonationReminderDialog.class.getName(); - private static boolean isShown = false; - - @InjectView(R.id.btnDonate) - Button btnDonate; - - @InjectView(R.id.btnLater) - Button btnLater; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - LayoutInflater inflater = getActivity().getLayoutInflater(); - View view = inflater.inflate(R.layout.donation_reminder_dialog, null); - ButterKnife.inject(this, view); - isShown = true; - - builder.setView(view); - btnDonate.setOnClickListener(v -> { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL)); - try { - startActivity(browserIntent); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - } - PreferenceHelper.putString(getContext(), LAST_DONATION_REMINDER_DATE, - DateHelper.getCurrentDateString()); - dismiss(); - }); - btnLater.setOnClickListener(v -> { - PreferenceHelper.putString(getContext(), LAST_DONATION_REMINDER_DATE, - DateHelper.getCurrentDateString()); - dismiss(); - }); - - return builder.create(); - } - - public static boolean isCallable(Context context) { - if (isShown) { - return false; - } - - if (!ENABLE_DONATION || !ENABLE_DONATION_REMINDER) { - return false; - } - - if (context == null) { - Log.e(TAG, "context is null!"); - return false; - } - - String firstTimeUserDate = PreferenceHelper.getString(context, FIRST_TIME_USER_DATE, null); - if (firstTimeUserDate == null) { - PreferenceHelper.putString(context, FIRST_TIME_USER_DATE, DateHelper.getCurrentDateString()); - return false; - } - - try { - long diffDays; - - diffDays = DateHelper.getDateDiffToCurrentDateInDays(firstTimeUserDate); - if (diffDays < 1) { - return false; - } - - String lastDonationReminderDate = PreferenceHelper.getString(context, LAST_DONATION_REMINDER_DATE, null); - if (lastDonationReminderDate == null) { - return true; - } - diffDays = DateHelper.getDateDiffToCurrentDateInDays(lastDonationReminderDate); - return diffDays >= DONATION_REMINDER_DURATION; - - } catch (ParseException e) { - e.printStackTrace(); - Log.e(TAG, e.getMessage()); - return false; - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/ExcludeAppsFragment.java b/app/src/main/java/se/leap/bitmaskclient/fragments/ExcludeAppsFragment.java deleted file mode 100644 index 9559978b..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/fragments/ExcludeAppsFragment.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright (c) 2012-2016 Arne Schwabe - * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt - */ - -package se.leap.bitmaskclient.fragments; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.os.Bundle; -import androidx.fragment.app.Fragment; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.CompoundButton; -import android.widget.Filter; -import android.widget.Filterable; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.SearchView; -import android.widget.TextView; - -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.Vector; - -import de.blinkt.openvpn.VpnProfile; -import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.utils.PreferenceHelper; - -/** - * Created by arne on 16.11.14. - */ -public class ExcludeAppsFragment extends Fragment implements AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, View.OnClickListener { - private ListView mListView; - private VpnProfile mProfile; - private PackageAdapter mListAdapter; - - private Set apps; - - public interface ExcludedAppsCallback { - void onAppsExcluded(int number); - } - - private ExcludedAppsCallback callback; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (context instanceof ExcludedAppsCallback) { - callback = (ExcludedAppsCallback) context; - } - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - AppViewHolder avh = (AppViewHolder) view.getTag(); - avh.checkBox.toggle(); - } - - @Override - public void onClick(View v) { - - } - - static class AppViewHolder { - public ApplicationInfo mInfo; - public View rootView; - public TextView appName; - public ImageView appIcon; - //public TextView appSize; - //public TextView disabled; - public CompoundButton checkBox; - - static public AppViewHolder createOrRecycle(LayoutInflater inflater, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = inflater.inflate(R.layout.allowed_application_layout, parent, false); - - // Creates a ViewHolder and store references to the two children views - // we want to bind data to. - AppViewHolder holder = new AppViewHolder(); - holder.rootView = convertView; - holder.appName = convertView.findViewById(R.id.app_name); - holder.appIcon = convertView.findViewById(R.id.app_icon); - holder.checkBox = convertView.findViewById(R.id.app_selected); - convertView.setTag(holder); - - return holder; - } else { - // Get the ViewHolder back to get fast access to the TextView - // and the ImageView. - return (AppViewHolder) convertView.getTag(); - } - } - - } - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - String packageName = (String) buttonView.getTag(); - - if (isChecked) { - Log.d("openvpn", "adding to allowed apps" + packageName); - apps.add(packageName); - - } else { - Log.d("openvpn", "removing from allowed apps" + packageName); - apps.remove(packageName); - } - - if (callback != null) { - callback.onAppsExcluded(apps.size()); - } - } - - class PackageAdapter extends BaseAdapter implements Filterable { - private Vector mPackages; - private final LayoutInflater mInflater; - private final PackageManager mPm; - private ItemFilter mFilter = new ItemFilter(); - private Vector mFilteredData; - - - private class ItemFilter extends Filter { - @Override - protected FilterResults performFiltering(CharSequence constraint) { - - String filterString = constraint.toString().toLowerCase(Locale.getDefault()); - - FilterResults results = new FilterResults(); - - - int count = mPackages.size(); - final Vector nlist = new Vector<>(count); - - for (int i = 0; i < count; i++) { - ApplicationInfo pInfo = mPackages.get(i); - CharSequence appName = pInfo.loadLabel(mPm); - - if (TextUtils.isEmpty(appName)) - appName = pInfo.packageName; - - if (appName instanceof String) { - if (((String) appName).toLowerCase(Locale.getDefault()).contains(filterString)) - nlist.add(pInfo); - } else { - if (appName.toString().toLowerCase(Locale.getDefault()).contains(filterString)) - nlist.add(pInfo); - } - } - results.values = nlist; - results.count = nlist.size(); - - return results; - } - - @Override - protected void publishResults(CharSequence constraint, FilterResults results) { - mFilteredData = (Vector) results.values; - notifyDataSetChanged(); - } - - } - - - PackageAdapter(Context c, VpnProfile vp) { - mPm = c.getPackageManager(); - mProfile = vp; - mInflater = LayoutInflater.from(c); - - mPackages = new Vector<>(); - mFilteredData = mPackages; - } - - private void populateList(Activity c) { - List installedPackages = mPm.getInstalledApplications(PackageManager.GET_META_DATA); - - // Remove apps not using Internet - - int androidSystemUid = 0; - ApplicationInfo system = null; - Vector apps = new Vector(); - - try { - system = mPm.getApplicationInfo("android", PackageManager.GET_META_DATA); - androidSystemUid = system.uid; - apps.add(system); - } catch (PackageManager.NameNotFoundException e) { - } - - - for (ApplicationInfo app : installedPackages) { - - if (mPm.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED && - app.uid != androidSystemUid) { - - apps.add(app); - } - } - - Collections.sort(apps, new ApplicationInfo.DisplayNameComparator(mPm)); - mPackages = apps; - mFilteredData = apps; - c.runOnUiThread(new Runnable() { - @Override - public void run() { - notifyDataSetChanged(); - } - }); - } - - @Override - public int getCount() { - return mFilteredData.size(); - } - - @Override - public Object getItem(int position) { - return mFilteredData.get(position); - } - - @Override - public long getItemId(int position) { - return mFilteredData.get(position).packageName.hashCode(); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - AppViewHolder viewHolder = AppViewHolder.createOrRecycle(mInflater, convertView, parent); - - viewHolder.mInfo = mFilteredData.get(position); - final ApplicationInfo mInfo = mFilteredData.get(position); - - - CharSequence appName = mInfo.loadLabel(mPm); - - if (TextUtils.isEmpty(appName)) - appName = mInfo.packageName; - viewHolder.appName.setText(appName); - viewHolder.appIcon.setImageDrawable(mInfo.loadIcon(mPm)); - viewHolder.checkBox.setTag(mInfo.packageName); - viewHolder.checkBox.setOnCheckedChangeListener(ExcludeAppsFragment.this); - viewHolder.checkBox.setChecked(apps.contains(mInfo.packageName)); - - return viewHolder.rootView; - } - - @Override - public Filter getFilter() { - return mFilter; - } - } - - @Override - public void onResume() { - super.onResume(); - } - - @Override - public void onDestroy() { - PreferenceHelper.setExcludedApps(this.getActivity().getApplicationContext(), apps); - super.onDestroy(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - apps = PreferenceHelper.getExcludedApps(this.getContext()); - - setHasOptionsMenu(true); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.allowed_apps, menu); - - SearchView searchView = (SearchView) menu.findItem( R.id.app_search_widget ).getActionView(); - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - mListView.setFilterText(query); - mListView.setTextFilterEnabled(true); - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - mListView.setFilterText(newText); - if (TextUtils.isEmpty(newText)) - mListView.setTextFilterEnabled(false); - else - mListView.setTextFilterEnabled(true); - - return true; - } - }); - searchView.setOnCloseListener(() -> { - mListView.clearTextFilter(); - mListAdapter.getFilter().filter(""); - return false; - }); - - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.allowed_vpn_apps, container, false); - - mListView = v.findViewById(android.R.id.list); - - mListAdapter = new PackageAdapter(getActivity(), mProfile); - mListView.setAdapter(mListAdapter); - mListView.setOnItemClickListener(this); - - mListView.setEmptyView(v.findViewById(R.id.loading_container)); - - new Thread(() -> mListAdapter.populateList(getActivity())).start(); - - return v; - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/LogFragment.java b/app/src/main/java/se/leap/bitmaskclient/fragments/LogFragment.java deleted file mode 100644 index 19787dc3..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/fragments/LogFragment.java +++ /dev/null @@ -1,587 +0,0 @@ -/* - * Copyright (c) 2012-2016 Arne Schwabe - * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt - */ - -package se.leap.bitmaskclient.fragments; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.database.DataSetObserver; -import android.os.Bundle; -import android.os.Handler; -import android.os.Handler.Callback; -import android.os.Message; -import android.preference.PreferenceManager; -import androidx.annotation.Nullable; -import androidx.fragment.app.ListFragment; -import android.text.SpannableString; -import android.text.format.DateFormat; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.CheckBox; -import android.widget.LinearLayout; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.RadioGroup; -import android.widget.SeekBar; -import android.widget.TextView; -import android.widget.Toast; - -import java.text.SimpleDateFormat; -import java.util.Collections; -import java.util.Date; -import java.util.Locale; -import java.util.Vector; - -import de.blinkt.openvpn.VpnProfile; -import de.blinkt.openvpn.core.ConnectionStatus; -import de.blinkt.openvpn.core.LogItem; -import de.blinkt.openvpn.core.OpenVPNManagement; -import de.blinkt.openvpn.core.OpenVPNService; -import de.blinkt.openvpn.core.Preferences; -import de.blinkt.openvpn.core.VpnStatus; -import de.blinkt.openvpn.core.VpnStatus.LogListener; -import de.blinkt.openvpn.core.VpnStatus.StateListener; -import se.leap.bitmaskclient.Constants; -import se.leap.bitmaskclient.R; - -import static de.blinkt.openvpn.core.OpenVPNService.humanReadableByteCount; - -public class LogFragment extends ListFragment implements StateListener, SeekBar.OnSeekBarChangeListener, RadioGroup.OnCheckedChangeListener, VpnStatus.ByteCountListener { - public static final String TAG = LogFragment.class.getSimpleName(); - private static final String LOGTIMEFORMAT = "logtimeformat"; - private static final String VERBOSITYLEVEL = "verbositylevel"; - - - - private SeekBar mLogLevelSlider; - private LinearLayout mOptionsLayout; - private RadioGroup mTimeRadioGroup; - private TextView mUpStatus; - private TextView mDownStatus; - private TextView mConnectStatus; - private boolean mShowOptionsLayout; - private CheckBox mClearLogCheckBox; - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - ladapter.setLogLevel(progress + 1); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onCheckedChanged(RadioGroup group, int checkedId) { - switch (checkedId) { - case R.id.radioISO: - ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_ISO); - break; - case R.id.radioNone: - ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_NONE); - break; - case R.id.radioShort: - ladapter.setTimeFormat(LogWindowListAdapter.TIME_FORMAT_SHORT); - break; - - } - } - - @Override - public void updateByteCount(long in, long out, long diffIn, long diffOut) { - //%2$s/s %1$s - ↑%4$s/s %3$s - Resources res = getActivity().getResources(); - final String down = String.format("%2$s %1$s", humanReadableByteCount(in, false, res), humanReadableByteCount(diffIn / OpenVPNManagement.mBytecountInterval, true, res)); - final String up = String.format("%2$s %1$s", humanReadableByteCount(out, false, res), humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, res)); - - if (mUpStatus != null && mDownStatus != null) { - if (getActivity() != null) { - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - mUpStatus.setText(up); - mDownStatus.setText(down); - } - }); - } - } - - } - - - class LogWindowListAdapter implements ListAdapter, LogListener, Callback { - - private static final int MESSAGE_NEWLOG = 0; - - private static final int MESSAGE_CLEARLOG = 1; - - private static final int MESSAGE_NEWTS = 2; - private static final int MESSAGE_NEWLOGLEVEL = 3; - - public static final int TIME_FORMAT_NONE = 0; - public static final int TIME_FORMAT_SHORT = 1; - public static final int TIME_FORMAT_ISO = 2; - private static final int MAX_STORED_LOG_ENTRIES = 1000; - - private Vector allEntries = new Vector<>(); - - private Vector currentLevelEntries = new Vector(); - - private Handler mHandler; - - private Vector observers = new Vector(); - - private int mTimeFormat = 0; - private int mLogLevel = 3; - - - public LogWindowListAdapter() { - initLogBuffer(); - if (mHandler == null) { - mHandler = new Handler(this); - } - - VpnStatus.addLogListener(this); - } - - - private void initLogBuffer() { - allEntries.clear(); - Collections.addAll(allEntries, VpnStatus.getlogbuffer()); - initCurrentMessages(); - } - - String getLogStr() { - String str = ""; - for (LogItem entry : allEntries) { - str += getTime(entry, TIME_FORMAT_ISO) + entry.getString(getActivity()) + '\n'; - } - return str; - } - - - private void shareLog() { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr()); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.ics_openvpn_log_file)); - shareIntent.setType("text/plain"); - startActivity(Intent.createChooser(shareIntent, "Send Logfile")); - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - observers.add(observer); - - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - observers.remove(observer); - } - - @Override - public int getCount() { - return currentLevelEntries.size(); - } - - @Override - public Object getItem(int position) { - return currentLevelEntries.get(position); - } - - @Override - public long getItemId(int position) { - return ((Object) currentLevelEntries.get(position)).hashCode(); - } - - @Override - public boolean hasStableIds() { - return true; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - TextView v; - if (convertView == null) - v = new TextView(getActivity()); - else - v = (TextView) convertView; - - LogItem le = currentLevelEntries.get(position); - String msg = le.getString(getActivity()); - String time = getTime(le, mTimeFormat); - msg = time + msg; - - int spanStart = time.length(); - - SpannableString t = new SpannableString(msg); - - v.setText(t); - return v; - } - - private String getTime(LogItem le, int time) { - if (time != TIME_FORMAT_NONE) { - Date d = new Date(le.getLogtime()); - java.text.DateFormat timeformat; - if (time == TIME_FORMAT_ISO) - timeformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - else - timeformat = DateFormat.getTimeFormat(getActivity()); - - return timeformat.format(d) + " "; - - } else { - return ""; - } - - } - - @Override - public int getItemViewType(int position) { - return 0; - } - - @Override - public int getViewTypeCount() { - return 1; - } - - @Override - public boolean isEmpty() { - return currentLevelEntries.isEmpty(); - - } - - @Override - public boolean areAllItemsEnabled() { - return true; - } - - @Override - public boolean isEnabled(int position) { - return true; - } - - @Override - public void newLog(LogItem logMessage) { - Message msg = Message.obtain(); - assert (msg != null); - msg.what = MESSAGE_NEWLOG; - Bundle bundle = new Bundle(); - bundle.putParcelable("logmessage", logMessage); - msg.setData(bundle); - mHandler.sendMessage(msg); - } - - @Override - public boolean handleMessage(Message msg) { - // We have been called - if (msg.what == MESSAGE_NEWLOG) { - - LogItem logMessage = msg.getData().getParcelable("logmessage"); - if (addLogMessage(logMessage)) - for (DataSetObserver observer : observers) { - observer.onChanged(); - } - } else if (msg.what == MESSAGE_CLEARLOG) { - for (DataSetObserver observer : observers) { - observer.onInvalidated(); - } - initLogBuffer(); - } else if (msg.what == MESSAGE_NEWTS) { - for (DataSetObserver observer : observers) { - observer.onInvalidated(); - } - } else if (msg.what == MESSAGE_NEWLOGLEVEL) { - initCurrentMessages(); - - for (DataSetObserver observer : observers) { - observer.onChanged(); - } - - } - - return true; - } - - private void initCurrentMessages() { - currentLevelEntries.clear(); - for (LogItem li : allEntries) { - if (li.getVerbosityLevel() <= mLogLevel || - mLogLevel == VpnProfile.MAXLOGLEVEL) - currentLevelEntries.add(li); - } - } - - /** - * @param logmessage - * @return True if the current entries have changed - */ - private boolean addLogMessage(LogItem logmessage) { - allEntries.add(logmessage); - - if (allEntries.size() > MAX_STORED_LOG_ENTRIES) { - Vector oldAllEntries = allEntries; - allEntries = new Vector(allEntries.size()); - for (int i = 50; i < oldAllEntries.size(); i++) { - allEntries.add(oldAllEntries.elementAt(i)); - } - initCurrentMessages(); - return true; - } else { - if (logmessage.getVerbosityLevel() <= mLogLevel) { - currentLevelEntries.add(logmessage); - return true; - } else { - return false; - } - } - } - - void clearLog() { - // Actually is probably called from GUI Thread as result of the user - // pressing a button. But better safe than sorry - VpnStatus.clearLog(); - VpnStatus.logInfo(R.string.logCleared); - mHandler.sendEmptyMessage(MESSAGE_CLEARLOG); - } - - - public void setTimeFormat(int newTimeFormat) { - mTimeFormat = newTimeFormat; - mHandler.sendEmptyMessage(MESSAGE_NEWTS); - } - - public void setLogLevel(int logLevel) { - mLogLevel = logLevel; - mHandler.sendEmptyMessage(MESSAGE_NEWLOGLEVEL); - } - - } - - - private LogWindowListAdapter ladapter; - private TextView mSpeedView; - - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.clearlog) { - ladapter.clearLog(); - return true; - } else if (item.getItemId() == R.id.send) { - ladapter.shareLog(); - } else if (item.getItemId() == R.id.toggle_time) { - showHideOptionsPanel(); - } - return super.onOptionsItemSelected(item); - - } - - private void showHideOptionsPanel() { - boolean optionsVisible = (mOptionsLayout.getVisibility() != View.GONE); - - ObjectAnimator anim; - if (optionsVisible) { - anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 1.0f, 0f); - anim.addListener(collapseListener); - - } else { - mOptionsLayout.setVisibility(View.VISIBLE); - anim = ObjectAnimator.ofFloat(mOptionsLayout, "alpha", 0f, 1.0f); - //anim = new TranslateAnimation(0.0f, 0.0f, mOptionsLayout.getHeight(), 0.0f); - - } - - //anim.setInterpolator(new AccelerateInterpolator(1.0f)); - //anim.setDuration(300); - //mOptionsLayout.startAnimation(anim); - anim.start(); - - } - - AnimatorListenerAdapter collapseListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - mOptionsLayout.setVisibility(View.GONE); - } - - }; - - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.f_log, menu); - if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) - menu.removeItem(R.id.toggle_time); - } - - - @Override - public void onResume() { - super.onResume(); - Intent intent = new Intent(getActivity(), OpenVPNService.class); - intent.setAction(OpenVPNService.START_SERVICE); - } - - @Override - public void onStart() { - super.onStart(); - VpnStatus.addStateListener(this); - VpnStatus.addByteCountListener(this); - } - - @Override - public void onStop() { - super.onStop(); - VpnStatus.removeStateListener(this); - VpnStatus.removeByteCountListener(this); - - getActivity().getPreferences(0).edit().putInt(LOGTIMEFORMAT, ladapter.mTimeFormat) - .putInt(VERBOSITYLEVEL, ladapter.mLogLevel).apply(); - - } - - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - ListView lv = getListView(); - - lv.setOnItemLongClickListener(new OnItemLongClickListener() { - - @Override - public boolean onItemLongClick(AdapterView parent, View view, - int position, long id) { - ClipboardManager clipboard = (ClipboardManager) - getActivity().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Log Entry", ((TextView) view).getText()); - clipboard.setPrimaryClip(clip); - Toast.makeText(getActivity(), R.string.copied_entry, Toast.LENGTH_SHORT).show(); - return true; - } - }); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.f_log, container, false); - - setHasOptionsMenu(true); - - ladapter = new LogWindowListAdapter(); - ladapter.mTimeFormat = getActivity().getPreferences(0).getInt(LOGTIMEFORMAT, 1); - int logLevel = getActivity().getPreferences(0).getInt(VERBOSITYLEVEL, 1); - ladapter.setLogLevel(logLevel); - - setListAdapter(ladapter); - - mTimeRadioGroup = v.findViewById(R.id.timeFormatRadioGroup); - mTimeRadioGroup.setOnCheckedChangeListener(this); - - if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_ISO) { - mTimeRadioGroup.check(R.id.radioISO); - } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_NONE) { - mTimeRadioGroup.check(R.id.radioNone); - } else if (ladapter.mTimeFormat == LogWindowListAdapter.TIME_FORMAT_SHORT) { - mTimeRadioGroup.check(R.id.radioShort); - } - - mClearLogCheckBox = v.findViewById(R.id.clearlogconnect); - mClearLogCheckBox.setChecked(PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean(Constants.CLEARLOG, true)); - mClearLogCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> - Preferences.getDefaultSharedPreferences(getActivity()).edit().putBoolean(Constants.CLEARLOG, isChecked).apply()); - - mSpeedView = v.findViewById(R.id.speed); - - mOptionsLayout = v.findViewById(R.id.logOptionsLayout); - mLogLevelSlider = v.findViewById(R.id.LogLevelSlider); - mLogLevelSlider.setMax(VpnProfile.MAXLOGLEVEL - 1); - mLogLevelSlider.setProgress(logLevel - 1); - - mLogLevelSlider.setOnSeekBarChangeListener(this); - - if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) - mOptionsLayout.setVisibility(View.VISIBLE); - - mUpStatus = v.findViewById(R.id.speedUp); - mDownStatus = v.findViewById(R.id.speedDown); - mConnectStatus = v.findViewById(R.id.speedStatus); - if (mShowOptionsLayout) - mOptionsLayout.setVisibility(View.VISIBLE); - return v; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - // Scroll to the end of the list end - //getListView().setSelection(getListView().getAdapter().getCount()-1); - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (getResources().getBoolean(R.bool.logSildersAlwaysVisible)) { - mShowOptionsLayout = true; - if (mOptionsLayout != null) - mOptionsLayout.setVisibility(View.VISIBLE); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - } - - - @Override - public void updateState(final String status, final String logMessage, final int resId, final ConnectionStatus level) { - if (isAdded()) { - final String cleanLogMessage = VpnStatus.getLastCleanLogMessage(getActivity()); - - getActivity().runOnUiThread(() -> { - if (isAdded()) { - if (mSpeedView != null) { - mSpeedView.setText(cleanLogMessage); - } - if (mConnectStatus != null) - mConnectStatus.setText(cleanLogMessage); - } - }); - } - } - - @Override - public void setConnectedVPN(String uuid) { - } - - - @Override - public void onDestroy() { - VpnStatus.removeLogListener(ladapter); - super.onDestroy(); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java b/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java deleted file mode 100644 index 24e3c814..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/fragments/TetheringDialog.java +++ /dev/null @@ -1,258 +0,0 @@ -package se.leap.bitmaskclient.fragments; - -import android.app.Dialog; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.provider.Settings; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDialogFragment; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import java.util.Observable; -import java.util.Observer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import butterknife.ButterKnife; -import butterknife.InjectView; -import de.blinkt.openvpn.core.VpnStatus; -import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.firewall.FirewallManager; -import se.leap.bitmaskclient.tethering.TetheringObservable; -import se.leap.bitmaskclient.utils.PreferenceHelper; -import se.leap.bitmaskclient.views.IconCheckboxEntry; - -/** - * Copyright (c) 2020 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 . - */ - -public class TetheringDialog extends AppCompatDialogFragment implements Observer { - - public final static String TAG = TetheringDialog.class.getName(); - - @InjectView(R.id.tvTitle) - AppCompatTextView title; - - @InjectView(R.id.user_message) - AppCompatTextView userMessage; - - @InjectView(R.id.selection_list_view) - RecyclerView selectionListView; - DialogListAdapter adapter; - private DialogListAdapter.ViewModel[] dataset; - - public static class DialogListAdapter extends RecyclerView.Adapter { - - interface OnItemClickListener { - void onItemClick(ViewModel item); - } - - private ViewModel[] dataSet; - private OnItemClickListener clickListener; - - DialogListAdapter(ViewModel[] dataSet, OnItemClickListener clickListener) { - this.dataSet = dataSet; - this.clickListener = clickListener; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - IconCheckboxEntry v = new IconCheckboxEntry(viewGroup.getContext()); - return new ViewHolder(v); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { - viewHolder.bind(dataSet[i], clickListener); - } - - @Override - public int getItemCount() { - return dataSet.length; - } - - public static class ViewModel { - - public Drawable image; - public String text; - public boolean checked; - public boolean enabled; - - ViewModel(Drawable image, String text, boolean checked, boolean enabled) { - this.image = image; - this.text = text; - this.checked = checked; - this.enabled = enabled; - } - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - - ViewHolder(IconCheckboxEntry v) { - super(v); - } - - public void bind(ViewModel model, OnItemClickListener onClickListener) { - ((IconCheckboxEntry) this.itemView).bind(model); - this.itemView.setOnClickListener(v -> { - model.checked = !model.checked; - ((IconCheckboxEntry) itemView).setChecked(model.checked); - onClickListener.onItemClick(model); - }); - } - } - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - LayoutInflater inflater = getActivity().getLayoutInflater(); - View view = inflater.inflate(R.layout.d_list_selection, null); - ButterKnife.inject(this, view); - - title.setText(R.string.tethering); - userMessage.setMovementMethod(LinkMovementMethod.getInstance()); - userMessage.setLinkTextColor(getContext().getResources().getColor(R.color.colorPrimary)); - userMessage.setText(createUserMessage()); - - initDataset(); - adapter = new DialogListAdapter(dataset, this::onItemClick); - selectionListView.setAdapter(adapter); - selectionListView.setLayoutManager(new LinearLayoutManager(getActivity())); - - - builder.setView(view) - .setPositiveButton(android.R.string.ok, (dialog, id) -> { - PreferenceHelper.allowWifiTethering(getContext(), dataset[0].checked); - PreferenceHelper.allowUsbTethering(getContext(), dataset[1].checked); - PreferenceHelper.allowBluetoothTethering(getContext(), dataset[2].checked); - TetheringObservable.allowVpnWifiTethering(dataset[0].checked); - TetheringObservable.allowVpnUsbTethering(dataset[1].checked); - TetheringObservable.allowVpnBluetoothTethering(dataset[2].checked); - FirewallManager firewallManager = new FirewallManager(getContext().getApplicationContext(), false); - if (VpnStatus.isVPNActive()) { - if (TetheringObservable.getInstance().getTetheringState().hasAnyDeviceTetheringEnabled() && - TetheringObservable.getInstance().getTetheringState().hasAnyVpnTetheringAllowed()) { - firewallManager.startTethering(); - } else { - firewallManager.stopTethering(); - } - } - }).setNegativeButton(R.string.cancel, (dialog, id) -> dialog.cancel()); - return builder.create(); - } - - @Override - public void onResume() { - super.onResume(); - dataset[0].enabled = TetheringObservable.getInstance().isWifiTetheringEnabled(); - dataset[1].enabled = TetheringObservable.getInstance().isUsbTetheringEnabled(); - dataset[2].enabled = TetheringObservable.getInstance().isBluetoothTetheringEnabled(); - adapter.notifyDataSetChanged(); - TetheringObservable.getInstance().addObserver(this); - } - - @Override - public void onPause() { - super.onPause(); - TetheringObservable.getInstance().deleteObserver(this); - } - - public void onItemClick(DialogListAdapter.ViewModel item) { - - } - - private CharSequence createUserMessage() { - String tetheringMessage = getString(R.string.tethering_message); - String systemSettingsMessage = getString(R.string.tethering_enabled_message); - Pattern pattern = Pattern.compile("([\\w .]*)()+([\\w ]*)()([\\w .]*)"); - Matcher matcher = pattern.matcher(systemSettingsMessage); - int startIndex = 0; - int endIndex = 0; - if (matcher.matches()) { - startIndex = matcher.start(2); - endIndex = startIndex + matcher.group(3).length(); - } - systemSettingsMessage = systemSettingsMessage.replace("", "").replace("", ""); - String wholeMessage = systemSettingsMessage + "\n\n" + tetheringMessage; - Spannable spannable = new SpannableString(wholeMessage); - spannable.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View widget) { - try { - final Intent intent = new Intent(Intent.ACTION_MAIN, null); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - final ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.TetherSettings"); - intent.setComponent(cn); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } catch (ActivityNotFoundException e) { - Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); - startActivity(intent); - } - - } - }, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - - return spannable; - } - - private void initDataset() { - dataset = new DialogListAdapter.ViewModel[] { - new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_wifi), - getContext().getString(R.string.tethering_wifi), - PreferenceHelper.isWifiTetheringAllowed(getContext()), - TetheringObservable.getInstance().isWifiTetheringEnabled()), - new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_usb), - getContext().getString(R.string.tethering_usb), - PreferenceHelper.isUsbTetheringAllowed(getContext()), - TetheringObservable.getInstance().isUsbTetheringEnabled()), - new DialogListAdapter.ViewModel(getContext().getResources().getDrawable(R.drawable.ic_bluetooth), - getContext().getString(R.string.tethering_bluetooth), - PreferenceHelper.isBluetoothTetheringAllowed(getContext()), - TetheringObservable.getInstance().isUsbTetheringEnabled()) - }; - } - - @Override - public void update(Observable o, Object arg) { - if (o instanceof TetheringObservable) { - TetheringObservable observable = (TetheringObservable) o; - Log.d(TAG, "TetheringObservable is updated"); - dataset[0].enabled = observable.isWifiTetheringEnabled(); - dataset[1].enabled = observable.isUsbTetheringEnabled(); - dataset[2].enabled = observable.isBluetoothTetheringEnabled(); - adapter.notifyDataSetChanged(); - } - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java new file mode 100644 index 00000000..23c750a3 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2017 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.providersetup; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.core.app.JobIntentService; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator; + +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; + +/** + * Implements HTTP api methods (encapsulated in {{@link ProviderApiManager}}) + * used to manage communications with the provider server. + *

+ * It's an JobIntentService because it downloads data from the Internet, so it operates in the background. + * + * @author parmegv + * @author MeanderingCode + * @author cyberta + */ + +public class ProviderAPI extends JobIntentService implements ProviderApiManagerBase.ProviderApiServiceCallback { + + /** + * Unique job ID for this service. + */ + static final int JOB_ID = 161375; + + final public static String + TAG = ProviderAPI.class.getSimpleName(), + SET_UP_PROVIDER = "setUpProvider", + UPDATE_PROVIDER_DETAILS = "updateProviderDetails", + DOWNLOAD_GEOIP_JSON = "downloadGeoIpJson", + SIGN_UP = "srpRegister", + LOG_IN = "srpAuth", + LOG_OUT = "logOut", + DOWNLOAD_VPN_CERTIFICATE = "downloadUserAuthedVPNCertificate", + UPDATE_INVALID_VPN_CERTIFICATE = "ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE", + PARAMETERS = "parameters", + RECEIVER_KEY = "receiver", + ERRORS = "errors", + ERRORID = "errorId", + BACKEND_ERROR_KEY = "error", + BACKEND_ERROR_MESSAGE = "message", + USER_MESSAGE = "userMessage", + DOWNLOAD_SERVICE_JSON = "ProviderAPI.DOWNLOAD_SERVICE_JSON"; + + final public static int + SUCCESSFUL_LOGIN = 3, + FAILED_LOGIN = 4, + SUCCESSFUL_SIGNUP = 5, + FAILED_SIGNUP = 6, + SUCCESSFUL_LOGOUT = 7, + LOGOUT_FAILED = 8, + CORRECTLY_DOWNLOADED_VPN_CERTIFICATE = 9, + INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE = 10, + PROVIDER_OK = 11, + PROVIDER_NOK = 12, + CORRECTLY_DOWNLOADED_EIP_SERVICE = 13, + INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14, + CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE = 15, + INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE = 16, + CORRECTLY_DOWNLOADED_GEOIP_JSON = 17, + INCORRECTLY_DOWNLOADED_GEOIP_JSON = 18; + + ProviderApiManager providerApiManager; + + //TODO: refactor me, please! + //used in insecure flavor only + @SuppressLint("unused") + public static boolean lastDangerOn() { + return ProviderApiManager.lastDangerOn(); + } + + @Override + public void onCreate() { + super.onCreate(); + providerApiManager = initApiManager(); + } + + /** + * Convenience method for enqueuing work in to this service. + */ + static void enqueueWork(Context context, Intent work) { + try { + ProviderAPI.enqueueWork(context, ProviderAPI.class, JOB_ID, work); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + + @Override + protected void onHandleWork(@NonNull Intent command) { + providerApiManager.handleIntent(command); + } + + @Override + public void broadcastEvent(Intent intent) { + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + private ProviderApiManager initApiManager() { + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources()); + return new ProviderApiManager(preferences, getResources(), clientGenerator, this); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java new file mode 100644 index 00000000..79a107d1 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java @@ -0,0 +1,86 @@ +package se.leap.bitmaskclient.providersetup; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.ResultReceiver; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import se.leap.bitmaskclient.base.models.Constants; +import se.leap.bitmaskclient.base.models.Provider; + +public class ProviderAPICommand { + private static final String TAG = ProviderAPICommand.class.getSimpleName(); + private Context context; + + private String action; + private Bundle parameters; + private ResultReceiver resultReceiver; + private Provider provider; + + private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Provider provider, ResultReceiver resultReceiver) { + this(context.getApplicationContext(), action, Bundle.EMPTY, provider, resultReceiver); + } + private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Provider provider) { + this(context.getApplicationContext(), action, Bundle.EMPTY, provider); + } + + private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Bundle parameters, @NotNull Provider provider) { + this(context.getApplicationContext(), action, parameters, provider, null); + } + + private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Bundle parameters, @NotNull Provider provider, @Nullable ResultReceiver resultReceiver) { + super(); + this.context = context; + this.action = action; + this.parameters = parameters; + this.resultReceiver = resultReceiver; + this.provider = provider; + } + + private boolean isInitialized() { + return context != null; + } + + private void execute() { + if (isInitialized()) { + Intent intent = setUpIntent(); + ProviderAPI.enqueueWork(context, intent); + } + } + + private Intent setUpIntent() { + Intent command = new Intent(context, ProviderAPI.class); + + command.setAction(action); + command.putExtra(ProviderAPI.PARAMETERS, parameters); + if (resultReceiver != null) { + command.putExtra(ProviderAPI.RECEIVER_KEY, resultReceiver); + } + command.putExtra(Constants.PROVIDER_KEY, provider); + + return command; + } + + public static void execute(Context context, String action, @NotNull Provider provider) { + ProviderAPICommand command = new ProviderAPICommand(context, action, provider); + command.execute(); + } + + public static void execute(Context context, String action, Bundle parameters, @NotNull Provider provider) { + ProviderAPICommand command = new ProviderAPICommand(context, action, parameters, provider); + command.execute(); + } + + public static void execute(Context context, String action, Bundle parameters, @NotNull Provider provider, ResultReceiver resultReceiver) { + ProviderAPICommand command = new ProviderAPICommand(context, action, parameters, provider, resultReceiver); + command.execute(); + } + + public static void execute(Context context, String action, @NotNull Provider provider, ResultReceiver resultReceiver) { + ProviderAPICommand command = new ProviderAPICommand(context, action, provider, resultReceiver); + command.execute(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java new file mode 100644 index 00000000..ba902566 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java @@ -0,0 +1,98 @@ +/** + * 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.providersetup; + +import androidx.annotation.NonNull; +import android.util.Pair; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Locale; +import java.util.Scanner; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * Created by cyberta on 08.01.18. + */ + +public class ProviderApiConnector { + + private static final MediaType JSON + = MediaType.parse("application/json; charset=utf-8"); + + + public static boolean delete(OkHttpClient okHttpClient, String deleteUrl) { + try { + Request.Builder requestBuilder = new Request.Builder() + .url(deleteUrl) + .delete(); + Request request = requestBuilder.build(); + + Response response = okHttpClient.newCall(request).execute(); + //response code 401: already logged out + if (response.isSuccessful() || response.code() == 401) { + return true; + } + } catch (IOException | RuntimeException e) { + return false; + } + + return false; + } + + public static boolean canConnect(@NonNull OkHttpClient okHttpClient, String url) throws RuntimeException, IOException { + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .method("GET", null); + Request request = requestBuilder.build(); + + Response response = okHttpClient.newCall(request).execute(); + return response.isSuccessful(); + + } + + public static String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) throws RuntimeException, IOException { + + RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null; + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .method(request_method, jsonBody); + for (Pair keyValPair : headerArgs) { + requestBuilder.addHeader(keyValPair.first, keyValPair.second); + } + + //TODO: move to getHeaderArgs()? + String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); + requestBuilder.addHeader("Accept-Language", locale); + Request request = requestBuilder.build(); + + Response response = okHttpClient.newCall(request).execute(); + InputStream inputStream = response.body().byteStream(); + Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); + if (scanner.hasNext()) { + return scanner.next(); + } + return null; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java new file mode 100644 index 00000000..8a0c8f02 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java @@ -0,0 +1,946 @@ +/** + * 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.providersetup; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.util.Base64; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.ConnectException; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.net.UnknownServiceException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; + +import okhttp3.OkHttpClient; +import se.leap.bitmaskclient.base.models.Constants.CREDENTIAL_ERRORS; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator; +import se.leap.bitmaskclient.providersetup.models.LeapSRPSession; +import se.leap.bitmaskclient.providersetup.models.SrpCredentials; +import se.leap.bitmaskclient.providersetup.models.SrpRegistrationData; +import se.leap.bitmaskclient.base.utils.ConfigHelper; + +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_PASSWORD; +import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_USERNAME; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.base.models.Provider.CA_CERT; +import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL; +import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP; +import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.BACKEND_ERROR_KEY; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.BACKEND_ERROR_MESSAGE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_SERVICE_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.FAILED_LOGIN; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.FAILED_SIGNUP; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOGOUT_FAILED; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOG_IN; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOG_OUT; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.PARAMETERS; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_NOK; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_OK; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.RECEIVER_KEY; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.SIGN_UP; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_LOGIN; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_LOGOUT; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_SIGNUP; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_PROVIDER_DETAILS; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE; +import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING; +import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE; +import static se.leap.bitmaskclient.R.string.certificate_error; +import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; +import static se.leap.bitmaskclient.R.string.error_json_exception_user_message; +import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message; +import static se.leap.bitmaskclient.R.string.malformed_url; +import static se.leap.bitmaskclient.R.string.server_unreachable_message; +import static se.leap.bitmaskclient.R.string.service_is_down_error; +import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid; +import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert; +import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details; +import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.getFingerprintFromCertificate; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.parseRsaKeyFromString; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.deleteProviderDetailsFromPreferences; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getFromPersistedProvider; + +/** + * Implements the logic of the http api calls. The methods of this class needs to be called from + * a background thread. + */ + +public abstract class ProviderApiManagerBase { + + private final static String TAG = ProviderApiManagerBase.class.getName(); + + public interface ProviderApiServiceCallback { + void broadcastEvent(Intent intent); + } + + private ProviderApiServiceCallback serviceCallback; + + protected SharedPreferences preferences; + protected Resources resources; + OkHttpClientGenerator clientGenerator; + + ProviderApiManagerBase(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) { + this.preferences = preferences; + this.resources = resources; + this.serviceCallback = callback; + this.clientGenerator = clientGenerator; + } + + public void handleIntent(Intent command) { +// Log.d(TAG, "handleIntent was called!"); + ResultReceiver receiver = null; + if (command.getParcelableExtra(RECEIVER_KEY) != null) { + receiver = command.getParcelableExtra(RECEIVER_KEY); + } + String action = command.getAction(); + Bundle parameters = command.getBundleExtra(PARAMETERS); + + Provider provider = command.getParcelableExtra(PROVIDER_KEY); + + if (provider == null) { + //TODO: consider returning error back e.g. NO_PROVIDER + Log.e(TAG, action +" called without provider!"); + return; + } + if (action == null) { + Log.e(TAG, "Intent without action sent!"); + return; + } + + Bundle result = new Bundle(); + switch (action) { + case UPDATE_PROVIDER_DETAILS: + ProviderObservable.getInstance().setProviderForDns(provider); + resetProviderDetails(provider); + Bundle task = new Bundle(); + result = setUpProvider(provider, task); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + getGeoIPJson(provider); + sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider); + } else { + sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider); + } + ProviderObservable.getInstance().setProviderForDns(null); + break; + case SET_UP_PROVIDER: + ProviderObservable.getInstance().setProviderForDns(provider); + result = setUpProvider(provider, parameters); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + getGeoIPJson(provider); + sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider); + } else { + sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider); + } + ProviderObservable.getInstance().setProviderForDns(null); + break; + case SIGN_UP: + result = tryToRegister(provider, parameters); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, SUCCESSFUL_SIGNUP, result, provider); + } else { + sendToReceiverOrBroadcast(receiver, FAILED_SIGNUP, result, provider); + } + break; + case LOG_IN: + result = tryToAuthenticate(provider, parameters); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, SUCCESSFUL_LOGIN, result, provider); + } else { + sendToReceiverOrBroadcast(receiver, FAILED_LOGIN, result, provider); + } + break; + case LOG_OUT: + if (logOut(provider)) { + sendToReceiverOrBroadcast(receiver, SUCCESSFUL_LOGOUT, Bundle.EMPTY, provider); + } else { + sendToReceiverOrBroadcast(receiver, LOGOUT_FAILED, Bundle.EMPTY, provider); + } + break; + case DOWNLOAD_VPN_CERTIFICATE: + ProviderObservable.getInstance().setProviderForDns(provider); + result = updateVpnCertificate(provider); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider); + } else { + sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider); + } + ProviderObservable.getInstance().setProviderForDns(null); + break; + case UPDATE_INVALID_VPN_CERTIFICATE: + ProviderObservable.getInstance().setProviderForDns(provider); + result = updateVpnCertificate(provider); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider); + } else { + sendToReceiverOrBroadcast(receiver, INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider); + } + ProviderObservable.getInstance().setProviderForDns(null); + break; + case DOWNLOAD_SERVICE_JSON: + ProviderObservable.getInstance().setProviderForDns(provider); + Log.d(TAG, "update eip service json"); + result = getAndSetEipServiceJson(provider); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider); + } else { + sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider); + } + ProviderObservable.getInstance().setProviderForDns(null); + break; + case DOWNLOAD_GEOIP_JSON: + if (!provider.getGeoipUrl().isDefault()) { + boolean startEIP = parameters.getBoolean(EIP_ACTION_START); + ProviderObservable.getInstance().setProviderForDns(provider); + result = getGeoIPJson(provider); + result.putBoolean(EIP_ACTION_START, startEIP); + if (result.getBoolean(BROADCAST_RESULT_KEY)) { + sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider); + } else { + sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider); + } + ProviderObservable.getInstance().setProviderForDns(null); + } + } + } + + void resetProviderDetails(Provider provider) { + provider.reset(); + deleteProviderDetailsFromPreferences(preferences, provider.getDomain()); + } + + String formatErrorMessage(final int errorStringId) { + return formatErrorMessage(getProviderFormattedString(resources, errorStringId)); + } + + private String formatErrorMessage(String errorMessage) { + return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; + } + + private JSONObject getErrorMessageAsJson(final int toastStringId) { + try { + return new JSONObject(formatErrorMessage(toastStringId)); + } catch (JSONException e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) { + try { + jsonObject.put(ERRORS, errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) { + try { + jsonObject.put(ERRORS, errorMessage); + jsonObject.put(ERRORID, errorId); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + private Bundle tryToRegister(Provider provider, Bundle task) { + Bundle result = new Bundle(); + + String username = task.getString(CREDENTIALS_USERNAME); + String password = task.getString(CREDENTIALS_PASSWORD); + + if(provider == null) { + result.putBoolean(BROADCAST_RESULT_KEY, false); + Log.e(TAG, "no provider when trying to register"); + return result; + } + + if (validUserLoginData(username, password)) { + result = register(provider, username, password); + } else { + if (!wellFormedPassword(password)) { + result.putBoolean(BROADCAST_RESULT_KEY, false); + result.putString(CREDENTIALS_USERNAME, username); + result.putBoolean(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); + } + if (!validUsername(username)) { + result.putBoolean(BROADCAST_RESULT_KEY, false); + result.putBoolean(CREDENTIAL_ERRORS.USERNAME_MISSING.toString(), true); + } + } + + return result; + } + + private Bundle register(Provider provider, String username, String password) { + JSONObject stepResult = null; + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult); + if (okHttpClient == null) { + return backendErrorNotification(stepResult, username); + } + + LeapSRPSession client = new LeapSRPSession(username, password); + byte[] salt = client.calculateNewSalt(); + + BigInteger password_verifier = client.calculateV(username, password, salt); + + JSONObject api_result = sendNewUserDataToSRPServer(provider.getApiUrlWithVersion(), username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); + + Bundle result = new Bundle(); + if (api_result.has(ERRORS) || api_result.has(BACKEND_ERROR_KEY)) + result = backendErrorNotification(api_result, username); + else { + result.putString(CREDENTIALS_USERNAME, username); + result.putString(CREDENTIALS_PASSWORD, password); + result.putBoolean(BROADCAST_RESULT_KEY, true); + } + + return result; + } + + /** + * Starts the authentication process using SRP protocol. + * + * @param task containing: username, password and provider + * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if authentication was successful. + */ + private Bundle tryToAuthenticate(Provider provider, Bundle task) { + Bundle result = new Bundle(); + + String username = task.getString(CREDENTIALS_USERNAME); + String password = task.getString(CREDENTIALS_PASSWORD); + + if (validUserLoginData(username, password)) { + result = authenticate(provider, username, password); + } else { + if (!wellFormedPassword(password)) { + result.putBoolean(BROADCAST_RESULT_KEY, false); + result.putString(CREDENTIALS_USERNAME, username); + result.putBoolean(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); + } + if (!validUsername(username)) { + result.putBoolean(BROADCAST_RESULT_KEY, false); + result.putBoolean(CREDENTIAL_ERRORS.USERNAME_MISSING.toString(), true); + } + } + + return result; + } + + private Bundle authenticate(Provider provider, String username, String password) { + Bundle result = new Bundle(); + JSONObject stepResult = new JSONObject(); + + String providerApiUrl = provider.getApiUrlWithVersion(); + + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult); + if (okHttpClient == null) { + return backendErrorNotification(stepResult, username); + } + + LeapSRPSession client = new LeapSRPSession(username, password); + byte[] A = client.exponential(); + + JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient); + try { + String salt = step_result.getString(LeapSRPSession.SALT); + byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray(); + byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes); + if (M1 != null) { + step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient); + setTokenIfAvailable(step_result); + byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); + if (client.verify(M2)) { + result.putBoolean(BROADCAST_RESULT_KEY, true); + } else { + backendErrorNotification(step_result, username); + } + } else { + result.putBoolean(BROADCAST_RESULT_KEY, false); + result.putString(CREDENTIALS_USERNAME, username); + result.putString(USER_MESSAGE, resources.getString(R.string.error_srp_math_error_user_message)); + } + } catch (JSONException e) { + result = backendErrorNotification(step_result, username); + e.printStackTrace(); + } + + return result; + } + + private boolean setTokenIfAvailable(JSONObject authentication_step_result) { + try { + LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); + } catch (JSONException e) { + return false; + } + return true; + } + + private Bundle backendErrorNotification(JSONObject result, String username) { + Bundle userNotificationBundle = new Bundle(); + if (result.has(ERRORS)) { + Object baseErrorMessage = result.opt(ERRORS); + if (baseErrorMessage instanceof JSONObject) { + try { + JSONObject errorMessage = result.getJSONObject(ERRORS); + String errorType = errorMessage.keys().next().toString(); + String message = errorMessage.get(errorType).toString(); + userNotificationBundle.putString(USER_MESSAGE, message); + } catch (JSONException | NoSuchElementException | NullPointerException e) { + e.printStackTrace(); + } + } else if (baseErrorMessage instanceof String) { + try { + String errorMessage = result.getString(ERRORS); + userNotificationBundle.putString(USER_MESSAGE, errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } else if (result.has(BACKEND_ERROR_KEY)) { + try { + String backendErrorMessage = resources.getString(R.string.error_json_exception_user_message); + if (result.has(BACKEND_ERROR_MESSAGE)) { + backendErrorMessage = resources.getString(R.string.error) + result.getString(BACKEND_ERROR_MESSAGE); + } + userNotificationBundle.putString(USER_MESSAGE, backendErrorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + if (!username.isEmpty()) + userNotificationBundle.putString(CREDENTIALS_USERNAME, username); + userNotificationBundle.putBoolean(BROADCAST_RESULT_KEY, false); + + return userNotificationBundle; + } + + private void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) { + if (resultData == null || resultData == Bundle.EMPTY) { + resultData = new Bundle(); + } + resultData.putParcelable(PROVIDER_KEY, provider); + if (receiver != null) { + receiver.send(resultCode, resultData); + } else { + broadcastEvent(resultCode, resultData); + } + } + + private void broadcastEvent(int resultCode , Bundle resultData) { + Intent intentUpdate = new Intent(BROADCAST_PROVIDER_API_EVENT); + intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); + intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode); + intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData); + serviceCallback.broadcastEvent(intentUpdate); + } + + + /** + * Validates parameters entered by the user to log in + * + * @param username + * @param password + * @return true if both parameters are present and the entered password length is greater or equal to eight (8). + */ + private boolean validUserLoginData(String username, String password) { + return validUsername(username) && wellFormedPassword(password); + } + + private boolean validUsername(String username) { + return username != null && !username.isEmpty(); + } + + /** + * Validates a password + * + * @param password + * @return true if the entered password length is greater or equal to eight (8). + */ + private boolean wellFormedPassword(String password) { + return password != null && password.length() >= 8; + } + + /** + * Sends an HTTP POST request to the authentication server with the SRP Parameter A. + * + * @param server_url + * @param username + * @param clientA First SRP parameter sent + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { + SrpCredentials srpCredentials = new SrpCredentials(username, clientA); + return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); + } + + /** + * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M). + * + * @param server_url + * @param username + * @param m1 Second SRP parameter sent + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { + String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; + return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); + } + + /** + * Sends an HTTP POST request to the api server to register a new user. + * + * @param server_url + * @param username + * @param salt + * @param password_verifier + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { + return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); + } + + /** + * Executes an HTTP request expecting a JSON response. + * + * @param url + * @param request_method + * @return response from authentication server + */ + private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { + return requestJsonFromServer(url, request_method, jsonString, new ArrayList>(), okHttpClient); + } + + protected String sendGetStringToServer(@NonNull String url, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { + return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); + } + + + + private JSONObject requestJsonFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { + JSONObject responseJson; + String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + + try { + responseJson = new JSONObject(plain_response); + } catch (NullPointerException | JSONException e) { + e.printStackTrace(); + responseJson = getErrorMessageAsJson(error_json_exception_user_message); + } + return responseJson; + + } + + private String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { + String plainResponseBody; + + try { + + plainResponseBody = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + + } catch (NullPointerException npe) { + plainResponseBody = formatErrorMessage(error_json_exception_user_message); + } catch (UnknownHostException | SocketTimeoutException e) { + plainResponseBody = formatErrorMessage(server_unreachable_message); + } catch (MalformedURLException e) { + plainResponseBody = formatErrorMessage(malformed_url); + } catch (SSLHandshakeException | SSLPeerUnverifiedException e) { + plainResponseBody = formatErrorMessage(certificate_error); + } catch (ConnectException e) { + plainResponseBody = formatErrorMessage(service_is_down_error); + } catch (IllegalArgumentException e) { + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); + } catch (UnknownServiceException e) { + //unable to find acceptable protocols - tlsv1.2 not enabled? + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); + } catch (IOException e) { + plainResponseBody = formatErrorMessage(error_io_exception_user_message); + } + + return plainResponseBody; + } + + private boolean canConnect(Provider provider, Bundle result) { + JSONObject errorJson = new JSONObject(); + String providerUrl = provider.getApiUrlString() + "/provider.json"; + + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), errorJson); + if (okHttpClient == null) { + result.putString(ERRORS, errorJson.toString()); + return false; + } + + try { + + return ProviderApiConnector.canConnect(okHttpClient, providerUrl); + + } catch (UnknownHostException | SocketTimeoutException e) { + setErrorResult(result, server_unreachable_message, null); + } catch (MalformedURLException e) { + setErrorResult(result, malformed_url, null); + } catch (SSLHandshakeException e) { + setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); + } catch (ConnectException e) { + setErrorResult(result, service_is_down_error, null); + } catch (IllegalArgumentException e) { + setErrorResult(result, error_no_such_algorithm_exception_user_message, null); + } catch (UnknownServiceException e) { + //unable to find acceptable protocols - tlsv1.2 not enabled? + setErrorResult(result, error_no_such_algorithm_exception_user_message, null); + } catch (IOException e) { + setErrorResult(result, error_io_exception_user_message, null); + } + return false; + } + + /** + * Downloads a provider.json from a given URL, adding a new provider using the given name. + * + * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider + * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the update was successful. + */ + protected abstract Bundle setUpProvider(Provider provider, Bundle task); + + /** + * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways + * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the download was successful. + */ + protected abstract Bundle getAndSetEipServiceJson(Provider provider); + + /** + * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. + * + * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. + */ + protected abstract Bundle updateVpnCertificate(Provider provider); + + + /** + * Fetches the Geo ip Json, containing a list of gateways sorted by distance from the users current location + * + * @param provider + * @return + */ + protected abstract Bundle getGeoIPJson(Provider provider); + + + protected boolean isValidJson(String jsonString) { + try { + new JSONObject(jsonString); + return true; + } catch(JSONException e) { + return false; + } catch(NullPointerException e) { + e.printStackTrace(); + return false; + } + } + + protected boolean validCertificate(Provider provider, String certString) { + boolean result = false; + if (!ConfigHelper.checkErroneousDownload(certString)) { + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certString); + try { + if (certificate != null) { + JSONObject providerJson = provider.getDefinition(); + String fingerprint = providerJson.getString(Provider.CA_CERT_FINGERPRINT); + String encoding = fingerprint.split(":")[0]; + String expectedFingerprint = fingerprint.split(":")[1]; + String realFingerprint = getFingerprintFromCertificate(certificate, encoding); + + result = realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim()); + } else + result = false; + } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) { + result = false; + } + } + + return result; + } + + protected void getPersistedProviderUpdates(Provider provider) { + String providerDomain = getDomainFromMainURL(provider.getMainUrlString()); + if (hasUpdatedProviderDetails(providerDomain)) { + provider.setCaCert(getPersistedProviderCA(providerDomain)); + provider.define(getPersistedProviderDefinition(providerDomain)); + provider.setPrivateKey(getPersistedPrivateKey(providerDomain)); + provider.setVpnCertificate(getPersistedVPNCertificate(providerDomain)); + provider.setProviderApiIp(getPersistedProviderApiIp(providerDomain)); + provider.setProviderIp(getPersistedProviderIp(providerDomain)); + provider.setGeoipUrl(getPersistedGeoIp(providerDomain)); + } + } + + Bundle validateProviderDetails(Provider provider) { + Bundle result = new Bundle(); + result.putBoolean(BROADCAST_RESULT_KEY, false); + + if (!provider.hasDefinition()) { + return result; + } + + result = validateCertificateForProvider(result, provider); + + //invalid certificate or no certificate + if (result.containsKey(ERRORS) || (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) ) { + return result; + } + + result.putBoolean(BROADCAST_RESULT_KEY, true); + + return result; + } + + protected Bundle validateCertificateForProvider(Bundle result, Provider provider) { + String caCert = provider.getCaCert(); + + if (ConfigHelper.checkErroneousDownload(caCert)) { + return result; + } + + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(caCert); + if (certificate == null) { + return setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); + } + try { + certificate.checkValidity(); + String encoding = provider.getCertificatePinEncoding(); + String expectedFingerprint = provider.getCertificatePin(); + + String realFingerprint = getFingerprintFromCertificate(certificate, encoding); + if (!realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim())) { + return setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString()); + } + + if (!canConnect(provider, result)) { + return result; + } + } catch (NoSuchAlgorithmException e ) { + return setErrorResult(result, error_no_such_algorithm_exception_user_message, null); + } catch (ArrayIndexOutOfBoundsException e) { + return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString()); + } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) { + return setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); + } + + result.putBoolean(BROADCAST_RESULT_KEY, true); + return result; + } + + protected Bundle setErrorResult(Bundle result, String stringJsonErrorMessage) { + String reasonToFail = pickErrorMessage(stringJsonErrorMessage); + result.putString(ERRORS, reasonToFail); + result.putBoolean(BROADCAST_RESULT_KEY, false); + return result; + } + + Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) { + JSONObject errorJson = new JSONObject(); + String errorMessage = getProviderFormattedString(resources, errorMessageId); + if (errorId != null) { + addErrorMessageToJson(errorJson, errorMessage, errorId); + } else { + addErrorMessageToJson(errorJson, errorMessage); + } + result.putString(ERRORS, errorJson.toString()); + result.putBoolean(BROADCAST_RESULT_KEY, false); + return result; + } + + protected String getPersistedPrivateKey(String providerDomain) { + return getFromPersistedProvider(PROVIDER_PRIVATE_KEY, providerDomain, preferences); + } + + protected String getPersistedVPNCertificate(String providerDomain) { + return getFromPersistedProvider(PROVIDER_VPN_CERTIFICATE, providerDomain, preferences); + } + + protected JSONObject getPersistedProviderDefinition(String providerDomain) { + try { + return new JSONObject(getFromPersistedProvider(Provider.KEY, providerDomain, preferences)); + } catch (JSONException e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + protected String getPersistedProviderCA(String providerDomain) { + return getFromPersistedProvider(CA_CERT, providerDomain, preferences); + } + + protected String getPersistedProviderApiIp(String providerDomain) { + return getFromPersistedProvider(PROVIDER_API_IP, providerDomain, preferences); + } + + protected String getPersistedProviderIp(String providerDomain) { + return getFromPersistedProvider(PROVIDER_IP, providerDomain, preferences); + } + + protected String getPersistedGeoIp(String providerDomain) { + return getFromPersistedProvider(GEOIP_URL, providerDomain, preferences); + } + + protected boolean hasUpdatedProviderDetails(String domain) { + return preferences.contains(Provider.KEY + "." + domain) && preferences.contains(CA_CERT + "." + domain); + } + + protected String getDomainFromMainURL(String mainUrl) { + return mainUrl.replaceFirst("http[s]?://", "").replaceFirst("/.*", ""); + + } + + /** + * Interprets the error message as a JSON object and extract the "errors" keyword pair. + * If the error message is not a JSON object, then it is returned untouched. + * + * @param stringJsonErrorMessage + * @return final error message + */ + protected String pickErrorMessage(String stringJsonErrorMessage) { + String errorMessage = ""; + try { + JSONObject jsonErrorMessage = new JSONObject(stringJsonErrorMessage); + errorMessage = jsonErrorMessage.getString(ERRORS); + } catch (JSONException e) { + // TODO Auto-generated catch block + errorMessage = stringJsonErrorMessage; + } catch (NullPointerException e) { + //do nothing + } + + return errorMessage; + } + + @NonNull + protected List> getAuthorizationHeader() { + List> headerArgs = new ArrayList<>(); + if (!LeapSRPSession.getToken().isEmpty()) { + Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); + headerArgs.add(authorizationHeaderPair); + } + return headerArgs; + } + + private boolean logOut(Provider provider) { + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), new JSONObject()); + if (okHttpClient == null) { + return false; + } + + String deleteUrl = provider.getApiUrlWithVersion() + "/logout"; + + if (ProviderApiConnector.delete(okHttpClient, deleteUrl)) { + LeapSRPSession.setToken(""); + return true; + } + return false; + } + + protected Bundle loadCertificate(Provider provider, String certString) { + Bundle result = new Bundle(); + if (certString == null) { + setErrorResult(result, vpn_certificate_is_invalid, null); + return result; + } + + try { + // API returns concatenated cert & key. Split them for OpenVPN options + String certificateString = null, keyString = null; + String[] certAndKey = certString.split("(?<=-\n)"); + for (int i = 0; i < certAndKey.length - 1; i++) { + if (certAndKey[i].contains("KEY")) { + keyString = certAndKey[i++] + certAndKey[i]; + } else if (certAndKey[i].contains("CERTIFICATE")) { + certificateString = certAndKey[i++] + certAndKey[i]; + } + } + + RSAPrivateKey key = parseRsaKeyFromString(keyString); + keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); + provider.setPrivateKey( "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----"); + + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certificateString); + certificate.checkValidity(); + certificateString = Base64.encodeToString(certificate.getEncoded(), Base64.DEFAULT); + provider.setVpnCertificate( "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----"); + result.putBoolean(BROADCAST_RESULT_KEY, true); + } catch (CertificateException | NullPointerException e) { + e.printStackTrace(); + setErrorResult(result, vpn_certificate_is_invalid, null); + } + return result; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java new file mode 100644 index 00000000..710aee0f --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java @@ -0,0 +1,84 @@ +/** + * 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.providersetup; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import java.lang.ref.WeakReference; + +import se.leap.bitmaskclient.base.models.Constants; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState; +import se.leap.bitmaskclient.providersetup.activities.ProviderListBaseActivity; + +/** + * Broadcast receiver that handles callback intents of ProviderApi during provider setup. + * It is used by CustomProviderSetupActivity for custom branded apps and ProviderListActivity + * for 'normal' Bitmask. + * + * Created by cyberta on 17.08.18. + */ + +public class ProviderApiSetupBroadcastReceiver extends BroadcastReceiver { + private WeakReference setupInterfaceRef; + + public ProviderApiSetupBroadcastReceiver(ProviderSetupInterface setupInterface) { + this.setupInterfaceRef = new WeakReference<>(setupInterface); + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(ProviderListBaseActivity.TAG, "received Broadcast"); + ProviderSetupInterface setupInterface = setupInterfaceRef.get(); + String action = intent.getAction(); + if (action == null || !action.equalsIgnoreCase(Constants.BROADCAST_PROVIDER_API_EVENT) || setupInterface == null) { + return; + } + + if (setupInterface.getConfigState() != null && + setupInterface.getConfigState() == ProviderConfigState.SETTING_UP_PROVIDER) { + int resultCode = intent.getIntExtra(Constants.BROADCAST_RESULT_CODE, ProviderListBaseActivity.RESULT_CANCELED); + Log.d(ProviderListBaseActivity.TAG, "Broadcast resultCode: " + resultCode); + + Bundle resultData = intent.getParcelableExtra(Constants.BROADCAST_RESULT_KEY); + Provider handledProvider = resultData.getParcelable(Constants.PROVIDER_KEY); + + if (handledProvider != null && setupInterface.getProvider() != null && + handledProvider.getDomain().equalsIgnoreCase(setupInterface.getProvider().getDomain())) { + switch (resultCode) { + case ProviderAPI.PROVIDER_OK: + setupInterface.handleProviderSetUp(handledProvider); + break; + case ProviderAPI.PROVIDER_NOK: + setupInterface.handleProviderSetupFailed(resultData); + break; + case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE: + setupInterface.handleCorrectlyDownloadedCertificate(handledProvider); + break; + case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE: + setupInterface.handleIncorrectlyDownloadedCertificate(); + break; + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java new file mode 100644 index 00000000..76ee33f2 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java @@ -0,0 +1,21 @@ +package se.leap.bitmaskclient.providersetup; + +import android.view.LayoutInflater; + +import com.pedrogomez.renderers.AdapteeCollection; +import com.pedrogomez.renderers.RendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import se.leap.bitmaskclient.base.models.Provider; + +public class ProviderListAdapter extends RendererAdapter { + public ProviderListAdapter(LayoutInflater layoutInflater, RendererBuilder rendererBuilder, + AdapteeCollection collection) { + super(layoutInflater, rendererBuilder, collection); + } + + public void saveProviders() { + ProviderManager provider_manager = (ProviderManager) getCollection(); + provider_manager.saveCustomProvidersToFile(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java new file mode 100644 index 00000000..d33a175f --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java @@ -0,0 +1,272 @@ +package se.leap.bitmaskclient.providersetup; + +import android.content.res.AssetManager; +import androidx.annotation.VisibleForTesting; + +import com.pedrogomez.renderers.AdapteeCollection; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import se.leap.bitmaskclient.base.models.Provider; + +import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL; +import static se.leap.bitmaskclient.base.models.Provider.MAIN_URL; +import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP; +import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP; +import static se.leap.bitmaskclient.base.utils.FileHelper.createFile; +import static se.leap.bitmaskclient.base.utils.FileHelper.persistFile; +import static se.leap.bitmaskclient.base.utils.InputStreamHelper.getInputStreamFrom; +import static se.leap.bitmaskclient.base.utils.InputStreamHelper.loadInputStreamAsString; + +/** + * Created by parmegv on 4/12/14. + */ +public class ProviderManager implements AdapteeCollection { + + private AssetManager assetsManager; + private File externalFilesDir; + private Set defaultProviders; + private Set customProviders; + private Set defaultProviderURLs; + private Set customProviderURLs; + + private static ProviderManager instance; + + final private static String URLS = "urls"; + final private static String EXT_JSON = ".json"; + final private static String EXT_PEM = ".pem"; + + public static ProviderManager getInstance(AssetManager assetsManager, File externalFilesDir) { + if (instance == null) + instance = new ProviderManager(assetsManager, externalFilesDir); + + return instance; + } + + @VisibleForTesting + static void reset() { + instance = null; + } + + private ProviderManager(AssetManager assetManager, File externalFilesDir) { + this.assetsManager = assetManager; + addDefaultProviders(assetManager); + addCustomProviders(externalFilesDir); + } + + private void addDefaultProviders(AssetManager assets_manager) { + try { + defaultProviders = providersFromAssets(URLS, assets_manager.list(URLS)); + defaultProviderURLs = getProviderUrlSetFromProviderSet(defaultProviders); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private Set getProviderUrlSetFromProviderSet(Set providers) { + HashSet providerUrls = new HashSet<>(); + for (Provider provider : providers) { + providerUrls.add(provider.getMainUrl().getUrl()); + } + return providerUrls; + } + + private Set providersFromAssets(String directory, String[] relativeFilePaths) { + Set providers = new HashSet<>(); + + for (String file : relativeFilePaths) { + String mainUrl = null; + String providerIp = null; + String providerApiIp = null; + String certificate = null; + String providerDefinition = null; + String geoipUrl = null; + try { + String provider = file.substring(0, file.length() - ".url".length()); + InputStream providerFile = assetsManager.open(directory + "/" + file); + mainUrl = extractKeyFromInputStream(providerFile, MAIN_URL); + providerIp = extractKeyFromInputStream(providerFile, PROVIDER_IP); + providerApiIp = extractKeyFromInputStream(providerFile, PROVIDER_API_IP); + geoipUrl = extractKeyFromInputStream(providerFile, GEOIP_URL); + certificate = loadInputStreamAsString(assetsManager.open(provider + EXT_PEM)); + providerDefinition = loadInputStreamAsString(assetsManager.open(provider + EXT_JSON)); + } catch (IOException e) { + e.printStackTrace(); + } + providers.add(new Provider(mainUrl, geoipUrl, providerIp, providerApiIp, certificate, providerDefinition)); + } + + return providers; + } + + + private void addCustomProviders(File externalFilesDir) { + this.externalFilesDir = externalFilesDir; + customProviders = externalFilesDir != null && externalFilesDir.isDirectory() ? + providersFromFiles(externalFilesDir.list()) : + new HashSet<>(); + customProviderURLs = getProviderUrlSetFromProviderSet(customProviders); + } + + private Set providersFromFiles(String[] files) { + Set providers = new HashSet<>(); + try { + for (String file : files) { + InputStream inputStream = getInputStreamFrom(externalFilesDir.getAbsolutePath() + "/" + file); + String mainUrl = extractKeyFromInputStream(inputStream, MAIN_URL); + String providerIp = extractKeyFromInputStream(inputStream, PROVIDER_IP); + String providerApiIp = extractKeyFromInputStream(inputStream, PROVIDER_API_IP); + providers.add(new Provider(mainUrl, providerIp, providerApiIp)); + } + } catch (FileNotFoundException | NullPointerException e) { + e.printStackTrace(); + } + + return providers; + } + + private String extractKeyFromInputStream(InputStream inputStream, String key) { + String value = ""; + + JSONObject fileContents = inputStreamToJson(inputStream); + if (fileContents != null) + value = fileContents.optString(key); + return value; + } + + private JSONObject inputStreamToJson(InputStream inputStream) { + JSONObject json = null; + try { + byte[] bytes = new byte[inputStream.available()]; + if (inputStream.read(bytes) > 0) + json = new JSONObject(new String(bytes)); + inputStream.reset(); + } catch (IOException | JSONException e) { + e.printStackTrace(); + } + return json; + } + + public List providers() { + List allProviders = new ArrayList<>(); + allProviders.addAll(defaultProviders); + if(customProviders != null) + allProviders.addAll(customProviders); + //add an option to add a custom provider + //TODO: refactor me? + allProviders.add(new Provider()); + return allProviders; + } + + @Override + public int size() { + return providers().size(); + } + + @Override + public Provider get(int index) { + Iterator iterator = providers().iterator(); + while (iterator.hasNext() && index > 0) { + iterator.next(); + index--; + } + return iterator.next(); + } + + @Override + public boolean add(Provider element) { + return element != null && + !defaultProviderURLs.contains(element.getMainUrl().getUrl()) && + customProviders.add(element) && + customProviderURLs.add(element.getMainUrl().getUrl()); + } + + @Override + public boolean remove(Object element) { + return element instanceof Provider && + customProviders.remove(element) && + customProviderURLs.remove(((Provider) element).getMainUrl().getUrl()); + } + + @Override + public boolean addAll(Collection elements) { + Iterator iterator = elements.iterator(); + boolean addedAll = true; + while (iterator.hasNext()) { + Provider p = (Provider) iterator.next(); + addedAll = customProviders.add(p) && + customProviderURLs.add(p.getMainUrl().getUrl()) && + addedAll; + } + return addedAll; + } + + @Override + public boolean removeAll(Collection elements) { + Iterator iterator = elements.iterator(); + boolean removedAll = true; + try { + while (iterator.hasNext()) { + Provider p = (Provider) iterator.next(); + removedAll = ((defaultProviders.remove(p) && defaultProviderURLs.remove(p.getMainUrl().getUrl())) || + (customProviders.remove(p) && customProviderURLs.remove(p.getMainUrl().getUrl()))) && + removedAll; + } + } catch (ClassCastException e) { + return false; + } + + return removedAll; + } + + @Override + public void clear() { + defaultProviders.clear(); + customProviders.clear(); + customProviderURLs.clear(); + defaultProviderURLs.clear(); + } + + void saveCustomProvidersToFile() { + try { + deleteLegacyCustomProviders(); + + for (Provider provider : customProviders) { + File providerFile = createFile(externalFilesDir, provider.getName() + EXT_JSON); + if (!providerFile.exists()) { + persistFile(providerFile, provider.toJson().toString()); + } + } + } catch (IOException | SecurityException e) { + e.printStackTrace(); + } + } + + /** + * Deletes persisted custom providers from from internal storage that are not in customProviders list anymore + */ + private void deleteLegacyCustomProviders() throws IOException, SecurityException { + Set persistedCustomProviders = externalFilesDir != null && externalFilesDir.isDirectory() ? + providersFromFiles(externalFilesDir.list()) : new HashSet(); + persistedCustomProviders.removeAll(customProviders); + for (Provider providerToDelete : persistedCustomProviders) { + File providerFile = createFile(externalFilesDir, providerToDelete.getName() + EXT_JSON); + if (providerFile.exists()) { + providerFile.delete(); + } + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java new file mode 100644 index 00000000..52ee4656 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java @@ -0,0 +1,57 @@ +package se.leap.bitmaskclient.providersetup; + +import android.content.*; +import android.view.*; +import android.widget.*; + +import com.pedrogomez.renderers.*; + +import butterknife.*; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.R; + +/** + * Created by parmegv on 4/12/14. + */ +public class ProviderRenderer extends Renderer { + private final Context context; + + @InjectView(R.id.provider_name) + TextView name; + @InjectView(R.id.provider_domain) + TextView domain; + + public ProviderRenderer(Context context) { + this.context = context; + } + + @Override + protected View inflate(LayoutInflater inflater, ViewGroup parent) { + View view = inflater.inflate(R.layout.v_provider_list_item, parent, false); + ButterKnife.inject(this, view); + return view; + } + + @Override + protected void setUpView(View rootView) { + /* + * Empty implementation substituted with the usage of ButterKnife library by Jake Wharton. + */ + } + + @Override + protected void hookListeners(View rootView) { + //Empty + } + + @Override + public void render() { + Provider provider = getContent(); + if (!provider.isDefault()) { + name.setText(provider.getName()); + domain.setText(provider.getDomain()); + } else { + domain.setText(R.string.add_provider); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java new file mode 100644 index 00000000..7d2b4742 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java @@ -0,0 +1,21 @@ +package se.leap.bitmaskclient.providersetup; + +import com.pedrogomez.renderers.*; + +import java.util.*; + +import se.leap.bitmaskclient.base.models.Provider; + +/** + * Created by parmegv on 4/12/14. + */ +public class ProviderRendererBuilder extends RendererBuilder { + public ProviderRendererBuilder(Collection> prototypes) { + super(prototypes); + } + + @Override + protected Class getPrototypeClass(Provider content) { + return ProviderRenderer.class; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java new file mode 100644 index 00000000..947d1182 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java @@ -0,0 +1,189 @@ +/** + * 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.providersetup; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import org.json.JSONObject; + +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.R; + +import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.DEFAULT; +import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.valueOf; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS; + +/** + * Implements a dialog to show why a download failed. + * + * @author parmegv + */ +public class ProviderSetupFailedDialog extends DialogFragment { + + public static String TAG = "downloaded_failed_dialog"; + private final static String KEY_PROVIDER = "key provider"; + private final static String KEY_REASON_TO_FAIL = "key reason to fail"; + private final static String KEY_DOWNLOAD_ERROR = "key download error"; + private String reasonToFail; + private DOWNLOAD_ERRORS downloadError = DEFAULT; + + private Provider provider; + + /** + * Represent error types that need different error handling actions + */ + public enum DOWNLOAD_ERRORS { + DEFAULT, + ERROR_CORRUPTED_PROVIDER_JSON, + ERROR_INVALID_CERTIFICATE, + ERROR_CERTIFICATE_PINNING, + ERROR_NEW_URL_NO_VPN_PROVIDER + } + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, String reasonToFail) { + ProviderSetupFailedDialog dialogFragment = new ProviderSetupFailedDialog(); + dialogFragment.reasonToFail = reasonToFail; + dialogFragment.provider = provider; + return dialogFragment; + } + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(Provider provider, JSONObject errorJson, boolean testNewURL) { + ProviderSetupFailedDialog dialogFragment = new ProviderSetupFailedDialog(); + dialogFragment.provider = provider; + try { + if (errorJson.has(ERRORS)) { + dialogFragment.reasonToFail = errorJson.getString(ERRORS); + } else { + //default error msg + dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); + } + + if (errorJson.has(ERRORID)) { + dialogFragment.downloadError = valueOf(errorJson.getString(ERRORID)); + } else if (testNewURL) { + dialogFragment.downloadError = DOWNLOAD_ERRORS.ERROR_NEW_URL_NO_VPN_PROVIDER; + } + } catch (Exception e) { + e.printStackTrace(); + dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message); + } + return dialogFragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + restoreFromSavedInstance(savedInstanceState); + } + + @Override + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(reasonToFail) + .setNegativeButton(R.string.cancel, (dialog, id) + -> interfaceWithConfigurationWizard.cancelSettingUpProvider()); + switch (downloadError) { + case ERROR_CORRUPTED_PROVIDER_JSON: + builder.setPositiveButton(R.string.update_provider_details, (dialog, which) + -> interfaceWithConfigurationWizard.updateProviderDetails()); + break; + case ERROR_CERTIFICATE_PINNING: + case ERROR_INVALID_CERTIFICATE: + builder.setPositiveButton(R.string.update_certificate, (dialog, which) + -> interfaceWithConfigurationWizard.updateProviderDetails()); + break; + case ERROR_NEW_URL_NO_VPN_PROVIDER: + builder.setPositiveButton(R.string.retry, (dialog, id) + -> interfaceWithConfigurationWizard.addAndSelectNewProvider(provider.getMainUrlString())); + break; + default: + builder.setPositiveButton(R.string.retry, (dialog, id) + -> interfaceWithConfigurationWizard.retrySetUpProvider(provider)); + break; + } + + // Create the AlertDialog object and return it + return builder.create(); + } + + public interface DownloadFailedDialogInterface { + void retrySetUpProvider(@NonNull Provider provider); + + void cancelSettingUpProvider(); + + void updateProviderDetails(); + + void addAndSelectNewProvider(String url); + } + + DownloadFailedDialogInterface interfaceWithConfigurationWizard; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + interfaceWithConfigurationWizard = (DownloadFailedDialogInterface) context; + } catch (ClassCastException e) { + throw new ClassCastException(context.toString() + + " must implement NoticeDialogListener"); + } + } + + @Override + public void onCancel(DialogInterface dialog) { + dialog.dismiss(); + interfaceWithConfigurationWizard.cancelSettingUpProvider(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(KEY_PROVIDER, provider); + outState.putString(KEY_REASON_TO_FAIL, reasonToFail); + outState.putString(KEY_DOWNLOAD_ERROR, downloadError.toString()); + } + + private void restoreFromSavedInstance(Bundle savedInstanceState) { + if (savedInstanceState == null) { + return; + } + if (savedInstanceState.containsKey(KEY_PROVIDER)) { + this.provider = savedInstanceState.getParcelable(KEY_PROVIDER); + } + if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) { + this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL); + } + if (savedInstanceState.containsKey(KEY_DOWNLOAD_ERROR)) { + this.downloadError = valueOf(savedInstanceState.getString(KEY_DOWNLOAD_ERROR)); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java new file mode 100644 index 00000000..5b5c94b4 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java @@ -0,0 +1,43 @@ +/** + * 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.providersetup; + +import android.os.Bundle; + +import se.leap.bitmaskclient.base.models.Provider; + +/** + * Created by cyberta on 17.08.18. + */ + +public interface ProviderSetupInterface { + enum ProviderConfigState { + PROVIDER_NOT_SET, + SETTING_UP_PROVIDER, + SHOWING_PROVIDER_DETAILS, + PENDING_SHOW_PROVIDER_DETAILS, + PENDING_SHOW_FAILED_DIALOG, + SHOW_FAILED_DIALOG, + } + + void handleProviderSetUp(Provider provider); + void handleProviderSetupFailed(Bundle resultData); + void handleCorrectlyDownloadedCertificate(Provider provider); + void handleIncorrectlyDownloadedCertificate(); + Provider getProvider(); + ProviderConfigState getConfigState(); +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java new file mode 100644 index 00000000..b7325e03 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java @@ -0,0 +1,109 @@ +package se.leap.bitmaskclient.providersetup.activities; + +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import android.util.Log; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; + +import butterknife.InjectView; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.R; + +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; + +public abstract class AbstractProviderDetailActivity extends ConfigWizardBaseActivity { + + final public static String TAG = "providerDetailActivity"; + + @InjectView(R.id.provider_detail_description) + AppCompatTextView description; + + @InjectView(R.id.provider_detail_options) + ListView options; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + provider = getIntent().getParcelableExtra(PROVIDER_KEY); + setContentView(R.layout.a_provider_detail); + + if (provider == null) { + return; + } + + + setProviderHeaderText(provider.getName()); + description.setText(provider.getDescription()); + + // Show only the options allowed by the provider + ArrayList optionsList = new ArrayList<>(); + if (provider.allowsRegistered()) { + optionsList.add(getString(R.string.login_to_profile)); + optionsList.add(getString(R.string.create_profile)); + if (provider.allowsAnonymous()) { + optionsList.add(getString(R.string.use_anonymously_button)); + } + } else { + onAnonymouslySelected(); + } + + + options.setAdapter(new ArrayAdapter<>( + this, + R.layout.v_single_list_item, + android.R.id.text1, + optionsList.toArray(new String[optionsList.size()]) + )); + options.setOnItemClickListener((parent, view, position, id) -> { + String text = ((TextView) view).getText().toString(); + Intent intent; + if (text.equals(getString(R.string.login_to_profile))) { + Log.d(TAG, "login selected"); + intent = new Intent(getApplicationContext(), LoginActivity.class); + } else if (text.equals(getString(R.string.create_profile))) { + Log.d(TAG, "signup selected"); + intent = new Intent(getApplicationContext(), SignupActivity.class); + } else { + onAnonymouslySelected(); + return; + } + intent.putExtra(PROVIDER_KEY, provider); + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivityForResult(intent, REQUEST_CODE_CONFIGURE_LEAP); + }); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + provider = intent.getParcelableExtra(PROVIDER_KEY); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) { + if (resultCode == RESULT_OK) { + setResult(resultCode, data); + finish(); + } + } + } + + private void onAnonymouslySelected() { + Intent intent; + Log.d(TAG, "use anonymously selected"); + intent = new Intent(); + intent.putExtra(Provider.KEY, provider); + setResult(RESULT_OK, intent); + finish(); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java new file mode 100644 index 00000000..0031f48d --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java @@ -0,0 +1,125 @@ +package se.leap.bitmaskclient.providersetup.activities; + +import android.content.Intent; +import android.os.Bundle; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.Button; + +import butterknife.InjectView; +import se.leap.bitmaskclient.R; + +import static se.leap.bitmaskclient.providersetup.activities.ProviderListBaseActivity.EXTRAS_KEY_INVALID_URL; + +/** + * Created by cyberta on 30.06.18. + */ + +public abstract class AddProviderBaseActivity extends ConfigWizardBaseActivity { + + final public static String EXTRAS_KEY_NEW_URL = "NEW_URL"; + + @InjectView(R.id.text_uri_error) + TextInputLayout urlError; + + @InjectView(R.id.text_uri) + TextInputEditText editUrl; + + @InjectView(R.id.button_cancel) + Button cancelButton; + + @InjectView(R.id.button_save) + Button saveButton; + + + protected void init() { + Bundle extras = this.getIntent().getExtras(); + if (extras != null && extras.containsKey(EXTRAS_KEY_INVALID_URL)) { + editUrl.setText(extras.getString(EXTRAS_KEY_INVALID_URL)); + saveButton.setEnabled(true); + } + + setupSaveButton(); + setupCancelButton(); + setUpListeners(); + setUpInitialUI(); + } + + public abstract void setupSaveButton(); + + private void setupCancelButton() { + cancelButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + finish(); + } + }); + } + + private void setUpInitialUI() { + setProviderHeaderText(R.string.add_provider); + hideProgressBar(); + } + + protected void saveProvider() { + String entered_url = getURL(); + if (validURL(entered_url)) { + Intent intent = this.getIntent(); + intent.putExtra(EXTRAS_KEY_NEW_URL, entered_url); + setResult(RESULT_OK, intent); + finish(); + } else { + editUrl.setText(""); + urlError.setError(getString(R.string.not_valid_url_entered)); + } + } + + private void setUpListeners() { + + editUrl.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (!validURL(getURL())) { + urlError.setError(getString(R.string.not_valid_url_entered)); + saveButton.setEnabled(false); + + } else { + urlError.setError(null); + saveButton.setEnabled(true); + } + } + }); + } + + private String getURL() { + String entered_url = editUrl.getText().toString().trim(); + if (entered_url.contains("www.")) entered_url = entered_url.replaceFirst("www.", ""); + if (!entered_url.startsWith("https://")) { + if (entered_url.startsWith("http://")) { + entered_url = entered_url.substring("http://".length()); + } + entered_url = "https://".concat(entered_url); + } + return entered_url; + } + + /** + * Checks if the entered url is valid or not. + * + * @param enteredUrl + * @return true if it's not empty nor contains only the protocol. + */ + boolean validURL(String enteredUrl) { + return android.util.Patterns.WEB_URL.matcher(enteredUrl).matches(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java new file mode 100644 index 00000000..22edd9ee --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2020 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.providersetup.activities; + +import androidx.appcompat.app.AppCompatActivity; +import android.view.View; + +import butterknife.ButterKnife; + +/** + * Automatically inject with ButterKnife after calling setContentView + */ + +public abstract class ButterKnifeActivity extends AppCompatActivity { + + @Override + public void setContentView(View view) { + super.setContentView(view); + ButterKnife.inject(this); + } + + @Override + public void setContentView(int layoutResID) { + super.setContentView(layoutResID); + ButterKnife.inject(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java new file mode 100644 index 00000000..3712c544 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java @@ -0,0 +1,289 @@ +package se.leap.bitmaskclient.providersetup.activities; + +import android.content.SharedPreferences; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.Guideline; +import androidx.core.content.ContextCompat; +import androidx.appcompat.widget.AppCompatTextView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + +import butterknife.InjectView; +import butterknife.Optional; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.views.ProviderHeaderView; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; + +/** + * Base Activity for configuration wizard activities + * + * Created by fupduck on 09.01.18. + */ + +public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity { + + private static final String TAG = ConfigWizardBaseActivity.class.getName(); + public static final float GUIDE_LINE_COMPACT_DELTA = 0.1f; + protected SharedPreferences preferences; + + @InjectView(R.id.header) + ProviderHeaderView providerHeaderView; + + //Add provider screen has no loading screen + @Optional + @InjectView(R.id.loading_screen) + protected LinearLayout loadingScreen; + + @Optional + @InjectView(R.id.progressbar) + protected ProgressBar progressBar; + + @Optional + @InjectView(R.id.progressbar_description) + protected AppCompatTextView progressbarText; + + //Only tablet layouts have guidelines as they are based on a ConstraintLayout + @Optional + @InjectView(R.id.guideline_top) + protected Guideline guideline_top; + + @Optional + @InjectView(R.id.guideline_bottom) + protected Guideline guideline_bottom; + + @InjectView(R.id.content) + protected LinearLayout content; + + protected Provider provider; + + protected boolean isCompactLayout = false; + protected boolean isActivityShowing; + + private float defaultGuidelineTopPercentage; + private float defaultGuidelineBottomPercentage; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + provider = getIntent().getParcelableExtra(PROVIDER_KEY); + } + + @Override + public void setContentView(View view) { + super.setContentView(view); + initContentView(); + } + + @Override + public void setContentView(int layoutResID) { + super.setContentView(layoutResID); + initContentView(); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + super.setContentView(view, params); + initContentView(); + } + + private void initContentView() { + if (provider != null) { + setProviderHeaderText(provider.getName()); + } + setProgressbarColorForPreLollipop(); + setDefaultGuidelineValues(); + setGlobalLayoutChangeListener(); + } + + private void setDefaultGuidelineValues() { + if (isTabletLayout()) { + defaultGuidelineTopPercentage = ((ConstraintLayout.LayoutParams) guideline_top.getLayoutParams()).guidePercent; + defaultGuidelineBottomPercentage = ((ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams()).guidePercent; + } + } + + private void setProgressbarColorForPreLollipop() { + if (progressBar == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return; + } + progressBar.getIndeterminateDrawable().setColorFilter( + ContextCompat.getColor(this, R.color.colorPrimary), + PorterDuff.Mode.SRC_IN); + } + + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (provider != null) { + outState.putParcelable(PROVIDER_KEY, provider); + } + } + + @Override + protected void onPause() { + super.onPause(); + isActivityShowing = false; + } + + @Override + protected void onResume() { + super.onResume(); + isActivityShowing = true; + } + + protected void restoreState(Bundle savedInstanceState) { + if (savedInstanceState != null && savedInstanceState.containsKey(PROVIDER_KEY)) { + provider = savedInstanceState.getParcelable(PROVIDER_KEY); + } + } + + protected void setProviderHeaderLogo(@DrawableRes int providerHeaderLogo) { + providerHeaderView.setLogo(providerHeaderLogo); + } + + protected void setProviderHeaderText(String providerHeaderText) { + providerHeaderView.setTitle(providerHeaderText); + } + + protected void setProviderHeaderText(@StringRes int providerHeaderText) { + providerHeaderView.setTitle(providerHeaderText); + } + + protected void hideProgressBar() { + if (loadingScreen == null) { + return; + } + loadingScreen.setVisibility(GONE); + content.setVisibility(VISIBLE); + } + + protected void showProgressBar() { + if (loadingScreen == null) { + return; + } + content.setVisibility(GONE); + loadingScreen.setVisibility(VISIBLE); + } + + protected void setProgressbarText(@StringRes int progressbarText) { + if (this.progressbarText == null) { + return; + } + this.progressbarText.setText(progressbarText); + } + + + protected void showCompactLayout() { + if (isCompactLayout) { + return; + } + + if (isTabletLayoutInLandscape() || isPhoneLayout()) { + providerHeaderView.showCompactLayout(); + } + + showIncreasedTabletContentArea(); + isCompactLayout = true; + } + + protected void showStandardLayout() { + if (!isCompactLayout) { + return; + } + providerHeaderView.showStandardLayout(); + showStandardTabletContentArea(); + isCompactLayout = false; + } + + private boolean isTabletLayoutInLandscape() { + // TabletLayout is based on a ConstraintLayout and uses Guidelines whereas the phone layout + // has no such elements in it's layout xml file + return guideline_top != null && + guideline_bottom != null && + getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; + } + + protected boolean isPhoneLayout() { + return guideline_top == null && guideline_bottom == null; + } + + protected boolean isTabletLayout() { + return guideline_top != null && guideline_bottom != null; + } + + /** + * Increases the white content area in tablet layouts + */ + private void showIncreasedTabletContentArea() { + if (isPhoneLayout()) { + return; + } + ConstraintLayout.LayoutParams guideLineTopParams = (ConstraintLayout.LayoutParams) guideline_top.getLayoutParams(); + float increasedTopPercentage = defaultGuidelineTopPercentage - GUIDE_LINE_COMPACT_DELTA; + guideLineTopParams.guidePercent = increasedTopPercentage > 0f ? increasedTopPercentage : 0f; + guideline_top.setLayoutParams(guideLineTopParams); + + ConstraintLayout.LayoutParams guideLineBottomParams = (ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams(); + float increasedBottomPercentage = defaultGuidelineBottomPercentage + GUIDE_LINE_COMPACT_DELTA; + guideLineBottomParams.guidePercent = increasedBottomPercentage < 1f ? increasedBottomPercentage : 1f; + guideline_bottom.setLayoutParams(guideLineBottomParams); + } + + /** + * Restores the default size of the white content area in tablet layouts + */ + private void showStandardTabletContentArea() { + if (isPhoneLayout()) { + return; + } + ConstraintLayout.LayoutParams guideLineTopParams = (ConstraintLayout.LayoutParams) guideline_top.getLayoutParams(); + guideLineTopParams.guidePercent = defaultGuidelineTopPercentage; + guideline_top.setLayoutParams(guideLineTopParams); + + ConstraintLayout.LayoutParams guideLineBottomParams = (ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams(); + guideLineBottomParams.guidePercent = defaultGuidelineBottomPercentage; + guideline_bottom.setLayoutParams(guideLineBottomParams); + } + + /** + * Checks if the keyboard is shown and switches between the standard layout and the compact layout + */ + private void setGlobalLayoutChangeListener() { + final View rootView = content.getRootView(); + rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + Rect r = new Rect(); + //r will be populated with the coordinates of your view that area still visible. + rootView.getWindowVisibleDisplayFrame(r); + + float deltaHiddenScreen = 1f - ((float) (r.bottom - r.top) / (float) rootView.getHeight()); + if (deltaHiddenScreen > 0.25f) { + // if more than 1/4 of the screen is hidden + showCompactLayout(); + } else { + showStandardLayout(); + } + } + }); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java new file mode 100644 index 00000000..161c53d3 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java @@ -0,0 +1,121 @@ +/** + * 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.providersetup.activities; + +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import se.leap.bitmaskclient.BuildConfig; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderAPICommand; +import se.leap.bitmaskclient.R; + +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.preferAnonymousUsage; + +/** + * Created by cyberta on 17.08.18. + */ + +public class CustomProviderSetupActivity extends ProviderSetupBaseActivity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setUpInitialUI(); + restoreState(savedInstanceState); + setProvider(new Provider(BuildConfig.customProviderUrl, BuildConfig.geoipUrl, BuildConfig.customProviderIp, BuildConfig.customProviderApiIp)); + } + + @Override + protected void onResume() { + super.onResume(); + if (getConfigState() == ProviderConfigState.PROVIDER_NOT_SET) { + showProgressBar(); + setupProvider(); + } + } + + private void setUpInitialUI() { + setContentView(R.layout.a_custom_provider_setup); + setProviderHeaderText(R.string.setup_provider); + hideProgressBar(); + } + + private void setupProvider() { + setProviderConfigState(SETTING_UP_PROVIDER); + ProviderAPICommand.execute(this, SET_UP_PROVIDER, getProvider()); + } + + // ------- ProviderSetupInterface ---v + @Override + public void handleProviderSetUp(Provider provider) { + setProvider(provider); + if (provider.allowsAnonymous()) { + downloadVpnCertificate(); + } else { + showProviderDetails(); + } + } + + @Override + public void handleCorrectlyDownloadedCertificate(Provider provider) { + if (preferAnonymousUsage()) { + finishWithSetupWithProvider(provider); + } else { + this.provider = provider; + showProviderDetails(); + } + } + + // ------- DownloadFailedDialogInterface ---v + @Override + public void retrySetUpProvider(@NonNull Provider provider) { + setupProvider(); + showProgressBar(); + } + + @Override + public void cancelSettingUpProvider() { + super.cancelSettingUpProvider(); + finish(); + } + + @Override + public void addAndSelectNewProvider(String url) { + // ignore + } + + private void finishWithSetupWithProvider(Provider provider) { + Intent intent = new Intent(); + intent.putExtra(Provider.KEY, provider); + setResult(RESULT_OK, intent); + finish(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) { + setResult(resultCode, data); + finish(); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java new file mode 100644 index 00000000..a8bac6d8 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java @@ -0,0 +1,32 @@ +package se.leap.bitmaskclient.providersetup.activities; + +import android.os.Bundle; +import androidx.annotation.Nullable; + +import butterknife.OnClick; +import se.leap.bitmaskclient.R; + +/** + * Activity to login to chosen Provider + * + * Created by fupduck on 09.01.18. + */ + +public class LoginActivity extends ProviderCredentialsBaseActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setProgressbarText(R.string.logging_in); + setProviderHeaderLogo(R.drawable.logo); + setProviderHeaderText(R.string.login_to_profile); + } + + @Override + @OnClick(R.id.button) + void handleButton() { + super.handleButton(); + login(getUsername(), getPassword()); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java new file mode 100644 index 00000000..91d0de56 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java @@ -0,0 +1,479 @@ +/** + * 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.providersetup.activities; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatTextView; +import android.text.Editable; +import android.text.Html; +import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.util.Log; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; + +import org.json.JSONArray; +import org.json.JSONException; + +import butterknife.InjectView; +import butterknife.OnClick; +import se.leap.bitmaskclient.base.models.Constants.CREDENTIAL_ERRORS; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderAPI; +import se.leap.bitmaskclient.providersetup.ProviderAPICommand; +import se.leap.bitmaskclient.R; + +import static android.text.TextUtils.isEmpty; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE; +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY; +import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_PASSWORD; +import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_USERNAME; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOG_IN; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.SIGN_UP; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE; + +/** + * Base Activity for activities concerning a provider interaction + * + * Created by fupduck on 09.01.18. + */ + +public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseActivity { + + final protected static String TAG = ProviderCredentialsBaseActivity.class.getName(); + + final private static String ACTIVITY_STATE = "ACTIVITY STATE"; + + final private static String SHOWING_FORM = "SHOWING_FORM"; + final private static String PERFORMING_ACTION = "PERFORMING_ACTION"; + final private static String USERNAME_ERROR = "USERNAME_ERROR"; + final private static String PASSWORD_ERROR = "PASSWORD_ERROR"; + final private static String PASSWORD_VERIFICATION_ERROR = "PASSWORD_VERIFICATION_ERROR"; + + protected Intent mConfigState = new Intent(SHOWING_FORM); + protected ProviderAPIBroadcastReceiver providerAPIBroadcastReceiver; + + @InjectView(R.id.provider_credentials_user_message) + AppCompatTextView userMessage; + + @InjectView(R.id.provider_credentials_username) + TextInputEditText usernameField; + + @InjectView(R.id.provider_credentials_password) + TextInputEditText passwordField; + + @InjectView(R.id.provider_credentials_password_verification) + TextInputEditText passwordVerificationField; + + @InjectView(R.id.provider_credentials_username_error) + TextInputLayout usernameError; + + @InjectView(R.id.provider_credentials_password_error) + TextInputLayout passwordError; + + @InjectView(R.id.provider_credentials_password_verification_error) + TextInputLayout passwordVerificationError; + + @InjectView(R.id.button) + AppCompatButton button; + + private boolean isUsernameError = false; + private boolean isPasswordError = false; + private boolean isVerificationError = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.a_provider_credentials); + providerAPIBroadcastReceiver = new ProviderAPIBroadcastReceiver(); + + IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_PROVIDER_API_EVENT); + updateIntentFilter.addCategory(Intent.CATEGORY_DEFAULT); + LocalBroadcastManager.getInstance(this).registerReceiver(providerAPIBroadcastReceiver, updateIntentFilter); + + setUpListeners(); + restoreState(savedInstanceState); + + String userMessageString = getIntent().getStringExtra(USER_MESSAGE); + if (userMessageString != null) { + userMessage.setText(userMessageString); + userMessage.setVisibility(VISIBLE); + } + } + + @Override + protected void onResume() { + super.onResume(); + + String action = mConfigState.getAction(); + if (action == null) { + return; + } + + if(action.equalsIgnoreCase(PERFORMING_ACTION)) { + showProgressBar(); + } + } + + protected void restoreState(Bundle savedInstance) { + super.restoreState(savedInstance); + if (savedInstance == null) { + return; + } + if (savedInstance.getString(USER_MESSAGE) != null) { + userMessage.setText(savedInstance.getString(USER_MESSAGE)); + userMessage.setVisibility(VISIBLE); + } + updateUsernameError(savedInstance.getString(USERNAME_ERROR)); + updatePasswordError(savedInstance.getString(PASSWORD_ERROR)); + updateVerificationError(savedInstance.getString(PASSWORD_VERIFICATION_ERROR)); + if (savedInstance.getString(ACTIVITY_STATE) != null) { + mConfigState.setAction(savedInstance.getString(ACTIVITY_STATE)); + } + } + + private void updateUsernameError(String usernameErrorString) { + usernameError.setError(usernameErrorString); + isUsernameError = usernameErrorString != null; + updateButton(); + } + + private void updatePasswordError(String passwordErrorString) { + passwordError.setError(passwordErrorString); + isPasswordError = passwordErrorString != null; + updateButton(); + } + + private void updateVerificationError(String verificationErrorString) { + passwordVerificationError.setError(verificationErrorString); + isVerificationError = verificationErrorString != null; + updateButton(); + } + + private void updateButton() { + button.setEnabled(!isPasswordError && + !isUsernameError && + !isVerificationError && + !isEmpty(passwordField.getText()) && + !isEmpty(usernameField.getText()) && + !(passwordVerificationField.getVisibility() == VISIBLE && + getPasswordVerification().length() == 0)); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putString(ACTIVITY_STATE, mConfigState.getAction()); + if (userMessage.getText() != null && userMessage.getVisibility() == VISIBLE) { + outState.putString(USER_MESSAGE, userMessage.getText().toString()); + } + if (usernameError.getError() != null) { + outState.putString(USERNAME_ERROR, usernameError.getError().toString()); + } + if (passwordError.getError() != null) { + outState.putString(PASSWORD_ERROR, passwordError.getError().toString()); + } + if (passwordVerificationError.getError() != null) { + outState.putString(PASSWORD_VERIFICATION_ERROR, passwordVerificationError.getError().toString()); + } + + super.onSaveInstanceState(outState); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (providerAPIBroadcastReceiver != null) + LocalBroadcastManager.getInstance(this).unregisterReceiver(providerAPIBroadcastReceiver); + } + + @OnClick(R.id.button) + void handleButton() { + mConfigState.setAction(PERFORMING_ACTION); + hideKeyboard(); + showProgressBar(); + } + + protected void setButtonText(@StringRes int buttonText) { + button.setText(buttonText); + } + + String getUsername() { + String username = usernameField.getText().toString(); + String providerDomain = provider.getDomain(); + if (username.endsWith(providerDomain)) { + try { + return username.split("@" + providerDomain)[0]; + } catch (ArrayIndexOutOfBoundsException e) { + return ""; + } + } + return username; + } + + String getPassword() { + return passwordField.getText().toString(); + } + + String getPasswordVerification() { + return passwordVerificationField.getText().toString(); + } + + void login(String username, String password) { + + Bundle parameters = bundleUsernameAndPassword(username, password); + ProviderAPICommand.execute(this, LOG_IN, parameters, provider); + } + + public void signUp(String username, String password) { + + Bundle parameters = bundleUsernameAndPassword(username, password); + ProviderAPICommand.execute(this, SIGN_UP, parameters, provider); + } + + void downloadVpnCertificate(Provider handledProvider) { + provider = handledProvider; + ProviderAPICommand.execute(this, DOWNLOAD_VPN_CERTIFICATE, provider); + } + + protected Bundle bundleUsernameAndPassword(String username, String password) { + Bundle parameters = new Bundle(); + if (!username.isEmpty()) + parameters.putString(CREDENTIALS_USERNAME, username); + if (!password.isEmpty()) + parameters.putString(CREDENTIALS_PASSWORD, password); + return parameters; + } + + private void setUpListeners() { + usernameField.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (getUsername().equalsIgnoreCase("")) { + s.clear(); + updateUsernameError(getString(R.string.username_ask)); + } else { + updateUsernameError(null); + String suffix = "@" + provider.getDomain(); + if (!usernameField.getText().toString().endsWith(suffix)) { + s.append(suffix); + usernameField.setSelection(usernameField.getText().toString().indexOf('@')); + } + } + } + }); + usernameField.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == IME_ACTION_DONE + || event != null && event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + passwordField.requestFocus(); + return true; + } + return false; + } + }); + + passwordField.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if(getPassword().length() < 8) { + updatePasswordError(getString(R.string.error_not_valid_password_user_message)); + } else { + updatePasswordError(null); + } + } + }); + passwordField.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == IME_ACTION_DONE + || event != null && event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + if (passwordVerificationField.getVisibility() == VISIBLE) { + passwordVerificationField.requestFocus(); + } else { + button.performClick(); + } + return true; + } + return false; + } + }); + + passwordVerificationField.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if(getPassword().equals(getPasswordVerification())) { + updateVerificationError(null); + } else { + updateVerificationError(getString(R.string.password_mismatch)); + } + } + }); + passwordVerificationField.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == IME_ACTION_DONE + || event != null && event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + button.performClick(); + return true; + } + return false; + } + }); + } + + private void hideKeyboard() { + InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(passwordField.getWindowToken(), 0); + } + } + + private void handleReceivedErrors(Bundle arguments) { + if (arguments.containsKey(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString())) { + updatePasswordError(getString(R.string.error_not_valid_password_user_message)); + } else if (arguments.containsKey(CREDENTIAL_ERRORS.RISEUP_WARNING.toString())) { + userMessage.setVisibility(VISIBLE); + userMessage.setText(R.string.login_riseup_warning); + } + if (arguments.containsKey(CREDENTIALS_USERNAME)) { + String username = arguments.getString(CREDENTIALS_USERNAME); + usernameField.setText(username); + } + if (arguments.containsKey(CREDENTIAL_ERRORS.USERNAME_MISSING.toString())) { + updateUsernameError(getString(R.string.username_ask)); + } + if (arguments.containsKey(USER_MESSAGE)) { + String userMessageString = arguments.getString(USER_MESSAGE); + try { + userMessageString = new JSONArray(userMessageString).getString(0); + } catch (JSONException e) { + e.printStackTrace(); + } + + if (Build.VERSION.SDK_INT >= VERSION_CODES.N) { + userMessage.setText(Html.fromHtml(userMessageString, Html.FROM_HTML_MODE_LEGACY)); + } else { + userMessage.setText(Html.fromHtml(userMessageString)); + } + Linkify.addLinks(userMessage, Linkify.ALL); + userMessage.setMovementMethod(LinkMovementMethod.getInstance()); + userMessage.setVisibility(VISIBLE); + } else if (userMessage.getVisibility() != GONE) { + userMessage.setVisibility(GONE); + } + + if (!usernameField.getText().toString().isEmpty() && passwordField.isFocusable()) + passwordField.requestFocus(); + + mConfigState.setAction(SHOWING_FORM); + hideProgressBar(); + } + + private void successfullyFinished(Provider handledProvider) { + provider = handledProvider; + Intent resultData = new Intent(); + resultData.putExtra(Provider.KEY, provider); + setResult(RESULT_OK, resultData); + finish(); + } + + //TODO: replace with EipSetupObserver + public class ProviderAPIBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "received Broadcast"); + + String action = intent.getAction(); + if (action == null || !action.equalsIgnoreCase(BROADCAST_PROVIDER_API_EVENT)) { + return; + } + + int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED); + Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY); + Provider handledProvider = resultData.getParcelable(PROVIDER_KEY); + + switch (resultCode) { + case ProviderAPI.SUCCESSFUL_SIGNUP: + String password = resultData.getString(CREDENTIALS_PASSWORD); + String username = resultData.getString(CREDENTIALS_USERNAME); + login(username, password); + break; + case ProviderAPI.SUCCESSFUL_LOGIN: + downloadVpnCertificate(handledProvider); + break; + case ProviderAPI.FAILED_LOGIN: + case ProviderAPI.FAILED_SIGNUP: + handleReceivedErrors((Bundle) intent.getParcelableExtra(BROADCAST_RESULT_KEY)); + break; + + case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE: + // error handling takes place in MainActivity + case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE: + successfullyFinished(handledProvider); + break; + } + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java new file mode 100644 index 00000000..46a40d11 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2017 LEAP Encryption Access Project and contributors + *

+ * 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.providersetup.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.ListView; + +import androidx.annotation.NonNull; + +import com.pedrogomez.renderers.Renderer; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import butterknife.InjectView; +import butterknife.OnItemClick; +import se.leap.bitmaskclient.providersetup.AddProviderActivity; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderListActivity; +import se.leap.bitmaskclient.providersetup.ProviderRenderer; +import se.leap.bitmaskclient.providersetup.ProviderRendererBuilder; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.providersetup.ProviderListAdapter; + +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_ADD_PROVIDER; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SHOW_FAILED_DIALOG; + +/** + * abstract base Activity that builds and shows the list of known available providers. + * The implementation of ProviderListBaseActivity differ in that they may or may not allow to bypass + * secure download mechanisms including certificate validation. + *

+ * It also allows the user to enter custom providers with a button. + * + * @author parmegv + * @author cyberta + */ + +public abstract class ProviderListBaseActivity extends ProviderSetupBaseActivity { + + @InjectView(R.id.provider_list) + protected ListView providerListView; + @Inject + protected ProviderListAdapter adapter; + + final public static String TAG = ProviderListActivity.class.getSimpleName(); + final protected static String EXTRAS_KEY_INVALID_URL = "INVALID_URL"; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setUpInitialUI(); + initProviderList(); + restoreState(savedInstanceState); + } + + public abstract void retrySetUpProvider(@NonNull Provider provider); + + protected abstract void onItemSelectedLogic(); + + private void initProviderList() { + List> prototypes = new ArrayList<>(); + prototypes.add(new ProviderRenderer(this)); + ProviderRendererBuilder providerRendererBuilder = new ProviderRendererBuilder(prototypes); + adapter = new ProviderListAdapter(getLayoutInflater(), providerRendererBuilder, getProviderManager()); + providerListView.setAdapter(adapter); + } + + private void setUpInitialUI() { + setContentView(R.layout.a_provider_list); + setProviderHeaderText(R.string.setup_provider); + hideProgressBar(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) { + if (resultCode == RESULT_OK) { + setResult(resultCode, data); + finish(); + } + } else if (requestCode == REQUEST_CODE_ADD_PROVIDER) { + if (resultCode == RESULT_OK) { + testNewURL = true; + String newUrl = data.getStringExtra(AddProviderActivity.EXTRAS_KEY_NEW_URL); + this.provider.setMainUrl(newUrl); + showAndSelectProvider(newUrl); + } else { + cancelSettingUpProvider(); + } + } + } + + public void showAndSelectProvider(String newURL) { + provider = new Provider(newURL, null, null); + autoSelectProvider(); + } + + private void autoSelectProvider() { + onItemSelectedLogic(); + showProgressBar(); + } + + // ------- ProviderSetupInterface ---v + @Override + public void handleProviderSetUp(Provider handledProvider) { + this.provider = handledProvider; + adapter.add(provider); + adapter.saveProviders(); + if (provider.allowsAnonymous()) { + //FIXME: providerApiBroadcastReceiver.getConfigState().putExtra(SERVICES_RETRIEVED, true); DEAD CODE??? + downloadVpnCertificate(); + } else { + showProviderDetails(); + } + } + + @Override + public void handleCorrectlyDownloadedCertificate(Provider handledProvider) { + this.provider = handledProvider; + showProviderDetails(); + } + + @OnItemClick(R.id.provider_list) + void onItemSelected(int position) { + if (SETTING_UP_PROVIDER == getConfigState() || + SHOW_FAILED_DIALOG == getConfigState()) { + return; + } + + //TODO Code 2 pane view + provider = adapter.getItem(position); + if (provider != null && !provider.isDefault()) { + //TODO Code 2 pane view + providerConfigState = SETTING_UP_PROVIDER; + showProgressBar(); + onItemSelectedLogic(); + } else { + addAndSelectNewProvider(); + } + } + + @Override + public void onBackPressed() { + if (SETTING_UP_PROVIDER == providerConfigState || + SHOW_FAILED_DIALOG == providerConfigState) { + cancelSettingUpProvider(); + } else { + super.onBackPressed(); + } + } + + /** + * Open the new provider dialog + */ + public void addAndSelectNewProvider() { + Intent intent = new Intent(this, AddProviderActivity.class); + startActivityForResult(intent, REQUEST_CODE_ADD_PROVIDER); + } + + /** + * Open the new provider dialog + */ + @Override + public void addAndSelectNewProvider(String url) { + testNewURL = false; + Intent intent = new Intent(this, AddProviderActivity.class); + intent.putExtra(EXTRAS_KEY_INVALID_URL, url); + startActivityForResult(intent, REQUEST_CODE_ADD_PROVIDER); + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java new file mode 100644 index 00000000..e54fb048 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java @@ -0,0 +1,240 @@ +/** + * 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.providersetup.activities; + +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; + +import se.leap.bitmaskclient.base.FragmentManagerEnhanced; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.providersetup.ProviderAPICommand; +import se.leap.bitmaskclient.providersetup.ProviderDetailActivity; +import se.leap.bitmaskclient.providersetup.ProviderApiSetupBroadcastReceiver; +import se.leap.bitmaskclient.providersetup.ProviderManager; +import se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog; +import se.leap.bitmaskclient.providersetup.ProviderSetupInterface; + +import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_PROVIDER_DETAILS; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.PENDING_SHOW_FAILED_DIALOG; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.PENDING_SHOW_PROVIDER_DETAILS; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.PROVIDER_NOT_SET; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SHOWING_PROVIDER_DETAILS; +import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SHOW_FAILED_DIALOG; + +/** + * Created by cyberta on 19.08.18. + */ + +public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity implements ProviderSetupInterface, ProviderSetupFailedDialog.DownloadFailedDialogInterface { + final public static String TAG = "PoviderSetupActivity"; + final private static String ACTIVITY_STATE = "ACTIVITY STATE"; + final private static String REASON_TO_FAIL = "REASON TO FAIL"; + + protected ProviderSetupInterface.ProviderConfigState providerConfigState = PROVIDER_NOT_SET; + private ProviderManager providerManager; + private FragmentManagerEnhanced fragmentManager; + + private String reasonToFail; + protected boolean testNewURL; + + private ProviderApiSetupBroadcastReceiver providerAPIBroadcastReceiver; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + fragmentManager = new FragmentManagerEnhanced(getSupportFragmentManager()); + providerManager = ProviderManager.getInstance(getAssets(), getExternalFilesDir(null)); + setUpProviderAPIResultReceiver(); + } + + @Override + protected void onResume() { + super.onResume(); + Log.d(TAG, "resuming with ConfigState: " + providerConfigState.toString()); + if (SETTING_UP_PROVIDER == providerConfigState) { + showProgressBar(); + } else if (PENDING_SHOW_FAILED_DIALOG == providerConfigState) { + showProgressBar(); + showDownloadFailedDialog(); + } else if (SHOW_FAILED_DIALOG == providerConfigState) { + showProgressBar(); + } else if (SHOWING_PROVIDER_DETAILS == providerConfigState) { + cancelSettingUpProvider(); + } else if (PENDING_SHOW_PROVIDER_DETAILS == providerConfigState) { + showProviderDetails(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (providerAPIBroadcastReceiver != null) { + LocalBroadcastManager.getInstance(this).unregisterReceiver(providerAPIBroadcastReceiver); + } + providerAPIBroadcastReceiver = null; + } + + + @Override + public void onSaveInstanceState(@NotNull Bundle outState) { + outState.putString(ACTIVITY_STATE, providerConfigState.toString()); + outState.putString(REASON_TO_FAIL, reasonToFail); + + super.onSaveInstanceState(outState); + } + + protected FragmentManagerEnhanced getFragmentManagerEnhanced() { + return fragmentManager; + } + + protected ProviderManager getProviderManager() { + return providerManager; + } + + protected void setProviderConfigState(ProviderConfigState state) { + this.providerConfigState = state; + } + + protected void setProvider(Provider provider) { + this.provider = provider; + } + + // --------- ProviderSetupInterface ---v + @Override + public Provider getProvider() { + return provider; + } + + @Override + public ProviderConfigState getConfigState() { + return providerConfigState; + } + + @Override + public void handleProviderSetupFailed(Bundle resultData) { + reasonToFail = resultData.getString(ERRORS); + showDownloadFailedDialog(); + } + + @Override + public void handleIncorrectlyDownloadedCertificate() { + cancelSettingUpProvider(); + setResult(RESULT_CANCELED, new Intent(getConfigState().toString())); + } + + // -------- DownloadFailedDialogInterface ---v + @Override + public void cancelSettingUpProvider() { + providerConfigState = PROVIDER_NOT_SET; + provider = null; + hideProgressBar(); + } + + @Override + public void updateProviderDetails() { + providerConfigState = SETTING_UP_PROVIDER; + ProviderAPICommand.execute(this, UPDATE_PROVIDER_DETAILS, provider); + } + + protected void restoreState(Bundle savedInstanceState) { + super.restoreState(savedInstanceState); + if (savedInstanceState == null) { + return; + } + this.providerConfigState = ProviderSetupInterface.ProviderConfigState.valueOf(savedInstanceState.getString(ACTIVITY_STATE, PROVIDER_NOT_SET.toString())); + if (savedInstanceState.containsKey(REASON_TO_FAIL)) { + reasonToFail = savedInstanceState.getString(REASON_TO_FAIL); + } + } + + private void setUpProviderAPIResultReceiver() { + providerAPIBroadcastReceiver = new ProviderApiSetupBroadcastReceiver(this); + + IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_PROVIDER_API_EVENT); + updateIntentFilter.addCategory(Intent.CATEGORY_DEFAULT); + LocalBroadcastManager.getInstance(this).registerReceiver(providerAPIBroadcastReceiver, updateIntentFilter); + } + + /** + * Asks ProviderApiService to download an anonymous (anon) VPN certificate. + */ + protected void downloadVpnCertificate() { + ProviderAPICommand.execute(this, DOWNLOAD_VPN_CERTIFICATE, provider); + } + + /** + * Once selected a provider, this fragment offers the user to log in, + * use it anonymously (if possible) + * or cancel his/her election pressing the back button. + */ + public void showProviderDetails() { + // show only if current activity is shown + if (isActivityShowing && + providerConfigState != SHOWING_PROVIDER_DETAILS) { + providerConfigState = SHOWING_PROVIDER_DETAILS; + Intent intent = new Intent(this, ProviderDetailActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.putExtra(PROVIDER_KEY, provider); + startActivityForResult(intent, REQUEST_CODE_CONFIGURE_LEAP); + } else { + providerConfigState = PENDING_SHOW_PROVIDER_DETAILS; + } + } + + /** + * Shows an error dialog, if configuring of a provider failed. + */ + public void showDownloadFailedDialog() { + try { + providerConfigState = SHOW_FAILED_DIALOG; + FragmentTransaction fragmentTransaction = fragmentManager.removePreviousFragment(ProviderSetupFailedDialog.TAG); + DialogFragment newFragment; + try { + JSONObject errorJson = new JSONObject(reasonToFail); + newFragment = ProviderSetupFailedDialog.newInstance(provider, errorJson, testNewURL); + } catch (JSONException e) { + e.printStackTrace(); + newFragment = ProviderSetupFailedDialog.newInstance(provider, reasonToFail); + } catch (NullPointerException e) { + //reasonToFail was null + return; + } + newFragment.show(fragmentTransaction, ProviderSetupFailedDialog.TAG); + } catch (IllegalStateException e) { + e.printStackTrace(); + providerConfigState = PENDING_SHOW_FAILED_DIALOG; + } + } + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java new file mode 100644 index 00000000..c0245845 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java @@ -0,0 +1,55 @@ +/** + * 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.providersetup.activities; + +import android.os.Bundle; +import androidx.annotation.Nullable; + +import butterknife.OnClick; +import se.leap.bitmaskclient.R; + +import static android.view.View.VISIBLE; + +/** + * Create an account with a provider + */ + +public class SignupActivity extends ProviderCredentialsBaseActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setProviderHeaderLogo(R.drawable.logo); + setProviderHeaderText(R.string.create_profile); + + setProgressbarText(R.string.signing_up); + setButtonText(R.string.signup_button); + + passwordVerificationField.setVisibility(VISIBLE); + passwordVerificationError.setVisibility(VISIBLE); + } + + @Override + @OnClick(R.id.button) + void handleButton() { + super.handleButton(); + if (getPassword().equals(getPasswordVerification())) { + signUp(getUsername(), getPassword()); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java new file mode 100644 index 00000000..44de1e6d --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java @@ -0,0 +1,39 @@ +package se.leap.bitmaskclient.providersetup.connectivity; + +import org.jetbrains.annotations.NotNull; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +import okhttp3.Dns; +import se.leap.bitmaskclient.base.models.Provider; +import se.leap.bitmaskclient.base.models.ProviderObservable; +import se.leap.bitmaskclient.base.utils.IPAddress; + +class DnsResolver implements Dns { + + @Override + public List lookup(@NotNull String hostname) throws UnknownHostException { + try { + return Dns.SYSTEM.lookup(hostname); + } catch (UnknownHostException e) { + ProviderObservable observable = ProviderObservable.getInstance(); + Provider currentProvider; + if (observable.getProviderForDns() != null) { + currentProvider = observable.getProviderForDns(); + } else { + currentProvider = observable.getCurrentProvider(); + } + String ip = currentProvider.getIpForHostname(hostname); + if (!ip.isEmpty()) { + ArrayList addresses = new ArrayList<>(); + addresses.add(InetAddress.getByAddress(hostname, IPAddress.asBytes(ip))); + return addresses; + } else { + throw new UnknownHostException("Hostname " + hostname + " not found"); + } + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java new file mode 100644 index 00000000..2077a8b9 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java @@ -0,0 +1,182 @@ +/** + * 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.providersetup.connectivity; + +import android.content.res.Resources; +import android.os.Build; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import okhttp3.CipherSuite; +import okhttp3.ConnectionSpec; +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.TlsVersion; + +import static android.text.TextUtils.isEmpty; +import static se.leap.bitmaskclient.R.string.certificate_error; +import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; +import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message; +import static se.leap.bitmaskclient.R.string.keyChainAccessError; +import static se.leap.bitmaskclient.R.string.server_unreachable_message; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString; + +/** + * Created by cyberta on 08.01.18. + */ + +public class OkHttpClientGenerator { + + Resources resources; + + public OkHttpClientGenerator(/*SharedPreferences preferences,*/ Resources resources) { + this.resources = resources; + } + + public OkHttpClient initCommercialCAHttpClient(JSONObject initError) { + return initHttpClient(initError, null); + } + + public OkHttpClient initSelfSignedCAHttpClient(String caCert, JSONObject initError) { + return initHttpClient(initError, caCert); + } + + public OkHttpClient init() { + try { + return createClient(null); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private OkHttpClient initHttpClient(JSONObject initError, String certificate) { + if (resources == null) { + return null; + } + try { + return createClient(certificate); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + // TODO ca cert is invalid - show better error ?! + addErrorMessageToJson(initError, getProviderFormattedString(resources, certificate_error)); + } catch (IllegalStateException | KeyManagementException | KeyStoreException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage())); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, resources.getString(error_no_such_algorithm_exception_user_message)); + } catch (CertificateException e) { + e.printStackTrace(); + // TODO ca cert is invalid - show better error ?! + addErrorMessageToJson(initError, getProviderFormattedString(resources, certificate_error)); + } catch (UnknownHostException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, resources.getString(server_unreachable_message)); + } catch (IOException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message)); + } catch (Exception e) { + e.printStackTrace(); + // unexpected exception, should never happen + // only to shorten the method signature createClient(String certificate) + } + return null; + } + + private OkHttpClient createClient(String certificate) throws Exception { + TLSCompatSocketFactory sslCompatFactory; + ConnectionSpec spec = getConnectionSpec(); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + + if (!isEmpty(certificate)) { + sslCompatFactory = new TLSCompatSocketFactory(certificate); + } else { + sslCompatFactory = new TLSCompatSocketFactory(); + } + sslCompatFactory.initSSLSocketFactory(clientBuilder); + clientBuilder.cookieJar(getCookieJar()) + .connectionSpecs(Collections.singletonList(spec)); + clientBuilder.dns(new DnsResolver()); + return clientBuilder.build(); + } + + + + @NonNull + private ConnectionSpec getConnectionSpec() { + ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3); + //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels + //figure out how to use bcjsse for that purpose + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) + connectionSpecbuilder.cipherSuites( + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + ); + return connectionSpecbuilder.build(); + } + + @NonNull + private CookieJar getCookieJar() { + return new CookieJar() { + private final HashMap> cookieStore = new HashMap<>(); + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + cookieStore.put(url.host(), cookies); + } + + @Override + public List loadForRequest(HttpUrl url) { + List cookies = cookieStore.get(url.host()); + return cookies != null ? cookies : new ArrayList(); + } + }; + } + + private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) { + try { + jsonObject.put(ERRORS, errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java new file mode 100644 index 00000000..5357fd74 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java @@ -0,0 +1,158 @@ +package se.leap.bitmaskclient.providersetup.connectivity; + +import android.text.TextUtils; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.util.Arrays; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.OkHttpClient; +import se.leap.bitmaskclient.base.utils.ConfigHelper; + +/** + * Created by cyberta on 24.10.17. + * This class ensures that modern TLS algorithms will also be used on old devices (Android 4.1 - Android 4.4.4) in order to avoid + * attacks like POODLE. + */ + +public class TLSCompatSocketFactory extends SSLSocketFactory { + + private static final String TAG = TLSCompatSocketFactory.class.getName(); + private SSLSocketFactory internalSSLSocketFactory; + private TrustManager trustManager; + + public TLSCompatSocketFactory(String trustedCaCert) throws KeyManagementException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, NoSuchProviderException { + initForSelfSignedCAs(trustedCaCert); + } + + public TLSCompatSocketFactory() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, NoSuchProviderException, IOException { + initForCommercialCAs(); + } + + public void initSSLSocketFactory(OkHttpClient.Builder builder) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, IllegalStateException { + builder.sslSocketFactory(this, (X509TrustManager) trustManager); + } + + + private void initForSelfSignedCAs(String trustedSelfSignedCaCert) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, IllegalStateException, KeyManagementException, NoSuchProviderException { + // Create a KeyStore containing our trusted CAs + String defaultType = KeyStore.getDefaultType(); + KeyStore keyStore = KeyStore.getInstance(defaultType); + keyStore.load(null, null); + if (!TextUtils.isEmpty(trustedSelfSignedCaCert)) { + java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(trustedSelfSignedCaCert); + keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate); + } + + // Create a TrustManager that trusts the CAs in our KeyStore + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(keyStore); + + // Check if there's only 1 X509Trustmanager -> from okttp3 source code example + TrustManager[] trustManagers = tmf.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + + trustManager = trustManagers[0]; + + // Create a SSLContext that uses our TrustManager + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + internalSSLSocketFactory = sslContext.getSocketFactory(); + + } + + + private void initForCommercialCAs() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { + + // Create a TrustManager that trusts the CAs in our KeyStore + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init((KeyStore) null); + + // Check if there's only 1 X509Trustmanager -> from okttp3 source code example + TrustManager[] trustManagers = tmf.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + + trustManager = trustManagers[0]; + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, null, null); + internalSSLSocketFactory = context.getSocketFactory(); + } + + + @Override + public String[] getDefaultCipherSuites() { + return internalSSLSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return internalSSLSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException, IllegalArgumentException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException, IllegalArgumentException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException, IllegalArgumentException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException, IllegalArgumentException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException, IllegalArgumentException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException, IllegalArgumentException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); + } + + private Socket enableTLSOnSocket(Socket socket) throws IllegalArgumentException { + if(socket != null && (socket instanceof SSLSocket)) { + ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"}); + //TODO: add a android version check as soon as a new Android API or bcjsse supports TLSv1.3 + } + return socket; + + + } + + + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java new file mode 100644 index 00000000..8e9d3e32 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java @@ -0,0 +1,361 @@ +/** + * 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.providersetup.models; + + +import org.jboss.security.srp.SRPParameters; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +import se.leap.bitmaskclient.base.utils.ConfigHelper; + +/** + * Implements all SRP algorithm logic. + *

+ * It's derived from JBoss implementation, with adjustments to make it work with LEAP platform. + * + * @author parmegv + */ +public class LeapSRPSession { + + private static String token = ""; + + final public static String SALT = "salt"; + final public static String M1 = "M1"; + final public static String M2 = "M2"; + final public static String TOKEN = "token"; + final public static String AUTHORIZATION_HEADER = "Authorization"; + final public static String TAG = "Leap SRP session class tag"; + + private SRPParameters params; + private String username; + private String password; + private BigInteger N; + private byte[] N_bytes; + private BigInteger g; + private BigInteger x; + private BigInteger v; + private BigInteger a; + private BigInteger A; + private byte[] K; + private SecureRandom pseudoRng; + /** + * The M1 = H(H(N) xor H(g) | H(U) | s | A | B | K) hash + */ + private MessageDigest clientHash; + /** + * The M2 = H(A | M | K) hash + */ + private MessageDigest serverHash; + + private static int A_LEN; + + + /** + * Creates a new SRP server session object from the username, password + * verifier, + * + * @param username, the user ID + * @param password, the user clear text password + */ + public LeapSRPSession(String username, String password) { + this(username, password, null); + } + + /** + * Creates a new SRP server session object from the username, password + * verifier, + * + * @param username, the user ID + * @param password, the user clear text password + * @param abytes, the random exponent used in the A public key + */ + public LeapSRPSession(String username, String password, byte[] abytes) { + + params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), ConfigHelper.G.toByteArray(), BigInteger.ZERO.toByteArray(), "SHA-256"); + this.g = new BigInteger(1, params.g); + N_bytes = ConfigHelper.trim(params.N); + this.N = new BigInteger(1, N_bytes); + this.username = username; + this.password = password; + + try { + pseudoRng = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + if (abytes != null) { + A_LEN = 8 * abytes.length; + /* TODO Why did they put this condition? + if( 8*abytes.length != A_LEN ) + throw new IllegalArgumentException("The abytes param must be " + +(A_LEN/8)+" in length, abytes.length="+abytes.length); + */ + this.a = new BigInteger(abytes); + } else + A_LEN = 64; + + serverHash = newDigest(); + clientHash = newDigest(); + } + + /** + * Calculates the parameter x of the SRP-6a algorithm. + * + * @param username + * @param password + * @param salt the salt of the user + * @return x + */ + public byte[] calculatePasswordHash(String username, String password, byte[] salt) { + //password = password.replaceAll("\\\\", "\\\\\\\\"); + // Calculate x = H(s | H(U | ':' | password)) + MessageDigest x_digest = newDigest(); + // Try to convert the username to a byte[] using ISO-8859-1 + byte[] user = null; + byte[] password_bytes = null; + byte[] colon = {}; + String encoding = "ISO-8859-1"; + try { + user = ConfigHelper.trim(username.getBytes(encoding)); + colon = ConfigHelper.trim(":".getBytes(encoding)); + password_bytes = ConfigHelper.trim(password.getBytes(encoding)); + } catch (UnsupportedEncodingException e) { + // Use the default platform encoding + user = ConfigHelper.trim(username.getBytes()); + colon = ConfigHelper.trim(":".getBytes()); + password_bytes = ConfigHelper.trim(password.getBytes()); + } + + // Build the hash + x_digest.update(user); + x_digest.update(colon); + x_digest.update(password_bytes); + byte[] h = x_digest.digest(); + + x_digest.reset(); + x_digest.update(salt); + x_digest.update(h); + byte[] x_digest_bytes = x_digest.digest(); + + return x_digest_bytes; + } + + public byte[] calculateNewSalt() { + try { + BigInteger salt = new BigInteger(64, SecureRandom.getInstance("SHA1PRNG")); + return ConfigHelper.trim(salt.toByteArray()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + } + + /** + * Calculates the parameter V of the SRP-6a algorithm. + * + * @return the value of V + */ + public BigInteger calculateV(String username, String password, byte[] salt) { + byte[] x_bytes = calculatePasswordHash(username, password, ConfigHelper.trim(salt)); + x = new BigInteger(1, x_bytes); + BigInteger v = g.modPow(x, N); // g^x % N + return v; + } + + /** + * Calculates the trimmed xor from two BigInteger numbers + * + * @param b1 the positive source to build first BigInteger + * @param b2 the positive source to build second BigInteger + * @return + */ + public byte[] xor(byte[] b1, byte[] b2) { + //TODO Check if length matters in the order, when b2 is smaller than b1 or viceversa + byte[] xor_digest = new BigInteger(1, b1).xor(new BigInteger(1, b2)).toByteArray(); + return ConfigHelper.trim(xor_digest); + } + + /** + * @returns The exponential residue (parameter A) to be sent to the server. + */ + public byte[] exponential() { + byte[] Abytes = null; + if (A == null) { + /* If the random component of A has not been specified use a random + number */ + if (a == null) { + BigInteger one = BigInteger.ONE; + do { + a = new BigInteger(A_LEN, pseudoRng); + } while (a.compareTo(one) <= 0); + } + A = g.modPow(a, N); + Abytes = ConfigHelper.trim(A.toByteArray()); + } + return Abytes; + } + + /** + * Calculates the parameter M1, to be sent to the SRP server. + * It also updates hashes of client and server for further calculations in other methods. + * It uses a predefined k. + * + * @param salt_bytes + * @param Bbytes the parameter received from the server, in bytes + * @return the parameter M1 + * @throws NoSuchAlgorithmException + */ + public byte[] response(byte[] salt_bytes, byte[] Bbytes) { + // Calculate x = H(s | H(U | ':' | password)) + byte[] M1 = null; + if (new BigInteger(1, Bbytes).mod(new BigInteger(1, N_bytes)) != BigInteger.ZERO) { + this.v = calculateV(username, password, salt_bytes); + // H(N) + byte[] digest_of_n = newDigest().digest(N_bytes); + + // H(g) + byte[] digest_of_g = newDigest().digest(params.g); + + // clientHash = H(N) xor H(g) + byte[] xor_digest = xor(digest_of_n, digest_of_g); + clientHash.update(xor_digest); + + // clientHash = H(N) xor H(g) | H(U) + byte[] username_digest = newDigest().digest(ConfigHelper.trim(username.getBytes())); + username_digest = ConfigHelper.trim(username_digest); + clientHash.update(username_digest); + + // clientHash = H(N) xor H(g) | H(U) | s + clientHash.update(ConfigHelper.trim(salt_bytes)); + + K = null; + + // clientHash = H(N) xor H(g) | H(U) | A + byte[] Abytes = ConfigHelper.trim(A.toByteArray()); + clientHash.update(Abytes); + + // clientHash = H(N) xor H(g) | H(U) | s | A | B + Bbytes = ConfigHelper.trim(Bbytes); + clientHash.update(Bbytes); + + // Calculate S = (B - kg^x) ^ (a + u * x) % N + BigInteger S = calculateS(Bbytes); + byte[] S_bytes = ConfigHelper.trim(S.toByteArray()); + + // K = SessionHash(S) + MessageDigest sessionDigest = newDigest(); + K = ConfigHelper.trim(sessionDigest.digest(S_bytes)); + + // clientHash = H(N) xor H(g) | H(U) | A | B | K + clientHash.update(K); + + M1 = ConfigHelper.trim(clientHash.digest()); + + // serverHash = Astr + M + K + serverHash.update(Abytes); + serverHash.update(M1); + serverHash.update(K); + + } + return M1; + } + + /** + * It calculates the parameter S used by response() to obtain session hash K. + * + * @param Bbytes the parameter received from the server, in bytes + * @return the parameter S + */ + private BigInteger calculateS(byte[] Bbytes) { + byte[] Abytes = ConfigHelper.trim(A.toByteArray()); + Bbytes = ConfigHelper.trim(Bbytes); + byte[] u_bytes = getU(Abytes, Bbytes); + + BigInteger B = new BigInteger(1, Bbytes); + BigInteger u = new BigInteger(1, u_bytes); + String k_string = "bf66c44a428916cad64aa7c679f3fd897ad4c375e9bbb4cbf2f5de241d618ef0"; + BigInteger k = new BigInteger(k_string, 16); + BigInteger B_minus_v = B.subtract(k.multiply(v)); + BigInteger a_ux = a.add(u.multiply(x)); + BigInteger S = B_minus_v.modPow(a_ux, N); + return S; + } + + /** + * It calculates the parameter u used by calculateS to obtain S. + * + * @param Abytes the exponential residue sent to the server + * @param Bbytes the parameter received from the server, in bytes + * @return + */ + public byte[] getU(byte[] Abytes, byte[] Bbytes) { + MessageDigest u_digest = newDigest(); + u_digest.update(ConfigHelper.trim(Abytes)); + u_digest.update(ConfigHelper.trim(Bbytes)); + byte[] u_digest_bytes = u_digest.digest(); + return ConfigHelper.trim(new BigInteger(1, u_digest_bytes).toByteArray()); + } + + /** + * @param M2 The server's response to the client's challenge + * @returns True if and only if the server's response was correct. + */ + public boolean verify(byte[] M2) { + // M2 = H(A | M1 | K) + M2 = ConfigHelper.trim(M2); + byte[] myM2 = ConfigHelper.trim(serverHash.digest()); + boolean valid = Arrays.equals(M2, myM2); + return valid; + } + + public static void setToken(String token) { + LeapSRPSession.token = token; + } + + public static String getToken() { + return token; + } + + public static boolean loggedIn() { + return !token.isEmpty(); + } + + /** + * @return a new SHA-256 digest. + */ + public MessageDigest newDigest() { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return md; + } + + public byte[] getK() { + return K; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java new file mode 100644 index 00000000..46a62626 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java @@ -0,0 +1,26 @@ +package se.leap.bitmaskclient.providersetup.models; + +import com.google.gson.Gson; + +/** + * Created by cyberta on 23.10.17. + */ + +public class SrpCredentials { + + /** + * Parameter A of SRP authentication + */ + private String A; + private String login; + + public SrpCredentials(String username, String A) { + this.login = username; + this.A = A; + } + + @Override + public String toString() { + return new Gson().toJson(this); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java new file mode 100644 index 00000000..31228edf --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java @@ -0,0 +1,42 @@ +package se.leap.bitmaskclient.providersetup.models; + +import com.google.gson.Gson; + +/** + * Created by cyberta on 23.10.17. + */ + +public class SrpRegistrationData { + + + private User user; + + public SrpRegistrationData(String username, String passwordSalt, String passwordVerifier) { + user = new User(username, passwordSalt, passwordVerifier); + } + + + @Override + public String toString() { + return new Gson().toJson(this); + } + + + public class User { + + String login; + String password_salt; + String password_verifier; + + public User(String login, String password_salt, String password_verifier) { + this.login = login; + this.password_salt = password_salt; + this.password_verifier = password_verifier; + } + + @Override + public String toString() { + return new Gson().toJson(this); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java index 44e9da6e..d74175f5 100644 --- a/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/tethering/TetheringStateManager.java @@ -18,7 +18,6 @@ package se.leap.bitmaskclient.tethering; import android.content.Context; import android.content.IntentFilter; -import androidx.annotation.VisibleForTesting; import java.net.Inet4Address; import java.net.InterfaceAddress; @@ -26,11 +25,11 @@ import java.net.NetworkInterface; import java.util.Enumeration; import java.util.List; -import se.leap.bitmaskclient.utils.Cmd; +import se.leap.bitmaskclient.base.utils.Cmd; -import static se.leap.bitmaskclient.utils.PreferenceHelper.isBluetoothTetheringAllowed; -import static se.leap.bitmaskclient.utils.PreferenceHelper.isUsbTetheringAllowed; -import static se.leap.bitmaskclient.utils.PreferenceHelper.isWifiTetheringAllowed; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.isBluetoothTetheringAllowed; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.isUsbTetheringAllowed; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.isWifiTetheringAllowed; /** * This manager tries to figure out the current tethering states for Wifi, USB and Bluetooth diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java b/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java deleted file mode 100644 index 7b97add2..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/Cmd.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2019 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.utils; - -import androidx.annotation.WorkerThread; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; - -public class Cmd { - - private static final String TAG = Cmd.class.getSimpleName(); - - @WorkerThread - public static int runBlockingCmd(String[] cmds, StringBuilder log) throws Exception { - return runCmd(cmds, log, true); - } - - @WorkerThread - private static int runCmd(String[] cmds, StringBuilder log, - boolean waitFor) throws Exception { - - int exitCode = -1; - Process proc = Runtime.getRuntime().exec("sh"); - OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream()); - - try { - for (String cmd : cmds) { - out.write(cmd); - out.write("\n"); - } - - out.flush(); - out.write("exit\n"); - out.flush(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - out.close(); - } - - if (waitFor) { - // Consume the "stdout" - InputStreamReader reader = new InputStreamReader(proc.getInputStream()); - readToLogString(reader, log); - - // Consume the "stderr" - reader = new InputStreamReader(proc.getErrorStream()); - readToLogString(reader, log); - - try { - exitCode = proc.waitFor(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - return exitCode; - } - - private static void readToLogString(InputStreamReader reader, StringBuilder log) throws IOException { - final char buf[] = new char[10]; - int read = 0; - try { - while ((read = reader.read(buf)) != -1) { - if (log != null) - log.append(buf, 0, read); - } - } catch (IOException e) { - reader.close(); - throw new IOException(e); - } - reader.close(); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java deleted file mode 100644 index 5a142d90..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/ConfigHelper.java +++ /dev/null @@ -1,230 +0,0 @@ -/** - * 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.utils; - -import android.content.Context; -import android.content.res.Resources; -import android.os.Build; -import android.os.Looper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import org.json.JSONException; -import org.json.JSONObject; -import org.spongycastle.util.encoders.Base64; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.interfaces.RSAPrivateKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Calendar; - -import se.leap.bitmaskclient.BuildConfig; -import se.leap.bitmaskclient.ProviderAPI; -import se.leap.bitmaskclient.R; - -import static se.leap.bitmaskclient.Constants.DEFAULT_BITMASK; - -/** - * Stores constants, and implements auxiliary methods used across all Bitmask Android classes. - * Wraps BuildConfigFields for to support easier unit testing - * - * @author parmegv - * @author MeanderingCode - */ -public class ConfigHelper { - final public static String NG_1024 = - "eeaf0ab9adb38dd69c33f80afa8fc5e86072618775ff3c0b9ea2314c9c256576d674df7496ea81d3383b4813d692c6e0e0d5d8e250b98be48e495c1d6089dad15dc7d7b46154d6b6ce8ef4ad69b15d4982559b297bcf1885c529f566660e57ec68edbc3c05726cc02fd4cbf4976eaa9afd5138fe8376435b9fc61d2fc0eb06e3"; - final public static BigInteger G = new BigInteger("2"); - - public static boolean checkErroneousDownload(String downloadedString) { - try { - if (downloadedString == null || downloadedString.isEmpty() || new JSONObject(downloadedString).has(ProviderAPI.ERRORS) || new JSONObject(downloadedString).has(ProviderAPI.BACKEND_ERROR_KEY)) { - return true; - } else { - return false; - } - } catch (NullPointerException | JSONException e) { - return false; - } - } - - /** - * Treat the input as the MSB representation of a number, - * and lop off leading zero elements. For efficiency, the - * input is simply returned if no leading zeroes are found. - * - * @param in array to be trimmed - */ - public static byte[] trim(byte[] in) { - if (in.length == 0 || in[0] != 0) - return in; - - int len = in.length; - int i = 1; - while (in[i] == 0 && i < len) - ++i; - byte[] ret = new byte[len - i]; - System.arraycopy(in, i, ret, 0, len - i); - return ret; - } - - public static X509Certificate parseX509CertificateFromString(String certificateString) { - java.security.cert.Certificate certificate = null; - CertificateFactory cf; - try { - cf = CertificateFactory.getInstance("X.509"); - - certificateString = certificateString.replaceFirst("-----BEGIN CERTIFICATE-----", "").replaceFirst("-----END CERTIFICATE-----", "").trim(); - byte[] cert_bytes = Base64.decode(certificateString); - InputStream caInput = new ByteArrayInputStream(cert_bytes); - try { - certificate = cf.generateCertificate(caInput); - System.out.println("ca=" + ((X509Certificate) certificate).getSubjectDN()); - } finally { - caInput.close(); - } - } catch (NullPointerException | CertificateException | IOException | IllegalArgumentException e) { - return null; - } - return (X509Certificate) certificate; - } - - public static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) { - RSAPrivateKey key; - try { - KeyFactory kf; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - kf = KeyFactory.getInstance("RSA", "BC"); - } else { - kf = KeyFactory.getInstance("RSA"); - } - rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", ""); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString)); - key = (RSAPrivateKey) kf.generatePrivate(keySpec); - } catch (InvalidKeySpecException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return null; - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return null; - } catch (NullPointerException e) { - e.printStackTrace(); - return null; - } catch (NoSuchProviderException e) { - e.printStackTrace(); - return null; - } - - return key; - } - - private static String byteArrayToHex(byte[] input) { - int readBytes = input.length; - StringBuffer hexData = new StringBuffer(); - int onebyte; - for (int i = 0; i < readBytes; i++) { - onebyte = ((0x000000ff & input[i]) | 0xffffff00); - hexData.append(Integer.toHexString(onebyte).substring(6)); - } - return hexData.toString(); - } - - /** - * Calculates the hexadecimal representation of a sha256/sha1 fingerprint of a certificate - * - * @param certificate - * @param encoding - * @return - * @throws NoSuchAlgorithmException - * @throws CertificateEncodingException - */ - @NonNull - public static String getFingerprintFromCertificate(X509Certificate certificate, String encoding) throws NoSuchAlgorithmException, CertificateEncodingException /*, UnsupportedEncodingException*/ { - byte[] byteArray = MessageDigest.getInstance(encoding).digest(certificate.getEncoded()); - return byteArrayToHex(byteArray); - } - - public static void ensureNotOnMainThread(@NonNull Context context) throws IllegalStateException{ - Looper looper = Looper.myLooper(); - if (looper != null && looper == context.getMainLooper()) { - throw new IllegalStateException( - "calling this from your main thread can lead to deadlock"); - } - } - - public static boolean isDefaultBitmask() { - return BuildConfig.FLAVOR_branding.equals(DEFAULT_BITMASK); - } - - public static boolean preferAnonymousUsage() { - return BuildConfig.priotize_anonymous_usage; - } - - public static int getCurrentTimezone() { - return Calendar.getInstance().get(Calendar.ZONE_OFFSET) / 3600000; - } - - public static String getProviderFormattedString(Resources resources, @StringRes int resourceId) { - String appName = resources.getString(R.string.app_name); - return resources.getString(resourceId, appName); - } - - public static boolean stringEqual(@Nullable String string1, @Nullable String string2) { - return (string1 == null && string2 == null) || - (string1 != null && string1.equals(string2)); - } - - public static String getApkFileName() { - try { - return BuildConfig.update_apk_url.substring(BuildConfig.update_apk_url.lastIndexOf("/")); - } catch (Exception e) { - return null; - } - } - - public static String getVersionFileName() { - try { - return BuildConfig.version_file_url.substring(BuildConfig.version_file_url.lastIndexOf("/")); - } catch (Exception e) { - return null; - } - } - - public static String getSignatureFileName() { - try { - return BuildConfig.signature_url.substring(BuildConfig.signature_url.lastIndexOf("/")); - } catch (Exception e) { - return null; - } - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/DateHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/DateHelper.java deleted file mode 100644 index 523c8c4c..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/DateHelper.java +++ /dev/null @@ -1,29 +0,0 @@ -package se.leap.bitmaskclient.utils; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** - * Contains helper methods related to date manipulation. - * - * @author Janak - */ -public class DateHelper { - private static final String DATE_PATTERN = "dd/MM/yyyy"; - private static final int ONE_DAY = 86400000; //1000*60*60*24 - - public static long getDateDiffToCurrentDateInDays(String startDate) throws ParseException { - SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN, Locale.US); - Date lastDate = sdf.parse(startDate); - Date currentDate = new Date(); - return (currentDate.getTime() - lastDate.getTime()) / ONE_DAY; - } - - public static String getCurrentDateString() { - SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN, Locale.US); - Date lastDate = new Date(); - return sdf.format(lastDate); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java deleted file mode 100644 index ebcc32ba..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/FileHelper.java +++ /dev/null @@ -1,46 +0,0 @@ -package se.leap.bitmaskclient.utils; - -import android.content.Context; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -/** - * Created by cyberta on 18.03.18. - */ - -public class FileHelper { - public static File createFile(File dir, String fileName) { - return new File(dir, fileName); - } - - public static void persistFile(File file, String content) throws IOException { - FileWriter writer = new FileWriter(file); - writer.write(content); - writer.close(); - } - - public static String readPublicKey(Context context) { - { - InputStream inputStream; - try { - inputStream = context.getAssets().open("public.pgp"); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - return sb.toString(); - } catch (IOException errabi) { - return null; - } - } - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/IPAddress.java b/app/src/main/java/se/leap/bitmaskclient/utils/IPAddress.java deleted file mode 100644 index 2e3ef596..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/IPAddress.java +++ /dev/null @@ -1,102 +0,0 @@ -package se.leap.bitmaskclient.utils; - -/* - * Copyright (C) 2006-2008 Alfresco Software Limited. - * - * 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. - - * 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, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - - * As a special exception to the terms and conditions of version 2.0 of - * the GPL, you may redistribute this Program in connection with Free/Libre - * and Open Source Software ("FLOSS") applications as described in Alfresco's - * FLOSS exception. You should have recieved a copy of the text describing - * the FLOSS exception, and it is also available here: - * http://www.alfresco.com/legal/licensing" - */ - -import java.util.StringTokenizer; - -/** - * TCP/IP Address Utility Class - * - * @author gkspencer - */ -public class IPAddress { - - - /** - * Convert a TCP/IP address string into a byte array - * - * @param addr String - * @return byte[] - */ - public static byte[] asBytes(String addr) { - - // Convert the TCP/IP address string to an integer value - int ipInt = parseNumericAddress(addr); - if (ipInt == 0) - return null; - - // Convert to bytes - byte[] ipByts = new byte[4]; - - ipByts[3] = (byte) (ipInt & 0xFF); - ipByts[2] = (byte) ((ipInt >> 8) & 0xFF); - ipByts[1] = (byte) ((ipInt >> 16) & 0xFF); - ipByts[0] = (byte) ((ipInt >> 24) & 0xFF); - - // Return the TCP/IP bytes - return ipByts; - } - /** - * Check if the specified address is a valid numeric TCP/IP address and return as an integer value - * - * @param ipaddr String - * @return int - */ - private static int parseNumericAddress(String ipaddr) { - - // Check if the string is valid - if (ipaddr == null || ipaddr.length() < 7 || ipaddr.length() > 15) - return 0; - - // Check the address string, should be n.n.n.n format - StringTokenizer token = new StringTokenizer(ipaddr,"."); - if (token.countTokens() != 4) - return 0; - - int ipInt = 0; - while (token.hasMoreTokens()) { - - // Get the current token and convert to an integer value - String ipNum = token.nextToken(); - - try { - // Validate the current address part - int ipVal = Integer.valueOf(ipNum).intValue(); - if (ipVal < 0 || ipVal > 255) - return 0; - - // Add to the integer address - ipInt = (ipInt << 8) + ipVal; - } - catch (NumberFormatException ex) { - return 0; - } - } - - // Return the integer address - return ipInt; - } -} \ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/InputStreamHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/InputStreamHelper.java deleted file mode 100644 index 87996615..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/InputStreamHelper.java +++ /dev/null @@ -1,21 +0,0 @@ -package se.leap.bitmaskclient.utils; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; - -/** - * Created by cyberta on 18.03.18. - */ - -public class InputStreamHelper { - //allows us to mock FileInputStream - public static InputStream getInputStreamFrom(String filePath) throws FileNotFoundException { - return new FileInputStream(filePath); - } - - public static String loadInputStreamAsString(InputStream is) { - java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/KeyStoreHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/KeyStoreHelper.java deleted file mode 100644 index 48d4cbad..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/KeyStoreHelper.java +++ /dev/null @@ -1,78 +0,0 @@ -package se.leap.bitmaskclient.utils; - -import java.io.IOException; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -/** - * Created by cyberta on 18.03.18. - */ - -public class KeyStoreHelper { - private static KeyStore trustedKeystore; - - /** - * Adds a new X509 certificate given its input stream and its provider name - * - * @param provider used to store the certificate in the keystore - * @param inputStream from which X509 certificate must be generated. - */ - public static void addTrustedCertificate(String provider, InputStream inputStream) { - CertificateFactory cf; - try { - cf = CertificateFactory.getInstance("X.509"); - X509Certificate cert = - (X509Certificate) cf.generateCertificate(inputStream); - trustedKeystore.setCertificateEntry(provider, cert); - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (KeyStoreException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - /** - * Adds a new X509 certificate given in its string from and using its provider name - * - * @param provider used to store the certificate in the keystore - * @param certificate - */ - public static void addTrustedCertificate(String provider, String certificate) { - - try { - X509Certificate cert = ConfigHelper.parseX509CertificateFromString(certificate); - if (trustedKeystore == null) { - trustedKeystore = KeyStore.getInstance("BKS"); - trustedKeystore.load(null); - } - trustedKeystore.setCertificateEntry(provider, cert); - } catch (KeyStoreException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - /** - * @return class wide keystore - */ - public static KeyStore getKeystore() { - return trustedKeystore; - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java deleted file mode 100644 index 5b62d0ff..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java +++ /dev/null @@ -1,273 +0,0 @@ -package se.leap.bitmaskclient.utils; - -import android.content.Context; -import android.content.SharedPreferences; -import androidx.annotation.NonNull; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashSet; -import java.util.Set; - -import de.blinkt.openvpn.VpnProfile; -import se.leap.bitmaskclient.Provider; - -import static android.content.Context.MODE_PRIVATE; -import static se.leap.bitmaskclient.Constants.ALLOW_TETHERING_BLUETOOTH; -import static se.leap.bitmaskclient.Constants.ALLOW_TETHERING_USB; -import static se.leap.bitmaskclient.Constants.ALLOW_TETHERING_WIFI; -import static se.leap.bitmaskclient.Constants.ALWAYS_ON_SHOW_DIALOG; -import static se.leap.bitmaskclient.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER; -import static se.leap.bitmaskclient.Constants.EXCLUDED_APPS; -import static se.leap.bitmaskclient.Constants.LAST_UPDATE_CHECK; -import static se.leap.bitmaskclient.Constants.LAST_USED_PROFILE; -import static se.leap.bitmaskclient.Constants.PROVIDER_CONFIGURED; -import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION; -import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY; -import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.Constants.RESTART_ON_UPDATE; -import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES; -import static se.leap.bitmaskclient.Constants.SHOW_EXPERIMENTAL; -import static se.leap.bitmaskclient.Constants.USE_IPv6_FIREWALL; -import static se.leap.bitmaskclient.Constants.USE_PLUGGABLE_TRANSPORTS; - -/** - * Created by cyberta on 18.03.18. - */ - -public class PreferenceHelper { - - public static Provider getSavedProviderFromSharedPreferences(@NonNull SharedPreferences preferences) { - Provider provider = new Provider(); - try { - provider.setMainUrl(new URL(preferences.getString(Provider.MAIN_URL, ""))); - provider.setProviderIp(preferences.getString(Provider.PROVIDER_IP, "")); - provider.setProviderApiIp(preferences.getString(Provider.PROVIDER_API_IP, "")); - provider.setGeoipUrl(preferences.getString(Provider.GEOIP_URL, "")); - provider.define(new JSONObject(preferences.getString(Provider.KEY, ""))); - provider.setCaCert(preferences.getString(Provider.CA_CERT, "")); - provider.setVpnCertificate(preferences.getString(PROVIDER_VPN_CERTIFICATE, "")); - provider.setPrivateKey(preferences.getString(PROVIDER_PRIVATE_KEY, "")); - provider.setEipServiceJson(new JSONObject(preferences.getString(PROVIDER_EIP_DEFINITION, ""))); - } catch (MalformedURLException | JSONException e) { - e.printStackTrace(); - } - - return provider; - } - - public static String getFromPersistedProvider(String toFetch, String providerDomain, SharedPreferences preferences) { - return preferences.getString(toFetch + "." + providerDomain, ""); - } - - // TODO: replace commit with apply after refactoring EIP - //FIXME: don't save private keys in shared preferences! use the keystore - public static void storeProviderInPreferences(SharedPreferences preferences, Provider provider) { - preferences.edit().putBoolean(PROVIDER_CONFIGURED, true). - putString(Provider.PROVIDER_IP, provider.getProviderIp()). - putString(Provider.GEOIP_URL, provider.getGeoipUrl().toString()). - putString(Provider.PROVIDER_API_IP, provider.getProviderApiIp()). - putString(Provider.MAIN_URL, provider.getMainUrlString()). - putString(Provider.KEY, provider.getDefinitionString()). - putString(Provider.CA_CERT, provider.getCaCert()). - putString(PROVIDER_EIP_DEFINITION, provider.getEipServiceJsonString()). - putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKey()). - putString(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate()). - commit(); - - String providerDomain = provider.getDomain(); - preferences.edit().putBoolean(PROVIDER_CONFIGURED, true). - putString(Provider.PROVIDER_IP + "." + providerDomain, provider.getProviderIp()). - putString(Provider.PROVIDER_API_IP + "." + providerDomain, provider.getProviderApiIp()). - putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString()). - putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl().toString()). - putString(Provider.KEY + "." + providerDomain, provider.getDefinitionString()). - putString(Provider.CA_CERT + "." + providerDomain, provider.getCaCert()). - putString(PROVIDER_EIP_DEFINITION + "." + providerDomain, provider.getEipServiceJsonString()). - apply(); - } - - /** - * Sets the profile that is connected (to connect if the service restarts) - */ - public static void setLastUsedVpnProfile(Context context, VpnProfile connectedProfile) { - SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - SharedPreferences.Editor prefsedit = prefs.edit(); - prefsedit.putString(LAST_USED_PROFILE, connectedProfile.toJson()); - prefsedit.apply(); - } - - /** - * Returns the profile that was last connected (to connect if the service restarts) - */ - public static VpnProfile getLastConnectedVpnProfile(Context context) { - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - String lastConnectedProfileJson = preferences.getString(LAST_USED_PROFILE, null); - return VpnProfile.fromJson(lastConnectedProfileJson); - } - - public static void deleteProviderDetailsFromPreferences(@NonNull SharedPreferences preferences, String providerDomain) { - preferences.edit(). - remove(Provider.KEY + "." + providerDomain). - remove(Provider.CA_CERT + "." + providerDomain). - remove(Provider.PROVIDER_IP + "." + providerDomain). - remove(Provider.PROVIDER_API_IP + "." + providerDomain). - remove(Provider.MAIN_URL + "." + providerDomain). - remove(Provider.GEOIP_URL + "." + providerDomain). - remove(PROVIDER_EIP_DEFINITION + "." + providerDomain). - remove(PROVIDER_PRIVATE_KEY + "." + providerDomain). - remove(PROVIDER_VPN_CERTIFICATE + "." + providerDomain). - apply(); - } - - public static void setLastAppUpdateCheck(Context context) { - putLong(context, LAST_UPDATE_CHECK, System.currentTimeMillis()); - } - - public static long getLastAppUpdateCheck(Context context) { - return getLong(context, LAST_UPDATE_CHECK, 0); - } - - public static void restartOnUpdate(Context context, boolean isEnabled) { - putBoolean(context, RESTART_ON_UPDATE, isEnabled); - } - - public static boolean getRestartOnUpdate(Context context) { - return getBoolean(context, RESTART_ON_UPDATE, false); - } - - public static boolean getUsePluggableTransports(Context context) { - return getBoolean(context, USE_PLUGGABLE_TRANSPORTS, false); - } - - public static void usePluggableTransports(Context context, boolean isEnabled) { - putBoolean(context, USE_PLUGGABLE_TRANSPORTS, isEnabled); - } - - public static void saveBattery(Context context, boolean isEnabled) { - putBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, isEnabled); - } - - public static boolean getSaveBattery(Context context) { - return getBoolean(context, DEFAULT_SHARED_PREFS_BATTERY_SAVER, false); - } - - public static void allowUsbTethering(Context context, boolean isEnabled) { - putBoolean(context, ALLOW_TETHERING_USB, isEnabled); - } - - public static boolean isUsbTetheringAllowed(Context context) { - return getBoolean(context, ALLOW_TETHERING_USB, false); - } - - public static void allowWifiTethering(Context context, boolean isEnabled) { - putBoolean(context, ALLOW_TETHERING_WIFI, isEnabled); - } - - public static boolean isWifiTetheringAllowed(Context context) { - return getBoolean(context, ALLOW_TETHERING_WIFI, false); - } - - public static void allowBluetoothTethering(Context context, boolean isEnabled) { - putBoolean(context, ALLOW_TETHERING_BLUETOOTH, isEnabled); - } - - public static boolean isBluetoothTetheringAllowed(Context context) { - return getBoolean(context, ALLOW_TETHERING_BLUETOOTH, false); - } - - public static void setShowExperimentalFeatures(Context context, boolean show) { - putBoolean(context, SHOW_EXPERIMENTAL, show); - } - - public static boolean showExperimentalFeatures(Context context) { - return getBoolean(context, SHOW_EXPERIMENTAL, false); - } - - public static void setUseIPv6Firewall(Context context, boolean useFirewall) { - putBoolean(context, USE_IPv6_FIREWALL, useFirewall); - } - - public static boolean useIpv6Firewall(Context context) { - return getBoolean(context, USE_IPv6_FIREWALL, false); - } - - public static void saveShowAlwaysOnDialog(Context context, boolean showAlwaysOnDialog) { - putBoolean(context, ALWAYS_ON_SHOW_DIALOG, showAlwaysOnDialog); - } - - public static boolean getShowAlwaysOnDialog(Context context) { - return getBoolean(context, ALWAYS_ON_SHOW_DIALOG, true); - } - - public static JSONObject getEipDefinitionFromPreferences(SharedPreferences preferences) { - JSONObject result = new JSONObject(); - try { - String eipDefinitionString = preferences.getString(PROVIDER_EIP_DEFINITION, ""); - if (!eipDefinitionString.isEmpty()) { - result = new JSONObject(eipDefinitionString); - } - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return result; - } - - public static void setExcludedApps(Context context, Set apps) { - SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - SharedPreferences.Editor prefsedit = prefs.edit(); - prefsedit.putStringSet(EXCLUDED_APPS, apps); - prefsedit.apply(); - } - - public static Set getExcludedApps(Context context) { - if (context == null) { - return null; - } - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - return preferences.getStringSet(EXCLUDED_APPS, new HashSet<>()); - } - - public static long getLong(Context context, String key, long defValue) { - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - return preferences.getLong(key, defValue); - } - - public static void putLong(Context context, String key, long value) { - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - preferences.edit().putLong(key, value).apply(); - } - - public static String getString(Context context, String key, String defValue) { - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - return preferences.getString(key, defValue); - } - - public static void putString(Context context, String key, String value) { - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - preferences.edit().putString(key, value).apply(); - } - - public static Boolean getBoolean(Context context, String key, Boolean defValue) { - if (context == null) { - return false; - } - - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - return preferences.getBoolean(key, defValue); - } - - public static void putBoolean(Context context, String key, Boolean value) { - if (context == null) { - return; - } - - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - preferences.edit().putBoolean(key, value).apply(); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/ViewHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/ViewHelper.java deleted file mode 100644 index 5f4fc2a6..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/utils/ViewHelper.java +++ /dev/null @@ -1,17 +0,0 @@ -package se.leap.bitmaskclient.utils; - -import android.content.Context; - -import androidx.annotation.DimenRes; - -/** - * Created by cyberta on 29.06.18. - */ - -public class ViewHelper { - - public static int convertDimensionToPx(Context context, @DimenRes int dimension) { - return context.getResources().getDimensionPixelSize(dimension); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java deleted file mode 100644 index efe20b4c..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/views/IconCheckboxEntry.java +++ /dev/null @@ -1,86 +0,0 @@ -package se.leap.bitmaskclient.views; - -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.drawable.Drawable; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.appcompat.widget.AppCompatImageView; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; - -import butterknife.ButterKnife; -import butterknife.InjectView; -import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.fragments.TetheringDialog; - - -public class IconCheckboxEntry extends LinearLayout { - - @InjectView(android.R.id.text1) - TextView textView; - - @InjectView(R.id.material_icon) - AppCompatImageView iconView; - - @InjectView(R.id.checked_icon) - AppCompatImageView checkedIcon; - - public IconCheckboxEntry(Context context) { - super(context); - initLayout(context, null); - } - - public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - initLayout(context, attrs); - } - - public IconCheckboxEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initLayout(context, attrs); - } - - @TargetApi(21) - public IconCheckboxEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initLayout(context, attrs); - } - - void initLayout(Context context, AttributeSet attrs) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View rootview = inflater.inflate(R.layout.v_icon_select_text_list_item, this, true); - ButterKnife.inject(this, rootview); - - - - } - - public void bind(TetheringDialog.DialogListAdapter.ViewModel model) { - this.setEnabled(model.enabled); - textView.setText(model.text); - textView.setEnabled(model.enabled); - - Drawable checkIcon = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_check_bold)).mutate(); - if (model.enabled) { - DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorSuccess)); - } else { - DrawableCompat.setTint(checkIcon, ContextCompat.getColor(getContext(), R.color.colorDisabled)); - } - - iconView.setImageDrawable(model.image); - checkedIcon.setImageDrawable(checkIcon); - setChecked(model.checked); - } - - public void setChecked(boolean checked) { - checkedIcon.setVisibility(checked ? VISIBLE : GONE); - checkedIcon.setContentDescription(checked ? "selected" : "unselected"); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java deleted file mode 100644 index c9b6024d..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java +++ /dev/null @@ -1,116 +0,0 @@ -package se.leap.bitmaskclient.views; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import androidx.annotation.DrawableRes; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.appcompat.widget.SwitchCompat; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.CompoundButton; -import android.widget.LinearLayout; -import android.widget.TextView; - -import se.leap.bitmaskclient.R; - -public class IconSwitchEntry extends LinearLayout { - - private TextView textView; - private TextView subtitleView; - private AppCompatImageView iconView; - private SwitchCompat switchView; - private CompoundButton.OnCheckedChangeListener checkedChangeListener; - - public IconSwitchEntry(Context context) { - super(context); - initLayout(context, null); - } - - public IconSwitchEntry(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - initLayout(context, attrs); - } - - public IconSwitchEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initLayout(context, attrs); - } - - @TargetApi(21) - public IconSwitchEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initLayout(context, attrs); - } - - void initLayout(Context context, AttributeSet attrs) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View rootview = inflater.inflate(R.layout.v_switch_list_item, this, true); - textView = rootview.findViewById(android.R.id.text1); - subtitleView = rootview.findViewById(R.id.subtitle); - iconView = rootview.findViewById(R.id.material_icon); - switchView = rootview.findViewById(R.id.option_switch); - - if (attrs != null) { - TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconSwitchEntry); - - String entryText = typedArray.getString(R.styleable.IconTextEntry_text); - if (entryText != null) { - textView.setText(entryText); - } - - String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle); - if (subtitle != null) { - subtitleView.setText(subtitle); - subtitleView.setVisibility(VISIBLE); - } - - Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon); - if (drawable != null) { - iconView.setImageDrawable(drawable); - } - - typedArray.recycle(); - } - } - - public void setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener listener) { - checkedChangeListener = listener; - switchView.setOnCheckedChangeListener(checkedChangeListener); - } - - public void setText(@StringRes int id) { - textView.setText(id); - } - - public void showSubtitle(boolean show) { - subtitleView.setVisibility(show ? VISIBLE : GONE); - } - - public void setIcon(@DrawableRes int id) { - iconView.setImageResource(id); - } - - public void setChecked(boolean isChecked) { - switchView.setChecked(isChecked); - } - - public void setCheckedQuietly(boolean isChecked) { - switchView.setOnCheckedChangeListener(null); - switchView.setChecked(isChecked); - switchView.setOnCheckedChangeListener(checkedChangeListener); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - switchView.setVisibility(enabled ? VISIBLE : GONE); - textView.setTextColor(getResources().getColor(enabled ? android.R.color.black : R.color.colorDisabled)); - iconView.setImageAlpha(enabled ? 255 : 128); - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java deleted file mode 100644 index 7a1717e9..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java +++ /dev/null @@ -1,106 +0,0 @@ -package se.leap.bitmaskclient.views; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import androidx.annotation.ColorRes; -import androidx.annotation.DrawableRes; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import se.leap.bitmaskclient.R; - - -public class IconTextEntry extends LinearLayout { - - private TextView textView; - private ImageView iconView; - private TextView subtitleView; - - public IconTextEntry(Context context) { - super(context); - initLayout(context, null); - } - - public IconTextEntry(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - initLayout(context, attrs); - } - - public IconTextEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initLayout(context, attrs); - } - - @TargetApi(21) - public IconTextEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initLayout(context, attrs); - } - - void initLayout(Context context, AttributeSet attrs) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View rootview = inflater.inflate(R.layout.v_icon_text_list_item, this, true); - textView = rootview.findViewById(android.R.id.text1); - subtitleView = rootview.findViewById(R.id.subtitle); - iconView = rootview.findViewById(R.id.material_icon); - - if (attrs != null) { - TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconTextEntry); - - String entryText = typedArray.getString(R.styleable.IconTextEntry_text); - if (entryText != null) { - textView.setText(entryText); - } - - String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle); - if (subtitle != null) { - subtitleView.setText(subtitle); - subtitleView.setVisibility(VISIBLE); - } - - Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon); - if (drawable != null) { - iconView.setImageDrawable(drawable); - } - - typedArray.recycle(); - } - - - } - - public void setText(@StringRes int id) { - textView.setText(id); - } - - public void setSubtitle(String text) { - subtitleView.setText(text); - subtitleView.setVisibility(VISIBLE); - } - - public void hideSubtitle() { - subtitleView.setVisibility(GONE); - } - - public void setSubtitleColor(@ColorRes int color) { - subtitleView.setTextColor(getContext().getResources().getColor(color)); - } - - public void setText(CharSequence text) { - textView.setText(text); - } - - public void setIcon(@DrawableRes int id) { - iconView.setImageResource(id); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconTextView.java b/app/src/main/java/se/leap/bitmaskclient/views/IconTextView.java deleted file mode 100644 index 29c70859..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/views/IconTextView.java +++ /dev/null @@ -1,96 +0,0 @@ -package se.leap.bitmaskclient.views; - - -import android.content.Context; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import androidx.appcompat.widget.AppCompatTextView; -import android.text.Spannable; -import android.text.style.ImageSpan; -import android.util.AttributeSet; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class IconTextView extends AppCompatTextView { - - private int imageResource = 0; - /** - * Regex pattern that looks for embedded images of the format: [img src=imageName/] - */ - public static final String PATTERN = "\\Q[img src]\\E"; - - public IconTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public IconTextView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public IconTextView(Context context) { - super(context); - } - - @Override - public void setText(CharSequence text, BufferType type) { - final Spannable spannable = getTextWithImages(getContext(), text, getLineHeight(), getCurrentTextColor()); - super.setText(spannable, BufferType.SPANNABLE); - } - - public void setIcon(int imageResource) { - this.imageResource = imageResource; - } - - private Spannable getTextWithImages(Context context, CharSequence text, int lineHeight, int colour) { - final Spannable spannable = Spannable.Factory.getInstance().newSpannable(text); - addImages(context, spannable, lineHeight, colour); - return spannable; - } - - private void addImages(Context context, Spannable spannable, int lineHeight, int colour) { - final Pattern refImg = Pattern.compile(PATTERN); - - final Matcher matcher = refImg.matcher(spannable); - while (matcher.find()) { - boolean set = true; - for (ImageSpan span : spannable.getSpans(matcher.start(), matcher.end(), ImageSpan.class)) { - if (spannable.getSpanStart(span) >= matcher.start() - && spannable.getSpanEnd(span) <= matcher.end()) { - spannable.removeSpan(span); - } else { - set = false; - break; - } - } - if (set && imageResource != 0) { - spannable.setSpan(makeImageSpan(context, imageResource, lineHeight, colour), - matcher.start(), - matcher.end(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - } - } - - /** - * Create an ImageSpan for the given icon drawable. This also sets the image size and colour. - * Works best with a white, square icon because of the colouring and resizing. - * - * @param context The Android Context. - * @param drawableResId A drawable resource Id. - * @param size The desired size (i.e. width and height) of the image icon in pixels. - * Use the lineHeight of the TextView to make the image inline with the - * surrounding text. - * @param colour The colour (careful: NOT a resource Id) to apply to the image. - * @return An ImageSpan, aligned with the bottom of the text. - */ - private ImageSpan makeImageSpan(Context context, int drawableResId, int size, int colour) { - final Drawable drawable = context.getResources().getDrawable(drawableResId); - drawable.mutate(); - drawable.setColorFilter(colour, PorterDuff.Mode.MULTIPLY); - drawable.setBounds(0, 0, size, size); - return new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); - } - -} \ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/views/ProviderHeaderView.java b/app/src/main/java/se/leap/bitmaskclient/views/ProviderHeaderView.java deleted file mode 100644 index 4fa3771b..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/views/ProviderHeaderView.java +++ /dev/null @@ -1,109 +0,0 @@ -package se.leap.bitmaskclient.views; - -import android.content.Context; -import androidx.annotation.DrawableRes; -import androidx.annotation.RequiresApi; -import androidx.annotation.StringRes; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.appcompat.widget.AppCompatTextView; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.RelativeLayout; - -import se.leap.bitmaskclient.R; - -import static se.leap.bitmaskclient.utils.ViewHelper.convertDimensionToPx; - -/** - * Created by cyberta on 29.06.18. - */ - -public class ProviderHeaderView extends RelativeLayout { - private int stdPadding; - private int compactPadding; - private int stdImageSize; - private int compactImageSize; - - AppCompatImageView providerHeaderLogo; - AppCompatTextView providerHeaderText; - - public ProviderHeaderView(Context context) { - super(context); - initLayout(context); - } - - public ProviderHeaderView(Context context, AttributeSet attrs) { - super(context, attrs); - initLayout(context); - } - - public ProviderHeaderView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initLayout(context); - } - - @RequiresApi(21) - public ProviderHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - initLayout(context); - } - - - void initLayout(Context context) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View rootview = inflater.inflate(R.layout.v_provider_header, this, true); - providerHeaderLogo = rootview.findViewById(R.id.provider_header_logo); - providerHeaderText = rootview.findViewById(R.id.provider_header_text); - - stdPadding = convertDimensionToPx(context, R.dimen.stdpadding); - compactPadding = convertDimensionToPx(context, R.dimen.compact_padding); - stdImageSize = convertDimensionToPx(context, R.dimen.bitmask_logo); - compactImageSize = convertDimensionToPx(context, R.dimen.bitmask_logo_compact); - } - - public void setTitle(String title) { - providerHeaderText.setText(title); - } - - public void setTitle(@StringRes int stringRes) { - providerHeaderText.setText(stringRes); - } - - public void setLogo(@DrawableRes int drawableRes) { - providerHeaderLogo.setImageResource(drawableRes); - } - - public void showCompactLayout() { - LayoutParams logoLayoutParams = (LayoutParams) providerHeaderLogo.getLayoutParams(); - logoLayoutParams.width = compactImageSize; - logoLayoutParams.height = compactImageSize; - providerHeaderLogo.setLayoutParams(logoLayoutParams); - - LayoutParams textLayoutParams = (LayoutParams) providerHeaderText.getLayoutParams(); - textLayoutParams.addRule(RIGHT_OF, R.id.provider_header_logo); - textLayoutParams.addRule(BELOW, 0); - textLayoutParams.addRule(ALIGN_TOP, R.id.provider_header_logo); - textLayoutParams.setMargins(compactPadding, compactPadding, compactPadding, compactPadding); - - providerHeaderText.setLayoutParams(textLayoutParams); - providerHeaderText.setMaxLines(2); - } - - public void showStandardLayout() { - LayoutParams logoLayoutParams = (LayoutParams) providerHeaderLogo.getLayoutParams(); - logoLayoutParams.width = stdImageSize; - logoLayoutParams.height = stdImageSize; - providerHeaderLogo.setLayoutParams(logoLayoutParams); - - LayoutParams textLayoutParams = (LayoutParams) providerHeaderText.getLayoutParams(); - textLayoutParams.addRule(RIGHT_OF, 0); - textLayoutParams.addRule(BELOW, R.id.provider_header_logo); - textLayoutParams.addRule(ALIGN_TOP, 0); - textLayoutParams.setMargins(stdPadding, stdPadding, stdPadding, stdPadding); - providerHeaderText.setLayoutParams(textLayoutParams); - providerHeaderText.setMaxLines(1); - } - -} diff --git a/app/src/main/java/se/leap/bitmaskclient/views/VpnStateImage.java b/app/src/main/java/se/leap/bitmaskclient/views/VpnStateImage.java deleted file mode 100644 index c0432edc..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/views/VpnStateImage.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * 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.views; - -import android.content.Context; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.appcompat.widget.AppCompatImageView; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.widget.ProgressBar; - -import se.leap.bitmaskclient.R; - -/** - * Created by cyberta on 12.02.18. - */ - - -public class VpnStateImage extends ConstraintLayout { - - ProgressBar progressBar; - AppCompatImageView stateIcon; - - public VpnStateImage(Context context) { - super(context); - initLayout(context); - } - - public VpnStateImage(Context context, AttributeSet attrs) { - super(context, attrs); - initLayout(context); - } - - public VpnStateImage(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initLayout(context); - } - - void initLayout(Context context) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View rootview = inflater.inflate(R.layout.v_main_button, this, true); - stateIcon = rootview.findViewById(R.id.vpn_state_key); - progressBar = rootview.findViewById(R.id.progressBar); - progressBar.setIndeterminate(true); - } - - public void showProgress() { - progressBar.setVisibility(VISIBLE); - } - - - public void stopProgress(boolean animated) { - if (!animated) { - progressBar.setVisibility(GONE); - return; - } - - AlphaAnimation fadeOutAnimation = new AlphaAnimation(1.0f, 0.0f); - fadeOutAnimation.setDuration(1000); - fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) {} - - @Override - public void onAnimationEnd(Animation animation) { - progressBar.setVisibility(GONE); - } - - @Override - public void onAnimationRepeat(Animation animation) {} - }); - - progressBar.startAnimation(fadeOutAnimation); - } - - public void setStateIcon(int resource) { - stateIcon.setImageResource(resource); - } - - -} diff --git a/app/src/main/res/layout-xlarge-port/a_add_provider.xml b/app/src/main/res/layout-xlarge-port/a_add_provider.xml index 2ec2f1b7..9d1614aa 100644 --- a/app/src/main/res/layout-xlarge-port/a_add_provider.xml +++ b/app/src/main/res/layout-xlarge-port/a_add_provider.xml @@ -5,7 +5,7 @@ style="@style/BitmaskActivity" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ProviderCredentialsBaseActivity"> + tools:context=".providersetup.activities.ProviderCredentialsBaseActivity"> + tools:context=".providersetup.activities.CustomProviderSetupActivity"> + tools:context=".providersetup.activities.ProviderCredentialsBaseActivity"> + tools:context=".providersetup.ProviderListActivity"> + tools:context=".providersetup.activities.ProviderCredentialsBaseActivity"> + tools:context=".providersetup.activities.CustomProviderSetupActivity"> + tools:context=".providersetup.activities.ProviderCredentialsBaseActivity"> + tools:context=".providersetup.ProviderListActivity"> + tools:context=".base.MainActivity" > - + tools:context=".providersetup.AddProviderActivity"> - diff --git a/app/src/main/res/layout/a_add_provider_tablet_scrollview.xml b/app/src/main/res/layout/a_add_provider_tablet_scrollview.xml index 31c8d07c..4debbf9f 100644 --- a/app/src/main/res/layout/a_add_provider_tablet_scrollview.xml +++ b/app/src/main/res/layout/a_add_provider_tablet_scrollview.xml @@ -23,7 +23,7 @@ android:layout_height="wrap_content" android:orientation="vertical"> - diff --git a/app/src/main/res/layout/a_custom_provider_setup.xml b/app/src/main/res/layout/a_custom_provider_setup.xml index 34c59259..782537d9 100644 --- a/app/src/main/res/layout/a_custom_provider_setup.xml +++ b/app/src/main/res/layout/a_custom_provider_setup.xml @@ -4,7 +4,7 @@ android:id="@+id/custom_provider_setup_layout" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".CustomProviderSetupActivity" + tools:context=".providersetup.activities.CustomProviderSetupActivity" android:padding="@dimen/stdpadding" style="@style/BitmaskActivity" > @@ -15,7 +15,7 @@ android:layout_height="0dp" android:layout_width="0dp" android:id="@id/content"> - - + tools:context="se.leap.bitmaskclient.base.MainActivity"> - diff --git a/app/src/main/res/layout/a_provider_credentials_tablet_linear_layout.xml b/app/src/main/res/layout/a_provider_credentials_tablet_linear_layout.xml index bf022d51..ea0b6dd4 100644 --- a/app/src/main/res/layout/a_provider_credentials_tablet_linear_layout.xml +++ b/app/src/main/res/layout/a_provider_credentials_tablet_linear_layout.xml @@ -23,7 +23,7 @@ android:id="@+id/content" android:orientation="vertical"> - diff --git a/app/src/main/res/layout/a_provider_detail.xml b/app/src/main/res/layout/a_provider_detail.xml index 840d60df..bdc17ee9 100644 --- a/app/src/main/res/layout/a_provider_detail.xml +++ b/app/src/main/res/layout/a_provider_detail.xml @@ -15,7 +15,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - diff --git a/app/src/main/res/layout/a_provider_detail_tablet_linear_layout.xml b/app/src/main/res/layout/a_provider_detail_tablet_linear_layout.xml index 51c5ab0f..0c7e02d1 100644 --- a/app/src/main/res/layout/a_provider_detail_tablet_linear_layout.xml +++ b/app/src/main/res/layout/a_provider_detail_tablet_linear_layout.xml @@ -23,7 +23,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - diff --git a/app/src/main/res/layout/a_provider_list.xml b/app/src/main/res/layout/a_provider_list.xml index 2e2573eb..5d7efae7 100644 --- a/app/src/main/res/layout/a_provider_list.xml +++ b/app/src/main/res/layout/a_provider_list.xml @@ -3,7 +3,7 @@ android:id="@+id/provider_list_layout" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ProviderListActivity" + tools:context=".providersetup.ProviderListActivity" android:padding="@dimen/stdpadding" style="@style/BitmaskActivity" > @@ -15,7 +15,7 @@ android:layout_height="match_parent" android:orientation="vertical"> - diff --git a/app/src/main/res/layout/a_provider_list_tablet_linear_layout.xml b/app/src/main/res/layout/a_provider_list_tablet_linear_layout.xml index 107f25fb..487edd1d 100644 --- a/app/src/main/res/layout/a_provider_list_tablet_linear_layout.xml +++ b/app/src/main/res/layout/a_provider_list_tablet_linear_layout.xml @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - diff --git a/app/src/main/res/layout/d_checkbox_confirm.xml b/app/src/main/res/layout/d_checkbox_confirm.xml index b2b61eca..f8aace6e 100644 --- a/app/src/main/res/layout/d_checkbox_confirm.xml +++ b/app/src/main/res/layout/d_checkbox_confirm.xml @@ -24,7 +24,7 @@ android:textStyle="bold" /> - + tools:context=".base.MainActivity" > - - - - - - - - - - - - CircleImageView Copyright 2014 - 2020 Henning Dodenhof. Licensed under the Apache License, Version 2.0 - + + \ No newline at end of file -- cgit v1.2.3