diff options
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/fragment')
7 files changed, 160 insertions, 119 deletions
diff --git a/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt b/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt index c56462d6..78b9f3d7 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.fragment @@ -34,10 +34,6 @@ class AddTunnelsSheet : BottomSheetDialogFragment() { } } - override fun getTheme(): Int { - return R.style.BottomSheetDialogTheme - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { if (savedInstanceState != null) dismiss() val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false) @@ -76,7 +72,7 @@ class AddTunnelsSheet : BottomSheetDialogFragment() { } }) val gradientDrawable = GradientDrawable().apply { - setColor(requireContext().resolveAttribute(R.attr.colorBackground)) + setColor(requireContext().resolveAttribute(com.google.android.material.R.attr.colorSurface)) } view.background = gradientDrawable } diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt index 1a40a1cd..1cd19934 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt @@ -1,11 +1,15 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.fragment import android.Manifest import android.app.Dialog +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.PackageInfoFlags +import android.os.Build import android.os.Bundle import android.widget.Button import android.widget.Toast @@ -41,11 +45,12 @@ class AppListDialogFragment : DialogFragment() { try { val applicationData: MutableList<ApplicationData> = ArrayList() withContext(Dispatchers.IO) { - val packageInfos = pm.getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET), 0) + val packageInfos = getPackagesHoldingPermissions(pm, arrayOf(Manifest.permission.INTERNET)) packageInfos.forEach { val packageName = it.packageName val appInfo = it.applicationInfo - val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName)) + val appData = + ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName)) applicationData.add(appData) appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable?, propertyId: Int) { @@ -59,6 +64,7 @@ class AppListDialogFragment : DialogFragment() { withContext(Dispatchers.Main.immediate) { appData.clear() appData.addAll(applicationData) + setButtonText() } } catch (e: Throwable) { withContext(Dispatchers.Main.immediate) { @@ -77,6 +83,15 @@ class AppListDialogFragment : DialogFragment() { initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true } + private fun getPackagesHoldingPermissions(pm: PackageManager, permissions: Array<String>): List<PackageInfo> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackagesHoldingPermissions(permissions, PackageInfoFlags.of(0L)) + } else { + @Suppress("DEPRECATION") + pm.getPackagesHoldingPermissions(permissions, 0) + } + } + private fun setButtonText() { val numSelected = appData.count { it.isSelected } button?.text = if (numSelected == 0) @@ -129,10 +144,12 @@ class AppListDialogFragment : DialogFragment() { selectedApps.add(data.packageName) } } - setFragmentResult(REQUEST_SELECTION, bundleOf( + setFragmentResult( + REQUEST_SELECTION, bundleOf( KEY_SELECTED_APPS to selectedApps.toTypedArray(), KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0) - )) + ) + ) dismiss() } @@ -140,6 +157,7 @@ class AppListDialogFragment : DialogFragment() { const val KEY_SELECTED_APPS = "selected_apps" const val KEY_IS_EXCLUDED = "is_excluded" const val REQUEST_SELECTION = "request_selection" + fun newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean): AppListDialogFragment { val extras = Bundle() extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps) diff --git a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt index 783f5722..d5c1723f 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.fragment @@ -67,12 +67,20 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener { val activity = activity ?: return activity.lifecycleScope.launch { if (Application.getBackend() is GoBackend) { - val intent = GoBackend.VpnService.prepare(activity) - if (intent != null) { - pendingTunnel = tunnel - pendingTunnelUp = checked - permissionActivityResultLauncher.launch(intent) - return@launch + try { + val intent = GoBackend.VpnService.prepare(activity) + if (intent != null) { + pendingTunnel = tunnel + pendingTunnelUp = checked + permissionActivityResultLauncher.launch(intent) + return@launch + } + } catch (e: Throwable) { + val message = activity.getString(R.string.error_prepare, ErrorMessages[e]) + Snackbar.make(view, message, Snackbar.LENGTH_LONG) + .setAnchorView(view.findViewById(R.id.create_fab)) + .show() + Log.e(TAG, message, e) } } setTunnelStateWithPermissionsResult(tunnel, checked) @@ -91,8 +99,8 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener { val view = view if (view != null) Snackbar.make(view, message, Snackbar.LENGTH_LONG) - .setAnchorView(view.findViewById(R.id.create_fab)) - .show() + .setAnchorView(view.findViewById(R.id.create_fab)) + .show() else Toast.makeText(activity, message, Toast.LENGTH_LONG).show() Log.e(TAG, message, e) diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt index 0c4e9d20..34c96505 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt @@ -1,19 +1,15 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.fragment import android.app.Dialog -import android.content.DialogInterface import android.os.Bundle -import android.view.inputmethod.InputMethodManager -import androidx.appcompat.app.AlertDialog -import androidx.core.content.getSystemService +import android.view.WindowManager import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.textfield.TextInputEditText import com.wireguard.android.Application import com.wireguard.android.R import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding @@ -27,7 +23,6 @@ import java.nio.charset.StandardCharsets class ConfigNamingDialogFragment : DialogFragment() { private var binding: ConfigNamingDialogFragmentBinding? = null private var config: Config? = null - private var imm: InputMethodManager? = null private fun createTunnelAndDismiss() { val binding = binding ?: return @@ -43,11 +38,6 @@ class ConfigNamingDialogFragment : DialogFragment() { } } - override fun dismiss() { - setKeyboardVisible(false) - super.dismiss() - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val configText = requireArguments().getString(KEY_CONFIG_TEXT) @@ -64,7 +54,6 @@ class ConfigNamingDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val activity = requireActivity() - imm = activity.getSystemService() val alertDialogBuilder = MaterialAlertDialogBuilder(activity) alertDialogBuilder.setTitle(R.string.import_from_qr_code) binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false) @@ -72,43 +61,16 @@ class ConfigNamingDialogFragment : DialogFragment() { executePendingBindings() alertDialogBuilder.setView(root) } - alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null) + alertDialogBuilder.setPositiveButton(R.string.create_tunnel) { _, _ -> createTunnelAndDismiss() } alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() } - return alertDialogBuilder.create().apply { - setOnShowListener { - findViewById<TextInputEditText>(R.id.tunnel_name_text)?.apply { - setOnFocusChangeListener { v, _ -> - v.post { - imm?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) - } - } - requestFocus() - } - } - } - } - - override fun onResume() { - super.onResume() - val dialog = dialog as AlertDialog? - if (dialog != null) { - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { createTunnelAndDismiss() } - setKeyboardVisible(true) - } - } - - private fun setKeyboardVisible(visible: Boolean) { - if (visible) { - imm!!.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) - } else if (binding != null) { - imm!!.hideSoftInputFromWindow(binding!!.tunnelNameText.windowToken, 0) - } + val dialog = alertDialogBuilder.create() + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + return dialog } companion object { private const val KEY_CONFIG_TEXT = "config_text" - @JvmStatic fun newInstance(configText: String?): ConfigNamingDialogFragment { val extras = Bundle() extras.putString(KEY_CONFIG_TEXT, configText) diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt index 7046cb96..81d8e8c6 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.fragment @@ -8,9 +8,12 @@ import android.os.Bundle 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 androidx.core.view.MenuProvider import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.wireguard.android.R import com.wireguard.android.backend.Tunnel @@ -24,28 +27,34 @@ import kotlinx.coroutines.launch /** * Fragment that shows details about a specific tunnel. */ -class TunnelDetailFragment : BaseFragment() { +class TunnelDetailFragment : BaseFragment(), MenuProvider { private var binding: TunnelDetailFragmentBinding? = null private var lastState = Tunnel.State.TOGGLE private var timerActive = true - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return false } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.tunnel_detail, menu) + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.tunnel_detail, menu) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { super.onCreateView(inflater, container, savedInstanceState) binding = TunnelDetailFragmentBinding.inflate(inflater, container, false) binding?.executePendingBindings() return binding?.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + override fun onDestroyView() { binding = null super.onDestroyView() @@ -103,25 +112,38 @@ class TunnelDetailFragment : BaseFragment() { val statistics = tunnel.getStatisticsAsync() for (i in 0 until binding.peersLayout.childCount) { val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i)) - ?: continue + ?: continue val publicKey = peer.item!!.publicKey - val rx = statistics.peerRx(publicKey) - val tx = statistics.peerTx(publicKey) - if (rx == 0L && tx == 0L) { + val peerStats = statistics.peer(publicKey) + if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) { peer.transferLabel.visibility = View.GONE peer.transferText.visibility = View.GONE - continue + } else { + peer.transferText.text = getString( + R.string.transfer_rx_tx, + QuantityFormatter.formatBytes(peerStats.rxBytes), + QuantityFormatter.formatBytes(peerStats.txBytes) + ) + peer.transferLabel.visibility = View.VISIBLE + peer.transferText.visibility = View.VISIBLE + } + if (peerStats == null || peerStats.latestHandshakeEpochMillis == 0L) { + peer.latestHandshakeLabel.visibility = View.GONE + peer.latestHandshakeText.visibility = View.GONE + } else { + peer.latestHandshakeText.text = QuantityFormatter.formatEpochAgo(peerStats.latestHandshakeEpochMillis) + peer.latestHandshakeLabel.visibility = View.VISIBLE + peer.latestHandshakeText.visibility = View.VISIBLE } - peer.transferText.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx)) - peer.transferLabel.visibility = View.VISIBLE - peer.transferText.visibility = View.VISIBLE } } catch (e: Throwable) { for (i in 0 until binding.peersLayout.childCount) { val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i)) - ?: continue + ?: continue peer.transferLabel.visibility = View.GONE peer.transferText.visibility = View.GONE + peer.latestHandshakeLabel.visibility = View.GONE + peer.latestHandshakeText.visibility = View.GONE } } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt index 6c6f53f9..edf4b226 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.fragment @@ -18,6 +18,9 @@ import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.Toast +import androidx.core.os.BundleCompat +import androidx.core.view.MenuProvider +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import com.wireguard.android.Application @@ -35,7 +38,7 @@ import kotlinx.coroutines.launch /** * Fragment for editing a WireGuard configuration. */ -class TunnelEditorFragment : BaseFragment() { +class TunnelEditorFragment : BaseFragment(), MenuProvider { private var haveShownKeys = false private var binding: TunnelEditorFragmentBinding? = null private var tunnel: ObservableTunnel? = null @@ -63,17 +66,14 @@ class TunnelEditorFragment : BaseFragment() { } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.config_editor, menu) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.config_editor, menu) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { super.onCreateView(inflater, container, savedInstanceState) binding = TunnelEditorFragmentBinding.inflate(inflater, container, false) binding?.apply { @@ -83,6 +83,11 @@ class TunnelEditorFragment : BaseFragment() { return binding?.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + override fun onDestroyView() { activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) binding = null @@ -95,8 +100,10 @@ class TunnelEditorFragment : BaseFragment() { 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) + inputManager?.hideSoftInputFromWindow( + focusedView.windowToken, + InputMethodManager.HIDE_NOT_ALWAYS + ) } parentFragmentManager.popBackStackImmediate() @@ -105,8 +112,8 @@ class TunnelEditorFragment : BaseFragment() { selectedTunnel = tunnel } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.menu_action_save) { + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (menuItem.itemId == R.id.menu_action_save) { binding ?: return false val newConfig = try { binding!!.config!!.resolve() @@ -130,6 +137,7 @@ class TunnelEditorFragment : BaseFragment() { onTunnelCreated(null, e) } } + tunnel!!.name != binding!!.name -> { Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name) try { @@ -139,6 +147,7 @@ class TunnelEditorFragment : BaseFragment() { onTunnelRenamed(tunnel!!, newConfig, e) } } + else -> { Log.d(TAG, "Attempting to save config of " + tunnel!!.name) try { @@ -152,7 +161,7 @@ class TunnelEditorFragment : BaseFragment() { } return true } - return super.onOptionsItemSelected(item) + return false } @Suppress("UNUSED_PARAMETER") @@ -194,8 +203,10 @@ class TunnelEditorFragment : BaseFragment() { super.onSaveInstanceState(outState) } - override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, - newTunnel: ObservableTunnel?) { + override fun onSelectedTunnelChanged( + oldTunnel: ObservableTunnel?, + newTunnel: ObservableTunnel? + ) { tunnel = newTunnel if (binding == null) return binding!!.config = ConfigProxy() @@ -232,8 +243,10 @@ class TunnelEditorFragment : BaseFragment() { } } - private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config, - throwable: Throwable?) { + private suspend fun onTunnelRenamed( + renamedTunnel: ObservableTunnel, newConfig: Config, + throwable: Throwable? + ) { val ctx = activity ?: Application.get() if (throwable == null) { val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name) @@ -265,7 +278,7 @@ class TunnelEditorFragment : BaseFragment() { onSelectedTunnelChanged(null, selectedTunnel) } else { tunnel = selectedTunnel - val config: ConfigProxy = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG)!! + val config = BundleCompat.getParcelable(savedInstanceState, KEY_LOCAL_CONFIG, ConfigProxy::class.java)!! val originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME) if (tunnel != null && tunnel!!.name != originalName) onSelectedTunnelChanged(null, tunnel) else binding!!.config = config } @@ -290,13 +303,15 @@ class TunnelEditorFragment : BaseFragment() { haveShownKeys = true showPrivateKey(edit) } + is BiometricAuthenticator.Result.Failure -> { Snackbar.make( - binding!!.mainContainer, - it.message, - Snackbar.LENGTH_SHORT + binding!!.mainContainer, + it.message, + Snackbar.LENGTH_SHORT ).show() } + is BiometricAuthenticator.Result.Cancelled -> {} } } diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt index 390a6396..cba7c476 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.fragment @@ -16,6 +16,8 @@ import android.view.ViewGroup import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode @@ -31,6 +33,7 @@ import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowC import com.wireguard.android.databinding.TunnelListFragmentBinding import com.wireguard.android.databinding.TunnelListItemBinding import com.wireguard.android.model.ObservableTunnel +import com.wireguard.android.updater.SnackbarUpdateShower import com.wireguard.android.util.ErrorMessages import com.wireguard.android.util.QrCodeFromFileScanner import com.wireguard.android.util.TunnelImporter @@ -39,8 +42,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch -import java.util.ArrayList -import java.util.HashSet /** * Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels. @@ -48,6 +49,7 @@ import java.util.HashSet class TunnelListFragment : BaseFragment() { private val actionModeListener = ActionModeListener() private var actionMode: ActionMode? = null + private var backPressedCallback: OnBackPressedCallback? = null private var binding: TunnelListFragmentBinding? = null private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data -> if (data == null) return@registerForActivityResult @@ -61,7 +63,7 @@ class TunnelListFragment : BaseFragment() { TunnelImporter.importTunnel(parentFragmentManager, result.text) { showSnackbar(it) } } catch (e: Exception) { val error = ErrorMessages[e] - val message = requireContext().getString(R.string.import_error, error) + val message = Application.get().resources.getString(R.string.import_error, error) Log.e(TAG, message, e) showSnackbar(message) } @@ -79,6 +81,8 @@ class TunnelListFragment : BaseFragment() { } } + private val snackbarUpdateShower = SnackbarUpdateShower(this) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (savedInstanceState != null) { @@ -89,33 +93,45 @@ class TunnelListFragment : BaseFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { super.onCreateView(inflater, container, savedInstanceState) binding = TunnelListFragmentBinding.inflate(inflater, container, false) val bottomSheet = AddTunnelsSheet() binding?.apply { createFab.setOnClickListener { + if (childFragmentManager.findFragmentByTag("BOTTOM_SHEET") != null) + return@setOnClickListener childFragmentManager.setFragmentResultListener(AddTunnelsSheet.REQUEST_KEY_NEW_TUNNEL, viewLifecycleOwner) { _, bundle -> when (bundle.getString(AddTunnelsSheet.REQUEST_METHOD)) { AddTunnelsSheet.REQUEST_CREATE -> { startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java)) } + AddTunnelsSheet.REQUEST_IMPORT -> { tunnelFileImportResultLauncher.launch("*/*") } + AddTunnelsSheet.REQUEST_SCAN -> { - qrImportResultLauncher.launch(ScanOptions() + qrImportResultLauncher.launch( + ScanOptions() .setOrientationLocked(false) .setBeepEnabled(false) - .setPrompt(getString(R.string.qr_code_hint))) + .setPrompt(getString(R.string.qr_code_hint)) + ) } } } - bottomSheet.show(childFragmentManager, "BOTTOM_SHEET") + bottomSheet.showNow(childFragmentManager, "BOTTOM_SHEET") } executePendingBindings() + snackbarUpdateShower.attach(mainContainer, createFab) } + backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() } + backPressedCallback?.isEnabled = false + return binding?.root } @@ -182,8 +198,8 @@ class TunnelListFragment : BaseFragment() { val binding = binding if (binding != null) Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG) - .setAnchorView(binding.createFab) - .show() + .setAnchorView(binding.createFab) + .show() else Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show() } @@ -225,6 +241,7 @@ class TunnelListFragment : BaseFragment() { mode.finish() true } + R.id.menu_action_select_all -> { lifecycleScope.launch { val tunnels = Application.getTunnelManager().getTunnels() @@ -234,12 +251,14 @@ class TunnelListFragment : BaseFragment() { } true } + else -> false } } override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { actionMode = mode + backPressedCallback?.isEnabled = true if (activity != null) { resources = activity!!.resources } @@ -251,6 +270,7 @@ class TunnelListFragment : BaseFragment() { override fun onDestroyActionMode(mode: ActionMode) { actionMode = null + backPressedCallback?.isEnabled = false resources = null animateFab(binding?.createFab, true) checkedItems.clear() @@ -270,7 +290,7 @@ class TunnelListFragment : BaseFragment() { } val adapter = if (binding == null) null else binding!!.tunnelList.adapter if (actionMode == null && !checkedItems.isEmpty() && activity != null) { - (activity as AppCompatActivity?)!!.startSupportActionMode(this) + (activity as AppCompatActivity).startSupportActionMode(this) } else if (actionMode != null && checkedItems.isEmpty()) { actionMode!!.finish() } @@ -297,7 +317,7 @@ class TunnelListFragment : BaseFragment() { private fun animateFab(view: View?, show: Boolean) { view ?: return val animation = AnimationUtils.loadAnimation( - context, if (show) R.anim.scale_up else R.anim.scale_down + context, if (show) R.anim.scale_up else R.anim.scale_down ) animation.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationRepeat(animation: Animation?) { |