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.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 {
|
binding.importButton.setOnClickListener {
|
||||||
try {
|
if (filesRoot.get()?.isEmpty() != false) {
|
||||||
tunnelFileImportResultLauncher.launch("*/*")
|
navigateTo(myComputerFile)
|
||||||
} catch (e: ActivityNotFoundException) {
|
runOnUiThread {
|
||||||
Toast.makeText(this@TvMainActivity, getString(R.string.tv_error), Toast.LENGTH_LONG).show()
|
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.get() -> {
|
||||||
isDeleting.set(false)
|
isDeleting.set(false)
|
||||||
else
|
runOnUiThread {
|
||||||
super.onBackPressed()
|
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