2020-09-22 13:11:08 +02:00
|
|
|
/*
|
2023-03-21 15:41:11 +01:00
|
|
|
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
|
2020-09-22 13:11:08 +02:00
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
*/
|
|
|
|
|
|
|
|
package com.wireguard.android.activity
|
|
|
|
|
2020-09-23 12:04:36 +02:00
|
|
|
import android.Manifest
|
|
|
|
import android.content.pm.PackageManager
|
|
|
|
import android.net.Uri
|
|
|
|
import android.os.Build
|
2020-09-22 13:11:08 +02:00
|
|
|
import android.os.Bundle
|
2020-09-23 12:04:36 +02:00
|
|
|
import android.os.Environment
|
|
|
|
import android.os.storage.StorageManager
|
|
|
|
import android.os.storage.StorageVolume
|
2020-09-22 15:31:02 +02:00
|
|
|
import android.util.Log
|
2020-09-22 15:52:58 +02:00
|
|
|
import android.view.View
|
2020-09-22 13:51:57 +02:00
|
|
|
import android.widget.Toast
|
2020-09-22 13:11:08 +02:00
|
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
2020-09-22 14:33:04 +02:00
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
2020-09-23 12:04:36 +02:00
|
|
|
import androidx.core.content.ContextCompat
|
|
|
|
import androidx.core.content.getSystemService
|
2020-09-22 15:52:58 +02:00
|
|
|
import androidx.core.view.forEach
|
|
|
|
import androidx.databinding.DataBindingUtil
|
2020-09-22 16:28:13 +02:00
|
|
|
import androidx.databinding.ObservableBoolean
|
2020-09-23 12:04:36 +02:00
|
|
|
import androidx.databinding.ObservableField
|
2020-09-22 13:11:08 +02:00
|
|
|
import androidx.lifecycle.lifecycleScope
|
tv: hack gridlayoutmanager to fill columns before row if we're not scrolling
If we're horizontally scrolling, it makes sense to fill rows before
columns. But if it all fits in one page and we don't need to scroll
horizontally, it looks ridiculous. So, in this case, rearrange the tiles
so that it appears to fill columns before rows. But we don't want things
suddenly jumping around, so actually, keep the same ordering as
rows-before-columns, but add invisible spaces after certain items, so
that the fill area makes it look as though it's columns-before-rows.
This winds up being much more visually pleasing.
We do this by figuring out this kind of transformation:
If we convert this matrix:
0 3 6
1 4 _
2 5 _
To this one:
0 2 4 6
1 3 5 _
_ _ _ _
For a given index, how many spaces are under it? This changes depending
on how many total are in a grid. Going from 3x3 to 4x3, for example, we
have:
count == 12, index =
count == 11, index = 10
count == 10, index = 7,9
count == 9, index = 4,6,8
count == 8, index = 1,3,5,7
count == 7, index = 1,3,5,6!
count == 6, index = 1,3,4!,5!
count == 5, index = 1,2!,3!,4!
count == 4, index = 0!,1!,2!,3!
count == 3, index = 0!,1!,2!
count == 2, index = 0!,1!
count == 1, index = 0!
count == 0, index =
The '!' means two blanks below, no '!' means one blank below, and no
mention means no blanks below.
This commit adds code to compute such a table on the fly.
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-27 01:56:22 +02:00
|
|
|
import androidx.recyclerview.widget.GridLayoutManager
|
|
|
|
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
2020-09-22 14:33:04 +02:00
|
|
|
import com.wireguard.android.Application
|
2020-09-22 15:31:02 +02:00
|
|
|
import com.wireguard.android.R
|
|
|
|
import com.wireguard.android.backend.GoBackend
|
|
|
|
import com.wireguard.android.backend.Tunnel
|
2020-09-23 12:04:36 +02:00
|
|
|
import com.wireguard.android.databinding.Keyed
|
|
|
|
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
2020-09-22 15:31:02 +02:00
|
|
|
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter
|
2020-09-22 14:33:04 +02:00
|
|
|
import com.wireguard.android.databinding.TvActivityBinding
|
2020-09-23 12:04:36 +02:00
|
|
|
import com.wireguard.android.databinding.TvFileListItemBinding
|
2020-09-22 15:31:02 +02:00
|
|
|
import com.wireguard.android.databinding.TvTunnelListItemBinding
|
|
|
|
import com.wireguard.android.model.ObservableTunnel
|
|
|
|
import com.wireguard.android.util.ErrorMessages
|
2020-09-22 15:52:58 +02:00
|
|
|
import com.wireguard.android.util.QuantityFormatter
|
2020-09-22 13:51:57 +02:00
|
|
|
import com.wireguard.android.util.TunnelImporter
|
2020-09-23 12:04:36 +02:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
2020-09-22 15:52:58 +02:00
|
|
|
import kotlinx.coroutines.delay
|
2020-09-22 13:11:08 +02:00
|
|
|
import kotlinx.coroutines.launch
|
2020-09-23 12:04:36 +02:00
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
import java.io.File
|
2020-09-22 13:11:08 +02:00
|
|
|
|
2020-09-22 14:33:04 +02:00
|
|
|
class TvMainActivity : AppCompatActivity() {
|
2020-09-23 14:37:15 +02:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-22 15:31:02 +02:00
|
|
|
private var pendingTunnel: ObservableTunnel? = null
|
|
|
|
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
|
|
val tunnel = pendingTunnel
|
|
|
|
if (tunnel != null)
|
|
|
|
setTunnelStateWithPermissionsResult(tunnel)
|
|
|
|
pendingTunnel = null
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel) {
|
|
|
|
lifecycleScope.launch {
|
|
|
|
try {
|
|
|
|
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
val error = ErrorMessages[e]
|
|
|
|
val message = getString(R.string.error_up, error)
|
|
|
|
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
|
|
|
|
Log.e(TAG, message, e)
|
|
|
|
}
|
2020-09-22 15:52:58 +02:00
|
|
|
updateStats()
|
2020-09-22 15:31:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-22 17:55:33 +02:00
|
|
|
private lateinit var binding: TvActivityBinding
|
|
|
|
private val isDeleting = ObservableBoolean()
|
2020-09-23 12:04:36 +02:00
|
|
|
private val files = ObservableKeyedArrayList<String, KeyedFile>()
|
|
|
|
private val filesRoot = ObservableField("")
|
2020-09-22 15:52:58 +02:00
|
|
|
|
2020-09-22 13:11:08 +02:00
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
2020-09-22 15:52:58 +02:00
|
|
|
binding = TvActivityBinding.inflate(layoutInflater)
|
2020-09-22 19:35:04 +02:00
|
|
|
lifecycleScope.launch {
|
|
|
|
binding.tunnels = Application.getTunnelManager().getTunnels()
|
|
|
|
if (binding.tunnels?.isEmpty() == true)
|
|
|
|
binding.importButton.requestFocus()
|
|
|
|
else
|
|
|
|
binding.tunnelList.requestFocus()
|
|
|
|
}
|
2020-09-22 16:28:13 +02:00
|
|
|
binding.isDeleting = isDeleting
|
2020-09-23 12:04:36 +02:00
|
|
|
binding.files = files
|
|
|
|
binding.filesRoot = filesRoot
|
tv: hack gridlayoutmanager to fill columns before row if we're not scrolling
If we're horizontally scrolling, it makes sense to fill rows before
columns. But if it all fits in one page and we don't need to scroll
horizontally, it looks ridiculous. So, in this case, rearrange the tiles
so that it appears to fill columns before rows. But we don't want things
suddenly jumping around, so actually, keep the same ordering as
rows-before-columns, but add invisible spaces after certain items, so
that the fill area makes it look as though it's columns-before-rows.
This winds up being much more visually pleasing.
We do this by figuring out this kind of transformation:
If we convert this matrix:
0 3 6
1 4 _
2 5 _
To this one:
0 2 4 6
1 3 5 _
_ _ _ _
For a given index, how many spaces are under it? This changes depending
on how many total are in a grid. Going from 3x3 to 4x3, for example, we
have:
count == 12, index =
count == 11, index = 10
count == 10, index = 7,9
count == 9, index = 4,6,8
count == 8, index = 1,3,5,7
count == 7, index = 1,3,5,6!
count == 6, index = 1,3,4!,5!
count == 5, index = 1,2!,3!,4!
count == 4, index = 0!,1!,2!,3!
count == 3, index = 0!,1!,2!
count == 2, index = 0!,1!
count == 1, index = 0!
count == 0, index =
The '!' means two blanks below, no '!' means one blank below, and no
mention means no blanks below.
This commit adds code to compute such a table on the fly.
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-27 01:56:22 +02:00
|
|
|
val gridManager = binding.tunnelList.layoutManager as GridLayoutManager
|
|
|
|
gridManager.spanSizeLookup = SlatedSpanSizeLookup(gridManager)
|
2020-09-23 12:04:36 +02:00
|
|
|
binding.tunnelRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
|
2020-09-22 15:31:02 +02:00
|
|
|
override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
2020-09-22 16:28:13 +02:00
|
|
|
binding.isDeleting = isDeleting
|
2020-09-22 23:50:26 +02:00
|
|
|
binding.isFocused = ObservableBoolean()
|
|
|
|
binding.root.setOnFocusChangeListener { _, focused ->
|
|
|
|
binding.isFocused?.set(focused)
|
|
|
|
}
|
2020-09-22 16:28:13 +02:00
|
|
|
binding.root.setOnClickListener {
|
2020-09-22 15:31:02 +02:00
|
|
|
lifecycleScope.launch {
|
2020-09-22 16:28:13 +02:00
|
|
|
if (isDeleting.get()) {
|
|
|
|
try {
|
|
|
|
item.deleteAsync()
|
2020-09-22 20:37:41 +02:00
|
|
|
if (this@TvMainActivity.binding.tunnels?.isEmpty() != false)
|
|
|
|
isDeleting.set(false)
|
2020-09-22 16:28:13 +02:00
|
|
|
} catch (e: Throwable) {
|
|
|
|
val error = ErrorMessages[e]
|
|
|
|
val message = getString(R.string.config_delete_error, error)
|
|
|
|
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
|
|
|
|
Log.e(TAG, message, e)
|
2020-09-22 15:31:02 +02:00
|
|
|
}
|
2020-09-22 16:28:13 +02:00
|
|
|
} else {
|
|
|
|
if (Application.getBackend() is GoBackend) {
|
|
|
|
val intent = GoBackend.VpnService.prepare(binding.root.context)
|
|
|
|
if (intent != null) {
|
|
|
|
pendingTunnel = item
|
|
|
|
permissionActivityResultLauncher.launch(intent)
|
|
|
|
return@launch
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTunnelStateWithPermissionsResult(item)
|
2020-09-22 15:31:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-23 12:04:36 +02:00
|
|
|
|
|
|
|
binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
|
|
|
|
override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
|
|
|
|
binding.root.setOnClickListener {
|
2020-09-24 12:43:04 +02:00
|
|
|
if (item.file.isDirectory)
|
|
|
|
navigateTo(item.file)
|
2020-09-23 12:04:36 +02:00
|
|
|
else {
|
2020-09-24 12:43:04 +02:00
|
|
|
val uri = Uri.fromFile(item.file)
|
2020-09-23 12:04:36 +02:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-22 14:33:04 +02:00
|
|
|
binding.importButton.setOnClickListener {
|
2020-09-24 12:43:04 +02:00
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
2020-09-23 14:37:15 +02:00
|
|
|
if (filesRoot.get()?.isEmpty() != false) {
|
2020-09-24 12:43:04 +02:00
|
|
|
navigateTo(File("/"))
|
2020-09-23 14:37:15 +02:00
|
|
|
runOnUiThread {
|
|
|
|
binding.filesList.requestFocus()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
files.clear()
|
|
|
|
filesRoot.set("")
|
|
|
|
runOnUiThread {
|
|
|
|
binding.tunnelList.requestFocus()
|
|
|
|
}
|
2020-09-23 12:04:36 +02:00
|
|
|
}
|
2020-09-24 12:43:04 +02:00
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
tunnelFileImportResultLauncher.launch("*/*")
|
|
|
|
} catch (_: Throwable) {
|
|
|
|
Toast.makeText(this@TvMainActivity, getString(R.string.tv_no_file_picker), Toast.LENGTH_LONG).show()
|
|
|
|
}
|
2020-09-22 18:17:11 +02:00
|
|
|
}
|
2020-09-22 13:11:08 +02:00
|
|
|
}
|
2020-09-23 12:04:36 +02:00
|
|
|
|
2020-09-22 16:28:13 +02:00
|
|
|
binding.deleteButton.setOnClickListener {
|
|
|
|
isDeleting.set(!isDeleting.get())
|
2020-09-22 23:59:40 +02:00
|
|
|
runOnUiThread {
|
|
|
|
binding.tunnelList.requestFocus()
|
|
|
|
}
|
2020-09-22 16:28:13 +02:00
|
|
|
}
|
2020-09-22 14:33:04 +02:00
|
|
|
binding.executePendingBindings()
|
|
|
|
setContentView(binding.root)
|
2020-09-22 15:52:58 +02:00
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
while (true) {
|
|
|
|
updateStats()
|
|
|
|
delay(1000)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-23 12:04:36 +02:00
|
|
|
private var pendingNavigation: File? = null
|
|
|
|
private val permissionRequestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
|
|
|
val to = pendingNavigation
|
|
|
|
if (it && to != null)
|
|
|
|
navigateTo(to)
|
|
|
|
pendingNavigation = null
|
|
|
|
}
|
|
|
|
|
2020-09-24 12:43:04 +02:00
|
|
|
private var cachedRoots: Collection<KeyedFile>? = null
|
|
|
|
|
2020-09-23 12:04:36 +02:00
|
|
|
private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) {
|
2020-09-24 12:43:04 +02:00
|
|
|
cachedRoots?.let { return@withContext it }
|
2020-09-23 12:04:36 +02:00
|
|
|
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) {
|
2020-09-24 12:43:04 +02:00
|
|
|
volume.directory?.let { KeyedFile(it, volume.getDescription(this@TvMainActivity)) }
|
2020-09-23 12:04:36 +02:00
|
|
|
} else {
|
2020-09-24 12:43:04 +02:00
|
|
|
KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File), volume.getDescription(this@TvMainActivity))
|
2020-09-23 12:04:36 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
@Suppress("DEPRECATION")
|
2020-09-24 12:43:04 +02:00
|
|
|
list.add(KeyedFile(Environment.getExternalStorageDirectory()))
|
2020-09-23 12:04:36 +02:00
|
|
|
try {
|
|
|
|
File("/storage").listFiles()?.forEach {
|
|
|
|
if (!it.isDirectory) return@forEach
|
|
|
|
try {
|
|
|
|
if (Environment.isExternalStorageRemovable(it)) {
|
2020-09-24 12:43:04 +02:00
|
|
|
list.add(KeyedFile(it))
|
2020-09-23 12:04:36 +02:00
|
|
|
}
|
|
|
|
} catch (_: Throwable) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (_: Throwable) {
|
|
|
|
}
|
|
|
|
}
|
2020-09-24 12:43:04 +02:00
|
|
|
cachedRoots = list
|
2020-09-23 12:04:36 +02:00
|
|
|
list
|
|
|
|
}
|
|
|
|
|
2020-09-24 12:43:04 +02:00
|
|
|
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
|
|
|
|
}
|
2020-09-23 12:04:36 +02:00
|
|
|
|
|
|
|
private fun navigateTo(directory: File) {
|
2020-09-24 12:43:04 +02:00
|
|
|
require(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
|
|
|
|
|
2020-09-23 12:04:36 +02:00
|
|
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
|
|
pendingNavigation = directory
|
|
|
|
permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
2020-09-24 12:43:04 +02:00
|
|
|
if (isBelowCachedRoots(directory)) {
|
2020-09-23 12:04:36 +02:00
|
|
|
val roots = makeStorageRoots()
|
|
|
|
if (roots.count() == 1) {
|
2020-09-24 12:43:04 +02:00
|
|
|
navigateTo(roots.first().file)
|
2020-09-23 12:04:36 +02:00
|
|
|
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 {
|
2020-09-24 12:43:04 +02:00
|
|
|
directory.parentFile?.let {
|
|
|
|
newFiles.add(KeyedFile(it, "../"))
|
|
|
|
}
|
2020-09-23 12:04:36 +02:00
|
|
|
val listing = directory.listFiles() ?: return@withContext null
|
|
|
|
listing.forEach {
|
|
|
|
if (it.extension == "conf" || it.extension == "zip" || it.isDirectory)
|
2020-09-24 12:43:04 +02:00
|
|
|
newFiles.add(KeyedFile(it))
|
2020-09-23 12:04:36 +02:00
|
|
|
}
|
|
|
|
newFiles.sortWith { a, b ->
|
2020-09-24 12:43:04 +02:00
|
|
|
if (a.file.isDirectory && !b.file.isDirectory) -1
|
|
|
|
else if (!a.file.isDirectory && b.file.isDirectory) 1
|
|
|
|
else a.file.compareTo(b.file)
|
2020-09-23 12:04:36 +02:00
|
|
|
}
|
|
|
|
} 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-23 10:08:28 +02:00
|
|
|
override fun onBackPressed() {
|
2020-09-23 12:04:36 +02:00
|
|
|
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()
|
|
|
|
}
|
2020-09-23 10:08:28 +02:00
|
|
|
}
|
|
|
|
|
2020-09-22 15:52:58 +02:00
|
|
|
private suspend fun updateStats() {
|
|
|
|
binding.tunnelList.forEach { viewItem ->
|
2020-09-22 19:35:04 +02:00
|
|
|
val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
|
|
|
|
?: return@forEach
|
2020-09-22 15:52:58 +02:00
|
|
|
try {
|
|
|
|
val tunnel = listItem.item!!
|
2020-09-22 17:55:33 +02:00
|
|
|
if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
|
2020-09-22 15:52:58 +02:00
|
|
|
throw Exception()
|
|
|
|
}
|
|
|
|
val statistics = tunnel.getStatisticsAsync()
|
|
|
|
val rx = statistics.totalRx()
|
|
|
|
val tx = statistics.totalTx()
|
|
|
|
listItem.tunnelTransfer.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
|
|
|
|
listItem.tunnelTransfer.visibility = View.VISIBLE
|
|
|
|
} catch (_: Throwable) {
|
|
|
|
listItem.tunnelTransfer.visibility = View.GONE
|
|
|
|
listItem.tunnelTransfer.text = ""
|
|
|
|
}
|
|
|
|
}
|
2020-09-22 13:11:08 +02:00
|
|
|
}
|
2020-09-22 15:31:02 +02:00
|
|
|
|
2020-09-24 12:43:04 +02:00
|
|
|
class KeyedFile(val file: File, private val forcedKey: String? = null) : Keyed<String> {
|
2020-09-23 12:04:36 +02:00
|
|
|
override val key: String
|
2020-09-24 12:43:04 +02:00
|
|
|
get() = forcedKey ?: if (file.isDirectory) "${file.name}/" else file.name
|
2020-09-23 12:04:36 +02:00
|
|
|
}
|
|
|
|
|
tv: hack gridlayoutmanager to fill columns before row if we're not scrolling
If we're horizontally scrolling, it makes sense to fill rows before
columns. But if it all fits in one page and we don't need to scroll
horizontally, it looks ridiculous. So, in this case, rearrange the tiles
so that it appears to fill columns before rows. But we don't want things
suddenly jumping around, so actually, keep the same ordering as
rows-before-columns, but add invisible spaces after certain items, so
that the fill area makes it look as though it's columns-before-rows.
This winds up being much more visually pleasing.
We do this by figuring out this kind of transformation:
If we convert this matrix:
0 3 6
1 4 _
2 5 _
To this one:
0 2 4 6
1 3 5 _
_ _ _ _
For a given index, how many spaces are under it? This changes depending
on how many total are in a grid. Going from 3x3 to 4x3, for example, we
have:
count == 12, index =
count == 11, index = 10
count == 10, index = 7,9
count == 9, index = 4,6,8
count == 8, index = 1,3,5,7
count == 7, index = 1,3,5,6!
count == 6, index = 1,3,4!,5!
count == 5, index = 1,2!,3!,4!
count == 4, index = 0!,1!,2!,3!
count == 3, index = 0!,1!,2!
count == 2, index = 0!,1!
count == 1, index = 0!
count == 0, index =
The '!' means two blanks below, no '!' means one blank below, and no
mention means no blanks below.
This commit adds code to compute such a table on the fly.
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-27 01:56:22 +02:00
|
|
|
private class SlatedSpanSizeLookup(private val gridManager: GridLayoutManager) : SpanSizeLookup() {
|
|
|
|
private val originalHeight = gridManager.spanCount
|
|
|
|
private var newWidth = 0
|
|
|
|
private lateinit var sizeMap: Array<IntArray?>
|
|
|
|
|
|
|
|
private fun emptyUnderIndex(index: Int, size: Int): Int {
|
|
|
|
sizeMap[size - 1]?.let { return it[index] }
|
|
|
|
val sizes = IntArray(size)
|
|
|
|
val oh = originalHeight
|
|
|
|
val nw = newWidth
|
|
|
|
var empties = 0
|
|
|
|
for (i in 0 until size) {
|
|
|
|
val ox = (i + empties) / oh
|
|
|
|
val oy = (i + empties) % oh
|
|
|
|
var empty = 0
|
|
|
|
for (j in oy + 1 until oh) {
|
|
|
|
val ni = nw * j + ox
|
|
|
|
if (ni < size)
|
|
|
|
break
|
|
|
|
empty++
|
|
|
|
}
|
|
|
|
empties += empty
|
|
|
|
sizes[i] = empty
|
|
|
|
}
|
|
|
|
sizeMap[size - 1] = sizes
|
|
|
|
return sizes[index]
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getSpanSize(position: Int): Int {
|
|
|
|
if (newWidth == 0) {
|
|
|
|
val child = gridManager.getChildAt(0) ?: return 1
|
|
|
|
if (child.width == 0) return 1
|
|
|
|
newWidth = gridManager.width / child.width
|
|
|
|
sizeMap = Array(originalHeight * newWidth - 1) { null }
|
|
|
|
}
|
|
|
|
val total = gridManager.itemCount
|
|
|
|
if (total >= originalHeight * newWidth || total == 0)
|
|
|
|
return 1
|
|
|
|
return emptyUnderIndex(position, total) + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-22 15:31:02 +02:00
|
|
|
companion object {
|
|
|
|
private const val TAG = "WireGuard/TvMainActivity"
|
|
|
|
}
|
2020-09-22 13:11:08 +02:00
|
|
|
}
|