diff options
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/activity')
8 files changed, 243 insertions, 132 deletions
diff --git a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt index bdf0f8d4..56810377 100644 --- a/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt @@ -1,28 +1,36 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.activity import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity import androidx.databinding.CallbackRegistry import androidx.databinding.CallbackRegistry.NotifierCallback import androidx.lifecycle.lifecycleScope import com.wireguard.android.Application import com.wireguard.android.model.ObservableTunnel +import kotlinx.coroutines.launch /** * Base class for activities that need to remember the currently-selected tunnel. */ -abstract class BaseActivity : ThemeChangeAwareActivity() { +abstract class BaseActivity : AppCompatActivity() { private val selectionChangeRegistry = SelectionChangeRegistry() + private var created = false var selectedTunnel: ObservableTunnel? = null set(value) { val oldTunnel = field if (oldTunnel == value) return field = value - onSelectedTunnelChanged(oldTunnel, value) - selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value) + if (created) { + if (!onSelectedTunnelChanged(oldTunnel, value)) { + field = oldTunnel + } else { + selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value) + } + } } fun addOnSelectedTunnelChangedListener(listener: OnSelectedTunnelChangedListener) { @@ -30,17 +38,25 @@ abstract class BaseActivity : ThemeChangeAwareActivity() { } override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Restore the saved tunnel if there is one; otherwise grab it from the arguments. val savedTunnelName = when { savedInstanceState != null -> savedInstanceState.getString(KEY_SELECTED_TUNNEL) intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL) else -> null } - if (savedTunnelName != null) - lifecycleScope.launchWhenCreated { selectedTunnel = Application.getTunnelManager().getTunnels()[savedTunnelName] } - - // The selected tunnel must be set before the superclass method recreates fragments. - super.onCreate(savedInstanceState) + if (savedTunnelName != null) { + lifecycleScope.launch { + val tunnel = Application.getTunnelManager().getTunnels()[savedTunnelName] + if (tunnel == null) + created = true + selectedTunnel = tunnel + created = true + } + } else { + created = true + } } override fun onSaveInstanceState(outState: Bundle) { @@ -48,10 +64,11 @@ abstract class BaseActivity : ThemeChangeAwareActivity() { super.onSaveInstanceState(outState) } - protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) + protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean fun removeOnSelectedTunnelChangedListener( - listener: OnSelectedTunnelChangedListener) { + listener: OnSelectedTunnelChangedListener + ) { selectionChangeRegistry.remove(listener) } @@ -61,17 +78,17 @@ abstract class BaseActivity : ThemeChangeAwareActivity() { private class SelectionChangeNotifier : NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>() { override fun onNotifyCallback( - listener: OnSelectedTunnelChangedListener, - oldTunnel: ObservableTunnel?, - ignored: Int, - newTunnel: ObservableTunnel? + listener: OnSelectedTunnelChangedListener, + oldTunnel: ObservableTunnel?, + ignored: Int, + newTunnel: ObservableTunnel? ) { listener.onSelectedTunnelChanged(oldTunnel, newTunnel) } } private class SelectionChangeRegistry : - CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier()) + CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier()) companion object { private const val KEY_SELECTED_TUNNEL = "selected_tunnel" diff --git a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt index dd100cf9..69e33bc9 100644 --- a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -27,6 +27,7 @@ import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.collection.CircularArray import androidx.core.app.ShareCompat import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.lifecycleScope @@ -40,6 +41,7 @@ import com.wireguard.android.R import com.wireguard.android.databinding.LogViewerActivityBinding import com.wireguard.android.util.DownloadsFileSaver import com.wireguard.android.util.ErrorMessages +import com.wireguard.android.util.resolveAttribute import com.wireguard.crypto.KeyPair import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -61,8 +63,8 @@ import java.util.regex.Pattern class LogViewerActivity : AppCompatActivity() { private lateinit var binding: LogViewerActivityBinding private lateinit var logAdapter: LogEntryAdapter - private var logLines = arrayListOf<LogLine>() - private var rawLogLines = StringBuffer() + private var logLines = CircularArray<LogLine>() + private var rawLogLines = CircularArray<String>() private var recyclerView: RecyclerView? = null private var saveButton: MenuItem? = null private val year by lazy { @@ -70,7 +72,7 @@ class LogViewerActivity : AppCompatActivity() { yearFormatter.format(Date()) } - private val defaultColor by lazy { ResourcesCompat.getColor(resources, R.color.primary_text_color, theme) } + private val defaultColor by lazy { resolveAttribute(com.google.android.material.R.attr.colorOnSurface) } private val debugColor by lazy { ResourcesCompat.getColor(resources, R.color.debug_tag_color, theme) } @@ -110,19 +112,21 @@ class LogViewerActivity : AppCompatActivity() { } binding.shareFab.setOnClickListener { - revokeLastUri() - val key = KeyPair().privateKey.toHex() - LOGS[key] = rawLogLines.toString().toByteArray(Charsets.UTF_8) - lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key") - val shareIntent = ShareCompat.IntentBuilder(this) + lifecycleScope.launch { + revokeLastUri() + val key = KeyPair().privateKey.toHex() + LOGS[key] = rawLogBytes() + lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key") + val shareIntent = ShareCompat.IntentBuilder(this@LogViewerActivity) .setType("text/plain") .setSubject(getString(R.string.log_export_subject)) .setStream(lastUri) .setChooserTitle(R.string.log_export_title) .createChooserIntent() .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) - revokeLastActivityResultLauncher.launch(shareIntent) + grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + revokeLastActivityResultLauncher.launch(shareIntent) + } } } @@ -138,24 +142,37 @@ class LogViewerActivity : AppCompatActivity() { finish() true } + R.id.save_log -> { saveButton?.isEnabled = false lifecycleScope.launch { saveLog() } true } + else -> super.onOptionsItemSelected(item) } } private val downloadsFileSaver = DownloadsFileSaver(this) + private suspend fun rawLogBytes(): ByteArray { + val builder = StringBuilder() + withContext(Dispatchers.IO) { + for (i in 0 until rawLogLines.size()) { + builder.append(rawLogLines[i]) + builder.append('\n') + } + } + return builder.toString().toByteArray(Charsets.UTF_8) + } + private suspend fun saveLog() { var exception: Throwable? = null var outputFile: DownloadsFileSaver.DownloadsFile? = null withContext(Dispatchers.IO) { try { outputFile = downloadsFileSaver.save("wireguard-log.txt", "text/plain", true) - outputFile?.outputStream?.write(rawLogLines.toString().toByteArray(Charsets.UTF_8)) + outputFile?.outputStream?.write(rawLogBytes()) } catch (e: Throwable) { outputFile?.delete() exception = e @@ -164,12 +181,14 @@ class LogViewerActivity : AppCompatActivity() { saveButton?.isEnabled = true if (outputFile == null) return - Snackbar.make(findViewById(android.R.id.content), - if (exception == null) getString(R.string.log_export_success, outputFile?.fileName) - else getString(R.string.log_export_error, ErrorMessages[exception]), - if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG) - .setAnchorView(binding.shareFab) - .show() + Snackbar.make( + findViewById(android.R.id.content), + if (exception == null) getString(R.string.log_export_success, outputFile?.fileName) + else getString(R.string.log_export_error, ErrorMessages[exception]), + if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG + ) + .setAnchorView(binding.shareFab) + .show() } private suspend fun streamingLog() = withContext(Dispatchers.IO) { @@ -184,36 +203,60 @@ class LogViewerActivity : AppCompatActivity() { return@withContext } val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8)) - var haveScrolled = false - val start = System.nanoTime() - var startPeriod = start + + var posStart = 0 + var timeLastNotify = System.nanoTime() + var priorModified = false + val bufferedLogLines = arrayListOf<LogLine>() + var timeout = 1000000000L / 2 // The timeout is initially small so that the view gets populated immediately. + val MAX_LINES = (1 shl 16) - 1 + val MAX_BUFFERED_LINES = (1 shl 14) - 1 + while (true) { val line = stdout.readLine() ?: break - rawLogLines.append(line) - rawLogLines.append('\n') + if (rawLogLines.size() >= MAX_LINES) + rawLogLines.popFirst() + rawLogLines.addLast(line) val logLine = parseLine(line) + if (logLine != null) { + bufferedLogLines.add(logLine) + } else { + if (bufferedLogLines.isNotEmpty()) { + bufferedLogLines.last().msg += "\n$line" + } else if (!logLines.isEmpty()) { + logLines[logLines.size() - 1].msg += "\n$line" + priorModified = true + } + } + val timeNow = System.nanoTime() + if (bufferedLogLines.size < MAX_BUFFERED_LINES && (timeNow - timeLastNotify) < timeout && stdout.ready()) + continue + timeout = 1000000000L * 5 / 2 // Increase the timeout after the initial view has something in it. + timeLastNotify = timeNow + withContext(Dispatchers.Main.immediate) { - if (logLine != null) { - recyclerView?.let { - val shouldScroll = haveScrolled && !it.canScrollVertically(1) - logLines.add(logLine) - if (haveScrolled) logAdapter.notifyDataSetChanged() - if (shouldScroll) - it.scrollToPosition(logLines.size - 1) - } - } else { - logLines.lastOrNull()?.msg += "\n$line" - if (haveScrolled) logAdapter.notifyDataSetChanged() + val isScrolledToBottomAlready = recyclerView?.canScrollVertically(1) == false + if (priorModified) { + logAdapter.notifyItemChanged(posStart - 1) + priorModified = false + } + val fullLen = logLines.size() + bufferedLogLines.size + if (fullLen >= MAX_LINES) { + val numToRemove = fullLen - MAX_LINES + 1 + logLines.removeFromStart(numToRemove) + logAdapter.notifyItemRangeRemoved(0, numToRemove) + posStart -= numToRemove + + } + for (bufferedLine in bufferedLogLines) { + logLines.addLast(bufferedLine) } - if (!haveScrolled) { - val end = System.nanoTime() - val scroll = (end - start) > 1000000000L * 2.5 || !stdout.ready() - if (logLines.isNotEmpty() && (scroll || (end - startPeriod) > 1000000000L / 4)) { - logAdapter.notifyDataSetChanged() - recyclerView?.scrollToPosition(logLines.size - 1) - startPeriod = end - } - if (scroll) haveScrolled = true + bufferedLogLines.clear() + logAdapter.notifyItemRangeInserted(posStart, logLines.size() - posStart) + posStart = logLines.size() + + if (isScrolledToBottomAlready) { + recyclerView?.scrollToPosition(logLines.size() - 1) } } } @@ -248,7 +291,8 @@ class LogViewerActivity : AppCompatActivity() { * * <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre> */ - 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 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" } @@ -259,7 +303,7 @@ class LogViewerActivity : AppCompatActivity() { private fun levelToColor(level: String): Int { return when (level) { - "D" -> debugColor + "V", "D" -> debugColor "E" -> errorColor "I" -> infoColor "W" -> warningColor @@ -267,11 +311,11 @@ class LogViewerActivity : AppCompatActivity() { } } - override fun getItemCount() = logLines.size + override fun getItemCount() = logLines.size() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) - .inflate(R.layout.log_viewer_entry, parent, false) + .inflate(R.layout.log_viewer_entry, parent, false) return ViewHolder(view) } @@ -282,8 +326,10 @@ class LogViewerActivity : AppCompatActivity() { else SpannableString("${line.tag}: ${line.msg}").apply { setSpan(StyleSpan(BOLD), 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - setSpan(ForegroundColorSpan(levelToColor(line.level)), - 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + setSpan( + ForegroundColorSpan(levelToColor(line.level)), + 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) } holder.layout.apply { findViewById<MaterialTextView>(R.id.log_date).text = line.time.toString() @@ -305,11 +351,11 @@ class LogViewerActivity : AppCompatActivity() { override fun insert(uri: Uri, values: ContentValues?): Uri? = null override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = - logForUri(uri)?.let { - val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1) - m.addRow(arrayOf("wireguard-log.txt", it.size.toLong())) - m - } + logForUri(uri)?.let { + val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1) + m.addRow(arrayOf("wireguard-log.txt", it.size.toLong())) + m + } override fun onCreate(): Boolean = true @@ -319,7 +365,8 @@ class LogViewerActivity : AppCompatActivity() { override fun getType(uri: Uri): String? = logForUri(uri)?.let { "text/plain" } - override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? = getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null } + override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? = + getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null } override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { if (mode != "r") return null diff --git a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt index aac08e18..80c4868c 100644 --- a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.activity @@ -9,6 +9,8 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.appcompat.app.ActionBar import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction @@ -26,27 +28,29 @@ import com.wireguard.android.model.ObservableTunnel class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener { private var actionBar: ActionBar? = null private var isTwoPaneLayout = false + private var backPressedCallback: OnBackPressedCallback? = null - override fun onBackPressed() { + private fun handleBackPressed() { val backStackEntries = supportFragmentManager.backStackEntryCount // If the two-pane layout does not have an editor open, going back should exit the app. if (isTwoPaneLayout && backStackEntries <= 1) { finish() return } - // Deselect the current tunnel on navigating back from the detail pane to the one-pane list. - if (!isTwoPaneLayout && backStackEntries == 1) { + + if (backStackEntries >= 1) supportFragmentManager.popBackStack() + + // Deselect the current tunnel on navigating back from the detail pane to the one-pane list. + if (backStackEntries == 1) selectedTunnel = null - return - } - super.onBackPressed() } override fun onBackStackChanged() { + val backStackEntries = supportFragmentManager.backStackEntryCount + backPressedCallback?.isEnabled = backStackEntries >= 1 if (actionBar == null) return // Do not show the home menu when the two-pane layout is at the detail view (see above). - val backStackEntries = supportFragmentManager.backStackEntryCount val minBackStackEntries = if (isTwoPaneLayout) 2 else 1 actionBar!!.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries) } @@ -57,6 +61,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener actionBar = supportActionBar isTwoPaneLayout = findViewById<View?>(R.id.master_detail_wrapper) != null supportFragmentManager.addOnBackStackChangedListener(this) + backPressedCallback = onBackPressedDispatcher.addCallback(this) { handleBackPressed() } onBackStackChanged() } @@ -69,9 +74,10 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener return when (item.itemId) { android.R.id.home -> { // The back arrow in the action bar should act the same as the back button. - onBackPressed() + onBackPressedDispatcher.onBackPressed() true } + R.id.menu_action_edit -> { supportFragmentManager.commit { replace(R.id.detail_container, TunnelEditorFragment()) @@ -86,18 +92,25 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener startActivity(Intent(this, SettingsActivity::class.java)) true } + else -> super.onOptionsItemSelected(item) } } - override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, - newTunnel: ObservableTunnel?) { + override fun onSelectedTunnelChanged( + oldTunnel: ObservableTunnel?, + newTunnel: ObservableTunnel? + ): Boolean { val fragmentManager = supportFragmentManager + if (fragmentManager.isStateSaved) { + return false + } + val backStackEntries = fragmentManager.backStackEntryCount if (newTunnel == null) { // Clear everything off the back stack (all editors and detail fragments). fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE) - return + return true } if (backStackEntries == 2) { // Pop the editor off the back stack to reveal the detail fragment. Use the immediate @@ -111,5 +124,6 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener addToBackStack(null) } } + return true } } diff --git a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt index df025163..bd6e1f78 100644 --- a/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt @@ -1,18 +1,22 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.activity +import android.content.ComponentName import android.content.Intent import android.os.Build import android.os.Bundle +import android.service.quicksettings.TileService import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.wireguard.android.Application +import com.wireguard.android.QuickTileService import com.wireguard.android.R import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.preference.PreferencesPreferenceDataStore @@ -24,7 +28,7 @@ import kotlinx.coroutines.withContext /** * Interface for changing application-global persistent settings. */ -class SettingsActivity : ThemeChangeAwareActivity() { +class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (supportFragmentManager.findFragmentById(android.R.id.content) == null) { @@ -46,7 +50,13 @@ class SettingsActivity : ThemeChangeAwareActivity() { override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) { preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore()) addPreferencesFromResource(R.xml.preferences) - preferenceScreen.initialExpandedChildrenCount = 4 + preferenceScreen.initialExpandedChildrenCount = 5 + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || QuickTileService.isAdded) { + val quickTile = preferenceManager.findPreference<Preference>("quick_tile") + quickTile?.parent?.removePreference(quickTile) + --preferenceScreen.initialExpandedChildrenCount + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val darkTheme = preferenceManager.findPreference<Preference>("dark_theme") darkTheme?.parent?.removePreference(darkTheme) @@ -61,9 +71,9 @@ class SettingsActivity : ThemeChangeAwareActivity() { zipExporter?.parent?.removePreference(zipExporter) } val wgQuickOnlyPrefs = arrayOf( - preferenceManager.findPreference("tools_installer"), - preferenceManager.findPreference("restore_on_boot"), - preferenceManager.findPreference<Preference>("multiple_tunnels") + preferenceManager.findPreference("tools_installer"), + preferenceManager.findPreference("restore_on_boot"), + preferenceManager.findPreference<Preference>("multiple_tunnels") ).filterNotNull() wgQuickOnlyPrefs.forEach { it.isVisible = false } lifecycleScope.launch { diff --git a/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt b/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt deleted file mode 100644 index 2158858b..00000000 --- a/ui/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package com.wireguard.android.activity - -import android.os.Build -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate -import androidx.lifecycle.lifecycleScope -import com.wireguard.android.util.UserKnobs -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -abstract class ThemeChangeAwareActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - UserKnobs.darkTheme.onEach { - val newMode = if (it) { - AppCompatDelegate.MODE_NIGHT_YES - } else { - AppCompatDelegate.MODE_NIGHT_NO - } - if (AppCompatDelegate.getDefaultNightMode() != newMode) { - AppCompatDelegate.setDefaultNightMode(newMode) - recreate() - } - }.launchIn(lifecycleScope) - } - } -} diff --git a/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt index 28d5da3a..bdf798ca 100644 --- a/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.activity @@ -22,7 +22,8 @@ class TunnelCreatorActivity : BaseActivity() { } } - override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) { + override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean { finish() + return true } } diff --git a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt index ebf059a5..59b9349f 100644 --- a/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.activity @@ -24,7 +24,8 @@ import kotlinx.coroutines.launch @RequiresApi(Build.VERSION_CODES.N) class TunnelToggleActivity : AppCompatActivity() { - private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() } + private val permissionActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() } private fun toggleTunnelWithPermissionsResult() { val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return diff --git a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt index 7b0737e8..4c86b4c8 100644 --- a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt +++ b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt @@ -1,11 +1,14 @@ /* - * Copyright © 2017-2021 WireGuard LLC. All Rights Reserved. + * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ package com.wireguard.android.activity import android.Manifest +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build @@ -16,17 +19,21 @@ import android.os.storage.StorageVolume import android.util.Log import android.view.View import android.widget.Toast +import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.view.forEach import androidx.databinding.DataBindingUtil +import androidx.databinding.Observable 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.google.android.material.dialog.MaterialAlertDialogBuilder import com.wireguard.android.Application import com.wireguard.android.R import com.wireguard.android.backend.GoBackend @@ -41,6 +48,8 @@ import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.util.ErrorMessages import com.wireguard.android.util.QuantityFormatter import com.wireguard.android.util.TunnelImporter +import com.wireguard.android.util.UserKnobs +import com.wireguard.android.util.applicationScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -48,7 +57,27 @@ import kotlinx.coroutines.withContext import java.io.File class TvMainActivity : AppCompatActivity() { - private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data -> + private val tunnelFileImportResultLauncher = registerForActivityResult(object : ActivityResultContracts.GetContent() { + override fun createIntent(context: Context, input: String): Intent { + val intent = super.createIntent(context, input) + + /* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than + * what we can do, so detect this and throw an exception that we can catch later. */ + val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())) + } else { + @Suppress("DEPRECATION") + context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + } + if (activitiesToResolveIntent.all { + val name = it.activityInfo.packageName + name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs") + }) { + throw ActivityNotFoundException() + } + return intent + } + }) { data -> if (data == null) return@registerForActivityResult lifecycleScope.launch { TunnelImporter.importTunnel(contentResolver, data) { @@ -84,6 +113,14 @@ class TvMainActivity : AppCompatActivity() { private val filesRoot = ObservableField("") override fun onCreate(savedInstanceState: Bundle?) { + if (AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + applicationScope.launch { + UserKnobs.setDarkTheme(true) + } + } + } super.onCreate(savedInstanceState) binding = TvActivityBinding.inflate(layoutInflater) lifecycleScope.launch { @@ -174,7 +211,13 @@ class TvMainActivity : AppCompatActivity() { try { tunnelFileImportResultLauncher.launch("*/*") } catch (_: Throwable) { - Toast.makeText(this@TvMainActivity, getString(R.string.tv_no_file_picker), Toast.LENGTH_LONG).show() + MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false) + .setPositiveButton(android.R.string.ok) { _, _ -> + try { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect"))) + } catch (_: Throwable) { + } + }.show() } } } @@ -185,6 +228,17 @@ class TvMainActivity : AppCompatActivity() { binding.tunnelList.requestFocus() } } + + val backPressedCallback = onBackPressedDispatcher.addCallback(this) { handleBackPressed() } + val updateBackPressedCallback = object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { + backPressedCallback.isEnabled = isDeleting.get() || filesRoot.get()?.isNotEmpty() == true + } + } + isDeleting.addOnPropertyChangedCallback(updateBackPressedCallback) + filesRoot.addOnPropertyChangedCallback(updateBackPressedCallback) + backPressedCallback.isEnabled = false + binding.executePendingBindings() setContentView(binding.root) @@ -298,7 +352,7 @@ class TvMainActivity : AppCompatActivity() { } } - override fun onBackPressed() { + private fun handleBackPressed() { when { isDeleting.get() -> { isDeleting.set(false) @@ -306,6 +360,7 @@ class TvMainActivity : AppCompatActivity() { binding.tunnelList.requestFocus() } } + filesRoot.get()?.isNotEmpty() == true -> { files.clear() filesRoot.set("") @@ -313,14 +368,13 @@ class TvMainActivity : AppCompatActivity() { binding.tunnelList.requestFocus() } } - else -> super.onBackPressed() } } private suspend fun updateStats() { binding.tunnelList.forEach { viewItem -> val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem) - ?: return@forEach + ?: return@forEach try { val tunnel = listItem.item!! if (tunnel.state != Tunnel.State.UP || isDeleting.get()) { |