diff options
Diffstat (limited to 'android')
8 files changed, 463 insertions, 13 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt index 15d33fbb04..ffc11354ff 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt @@ -99,11 +99,6 @@ class MainActivity : FragmentActivity() { } } - override fun onResume() { - super.onResume() - keyStatusListener.onResume() - } - override fun onStop() { if (shouldStopService) { runBlocking { service.await().stop() } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt index 06b580d013..940fabbf77 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt @@ -35,6 +35,7 @@ class MullvadDaemon(val vpnService: MullvadVpnService) { external fun setAccount(accountToken: String?) external fun shutdown() external fun updateRelaySettings(update: RelaySettingsUpdate) + external fun verifyWireguardKey(): Boolean? private external fun initialize(vpnService: MullvadVpnService) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt index ed9c2bf4ea..8465a66140 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt @@ -55,8 +55,11 @@ class SettingsFragment : Fragment() { view.findViewById<View>(R.id.account).setOnClickListener { openSubFragment(AccountFragment()) } + view.findViewById<View>(R.id.wireguard_keys).setOnClickListener { + openSubFragment(WireguardKeyFragment()) + } view.findViewById<View>(R.id.app_version).setOnClickListener { - openLink("https://mullvad.net/download/") + openLink(R.string.download_url) } view.findViewById<View>(R.id.report_a_problem).setOnClickListener { openSubFragment(ProblemReportFragment()) @@ -104,8 +107,8 @@ class SettingsFragment : Fragment() { } } - private fun openLink(url: String) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + private fun openLink(urlResourceId: Int) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(parentActivity.getString(urlResourceId))) startActivity(intent) } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/WireguardKeyFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/WireguardKeyFragment.kt new file mode 100644 index 0000000000..e8be9d7228 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/WireguardKeyFragment.kt @@ -0,0 +1,240 @@ +package net.mullvad.mullvadvpn + +import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.support.v4.app.Fragment +import android.util.Base64 +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast + +import net.mullvad.mullvadvpn.dataproxy.ConnectionProxy +import net.mullvad.mullvadvpn.dataproxy.KeyStatusListener +import net.mullvad.mullvadvpn.model.KeygenEvent +import net.mullvad.mullvadvpn.model.TunnelState + +class WireguardKeyFragment : Fragment() { + private var TAG = "keyfragment"; + private var keyState: KeygenEvent? = null; + private var currentJob: Job? = null; + private var updateViewsJob: Job? = null; + private lateinit var parentActivity: MainActivity + private lateinit var connectionProxy: ConnectionProxy + private lateinit var keyStatusListener: KeyStatusListener + private var generatingKey = false + private var validatingKey = false + + private lateinit var publicKey: TextView + private lateinit var statusMessage: TextView + private lateinit var visitWebsiteView: View + private lateinit var actionButton: Button + private lateinit var actionSpinner: ProgressBar + + + override fun onAttach(context: Context) { + super.onAttach(context) + parentActivity = context as MainActivity + keyStatusListener = parentActivity.keyStatusListener + connectionProxy = parentActivity.connectionProxy + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.wireguard_key, container, false) + + view.findViewById<View>(R.id.back).setOnClickListener { + parentActivity.onBackPressed() + } + + + statusMessage = view.findViewById<TextView>(R.id.wireguard_key_status) + visitWebsiteView = view.findViewById<View>(R.id.wireguard_key_visit_website) + publicKey = view.findViewById<TextView>(R.id.wireguard_public_key) + actionButton = view.findViewById<Button>(R.id.wg_key_button) + actionSpinner = view.findViewById<ProgressBar>(R.id.wg_action_spinner) + + updateViews() + + connectionProxy.onUiStateChange = { _ -> + updateViewsJob?.cancel() + updateViewsJob = updateViewJob() + } + + keyStatusListener.onKeyStatusChange = { _ -> + updateViewsJob?.cancel() + updateViewsJob = updateViewJob() + } + + return view + } + + private fun updateViewJob() = GlobalScope.launch(Dispatchers.Main) { + updateViews() + } + + + private fun updateViews() { + clearErrorMessage() + visitWebsiteView.visibility = View.GONE + + actionButton.setClickable(true) + + when (val keyState = keyStatusListener.keyStatus) { + null -> { + publicKey.visibility = View.INVISIBLE + setGenerateButton() + } + is KeygenEvent.TooManyKeys -> { + visitWebsiteView.visibility = View.VISIBLE + visitWebsiteView.setOnClickListener { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(parentActivity.getString(R.string.account_url))) + startActivity(intent) + } + + setStatusMessage(R.string.too_many_keys, R.color.red) + setGenerateButton() + } + is KeygenEvent.GenerationFailure -> { + setStatusMessage(R.string.failed_to_generate_key, R.color.red) + setGenerateButton() + } + is KeygenEvent.NewKey -> { + val publicKeyString = Base64.encodeToString(keyState.publicKey.key, Base64.DEFAULT) + publicKey.visibility = View.VISIBLE + publicKey.setText(publicKeyString) + + setValidateButton() + + if (keyState.verified != null) { + if (keyState.verified) { + setStatusMessage(R.string.wireguard_key_valid, R.color.green) + } else { + setStatusMessage(R.string.wireguard_key_invalid, R.color.red) + setGenerateButton() + } + } + } + } + drawNoConnectionState() + } + + private fun setStatusMessage(message: Int, color: Int) { + statusMessage.setText(message) + statusMessage.setTextColor(parentActivity.getColor(color)) + statusMessage.visibility = View.VISIBLE + } + + private fun clearErrorMessage() { + statusMessage.visibility = View.GONE + } + + private fun setGenerateButton() { + if (generatingKey) { + showActionSpinner() + return; + } + actionSpinner.visibility = View.GONE + actionButton.visibility = View.VISIBLE + actionButton.setText(R.string.wireguard_generate_key) + actionButton.setOnClickListener { + onGenerateKeyPress() + } + } + + private fun setValidateButton() { + if (validatingKey) { + showActionSpinner() + return; + } + actionSpinner.visibility = View.GONE + actionButton.visibility = View.VISIBLE + actionButton.setText(R.string.wireguard_validate_key) + actionButton.setOnClickListener { + onValidateKeyPress() + } + } + + private fun showActionSpinner() { + actionButton.visibility = View.GONE + actionSpinner.visibility = View.VISIBLE + } + + private fun drawNoConnectionState() { + when (connectionProxy.state) { + is TunnelState.Connecting, is TunnelState.Disconnecting -> { + statusMessage.setText(R.string.wireguard_key_connectivity) + statusMessage.visibility = View.VISIBLE + actionButton.visibility = View.GONE + actionSpinner.visibility = View.VISIBLE + } + } + } + + private fun onGenerateKeyPress() { + currentJob?.cancel() + generatingKey = true; + validatingKey = false; + updateViews() + currentJob = GlobalScope.launch(Dispatchers.Main) { + keyStatusListener.generateKey().join() + generatingKey = false; + updateViews() + } + } + + private fun onValidateKeyPress() { + currentJob?.cancel() + validatingKey = true; + generatingKey = false; + updateViews() + currentJob = GlobalScope.launch(Dispatchers.Main) { + keyStatusListener.verifyKey().join() + validatingKey = false; + when (val state = keyStatusListener.keyStatus) { + is KeygenEvent.NewKey -> { + if (state.verified == null) { + Toast.makeText(parentActivity, R.string.wireguard_key_verification_failure, Toast.LENGTH_SHORT).show() + } + } + } + updateViews() + } + } + + override fun onPause() { + connectionProxy.onUiStateChange = null + keyStatusListener.onKeyStatusChange = null + currentJob?.cancel() + updateViewsJob?.cancel() + validatingKey = false; + generatingKey = false; + super.onPause() + } + + override fun onResume() { + super.onResume() + connectionProxy.onUiStateChange = { _ -> + updateViewsJob?.cancel() + updateViewsJob = updateViewJob() + } + + keyStatusListener.onKeyStatusChange = { _ -> + updateViewsJob?.cancel() + updateViewsJob = updateViewJob() + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/KeyStatusListener.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/KeyStatusListener.kt index 66b7a96e44..74ad587eea 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/KeyStatusListener.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/KeyStatusListener.kt @@ -38,13 +38,26 @@ class KeyStatusListener(val asyncDaemon: Deferred<MullvadDaemon>) { private fun setUp() = GlobalScope.launch(Dispatchers.Default) { daemon = asyncDaemon.await() daemon?.onKeygenEvent = { event -> keyStatus = event } + val wireguardKey = daemon?.getWireguardKey() + if (wireguardKey != null) { + keyStatus = KeygenEvent.NewKey(wireguardKey, null) + } } - fun onResume() { - if (keyStatus is KeygenEvent.TooManyKeys || keyStatus is KeygenEvent.GenerationFailure) { - retryJob?.cancel() - retryJob = retryKeyGeneration() - } + fun generateKey() = GlobalScope.launch(Dispatchers.Default) { + setUpJob.join() + keyStatus = daemon?.generateWireguardKey() + } + + fun verifyKey() = GlobalScope.launch(Dispatchers.Default) { + setUpJob.join() + val verified = daemon?.verifyWireguardKey() + // Only update verification status if the key is actually there + when (val state = keyStatus) { + is KeygenEvent.NewKey -> { + keyStatus = KeygenEvent.NewKey(state.publicKey, verified) + } + } } fun onDestroy() { diff --git a/android/src/main/res/layout/settings.xml b/android/src/main/res/layout/settings.xml index d0308f163b..7b4161406a 100644 --- a/android/src/main/res/layout/settings.xml +++ b/android/src/main/res/layout/settings.xml @@ -64,6 +64,34 @@ android:src="@drawable/icon_chevron" /> </LinearLayout> + <LinearLayout android:id="@+id/wireguard_keys" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="16dp" + android:layout_marginTop="24dp" + android:background="@drawable/cell_button_background" + android:clickable="true" + android:gravity="center" + > + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="8dp" + android:paddingVertical="17dp" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" + android:text="@string/wireguard_key" + /> + <ImageView + android:layout_width="14dp" + android:layout_height="24dp" + android:layout_weight="0" + android:alpha="0.6" + android:src="@drawable/icon_chevron" + /> + </LinearLayout> <LinearLayout android:id="@+id/app_version" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/android/src/main/res/layout/wireguard_key.xml b/android/src/main/res/layout/wireguard_key.xml new file mode 100644 index 0000000000..b0da0776ca --- /dev/null +++ b/android/src/main/res/layout/wireguard_key.xml @@ -0,0 +1,153 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/darkBlue" + android:elevation="3dp" + android:gravity="left" + android:orientation="vertical"> + + <LinearLayout android:id="@+id/back" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="12dp" + android:orientation="horizontal" + android:gravity="center_vertical | left" + android:clickable="true" + android:background="?android:attr/selectableItemBackground" + > + <ImageView + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginRight="8dp" + android:src="@drawable/icon_back" + /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white60" + android:textSize="13sp" + android:textStyle="bold" + android:text="@string/settings" + /> + </LinearLayout> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="24dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="24dp" + android:text="@string/wireguard_key" + android:textColor="@color/white" + android:textSize="32sp" + android:textStyle="bold" /> + + <LinearLayout + android:id="@+id/wireguard_public_key_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/cell_button_background" + android:clickable="true" + android:gravity="center" + android:orientation="vertical" + android:paddingHorizontal="4dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingTop="10dp" + android:paddingBottom="5dp" + android:text="@string/wireguard_public_key" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" /> + + <TextView + android:id="@+id/wireguard_public_key" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="center" + android:paddingTop="3dp" + android:textAllCaps="true" + android:textColor="@color/white60" + android:textSize="14sp" + android:textStyle="bold" /> + + <TextView + android:id="@+id/wireguard_key_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="5dp" + android:textColor="@color/red" + android:textSize="20sp" + android:textStyle="bold" + android:visibility="gone" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/wireguard_key_visit_website" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:background="@drawable/cell_button_background" + android:clickable="true" + android:gravity="center" + android:paddingHorizontal="16dp" + android:visibility="gone"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingHorizontal="8dp" + android:paddingVertical="17dp" + android:text="@string/wireguard_key_visit_website" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" /> + + <ImageView + android:layout_width="16dp" + android:layout_height="16dp" + android:alpha="0.6" + android:src="@drawable/icon_extlink" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/wireguard_button_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="15dp" + android:layout_marginBottom="15dp" + android:background="@drawable/cell_button_background" + android:clickable="true" + android:gravity="center" + android:orientation="vertical"> + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="50dp" + android:gravity="center" + android:orientation="vertical"> + + <Button + android:id="@+id/wg_key_button" + style="@style/Button" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:text="@string/wireguard_validate_key"/> + + <ProgressBar + android:id="@+id/wg_action_spinner" + android:layout_width="30dp" + android:layout_height="30dp" + android:indeterminate="true" + android:indeterminateDrawable="@drawable/icon_spinner" + android:indeterminateDuration="600" + android:indeterminateOnly="true" + android:visibility="gone" /> + </RelativeLayout> + </LinearLayout> + +</LinearLayout> diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 6b605a7bc2..02f4315083 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -95,6 +95,23 @@ While connected, your real location is masked with a private and secure location in the selected region </string> + <string name="wireguard_key">WireGuard key</string> + <string name="wireguard_public_key">Public key</string> + <string name="wireguard_validate_key">Validate key</string> + <string name="wireguard_generate_key">Generate key</string> + <string name="wireguard_key_visit_website">Manage your keys on website</string> + <string name="wireguard_key_connectivity"> + Connectivity required to manage your key. + </string> + <string name="wireguard_key_valid"> + Key is valid + </string> + <string name="wireguard_key_invalid"> + Key is invalid + </string> + <string name="wireguard_key_verification_failure"> + Failed to validate key + </string> <string name="account_url">https://mullvad.net/en/account</string> <string name="download_url">https://mullvad.net/en/download</string> |
