/* * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.fragment import android.content.Context import android.os.Bundle import android.text.InputType import android.util.Log 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.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.Toast import com.google.android.material.snackbar.Snackbar import com.wireguard.android.Application import com.wireguard.android.R import com.wireguard.android.backend.Tunnel import com.wireguard.android.databinding.TunnelEditorFragmentBinding import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.util.ErrorMessages import com.wireguard.android.viewmodel.ConfigProxy import com.wireguard.android.widget.EdgeToEdge.setUpRoot import com.wireguard.android.widget.EdgeToEdge.setUpScrollingContent import com.wireguard.config.Config /** * Fragment for editing a WireGuard configuration. */ class TunnelEditorFragment : BaseFragment(), AppExclusionListener { private var haveShownKeys = false private var binding: TunnelEditorFragmentBinding? = null private var tunnel: ObservableTunnel? = null private fun onConfigLoaded(config: Config) { binding?.config = ConfigProxy(config) } private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) { val message: String if (throwable == null) { message = getString(R.string.config_save_success, savedTunnel.name) Log.d(TAG, message) Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() onFinished() } else { val error = ErrorMessages[throwable] message = getString(R.string.config_save_error, savedTunnel.name, error) Log.e(TAG, message, throwable) binding?.let { Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show() } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.config_editor, menu) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { super.onCreateView(inflater, container, savedInstanceState) binding = TunnelEditorFragmentBinding.inflate(inflater, container, false) binding?.apply { executePendingBindings() setUpRoot(root as ViewGroup) setUpScrollingContent(mainContainer, null) privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() } } return binding?.root } override fun onDestroyView() { requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) binding = null super.onDestroyView() } override fun onExcludedAppsSelected(excludedApps: List) { requireNotNull(binding) { "Tried to set excluded apps while no view was loaded" } binding!!.config!!.`interface`.excludedApplications.apply { clear() addAll(excludedApps) } } private fun onFinished() { // Hide the keyboard; it rarely goes away on its own. val activity = activity ?: return val focusedView = activity.currentFocus if (focusedView != null) { val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager inputManager?.hideSoftInputFromWindow(focusedView.windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } // Tell the activity to finish itself or go back to the detail view. requireActivity().runOnUiThread { // TODO(smaeul): Remove this hack when fixing the Config ViewModel // The selected tunnel has to actually change, but we have to remember this one. val savedTunnel = tunnel if (savedTunnel === selectedTunnel) selectedTunnel = null selectedTunnel = savedTunnel } } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.menu_action_save) { binding ?: return false val newConfig = try { binding!!.config!!.resolve() } catch (e: Exception) { val error = ErrorMessages[e] val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name val message = getString(R.string.config_save_error, tunnelName, error) Log.e(TAG, message, e) Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show() return false } when { tunnel == null -> { Log.d(TAG, "Attempting to create new tunnel " + binding!!.name) val manager = Application.getTunnelManager() manager.create(binding!!.name!!, newConfig).whenComplete(this::onTunnelCreated) } tunnel!!.name != binding!!.name -> { Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name) tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) } } else -> { Log.d(TAG, "Attempting to save config of " + tunnel!!.name) tunnel!!.setConfigAsync(newConfig) .whenComplete { _, t -> onConfigSaved(tunnel!!, t) } } } return true } return super.onOptionsItemSelected(item) } @Suppress("UNUSED_PARAMETER") fun onRequestSetExcludedApplications(view: View?) { if (binding != null) { val excludedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications) val fragment = AppListDialogFragment.newInstance(excludedApps, this) fragment.show(parentFragmentManager, null) } } override fun onSaveInstanceState(outState: Bundle) { if (binding != null) outState.putParcelable(KEY_LOCAL_CONFIG, binding!!.config) outState.putString(KEY_ORIGINAL_NAME, if (tunnel == null) null else tunnel!!.name) super.onSaveInstanceState(outState) } override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) { tunnel = newTunnel if (binding == null) return binding!!.config = ConfigProxy() if (tunnel != null) { binding!!.name = tunnel!!.name tunnel!!.configAsync.thenAccept(this::onConfigLoaded) } else { binding!!.name = "" } } private fun onTunnelCreated(newTunnel: ObservableTunnel, throwable: Throwable?) { val message: String if (throwable == null) { tunnel = newTunnel message = getString(R.string.tunnel_create_success, tunnel!!.name) Log.d(TAG, message) Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() onFinished() } else { val error = ErrorMessages[throwable] message = getString(R.string.tunnel_create_error, error) Log.e(TAG, message, throwable) binding?.let { Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show() } } } private fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config, throwable: Throwable?) { val message: String if (throwable == null) { message = getString(R.string.tunnel_rename_success, renamedTunnel.name) Log.d(TAG, message) // Now save the rest of configuration changes. Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name) renamedTunnel.setConfigAsync(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) } } else { val error = ErrorMessages[throwable] message = getString(R.string.tunnel_rename_error, error) Log.e(TAG, message, throwable) binding?.let { Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show() } } } override fun onViewStateRestored(savedInstanceState: Bundle?) { binding ?: return binding!!.fragment = this if (savedInstanceState == null) { onSelectedTunnelChanged(null, selectedTunnel) } else { tunnel = selectedTunnel val config: ConfigProxy = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG)!! val originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME) if (tunnel != null && tunnel!!.name != originalName) onSelectedTunnelChanged(null, tunnel) else binding!!.config = config } super.onViewStateRestored(savedInstanceState) } fun onKeyClick(view: View) = onKeyFocusChange(view, true) fun onKeyFocusChange(view: View, isFocused: Boolean) { if (!isFocused) return val edit = view as? EditText ?: return if (!haveShownKeys && edit.text.isNotEmpty()) { if (true /* TODO: do biometric auth prompt */) { haveShownKeys = true } else { /* Unauthorized, so return and don't change visibility. */ return } } requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) edit.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD } companion object { private const val KEY_LOCAL_CONFIG = "local_config" private const val KEY_ORIGINAL_NAME = "original_name" private const val TAG = "WireGuard/TunnelEditorFragment" } }