tv: handle going up directories better
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
e729c5dc51
commit
7a8f708157
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user