tv: use our own file picker
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
		
							parent
							
								
									7bec539722
								
							
						
					
					
						commit
						b3c43e428f
					
				@ -5,41 +5,47 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
package com.wireguard.android.activity
 | 
					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.Bundle
 | 
				
			||||||
 | 
					import android.os.Environment
 | 
				
			||||||
 | 
					import android.os.storage.StorageManager
 | 
				
			||||||
 | 
					import android.os.storage.StorageVolume
 | 
				
			||||||
import android.util.Log
 | 
					import android.util.Log
 | 
				
			||||||
import android.view.View
 | 
					import android.view.View
 | 
				
			||||||
import android.widget.Toast
 | 
					import android.widget.Toast
 | 
				
			||||||
import androidx.activity.result.contract.ActivityResultContracts
 | 
					import androidx.activity.result.contract.ActivityResultContracts
 | 
				
			||||||
import androidx.appcompat.app.AppCompatActivity
 | 
					import androidx.appcompat.app.AppCompatActivity
 | 
				
			||||||
 | 
					import androidx.core.content.ContextCompat
 | 
				
			||||||
 | 
					import androidx.core.content.getSystemService
 | 
				
			||||||
import androidx.core.view.forEach
 | 
					import androidx.core.view.forEach
 | 
				
			||||||
import androidx.databinding.DataBindingUtil
 | 
					import androidx.databinding.DataBindingUtil
 | 
				
			||||||
import androidx.databinding.ObservableBoolean
 | 
					import androidx.databinding.ObservableBoolean
 | 
				
			||||||
 | 
					import androidx.databinding.ObservableField
 | 
				
			||||||
import androidx.lifecycle.lifecycleScope
 | 
					import androidx.lifecycle.lifecycleScope
 | 
				
			||||||
import com.wireguard.android.Application
 | 
					import com.wireguard.android.Application
 | 
				
			||||||
import com.wireguard.android.R
 | 
					import com.wireguard.android.R
 | 
				
			||||||
import com.wireguard.android.backend.GoBackend
 | 
					import com.wireguard.android.backend.GoBackend
 | 
				
			||||||
import com.wireguard.android.backend.Tunnel
 | 
					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.ObservableKeyedRecyclerViewAdapter
 | 
				
			||||||
import com.wireguard.android.databinding.TvActivityBinding
 | 
					import com.wireguard.android.databinding.TvActivityBinding
 | 
				
			||||||
 | 
					import com.wireguard.android.databinding.TvFileListItemBinding
 | 
				
			||||||
import com.wireguard.android.databinding.TvTunnelListItemBinding
 | 
					import com.wireguard.android.databinding.TvTunnelListItemBinding
 | 
				
			||||||
import com.wireguard.android.model.ObservableTunnel
 | 
					import com.wireguard.android.model.ObservableTunnel
 | 
				
			||||||
import com.wireguard.android.util.ErrorMessages
 | 
					import com.wireguard.android.util.ErrorMessages
 | 
				
			||||||
import com.wireguard.android.util.QuantityFormatter
 | 
					import com.wireguard.android.util.QuantityFormatter
 | 
				
			||||||
import com.wireguard.android.util.TunnelImporter
 | 
					import com.wireguard.android.util.TunnelImporter
 | 
				
			||||||
 | 
					import kotlinx.coroutines.Dispatchers
 | 
				
			||||||
import kotlinx.coroutines.delay
 | 
					import kotlinx.coroutines.delay
 | 
				
			||||||
import kotlinx.coroutines.launch
 | 
					import kotlinx.coroutines.launch
 | 
				
			||||||
 | 
					import kotlinx.coroutines.withContext
 | 
				
			||||||
 | 
					import java.io.File
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TvMainActivity : AppCompatActivity() {
 | 
					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 var pendingTunnel: ObservableTunnel? = null
 | 
				
			||||||
    private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
 | 
					    private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
 | 
				
			||||||
        val tunnel = pendingTunnel
 | 
					        val tunnel = pendingTunnel
 | 
				
			||||||
@ -64,6 +70,8 @@ class TvMainActivity : AppCompatActivity() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private lateinit var binding: TvActivityBinding
 | 
					    private lateinit var binding: TvActivityBinding
 | 
				
			||||||
    private val isDeleting = ObservableBoolean()
 | 
					    private val isDeleting = ObservableBoolean()
 | 
				
			||||||
 | 
					    private val files = ObservableKeyedArrayList<String, KeyedFile>()
 | 
				
			||||||
 | 
					    private val filesRoot = ObservableField("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
					    override fun onCreate(savedInstanceState: Bundle?) {
 | 
				
			||||||
        super.onCreate(savedInstanceState)
 | 
					        super.onCreate(savedInstanceState)
 | 
				
			||||||
@ -76,7 +84,9 @@ class TvMainActivity : AppCompatActivity() {
 | 
				
			|||||||
                binding.tunnelList.requestFocus()
 | 
					                binding.tunnelList.requestFocus()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        binding.isDeleting = isDeleting
 | 
					        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) {
 | 
					            override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
 | 
				
			||||||
                binding.isDeleting = isDeleting
 | 
					                binding.isDeleting = isDeleting
 | 
				
			||||||
                binding.isFocused = ObservableBoolean()
 | 
					                binding.isFocused = ObservableBoolean()
 | 
				
			||||||
@ -111,13 +121,44 @@ class TvMainActivity : AppCompatActivity() {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        binding.importButton.setOnClickListener {
 | 
					
 | 
				
			||||||
            try {
 | 
					        binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
 | 
				
			||||||
                tunnelFileImportResultLauncher.launch("*/*")
 | 
					            override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
 | 
				
			||||||
            } catch (e: ActivityNotFoundException) {
 | 
					                binding.root.setOnClickListener {
 | 
				
			||||||
                Toast.makeText(this@TvMainActivity, getString(R.string.tv_error), Toast.LENGTH_LONG).show()
 | 
					                    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 {
 | 
				
			||||||
 | 
					            if (filesRoot.get()?.isEmpty() != false) {
 | 
				
			||||||
 | 
					                navigateTo(myComputerFile)
 | 
				
			||||||
 | 
					                runOnUiThread {
 | 
				
			||||||
 | 
					                    binding.filesList.requestFocus()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                files.clear()
 | 
				
			||||||
 | 
					                filesRoot.set("")
 | 
				
			||||||
 | 
					                runOnUiThread {
 | 
				
			||||||
 | 
					                    binding.tunnelList.requestFocus()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        binding.deleteButton.setOnClickListener {
 | 
					        binding.deleteButton.setOnClickListener {
 | 
				
			||||||
            isDeleting.set(!isDeleting.get())
 | 
					            isDeleting.set(!isDeleting.get())
 | 
				
			||||||
            runOnUiThread {
 | 
					            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() {
 | 
					    override fun onBackPressed() {
 | 
				
			||||||
        if (isDeleting.get())
 | 
					        when {
 | 
				
			||||||
            isDeleting.set(false)
 | 
					            isDeleting.get() -> {
 | 
				
			||||||
        else
 | 
					                isDeleting.set(false)
 | 
				
			||||||
            super.onBackPressed()
 | 
					                runOnUiThread {
 | 
				
			||||||
 | 
					                    binding.tunnelList.requestFocus()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            filesRoot.get()?.isNotEmpty() == true -> {
 | 
				
			||||||
 | 
					                files.clear()
 | 
				
			||||||
 | 
					                filesRoot.set("")
 | 
				
			||||||
 | 
					                runOnUiThread {
 | 
				
			||||||
 | 
					                    binding.tunnelList.requestFocus()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else -> super.onBackPressed()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private suspend fun updateStats() {
 | 
					    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 {
 | 
					    companion object {
 | 
				
			||||||
        private const val TAG = "WireGuard/TvMainActivity"
 | 
					        private const val TAG = "WireGuard/TvMainActivity"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -9,16 +9,30 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <import type="com.wireguard.android.model.ObservableTunnel" />
 | 
					        <import type="com.wireguard.android.model.ObservableTunnel" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <import type="com.wireguard.android.activity.TvMainActivity.KeyedFile" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <variable
 | 
					        <variable
 | 
				
			||||||
            name="isDeleting"
 | 
					            name="isDeleting"
 | 
				
			||||||
            type="androidx.databinding.ObservableBoolean" />
 | 
					            type="androidx.databinding.ObservableBoolean" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <variable
 | 
				
			||||||
 | 
					            name="files"
 | 
				
			||||||
 | 
					            type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, KeyedFile>" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <variable
 | 
				
			||||||
 | 
					            name="filesRoot"
 | 
				
			||||||
 | 
					            type="androidx.databinding.ObservableField<String>" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <variable
 | 
					        <variable
 | 
				
			||||||
            name="tunnels"
 | 
					            name="tunnels"
 | 
				
			||||||
            type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ObservableTunnel>" />
 | 
					            type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ObservableTunnel>" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <variable
 | 
					        <variable
 | 
				
			||||||
            name="rowConfigurationHandler"
 | 
					            name="tunnelRowConfigurationHandler"
 | 
				
			||||||
 | 
					            type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <variable
 | 
				
			||||||
 | 
					            name="filesRowConfigurationHandler"
 | 
				
			||||||
            type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
 | 
					            type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
 | 
				
			||||||
    </data>
 | 
					    </data>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -54,8 +68,8 @@
 | 
				
			|||||||
            android:layout_height="0dp"
 | 
					            android:layout_height="0dp"
 | 
				
			||||||
            android:layout_marginTop="16dp"
 | 
					            android:layout_marginTop="16dp"
 | 
				
			||||||
            android:orientation="horizontal"
 | 
					            android:orientation="horizontal"
 | 
				
			||||||
            android:visibility="@{tunnels.isEmpty() ? View.GONE : View.VISIBLE}"
 | 
					            android:visibility="@{(tunnels.isEmpty || !filesRoot.isEmpty) ? View.GONE : View.VISIBLE}"
 | 
				
			||||||
            app:configurationHandler="@{rowConfigurationHandler}"
 | 
					            app:configurationHandler="@{tunnelRowConfigurationHandler}"
 | 
				
			||||||
            app:items="@{tunnels}"
 | 
					            app:items="@{tunnels}"
 | 
				
			||||||
            app:layout="@{@layout/tv_tunnel_list_item}"
 | 
					            app:layout="@{@layout/tv_tunnel_list_item}"
 | 
				
			||||||
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
 | 
					            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
 | 
				
			||||||
@ -66,13 +80,45 @@
 | 
				
			|||||||
            tools:itemCount="10"
 | 
					            tools:itemCount="10"
 | 
				
			||||||
            tools:listitem="@layout/tv_tunnel_list_item" />
 | 
					            tools:listitem="@layout/tv_tunnel_list_item" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <TextView
 | 
				
			||||||
 | 
					            android:id="@+id/files_root_label"
 | 
				
			||||||
 | 
					            style="@style/TextAppearance.MaterialComponents.Headline5"
 | 
				
			||||||
 | 
					            android:layout_width="wrap_content"
 | 
				
			||||||
 | 
					            android:layout_height="wrap_content"
 | 
				
			||||||
 | 
					            android:layout_gravity="center_horizontal"
 | 
				
			||||||
 | 
					            android:layout_marginStart="8dp"
 | 
				
			||||||
 | 
					            android:text="@{filesRoot}"
 | 
				
			||||||
 | 
					            android:visibility="@{filesRoot.isEmpty ? View.GONE : View.VISIBLE}"
 | 
				
			||||||
 | 
					            app:layout_constraintStart_toStartOf="parent"
 | 
				
			||||||
 | 
					            app:layout_constraintTop_toBottomOf="@id/banner_logo"
 | 
				
			||||||
 | 
					            tools:visibility="gone" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <androidx.recyclerview.widget.RecyclerView
 | 
				
			||||||
 | 
					            android:id="@+id/files_list"
 | 
				
			||||||
 | 
					            android:layout_width="match_parent"
 | 
				
			||||||
 | 
					            android:layout_height="0dp"
 | 
				
			||||||
 | 
					            android:layout_marginTop="16dp"
 | 
				
			||||||
 | 
					            android:orientation="horizontal"
 | 
				
			||||||
 | 
					            android:visibility="@{filesRoot.isEmpty ? View.GONE : View.VISIBLE}"
 | 
				
			||||||
 | 
					            app:configurationHandler="@{filesRowConfigurationHandler}"
 | 
				
			||||||
 | 
					            app:items="@{files}"
 | 
				
			||||||
 | 
					            app:layout="@{@layout/tv_file_list_item}"
 | 
				
			||||||
 | 
					            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
 | 
				
			||||||
 | 
					            app:layout_constraintBottom_toTopOf="@id/import_button"
 | 
				
			||||||
 | 
					            app:layout_constraintStart_toStartOf="parent"
 | 
				
			||||||
 | 
					            app:layout_constraintTop_toBottomOf="@id/files_root_label"
 | 
				
			||||||
 | 
					            app:spanCount="5"
 | 
				
			||||||
 | 
					            tools:itemCount="10"
 | 
				
			||||||
 | 
					            tools:listitem="@layout/tv_file_list_item"
 | 
				
			||||||
 | 
					            tools:visibility="gone" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <TextView
 | 
					        <TextView
 | 
				
			||||||
            style="@style/TextAppearance.MaterialComponents.Headline4"
 | 
					            style="@style/TextAppearance.MaterialComponents.Headline4"
 | 
				
			||||||
            android:layout_width="wrap_content"
 | 
					            android:layout_width="wrap_content"
 | 
				
			||||||
            android:layout_height="wrap_content"
 | 
					            android:layout_height="wrap_content"
 | 
				
			||||||
            android:layout_gravity="center_horizontal"
 | 
					            android:layout_gravity="center_horizontal"
 | 
				
			||||||
            android:text="@string/tv_add_tunnel_get_started"
 | 
					            android:text="@string/tv_add_tunnel_get_started"
 | 
				
			||||||
            android:visibility="@{tunnels.isEmpty() ? View.VISIBLE : View.GONE}"
 | 
					            android:visibility="@{(filesRoot.isEmpty && tunnels.isEmpty) ? View.VISIBLE : View.GONE}"
 | 
				
			||||||
            app:layout_constraintBottom_toTopOf="@id/delete_button"
 | 
					            app:layout_constraintBottom_toTopOf="@id/delete_button"
 | 
				
			||||||
            app:layout_constraintEnd_toEndOf="parent"
 | 
					            app:layout_constraintEnd_toEndOf="parent"
 | 
				
			||||||
            app:layout_constraintStart_toStartOf="parent"
 | 
					            app:layout_constraintStart_toStartOf="parent"
 | 
				
			||||||
@ -87,7 +133,7 @@
 | 
				
			|||||||
            android:layout_margin="16dp"
 | 
					            android:layout_margin="16dp"
 | 
				
			||||||
            android:minWidth="0dp"
 | 
					            android:minWidth="0dp"
 | 
				
			||||||
            android:visibility="@{isDeleting ? View.GONE : View.VISIBLE}"
 | 
					            android:visibility="@{isDeleting ? View.GONE : View.VISIBLE}"
 | 
				
			||||||
            app:icon="@drawable/ic_action_add_white"
 | 
					            app:icon="@{filesRoot.isEmpty ? @drawable/ic_action_add_white : @drawable/ic_arrow_back}"
 | 
				
			||||||
            app:iconPadding="0dp"
 | 
					            app:iconPadding="0dp"
 | 
				
			||||||
            app:iconTint="?attr/colorOnPrimary"
 | 
					            app:iconTint="?attr/colorOnPrimary"
 | 
				
			||||||
            app:layout_constraintBottom_toBottomOf="parent"
 | 
					            app:layout_constraintBottom_toBottomOf="parent"
 | 
				
			||||||
@ -100,7 +146,7 @@
 | 
				
			|||||||
            android:layout_height="wrap_content"
 | 
					            android:layout_height="wrap_content"
 | 
				
			||||||
            android:layout_margin="16dp"
 | 
					            android:layout_margin="16dp"
 | 
				
			||||||
            android:minWidth="0dp"
 | 
					            android:minWidth="0dp"
 | 
				
			||||||
            android:visibility="@{tunnels.isEmpty && !isDeleting ? View.GONE : View.VISIBLE}"
 | 
					            android:visibility="@{((tunnels.isEmpty && !isDeleting) || !filesRoot.isEmpty) ? View.GONE : View.VISIBLE}"
 | 
				
			||||||
            app:icon="@{isDeleting ? @drawable/ic_arrow_back : @drawable/ic_action_delete}"
 | 
					            app:icon="@{isDeleting ? @drawable/ic_arrow_back : @drawable/ic_action_delete}"
 | 
				
			||||||
            app:iconPadding="0dp"
 | 
					            app:iconPadding="0dp"
 | 
				
			||||||
            app:iconTint="?attr/colorOnPrimary"
 | 
					            app:iconTint="?attr/colorOnPrimary"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										46
									
								
								ui/src/main/res/layout/tv_file_list_item.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								ui/src/main/res/layout/tv_file_list_item.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<layout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    xmlns:app="http://schemas.android.com/apk/res-auto">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <data>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <import type="com.wireguard.android.activity.TvMainActivity.KeyedFile" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <variable
 | 
				
			||||||
 | 
					            name="key"
 | 
				
			||||||
 | 
					            type="String" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <variable
 | 
				
			||||||
 | 
					            name="item"
 | 
				
			||||||
 | 
					            type="KeyedFile" />
 | 
				
			||||||
 | 
					    </data>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <com.google.android.material.card.MaterialCardView
 | 
				
			||||||
 | 
					        android:layout_width="320dp"
 | 
				
			||||||
 | 
					        android:layout_height="50dp"
 | 
				
			||||||
 | 
					        android:layout_margin="8dp"
 | 
				
			||||||
 | 
					        android:layout_marginTop="4dp"
 | 
				
			||||||
 | 
					        android:layout_marginBottom="0dp"
 | 
				
			||||||
 | 
					        android:backgroundTint="@color/tv_card_background"
 | 
				
			||||||
 | 
					        android:checkable="true"
 | 
				
			||||||
 | 
					        android:focusable="true"
 | 
				
			||||||
 | 
					        app:contentPadding="8dp">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <androidx.constraintlayout.widget.ConstraintLayout
 | 
				
			||||||
 | 
					            android:layout_width="match_parent"
 | 
				
			||||||
 | 
					            android:layout_height="match_parent">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <com.google.android.material.textview.MaterialTextView
 | 
				
			||||||
 | 
					                style="@style/TextAppearance.MaterialComponents.Headline5"
 | 
				
			||||||
 | 
					                android:layout_width="wrap_content"
 | 
				
			||||||
 | 
					                android:layout_height="wrap_content"
 | 
				
			||||||
 | 
					                android:text="@{key}"
 | 
				
			||||||
 | 
					                android:textColor="?attr/colorOnPrimary"
 | 
				
			||||||
 | 
					                app:layout_constraintStart_toStartOf="parent"
 | 
				
			||||||
 | 
					                app:layout_constraintTop_toTopOf="parent" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </androidx.constraintlayout.widget.ConstraintLayout>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </com.google.android.material.card.MaterialCardView>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</layout>
 | 
				
			||||||
@ -102,7 +102,7 @@
 | 
				
			|||||||
    <string name="dark_theme_title">Use dark theme</string>
 | 
					    <string name="dark_theme_title">Use dark theme</string>
 | 
				
			||||||
    <string name="delete">Delete</string>
 | 
					    <string name="delete">Delete</string>
 | 
				
			||||||
    <string name="tv_delete">Select tunnel to delete</string>
 | 
					    <string name="tv_delete">Select tunnel to delete</string>
 | 
				
			||||||
    <string name="tv_error">Your TV does not have a file picker</string>
 | 
					    <string name="tv_select_a_storage_drive">Select a storage drive</string>
 | 
				
			||||||
    <string name="tv_add_tunnel_get_started">Add a tunnel to get started</string>
 | 
					    <string name="tv_add_tunnel_get_started">Add a tunnel to get started</string>
 | 
				
			||||||
    <string name="disable_config_export_title">Disable config exporting</string>
 | 
					    <string name="disable_config_export_title">Disable config exporting</string>
 | 
				
			||||||
    <string name="disable_config_export_description">Disabling config exporting makes private keys less accessible</string>
 | 
					    <string name="disable_config_export_description">Disabling config exporting makes private keys less accessible</string>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user