Compare commits

..

5 Commits

Author SHA1 Message Date
Jason A. Donenfeld 8a6f8f73cd version: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-27 16:30:30 +02:00
Jason A. Donenfeld f4fc15538d 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 13:17:56 +02:00
Jason A. Donenfeld 938399d881 ui: queue up tunnel mutating on activity scope instead of fragment scope
Fragment scopes get cancelled when the fragment goes away, but we don't
actually want to cancel an in-flight transition in that case. Also,
before when the fragment would cancel, there'd be an exception, and the
exception handler would call Fragment::getString, which in turn called
requireContext, which caused an exception. Work around this by using the
`activity ?: Application.get()` idiom to always have a context for
strings and toasts.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-26 13:49:14 +02:00
Jason A. Donenfeld 53ca421a85 ui: print proper exception trace from log viewer
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-26 12:01:58 +02:00
Jason A. Donenfeld 32778d1c03 ui: request intent permissions from hidden activity
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-26 11:44:24 +02:00
10 changed files with 150 additions and 74 deletions
@@ -19,6 +19,7 @@ import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
@@ -177,7 +178,7 @@ class LogViewerActivity : AppCompatActivity() {
process = try {
builder.start()
} catch (e: IOException) {
e.printStackTrace()
Log.e(TAG, Log.getStackTraceString(e))
return@withContext
}
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
@@ -251,6 +252,7 @@ class LogViewerActivity : AppCompatActivity() {
*/
private val THREADTIME_LINE: Pattern = Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$")
private val LOGS: MutableMap<String, ByteArray> = ConcurrentHashMap()
private const val TAG = "WireGuard/LogViewerActivity"
}
private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() {
@@ -10,20 +10,23 @@ import android.os.Bundle
import android.service.quicksettings.TileService
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.wireguard.android.Application
import com.wireguard.android.QuickTileService
import com.wireguard.android.R
import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.util.ErrorMessages
import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.N)
class TunnelToggleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
private fun toggleTunnelWithPermissionsResult() {
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
lifecycleScope.launch {
try {
@@ -42,6 +45,20 @@ class TunnelToggleActivity : AppCompatActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
if (Application.getBackend() is GoBackend) {
val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
if (intent != null) {
permissionActivityResultLauncher.launch(intent)
return@launch
}
}
toggleTunnelWithPermissionsResult()
}
}
companion object {
private const val TAG = "WireGuard/TunnelToggleActivity"
}
@@ -25,6 +25,8 @@ import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableBoolean
import androidx.databinding.ObservableField
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.GoBackend
@@ -94,6 +96,8 @@ class TvMainActivity : AppCompatActivity() {
binding.isDeleting = isDeleting
binding.files = files
binding.filesRoot = filesRoot
val gridManager = binding.tunnelList.layoutManager as GridLayoutManager
gridManager.spanSizeLookup = SlatedSpanSizeLookup(gridManager)
binding.tunnelRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
binding.isDeleting = isDeleting
@@ -339,6 +343,48 @@ class TvMainActivity : AppCompatActivity() {
get() = forcedKey ?: if (file.isDirectory) "${file.name}/" else file.name
}
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
}
}
companion object {
private const val TAG = "WireGuard/TvMainActivity"
}
@@ -31,7 +31,6 @@ import kotlinx.coroutines.launch
* attached to a `BaseActivity`.
*/
abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
private var baseActivity: BaseActivity? = null
private var pendingTunnel: ObservableTunnel? = null
private var pendingTunnelUp: Boolean? = null
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -44,24 +43,18 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
}
protected var selectedTunnel: ObservableTunnel?
get() = baseActivity?.selectedTunnel
get() = (activity as? BaseActivity)?.selectedTunnel
protected set(tunnel) {
baseActivity?.selectedTunnel = tunnel
(activity as? BaseActivity)?.selectedTunnel = tunnel
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is BaseActivity) {
baseActivity = context
baseActivity?.addOnSelectedTunnelChangedListener(this)
} else {
baseActivity = null
}
(activity as? BaseActivity)?.addOnSelectedTunnelChangedListener(this)
}
override fun onDetach() {
baseActivity?.removeOnSelectedTunnelChangedListener(this)
baseActivity = null
(activity as? BaseActivity)?.removeOnSelectedTunnelChangedListener(this)
super.onDetach()
}
@@ -71,9 +64,10 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
is TunnelListItemBinding -> binding.item
else -> return
} ?: return
lifecycleScope.launch {
val activity = activity ?: return
activity.lifecycleScope.launch {
if (Application.getBackend() is GoBackend) {
val intent = GoBackend.VpnService.prepare(view.context)
val intent = GoBackend.VpnService.prepare(activity)
if (intent != null) {
pendingTunnel = tunnel
pendingTunnelUp = checked
@@ -86,20 +80,21 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
}
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
lifecycleScope.launch {
val activity = activity ?: return
activity.lifecycleScope.launch {
try {
tunnel.setStateAsync(Tunnel.State.of(checked))
} catch (e: Throwable) {
val error = ErrorMessages[e]
val messageResId = if (checked) R.string.error_up else R.string.error_down
val message = getString(messageResId, error)
val message = activity.getString(messageResId, error)
val view = view
if (view != null)
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
.setAnchorView(view.findViewById(R.id.create_fab))
.show()
else
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_LONG).show()
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, message, e)
}
}
@@ -29,15 +29,15 @@ class ConfigNamingDialogFragment : DialogFragment() {
private var imm: InputMethodManager? = null
private fun createTunnelAndDismiss() {
binding?.let {
val name = it.tunnelNameText.text.toString()
lifecycleScope.launch {
try {
Application.getTunnelManager().create(name, config)
dismiss()
} catch (e: Throwable) {
it.tunnelNameTextLayout.error = e.message
}
val binding = binding ?: return
val activity = activity ?: return
val name = binding.tunnelNameText.text.toString()
activity.lifecycleScope.launch {
try {
Application.getTunnelManager().create(name, config)
dismiss()
} catch (e: Throwable) {
binding.tunnelNameTextLayout.error = e.message
}
}
}
@@ -39,24 +39,27 @@ class TunnelEditorFragment : BaseFragment() {
private var haveShownKeys = false
private var binding: TunnelEditorFragmentBinding? = null
private var tunnel: ObservableTunnel? = null
private fun onConfigLoaded(config: Config) {
binding?.config = ConfigProxy(config)
}
private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) {
val message: String
val ctx = activity ?: Application.get()
if (throwable == null) {
message = getString(R.string.config_save_success, savedTunnel.name)
val message = ctx.getString(R.string.config_save_success, savedTunnel.name)
Log.d(TAG, message)
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
onFinished()
} else {
val error = ErrorMessages[throwable]
message = getString(R.string.config_save_error, savedTunnel.name, error)
val message = ctx.getString(R.string.config_save_error, savedTunnel.name, error)
Log.e(TAG, message, throwable)
binding?.let {
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
}
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
else
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
}
}
@@ -115,7 +118,8 @@ class TunnelEditorFragment : BaseFragment() {
Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
return false
}
lifecycleScope.launch {
val activity = requireActivity()
activity.lifecycleScope.launch {
when {
tunnel == null -> {
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
@@ -209,46 +213,48 @@ class TunnelEditorFragment : BaseFragment() {
}
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
val message: String
val ctx = activity ?: Application.get()
if (throwable == null) {
tunnel = newTunnel
message = getString(R.string.tunnel_create_success, tunnel!!.name)
val message = ctx.getString(R.string.tunnel_create_success, tunnel!!.name)
Log.d(TAG, message)
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
onFinished()
} else {
val error = ErrorMessages[throwable]
message = getString(R.string.tunnel_create_error, error)
val message = ctx.getString(R.string.tunnel_create_error, error)
Log.e(TAG, message, throwable)
binding?.let {
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
}
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
else
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
}
}
private fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
throwable: Throwable?) {
val message: String
private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
throwable: Throwable?) {
val ctx = activity ?: Application.get()
if (throwable == null) {
message = getString(R.string.tunnel_rename_success, renamedTunnel.name)
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
Log.d(TAG, message)
// Now save the rest of configuration changes.
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
lifecycleScope.launch {
try {
renamedTunnel.setConfigAsync(newConfig)
onConfigSaved(renamedTunnel, null)
} catch (e: Throwable) {
onConfigSaved(renamedTunnel, e)
}
try {
renamedTunnel.setConfigAsync(newConfig)
onConfigSaved(renamedTunnel, null)
} catch (e: Throwable) {
onConfigSaved(renamedTunnel, e)
}
} else {
val error = ErrorMessages[throwable]
message = getString(R.string.tunnel_rename_error, error)
val message = ctx.getString(R.string.tunnel_rename_error, error)
Log.e(TAG, message, throwable)
binding?.let {
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
}
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
else
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
}
}
@@ -15,6 +15,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
@@ -46,9 +47,10 @@ class TunnelListFragment : BaseFragment() {
private var actionMode: ActionMode? = null
private var binding: TunnelListFragmentBinding? = null
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
lifecycleScope.launch {
if (data == null) return@launch
val contentResolver = activity?.contentResolver ?: return@launch
if (data == null) return@registerForActivityResult
val activity = activity ?: return@registerForActivityResult
val contentResolver = activity.contentResolver ?: return@registerForActivityResult
activity.lifecycleScope.launch {
TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
}
}
@@ -56,7 +58,9 @@ class TunnelListFragment : BaseFragment() {
private val qrImportResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val qrCode = IntentIntegrator.parseActivityResult(result.resultCode, result.data)?.contents
?: return@registerForActivityResult
lifecycleScope.launch { TunnelImporter.importTunnel(parentFragmentManager, qrCode) { showSnackbar(it) } }
val activity = activity ?: return@registerForActivityResult
val fragManager = parentFragmentManager
activity.lifecycleScope.launch { TunnelImporter.importTunnel(fragManager, qrCode) { showSnackbar(it) } }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -121,11 +125,12 @@ class TunnelListFragment : BaseFragment() {
private fun onTunnelDeletionFinished(count: Int, throwable: Throwable?) {
val message: String
val ctx = activity ?: Application.get()
if (throwable == null) {
message = resources.getQuantityString(R.plurals.delete_success, count, count)
message = ctx.resources.getQuantityString(R.plurals.delete_success, count, count)
} else {
val error = ErrorMessages[throwable]
message = resources.getQuantityString(R.plurals.delete_error, count, count, error)
message = ctx.resources.getQuantityString(R.plurals.delete_error, count, count, error)
Log.e(TAG, message, throwable)
}
showSnackbar(message)
@@ -159,11 +164,13 @@ class TunnelListFragment : BaseFragment() {
}
private fun showSnackbar(message: CharSequence) {
binding?.let {
Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG)
.setAnchorView(it.createFab)
val binding = binding
if (binding != null)
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
.setAnchorView(binding.createFab)
.show()
}
else
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
}
private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout? {
@@ -181,13 +188,14 @@ class TunnelListFragment : BaseFragment() {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_action_delete -> {
val activity = activity ?: return true
val copyCheckedItems = HashSet(checkedItems)
binding?.createFab?.apply {
visibility = View.VISIBLE
scaleX = 1f
scaleY = 1f
}
lifecycleScope.launch {
activity.lifecycleScope.launch {
try {
val tunnels = Application.getTunnelManager().getTunnels()
val tunnelsToDelete = ArrayList<ObservableTunnel>()
@@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.util.Log
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.wireguard.android.Application
import com.wireguard.android.R
@@ -15,6 +16,7 @@ import com.wireguard.android.activity.SettingsActivity
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.activity
import com.wireguard.android.util.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -39,7 +41,7 @@ class KernelModuleDisablerPreference(context: Context, attrs: AttributeSet?) : P
override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
override fun onClick() {
lifecycleScope.launch {
activity.lifecycleScope.launch {
if (state == State.DISABLED) {
setState(State.ENABLING)
UserKnobs.setDisableKernelModule(false)
@@ -6,7 +6,7 @@ package com.wireguard.android.util
import android.content.res.Resources
import android.os.RemoteException
import com.wireguard.android.Application.Companion.get
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.BackendException
import com.wireguard.android.util.RootShell.RootShellException
@@ -63,7 +63,7 @@ object ErrorMessages {
)
operator fun get(throwable: Throwable?): String {
val resources = get().resources
val resources = Application.get().resources
if (throwable == null) return resources.getString(R.string.unknown_error)
val rootCause = rootCause(throwable)
return when {
+2 -2
View File
@@ -1,6 +1,6 @@
buildscript {
ext {
wireguardVersionCode = 481
wireguardVersionName = '1.0.20200923'
wireguardVersionCode = 482
wireguardVersionName = '1.0.20200927'
}
}