summaryrefslogtreecommitdiffhomepage
path: root/ui/src/main/java/com/wireguard
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/main/java/com/wireguard')
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt185
1 files changed, 166 insertions, 19 deletions
diff --git a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
index fd9f31b5..004d26e0 100644
--- a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
@@ -5,41 +5,47 @@
package com.wireguard.android.activity
-import android.content.ActivityNotFoundException
+import android.Manifest
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.os.Environment
+import android.os.storage.StorageManager
+import android.os.storage.StorageVolume
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
import androidx.core.view.forEach
import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableBoolean
+import androidx.databinding.ObservableField
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.databinding.Keyed
+import com.wireguard.android.databinding.ObservableKeyedArrayList
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter
import com.wireguard.android.databinding.TvActivityBinding
+import com.wireguard.android.databinding.TvFileListItemBinding
import com.wireguard.android.databinding.TvTunnelListItemBinding
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.QuantityFormatter
import com.wireguard.android.util.TunnelImporter
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
class TvMainActivity : AppCompatActivity() {
- private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
- if (data == null) return@registerForActivityResult
- lifecycleScope.launch {
- TunnelImporter.importTunnel(contentResolver, data) {
- Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
- }
- }
- }
-
private var pendingTunnel: ObservableTunnel? = null
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val tunnel = pendingTunnel
@@ -64,6 +70,8 @@ class TvMainActivity : AppCompatActivity() {
private lateinit var binding: TvActivityBinding
private val isDeleting = ObservableBoolean()
+ private val files = ObservableKeyedArrayList<String, KeyedFile>()
+ private val filesRoot = ObservableField("")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -76,7 +84,9 @@ class TvMainActivity : AppCompatActivity() {
binding.tunnelList.requestFocus()
}
binding.isDeleting = isDeleting
- binding.rowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
+ binding.files = files
+ binding.filesRoot = filesRoot
+ binding.tunnelRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
binding.isDeleting = isDeleting
binding.isFocused = ObservableBoolean()
@@ -111,13 +121,44 @@ class TvMainActivity : AppCompatActivity() {
}
}
}
+
+ binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
+ override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
+ binding.root.setOnClickListener {
+ if (item.isDirectory)
+ navigateTo(item)
+ else {
+ val uri = Uri.fromFile(item.canonicalFile)
+ files.clear()
+ filesRoot.set("")
+ lifecycleScope.launch {
+ TunnelImporter.importTunnel(contentResolver, uri) {
+ Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
+ }
+ }
+ runOnUiThread {
+ this@TvMainActivity.binding.tunnelList.requestFocus()
+ }
+ }
+ }
+ }
+ }
+
binding.importButton.setOnClickListener {
- try {
- tunnelFileImportResultLauncher.launch("*/*")
- } catch (e: ActivityNotFoundException) {
- Toast.makeText(this@TvMainActivity, getString(R.string.tv_error), Toast.LENGTH_LONG).show()
+ if (filesRoot.get()?.isEmpty() != false) {
+ navigateTo(myComputerFile)
+ runOnUiThread {
+ binding.filesList.requestFocus()
+ }
+ } else {
+ files.clear()
+ filesRoot.set("")
+ runOnUiThread {
+ binding.tunnelList.requestFocus()
+ }
}
}
+
binding.deleteButton.setOnClickListener {
isDeleting.set(!isDeleting.get())
runOnUiThread {
@@ -135,11 +176,112 @@ class TvMainActivity : AppCompatActivity() {
}
}
+ private var pendingNavigation: File? = null
+ private val permissionRequestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+ val to = pendingNavigation
+ if (it && to != null)
+ navigateTo(to)
+ pendingNavigation = null
+ }
+
+ private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) {
+ val list = HashSet<KeyedFile>()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ val storageManager: StorageManager = getSystemService() ?: return@withContext list
+ list.addAll(storageManager.storageVolumes.mapNotNull { volume ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ volume.directory?.let { KeyedFile(it.canonicalPath) }
+ } else {
+ KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File).canonicalPath)
+ }
+ })
+ } else {
+ @Suppress("DEPRECATION")
+ list.add(KeyedFile(Environment.getExternalStorageDirectory().canonicalPath))
+ try {
+ File("/storage").listFiles()?.forEach {
+ if (!it.isDirectory) return@forEach
+ try {
+ if (Environment.isExternalStorageRemovable(it)) {
+ list.add(KeyedFile(it.canonicalPath))
+ }
+ } catch (_: Throwable) {
+ }
+ }
+ } catch (_: Throwable) {
+ }
+ }
+ list
+ }
+
+ private val myComputerFile = File("")
+
+ private fun navigateTo(directory: File) {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ pendingNavigation = directory
+ permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
+ return
+ }
+
+ lifecycleScope.launch {
+ if (directory == myComputerFile) {
+ val roots = makeStorageRoots()
+ if (roots.count() == 1) {
+ navigateTo(roots.first())
+ return@launch
+ }
+ files.clear()
+ files.addAll(roots)
+ filesRoot.set(getString(R.string.tv_select_a_storage_drive))
+ return@launch
+ }
+
+ val newFiles = withContext(Dispatchers.IO) {
+ val newFiles = ArrayList<KeyedFile>()
+ try {
+ val parent = KeyedFile(directory.canonicalPath + "/..")
+ if (directory.canonicalPath != "/" && parent.list() != null)
+ newFiles.add(parent)
+ val listing = directory.listFiles() ?: return@withContext null
+ listing.forEach {
+ if (it.extension == "conf" || it.extension == "zip" || it.isDirectory)
+ newFiles.add(KeyedFile(it.canonicalPath))
+ }
+ newFiles.sortWith { a, b ->
+ if (a.isDirectory && !b.isDirectory) -1
+ else if (!a.isDirectory && b.isDirectory) 1
+ else a.compareTo(b)
+ }
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ newFiles
+ }
+ if (newFiles?.isEmpty() != false)
+ return@launch
+ files.clear()
+ files.addAll(newFiles)
+ filesRoot.set(directory.canonicalPath)
+ }
+ }
+
override fun onBackPressed() {
- if (isDeleting.get())
- isDeleting.set(false)
- else
- super.onBackPressed()
+ when {
+ isDeleting.get() -> {
+ isDeleting.set(false)
+ runOnUiThread {
+ binding.tunnelList.requestFocus()
+ }
+ }
+ filesRoot.get()?.isNotEmpty() == true -> {
+ files.clear()
+ filesRoot.set("")
+ runOnUiThread {
+ binding.tunnelList.requestFocus()
+ }
+ }
+ else -> super.onBackPressed()
+ }
}
private suspend fun updateStats() {
@@ -163,6 +305,11 @@ class TvMainActivity : AppCompatActivity() {
}
}
+ class KeyedFile(pathname: String) : File(pathname), Keyed<String> {
+ override val key: String
+ get() = if (isDirectory) "$name/" else name
+ }
+
companion object {
private const val TAG = "WireGuard/TvMainActivity"
}