tv: handle going up directories better

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2020-09-24 12:43:04 +02:00
parent e729c5dc51
commit 7a8f708157
2 changed files with 41 additions and 25 deletions

View File

@ -133,10 +133,10 @@ class TvMainActivity : AppCompatActivity() {
binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> { binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) { override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
binding.root.setOnClickListener { binding.root.setOnClickListener {
if (item.isDirectory) if (item.file.isDirectory)
navigateTo(item) navigateTo(item.file)
else { else {
val uri = Uri.fromFile(item.canonicalFile) val uri = Uri.fromFile(item.file)
files.clear() files.clear()
filesRoot.set("") filesRoot.set("")
lifecycleScope.launch { lifecycleScope.launch {
@ -153,13 +153,9 @@ class TvMainActivity : AppCompatActivity() {
} }
binding.importButton.setOnClickListener { binding.importButton.setOnClickListener {
try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
throw Exception()
tunnelFileImportResultLauncher.launch("*/*")
} catch (_: Throwable) {
if (filesRoot.get()?.isEmpty() != false) { if (filesRoot.get()?.isEmpty() != false) {
navigateTo(myComputerFile) navigateTo(File("/"))
runOnUiThread { runOnUiThread {
binding.filesList.requestFocus() binding.filesList.requestFocus()
} }
@ -170,6 +166,12 @@ class TvMainActivity : AppCompatActivity() {
binding.tunnelList.requestFocus() binding.tunnelList.requestFocus()
} }
} }
} else {
try {
tunnelFileImportResultLauncher.launch("*/*")
} catch (_: Throwable) {
Toast.makeText(this@TvMainActivity, getString(R.string.tv_no_file_picker), Toast.LENGTH_LONG).show()
}
} }
} }
@ -198,26 +200,29 @@ class TvMainActivity : AppCompatActivity() {
pendingNavigation = null pendingNavigation = null
} }
private var cachedRoots: Collection<KeyedFile>? = null
private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) { private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) {
cachedRoots?.let { return@withContext it }
val list = HashSet<KeyedFile>() val list = HashSet<KeyedFile>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val storageManager: StorageManager = getSystemService() ?: return@withContext list val storageManager: StorageManager = getSystemService() ?: return@withContext list
list.addAll(storageManager.storageVolumes.mapNotNull { volume -> list.addAll(storageManager.storageVolumes.mapNotNull { volume ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
volume.directory?.let { KeyedFile(it.canonicalPath, volume.getDescription(this@TvMainActivity)) } volume.directory?.let { KeyedFile(it, volume.getDescription(this@TvMainActivity)) }
} else { } else {
KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File).canonicalPath, volume.getDescription(this@TvMainActivity)) KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File), volume.getDescription(this@TvMainActivity))
} }
}) })
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
list.add(KeyedFile(Environment.getExternalStorageDirectory().canonicalPath)) list.add(KeyedFile(Environment.getExternalStorageDirectory()))
try { try {
File("/storage").listFiles()?.forEach { File("/storage").listFiles()?.forEach {
if (!it.isDirectory) return@forEach if (!it.isDirectory) return@forEach
try { try {
if (Environment.isExternalStorageRemovable(it)) { if (Environment.isExternalStorageRemovable(it)) {
list.add(KeyedFile(it.canonicalPath)) list.add(KeyedFile(it))
} }
} catch (_: Throwable) { } catch (_: Throwable) {
} }
@ -225,12 +230,22 @@ class TvMainActivity : AppCompatActivity() {
} catch (_: Throwable) { } catch (_: Throwable) {
} }
} }
cachedRoots = list
list list
} }
private val myComputerFile = File("") private fun isBelowCachedRoots(maybeChild: File): Boolean {
val cachedRoots = cachedRoots ?: return true
for (root in cachedRoots) {
if (maybeChild.canonicalPath.startsWith(root.file.canonicalPath))
return false
}
return true
}
private fun navigateTo(directory: File) { private fun navigateTo(directory: File) {
require(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
pendingNavigation = directory pendingNavigation = directory
permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
@ -238,10 +253,10 @@ class TvMainActivity : AppCompatActivity() {
} }
lifecycleScope.launch { lifecycleScope.launch {
if (directory == myComputerFile) { if (isBelowCachedRoots(directory)) {
val roots = makeStorageRoots() val roots = makeStorageRoots()
if (roots.count() == 1) { if (roots.count() == 1) {
navigateTo(roots.first()) navigateTo(roots.first().file)
return@launch return@launch
} }
files.clear() files.clear()
@ -253,18 +268,18 @@ class TvMainActivity : AppCompatActivity() {
val newFiles = withContext(Dispatchers.IO) { val newFiles = withContext(Dispatchers.IO) {
val newFiles = ArrayList<KeyedFile>() val newFiles = ArrayList<KeyedFile>()
try { try {
val parent = KeyedFile(directory.canonicalPath + "/..") directory.parentFile?.let {
if (directory.canonicalPath != "/" && parent.list() != null) newFiles.add(KeyedFile(it, "../"))
newFiles.add(parent) }
val listing = directory.listFiles() ?: return@withContext null val listing = directory.listFiles() ?: return@withContext null
listing.forEach { listing.forEach {
if (it.extension == "conf" || it.extension == "zip" || it.isDirectory) if (it.extension == "conf" || it.extension == "zip" || it.isDirectory)
newFiles.add(KeyedFile(it.canonicalPath)) newFiles.add(KeyedFile(it))
} }
newFiles.sortWith { a, b -> newFiles.sortWith { a, b ->
if (a.isDirectory && !b.isDirectory) -1 if (a.file.isDirectory && !b.file.isDirectory) -1
else if (!a.isDirectory && b.isDirectory) 1 else if (!a.file.isDirectory && b.file.isDirectory) 1
else a.compareTo(b) else a.file.compareTo(b.file)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e)) Log.e(TAG, Log.getStackTraceString(e))
@ -319,9 +334,9 @@ class TvMainActivity : AppCompatActivity() {
} }
} }
class KeyedFile(pathname: String, private val forcedKey: String? = null) : File(pathname), Keyed<String> { class KeyedFile(val file: File, private val forcedKey: String? = null) : Keyed<String> {
override val key: String override val key: String
get() = forcedKey ?: if (isDirectory) "$name/" else name get() = forcedKey ?: if (file.isDirectory) "${file.name}/" else file.name
} }
companion object { companion object {

View File

@ -103,6 +103,7 @@
<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_select_a_storage_drive">Select a storage drive</string> <string name="tv_select_a_storage_drive">Select a storage drive</string>
<string name="tv_no_file_picker">Please install a file management utility to browse files</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>