aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2020-03-16 14:46:36 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2020-03-19 09:24:48 +0530
commitfc0660ca8d5dd98b5a98a32e13896f9553a4311c (patch)
tree03dc9fb1c4a872496940a4f5364874ba63654285 /ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
parentcodestyle: Require atleast 10 references before using star imports (diff)
downloadwireguard-android-fc0660ca8d5dd98b5a98a32e13896f9553a4311c.tar.xz
wireguard-android-fc0660ca8d5dd98b5a98a32e13896f9553a4311c.zip
ui: Convert fragment package to Kotlin
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt')
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt415
1 files changed, 415 insertions, 0 deletions
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
new file mode 100644
index 00000000..051babb2
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
@@ -0,0 +1,415 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Intent
+import android.content.res.Resources
+import android.net.Uri
+import android.os.Bundle
+import android.provider.OpenableColumns
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.view.ActionMode
+import com.google.android.material.snackbar.Snackbar
+import com.google.zxing.integration.android.IntentIntegrator
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.activity.TunnelCreatorActivity
+import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
+import com.wireguard.android.databinding.TunnelListFragmentBinding
+import com.wireguard.android.databinding.TunnelListItemBinding
+import com.wireguard.android.fragment.ConfigNamingDialogFragment.Companion.newInstance
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.ui.EdgeToEdge.setUpFAB
+import com.wireguard.android.ui.EdgeToEdge.setUpRoot
+import com.wireguard.android.ui.EdgeToEdge.setUpScrollingContent
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.ObservableSortedKeyedList
+import com.wireguard.android.widget.MultiselectableRelativeLayout
+import com.wireguard.config.BadConfigException
+import com.wireguard.config.Config
+import java9.util.concurrent.CompletableFuture
+import java.io.BufferedReader
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
+import java.util.ArrayList
+import java.util.HashSet
+import java.util.Locale
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+
+/**
+ * Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
+ */
+class TunnelListFragment : BaseFragment() {
+ private val actionModeListener = ActionModeListener()
+ private var actionMode: ActionMode? = null
+ private var binding: TunnelListFragmentBinding? = null
+ private fun importTunnel(configText: String) {
+ try {
+ // Ensure the config text is parseable before proceeding…
+ Config.parse(ByteArrayInputStream(configText.toByteArray(StandardCharsets.UTF_8)))
+
+ // Config text is valid, now create the tunnel…
+ newInstance(configText).show(parentFragmentManager, null)
+ } catch (e: BadConfigException) {
+ onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
+ } catch (e: IOException) {
+ onTunnelImportFinished(emptyList(), listOf<Throwable>(e))
+ }
+ }
+
+ private fun importTunnel(uri: Uri?) {
+ val activity = activity
+ if (activity == null || uri == null) {
+ return
+ }
+ val contentResolver = activity.contentResolver
+
+ val futureTunnels = ArrayList<CompletableFuture<ObservableTunnel>>()
+ val throwables = ArrayList<Throwable>()
+ Application.getAsyncWorker().supplyAsync {
+ val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
+ var name = ""
+ contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
+ if (cursor.moveToFirst() && !cursor.isNull(0)) {
+ name = cursor.getString(0)
+ }
+ cursor.close()
+ }
+ if (name.isEmpty()) {
+ name = Uri.decode(uri.lastPathSegment)
+ }
+ var idx = name.lastIndexOf('/')
+ if (idx >= 0) {
+ require(idx < name.length - 1) { "Illegal file name: $name" }
+ name = name.substring(idx + 1)
+ }
+ val isZip = name.toLowerCase(Locale.ROOT).endsWith(".zip")
+ if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
+ name = name.substring(0, name.length - ".conf".length)
+ } else {
+ require(isZip) { "File must be .conf or .zip" }
+ }
+
+ if (isZip) {
+ ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
+ val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
+ var entry: ZipEntry
+ while (zip.nextEntry.also { entry = it } != null) {
+ name = entry.name
+ idx = name.lastIndexOf('/')
+ if (idx >= 0) {
+ if (idx >= name.length - 1) {
+ continue
+ }
+ name = name.substring(name.lastIndexOf('/') + 1)
+ }
+ if (name.toLowerCase(Locale.ROOT).endsWith(".conf")) {
+ name = name.substring(0, name.length - ".conf".length)
+ } else {
+ continue
+ }
+ val config: Config? = try {
+ Config.parse(reader)
+ } catch (e: Exception) {
+ throwables.add(e)
+ null
+ }
+
+ if (config != null) {
+ futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture())
+ }
+ }
+ }
+ } else {
+ futureTunnels.add(
+ Application.getTunnelManager().create(
+ name,
+ Config.parse(contentResolver.openInputStream(uri))
+ ).toCompletableFuture()
+ )
+ }
+
+ if (futureTunnels.isEmpty()) {
+ if (throwables.size == 1) {
+ throw throwables[0]
+ } else {
+ require(throwables.isNotEmpty()) { "No configurations found" }
+ }
+ }
+ CompletableFuture.allOf(*futureTunnels.toTypedArray())
+ }.whenComplete { future, exception ->
+ if (exception != null) {
+ onTunnelImportFinished(emptyList(), listOf(exception))
+ } else {
+ future.whenComplete { _, _ ->
+ val tunnels = mutableListOf<ObservableTunnel>()
+ for (futureTunnel in futureTunnels) {
+ val tunnel: ObservableTunnel? = try {
+ futureTunnel.getNow(null)
+ } catch (e: Exception) {
+ throwables.add(e)
+ null
+ }
+
+ if (tunnel != null) {
+ tunnels.add(tunnel)
+ }
+ }
+ onTunnelImportFinished(tunnels, throwables)
+ }
+ }
+ }
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ if (savedInstanceState != null) {
+ val checkedItems: Collection<Int>? = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS")
+ if (checkedItems != null) {
+ for (i in checkedItems) actionModeListener.setItemChecked(i, true)
+ }
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ when (requestCode) {
+ REQUEST_IMPORT -> {
+ if (resultCode == Activity.RESULT_OK && data != null) importTunnel(data.data)
+ return
+ }
+ IntentIntegrator.REQUEST_CODE -> {
+ val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
+ if (result != null && result.contents != null) {
+ importTunnel(result.contents)
+ }
+ return
+ }
+ else -> super.onActivityResult(requestCode, resultCode, data)
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+ binding = TunnelListFragmentBinding.inflate(inflater, container, false)
+ binding?.apply {
+ createFab.setOnClickListener {
+ val bottomSheet = AddTunnelsSheet()
+ bottomSheet.setTargetFragment(fragment, REQUEST_TARGET_FRAGMENT)
+ bottomSheet.show(parentFragmentManager, "BOTTOM_SHEET")
+ }
+ executePendingBindings()
+ setUpRoot(root as ViewGroup)
+ setUpFAB(createFab)
+ setUpScrollingContent(tunnelList, createFab)
+ }
+ return binding!!.root
+ }
+
+ override fun onDestroyView() {
+ binding = null
+ super.onDestroyView()
+ }
+
+ fun onRequestCreateConfig(view: View?) {
+ startActivity(Intent(activity, TunnelCreatorActivity::class.java))
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems())
+ }
+
+ override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
+ if (binding == null) return
+ Application.getTunnelManager().tunnels.thenAccept { tunnels: ObservableSortedKeyedList<String?, ObservableTunnel?> ->
+ if (newTunnel != null) viewForTunnel(newTunnel, tunnels).setSingleSelected(true)
+ if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels).setSingleSelected(false)
+ }
+ }
+
+ private fun onTunnelDeletionFinished(count: Int, throwable: Throwable?) {
+ val message: String
+ if (throwable == null) {
+ message = resources.getQuantityString(R.plurals.delete_success, count, count)
+ } else {
+ val error = ErrorMessages.get(throwable)
+ message = resources.getQuantityString(R.plurals.delete_error, count, count, error)
+ Log.e(TAG, message, throwable)
+ }
+ showSnackbar(message)
+ }
+
+ private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>) {
+ var message: String? = null
+ for (throwable in throwables) {
+ val error = ErrorMessages.get(throwable)
+ message = getString(R.string.import_error, error)
+ Log.e(TAG, message, throwable)
+ }
+ if (tunnels.size == 1 && throwables.isEmpty())
+ message = getString(R.string.import_success, tunnels[0].name)
+ else if (tunnels.isEmpty() && throwables.size == 1)
+ else if (throwables.isEmpty())
+ message = resources.getQuantityString(R.plurals.import_total_success,
+ tunnels.size, tunnels.size)
+ else if (!throwables.isEmpty())
+ message = resources.getQuantityString(R.plurals.import_partial_success,
+ tunnels.size + throwables.size,
+ tunnels.size, tunnels.size + throwables.size)
+ showSnackbar(message)
+ }
+
+ override fun onViewStateRestored(savedInstanceState: Bundle?) {
+ super.onViewStateRestored(savedInstanceState)
+ if (binding == null) {
+ return
+ }
+ binding!!.fragment = this
+ Application.getTunnelManager().tunnels.thenAccept { tunnels: ObservableSortedKeyedList<String?, ObservableTunnel?>? -> binding!!.tunnels = tunnels }
+ binding!!.rowConfigurationHandler = RowConfigurationHandler { binding: TunnelListItemBinding, tunnel: ObservableTunnel, position: Int ->
+ binding.fragment = this
+ binding.root.setOnClickListener {
+ if (actionMode == null) {
+ selectedTunnel = tunnel
+ } else {
+ actionModeListener.toggleItemChecked(position)
+ }
+ }
+ binding.root.setOnLongClickListener {
+ actionModeListener.toggleItemChecked(position)
+ true
+ }
+ if (actionMode != null)
+ (binding.root as MultiselectableRelativeLayout).setMultiSelected(actionModeListener.checkedItems.contains(position))
+ else
+ (binding.root as MultiselectableRelativeLayout).setSingleSelected(selectedTunnel === tunnel)
+ }
+ }
+
+ private fun showSnackbar(message: CharSequence?) {
+ if (binding != null) {
+ val snackbar = Snackbar.make(binding!!.mainContainer, message!!, Snackbar.LENGTH_LONG)
+ snackbar.anchorView = binding!!.createFab
+ snackbar.show()
+ }
+ }
+
+ private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout {
+ return binding!!.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))!!.itemView as MultiselectableRelativeLayout
+ }
+
+ private inner class ActionModeListener : ActionMode.Callback {
+ val checkedItems: MutableCollection<Int> = HashSet()
+ private var resources: Resources? = null
+ fun getCheckedItems(): ArrayList<Int> {
+ return ArrayList(checkedItems)
+ }
+
+ override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.menu_action_delete -> {
+ val copyCheckedItems: Iterable<Int> = HashSet(checkedItems)
+ Application.getTunnelManager().tunnels.thenAccept { tunnels: ObservableSortedKeyedList<String, ObservableTunnel> ->
+ val tunnelsToDelete = ArrayList<ObservableTunnel>()
+ for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
+ val futures = tunnelsToDelete
+ .map { obj -> obj.delete() }
+ .toTypedArray()
+ CompletableFuture.allOf(*futures as Array<out CompletableFuture<*>>)
+ .thenApply { futures.size }
+ .whenComplete { count: Int, throwable: Throwable? -> onTunnelDeletionFinished(count, throwable) }
+ }
+ checkedItems.clear()
+ mode.finish()
+ true
+ }
+ R.id.menu_action_select_all -> {
+ Application.getTunnelManager().tunnels.thenAccept { tunnels: ObservableSortedKeyedList<String?, ObservableTunnel?> ->
+ var i = 0
+ while (i < tunnels.size) {
+ setItemChecked(i, true)
+ ++i
+ }
+ }
+ true
+ }
+ else -> false
+ }
+ }
+
+ override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+ actionMode = mode
+ if (activity != null) {
+ resources = activity!!.resources
+ }
+ mode.menuInflater.inflate(R.menu.tunnel_list_action_mode, menu)
+ binding!!.tunnelList.adapter!!.notifyDataSetChanged()
+ return true
+ }
+
+ override fun onDestroyActionMode(mode: ActionMode) {
+ actionMode = null
+ resources = null
+ checkedItems.clear()
+ binding!!.tunnelList.adapter!!.notifyDataSetChanged()
+ }
+
+ override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+ updateTitle(mode)
+ return false
+ }
+
+ fun setItemChecked(position: Int, checked: Boolean) {
+ if (checked) {
+ checkedItems.add(position)
+ } else {
+ checkedItems.remove(position)
+ }
+ val adapter = if (binding == null) null else binding!!.tunnelList.adapter
+ if (actionMode == null && !checkedItems.isEmpty() && activity != null) {
+ (activity as AppCompatActivity?)!!.startSupportActionMode(this)
+ } else if (actionMode != null && checkedItems.isEmpty()) {
+ actionMode!!.finish()
+ }
+ adapter?.notifyItemChanged(position)
+ updateTitle(actionMode)
+ }
+
+ fun toggleItemChecked(position: Int) {
+ setItemChecked(position, !checkedItems.contains(position))
+ }
+
+ private fun updateTitle(mode: ActionMode?) {
+ if (mode == null) {
+ return
+ }
+ val count = checkedItems.size
+ if (count == 0) {
+ mode.title = ""
+ } else {
+ mode.title = resources!!.getQuantityString(R.plurals.delete_title, count, count)
+ }
+ }
+ }
+
+ companion object {
+ const val REQUEST_IMPORT = 1
+ private const val REQUEST_TARGET_FRAGMENT = 2
+ private val TAG = "WireGuard/" + TunnelListFragment::class.java.simpleName
+ }
+}