mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-07-03 14:05:13 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fa2e6164f | |||
| 9972d9607e | |||
| 6a72194106 | |||
| 254e6164d7 | |||
| 675386241b | |||
| 8671acc1a6 | |||
| fb08bb6bd4 | |||
| 70f8e5ab3a | |||
| e53a8d76f9 | |||
| e0b4551bca | |||
| 7b9757bf3b | |||
| 4156b83b84 | |||
| f829b8abe0 | |||
| 838e0bd6b0 | |||
| c3f6906309 | |||
| 5cafd1ebe4 | |||
| 51991324a9 | |||
| 33a8a51962 | |||
| 40eaa54cf0 | |||
| a3bfa6f1ab | |||
| 7778ff63b5 |
Generated
+1
@@ -468,6 +468,7 @@
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
<option name="RIGHT_MARGIN" value="160" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
wireguardVersionCode=502
|
||||
wireguardVersionName=1.0.20230504
|
||||
wireguardVersionCode=505
|
||||
wireguardVersionName=1.0.20230512
|
||||
wireguardPackageName=com.wireguard.android
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.0.1"
|
||||
agp = "8.1.0-beta02"
|
||||
kotlin = "1.8.21"
|
||||
|
||||
[libraries]
|
||||
@@ -16,10 +16,10 @@ androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.5.7"
|
||||
androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
|
||||
androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.0"
|
||||
desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.3"
|
||||
google-material = "com.google.android.material:material:1.8.0"
|
||||
google-material = "com.google.android.material:material:1.9.0"
|
||||
jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
|
||||
junit = "junit:junit:4.13.2"
|
||||
kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
||||
kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0"
|
||||
zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0"
|
||||
|
||||
[plugins]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
|
||||
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
|
||||
@@ -50,9 +51,6 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
create("googleplay") {
|
||||
initWith(getByName("release"))
|
||||
}
|
||||
}
|
||||
lint {
|
||||
disable += "LongLogTag"
|
||||
|
||||
+21
-2
@@ -1,6 +1,11 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import com.android.build.gradle.tasks.GenerateLocaleConfigTask
|
||||
|
||||
// Grotesque workaround for https://issuetracker.google.com/issues/279780940
|
||||
System.setProperty("com.android.tools.r8.disableApiModeling", "1")
|
||||
|
||||
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
|
||||
|
||||
@@ -25,7 +30,6 @@ android {
|
||||
versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
|
||||
versionName = providers.gradleProperty("wireguardVersionName").get()
|
||||
buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
|
||||
buildConfigField("boolean", "IS_GOOGLE_PLAY", false.toString())
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -41,6 +45,7 @@ android {
|
||||
resources {
|
||||
excludes += "DebugProbesKt.bin"
|
||||
excludes += "kotlin-tooling-metadata.json"
|
||||
excludes += "META-INF/*.version"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,9 +55,12 @@ android {
|
||||
}
|
||||
create("googleplay") {
|
||||
initWith(getByName("release"))
|
||||
buildConfigField("boolean", "IS_GOOGLE_PLAY", true.toString())
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
lint {
|
||||
disable += "LongLogTag"
|
||||
warning += "MissingTranslation"
|
||||
@@ -87,3 +95,14 @@ tasks.withType<JavaCompile>().configureEach {
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
|
||||
// Grotesque workaround for https://issuetracker.google.com/issues/281825213
|
||||
tasks.withType<GenerateLocaleConfigTask>().configureEach {
|
||||
doLast {
|
||||
localeConfig.asFileTree.files.forEach {
|
||||
val lines = it.readLines()
|
||||
val newLines = lines.subList(0, 2) + lines.subList(2, lines.size - 1).sorted() + lines.last()
|
||||
it.writeText(newLines.joinToString(separator = "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" />
|
||||
<application>
|
||||
<receiver android:name=".updater.Updater$AppUpdatedReceiver" tools:node="remove" />
|
||||
</application>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||
tools:node="remove" />
|
||||
</manifest>
|
||||
|
||||
@@ -45,10 +45,12 @@
|
||||
|
||||
<activity
|
||||
android:name=".activity.TunnelToggleActivity"
|
||||
android:theme="@style/NoBackgroundTheme"
|
||||
android:excludeFromRecents="true"/>
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/NoBackgroundTheme" />
|
||||
|
||||
<activity android:name=".activity.MainActivity" android:exported="true">
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -62,8 +64,8 @@
|
||||
|
||||
<activity
|
||||
android:name=".activity.TvMainActivity"
|
||||
android:theme="@style/TvTheme"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:theme="@style/TvTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
@@ -87,8 +89,8 @@
|
||||
|
||||
<activity
|
||||
android:name=".activity.LogViewerActivity"
|
||||
android:label="@string/log_viewer_title"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:label="@string/log_viewer_title">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
@@ -100,14 +102,18 @@
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<receiver android:name=".BootShutdownReceiver" android:exported="true">
|
||||
<receiver
|
||||
android:name=".BootShutdownReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".updater.Updater$AppUpdatedReceiver" android:exported="true">
|
||||
<receiver
|
||||
android:name=".updater.Updater$AppUpdatedReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
@@ -115,8 +121,8 @@
|
||||
|
||||
<receiver
|
||||
android:name=".model.TunnelManager$IntentReceiver"
|
||||
android:permission="${applicationId}.permission.CONTROL_TUNNELS"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
|
||||
<intent-filter>
|
||||
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
|
||||
<action android:name="com.wireguard.android.action.SET_TUNNEL_UP" />
|
||||
@@ -126,9 +132,9 @@
|
||||
|
||||
<service
|
||||
android:name=".QuickTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_tile"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
@@ -137,6 +143,10 @@
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
|
||||
@@ -49,32 +49,31 @@ class QuickTileService : TileService() {
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
if (tunnel != null) {
|
||||
unlockAndRun {
|
||||
val tile = qsTile
|
||||
if (tile != null) {
|
||||
tile.icon = if (tile.icon == iconOn) iconOff else iconOn
|
||||
tile.updateTile()
|
||||
}
|
||||
applicationScope.launch {
|
||||
try {
|
||||
tunnel!!.setStateAsync(Tunnel.State.TOGGLE)
|
||||
updateTile()
|
||||
} catch (_: Throwable) {
|
||||
val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
|
||||
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(toggleIntent)
|
||||
when (val tunnel = tunnel) {
|
||||
null -> {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
else -> {
|
||||
unlockAndRun {
|
||||
applicationScope.launch {
|
||||
try {
|
||||
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||
updateTile()
|
||||
} catch (_: Throwable) {
|
||||
val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
|
||||
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(toggleIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
isAdded = true
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
iconOn = Icon.createWithResource(this, R.drawable.ic_tile)
|
||||
iconOff = iconOn
|
||||
@@ -83,56 +82,65 @@ class QuickTileService : TileService() {
|
||||
val icon = SlashDrawable(resources.getDrawable(R.drawable.ic_tile, Application.get().theme))
|
||||
icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */
|
||||
icon.setSlashed(false)
|
||||
var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
?: return
|
||||
var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) ?: return
|
||||
var c = Canvas(b)
|
||||
icon.setBounds(0, 0, c.width, c.height)
|
||||
icon.draw(c)
|
||||
iconOn = Icon.createWithBitmap(b)
|
||||
icon.setSlashed(true)
|
||||
b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
?: return
|
||||
b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) ?: return
|
||||
c = Canvas(b)
|
||||
icon.setBounds(0, 0, c.width, c.height)
|
||||
icon.draw(c)
|
||||
iconOff = Icon.createWithBitmap(b)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
isAdded = false
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback)
|
||||
if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback)
|
||||
tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
|
||||
updateTile()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback)
|
||||
tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
|
||||
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback)
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
isAdded = true
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
isAdded = false
|
||||
}
|
||||
|
||||
private fun updateTile() {
|
||||
// Update the tunnel.
|
||||
val newTunnel = Application.getTunnelManager().lastUsedTunnel
|
||||
if (newTunnel != tunnel) {
|
||||
if (tunnel != null) tunnel!!.removeOnPropertyChangedCallback(onStateChangedCallback)
|
||||
tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
|
||||
tunnel = newTunnel
|
||||
if (tunnel != null) tunnel!!.addOnPropertyChangedCallback(onStateChangedCallback)
|
||||
tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
|
||||
}
|
||||
// Update the tile contents.
|
||||
val label: String
|
||||
val state: Int
|
||||
val tile = qsTile
|
||||
if (tunnel != null) {
|
||||
label = tunnel!!.name
|
||||
state = if (tunnel!!.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||
} else {
|
||||
label = getString(R.string.app_name)
|
||||
state = Tile.STATE_INACTIVE
|
||||
}
|
||||
if (tile == null) return
|
||||
tile.label = label
|
||||
if (tile.state != state) {
|
||||
tile.icon = if (state == Tile.STATE_ACTIVE) iconOn else iconOff
|
||||
tile.state = state
|
||||
val tile = qsTile ?: return
|
||||
|
||||
when (val tunnel = tunnel) {
|
||||
null -> {
|
||||
tile.label = getString(R.string.app_name)
|
||||
tile.state = Tile.STATE_INACTIVE
|
||||
tile.icon = iconOff
|
||||
}
|
||||
else -> {
|
||||
tile.label = tunnel.name
|
||||
tile.state = if (tunnel.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||
tile.icon = if (tunnel.state == Tunnel.State.UP) iconOn else iconOff
|
||||
}
|
||||
}
|
||||
tile.updateTile()
|
||||
}
|
||||
@@ -143,19 +151,23 @@ class QuickTileService : TileService() {
|
||||
sender.removeOnPropertyChangedCallback(this)
|
||||
return
|
||||
}
|
||||
if (propertyId != 0 && propertyId != BR.state) return
|
||||
if (propertyId != 0 && propertyId != BR.state)
|
||||
return
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OnTunnelChangedCallback : OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
|
||||
if (propertyId != 0 && propertyId != BR.lastUsedTunnel) return
|
||||
if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
|
||||
return
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WireGuard/QuickTileService"
|
||||
var isAdded: Boolean = false
|
||||
private set
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean
|
||||
|
||||
fun removeOnSelectedTunnelChangedListener(
|
||||
listener: OnSelectedTunnelChangedListener) {
|
||||
listener: OnSelectedTunnelChangedListener
|
||||
) {
|
||||
selectionChangeRegistry.remove(listener)
|
||||
}
|
||||
|
||||
@@ -77,17 +78,17 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
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"
|
||||
|
||||
@@ -112,19 +112,21 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
binding.shareFab.setOnClickListener {
|
||||
revokeLastUri()
|
||||
val key = KeyPair().privateKey.toHex()
|
||||
LOGS[key] = rawLogBytes()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,22 +142,26 @@ 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 fun rawLogBytes() : ByteArray {
|
||||
private suspend fun rawLogBytes(): ByteArray {
|
||||
val builder = StringBuilder()
|
||||
for (i in 0 until rawLogLines.size()) {
|
||||
builder.append(rawLogLines[i])
|
||||
builder.append('\n')
|
||||
withContext(Dispatchers.IO) {
|
||||
for (i in 0 until rawLogLines.size()) {
|
||||
builder.append(rawLogLines[i])
|
||||
builder.append('\n')
|
||||
}
|
||||
}
|
||||
return builder.toString().toByteArray(Charsets.UTF_8)
|
||||
}
|
||||
@@ -175,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) {
|
||||
@@ -283,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"
|
||||
}
|
||||
@@ -306,7 +315,7 @@ class LogViewerActivity : AppCompatActivity() {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -317,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()
|
||||
@@ -340,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
|
||||
|
||||
@@ -354,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
|
||||
|
||||
@@ -77,6 +77,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_action_edit -> {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.detail_container, TunnelEditorFragment())
|
||||
@@ -91,12 +92,15 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
|
||||
newTunnel: ObservableTunnel?): Boolean {
|
||||
override fun onSelectedTunnelChanged(
|
||||
oldTunnel: ObservableTunnel?,
|
||||
newTunnel: ObservableTunnel?
|
||||
): Boolean {
|
||||
val fragmentManager = supportFragmentManager
|
||||
if (fragmentManager.isStateSaved) {
|
||||
return false
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
*/
|
||||
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
|
||||
@@ -14,6 +16,7 @@ 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
|
||||
@@ -47,7 +50,13 @@ class SettingsActivity : AppCompatActivity() {
|
||||
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)
|
||||
@@ -62,9 +71,9 @@ class SettingsActivity : AppCompatActivity() {
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -211,12 +211,13 @@ class TvMainActivity : AppCompatActivity() {
|
||||
try {
|
||||
tunnelFileImportResultLauncher.launch("*/*")
|
||||
} catch (_: Throwable) {
|
||||
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()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,6 +360,7 @@ class TvMainActivity : AppCompatActivity() {
|
||||
binding.tunnelList.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
filesRoot.get()?.isNotEmpty() == true -> {
|
||||
files.clear()
|
||||
filesRoot.set("")
|
||||
@@ -372,7 +374,7 @@ class TvMainActivity : AppCompatActivity() {
|
||||
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()) {
|
||||
|
||||
@@ -40,9 +40,9 @@ class FileConfigStore(private val context: Context) : ConfigStore {
|
||||
|
||||
override fun enumerate(): Set<String> {
|
||||
return context.fileList()
|
||||
.filter { it.endsWith(".conf") }
|
||||
.map { it.substring(0, it.length - ".conf".length) }
|
||||
.toSet()
|
||||
.filter { it.endsWith(".conf") }
|
||||
.map { it.substring(0, it.length - ".conf".length) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
private fun fileFor(name: String): File {
|
||||
|
||||
@@ -47,9 +47,11 @@ object BindingAdapters {
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("items", "layout", "fragment")
|
||||
fun <E> setItems(view: LinearLayout,
|
||||
oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
|
||||
newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?) {
|
||||
fun <E> setItems(
|
||||
view: LinearLayout,
|
||||
oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
|
||||
newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?
|
||||
) {
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
return
|
||||
var listener: ItemChangeListener<E>? = ListenerUtil.getListener(view, R.id.item_change_listener)
|
||||
@@ -73,9 +75,11 @@ object BindingAdapters {
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("items", "layout")
|
||||
fun <E> setItems(view: LinearLayout,
|
||||
oldList: Iterable<E>?, oldLayoutId: Int,
|
||||
newList: Iterable<E>?, newLayoutId: Int) {
|
||||
fun <E> setItems(
|
||||
view: LinearLayout,
|
||||
oldList: Iterable<E>?, oldLayoutId: Int,
|
||||
newList: Iterable<E>?, newLayoutId: Int
|
||||
) {
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
return
|
||||
view.removeAllViews()
|
||||
@@ -93,11 +97,13 @@ object BindingAdapters {
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter(requireAll = false, value = ["items", "layout", "configurationHandler"])
|
||||
fun <K, E : Keyed<out K>> setItems(view: RecyclerView,
|
||||
oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
|
||||
@Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
|
||||
newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
|
||||
newRowConfigurationHandler: RowConfigurationHandler<*, *>?) {
|
||||
fun <K, E : Keyed<out K>> setItems(
|
||||
view: RecyclerView,
|
||||
oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
|
||||
@Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
|
||||
newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
|
||||
newRowConfigurationHandler: RowConfigurationHandler<*, *>?
|
||||
) {
|
||||
if (view.layoutManager == null)
|
||||
view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false)
|
||||
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||
@@ -123,16 +129,20 @@ object BindingAdapters {
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("onBeforeCheckedChanged")
|
||||
fun setOnBeforeCheckedChanged(view: ToggleSwitch,
|
||||
listener: OnBeforeCheckedChangeListener?) {
|
||||
fun setOnBeforeCheckedChanged(
|
||||
view: ToggleSwitch,
|
||||
listener: OnBeforeCheckedChangeListener?
|
||||
) {
|
||||
view.setOnBeforeCheckedChangeListener(listener)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BindingAdapter("onFocusChange")
|
||||
fun setOnFocusChange(view: EditText,
|
||||
listener: View.OnFocusChangeListener?) {
|
||||
view.setOnFocusChangeListener(listener)
|
||||
fun setOnFocusChange(
|
||||
view: EditText,
|
||||
listener: View.OnFocusChangeListener?
|
||||
) {
|
||||
view.onFocusChangeListener = listener
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -61,8 +61,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
for (i in positionStart until positionStart + itemCount) {
|
||||
@@ -75,8 +77,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
for (i in positionStart until positionStart + itemCount)
|
||||
@@ -86,8 +90,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int,
|
||||
toPosition: Int, itemCount: Int) {
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>, fromPosition: Int,
|
||||
toPosition: Int, itemCount: Int
|
||||
) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
val views = arrayOfNulls<View>(itemCount)
|
||||
@@ -99,8 +105,10 @@ internal class ItemChangeListener<T>(private val container: ViewGroup, private v
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int) {
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>, positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
val listener = weakListener.get()
|
||||
if (listener != null) {
|
||||
listener.container.removeViews(positionStart, itemCount)
|
||||
|
||||
@@ -49,7 +49,8 @@ class AppListDialogFragment : DialogFragment() {
|
||||
packageInfos.forEach {
|
||||
val packageName = it.packageName
|
||||
val appInfo = it.applicationInfo
|
||||
val appData = ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
|
||||
val appData =
|
||||
ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
|
||||
applicationData.add(appData)
|
||||
appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
@@ -143,10 +144,12 @@ class AppListDialogFragment : DialogFragment() {
|
||||
selectedApps.add(data.packageName)
|
||||
}
|
||||
}
|
||||
setFragmentResult(REQUEST_SELECTION, bundleOf(
|
||||
setFragmentResult(
|
||||
REQUEST_SELECTION, bundleOf(
|
||||
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
|
||||
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
|
||||
))
|
||||
)
|
||||
)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||
val view = view
|
||||
if (view != null)
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(view.findViewById(R.id.create_fab))
|
||||
.show()
|
||||
.setAnchorView(view.findViewById(R.id.create_fab))
|
||||
.show()
|
||||
else
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, message, e)
|
||||
|
||||
@@ -37,6 +37,7 @@ class ConfigNamingDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val configText = requireArguments().getString(KEY_CONFIG_TEXT)
|
||||
|
||||
@@ -40,8 +40,10 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
|
||||
menuInflater.inflate(R.menu.tunnel_detail, menu)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
|
||||
binding?.executePendingBindings()
|
||||
@@ -110,16 +112,18 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
|
||||
val statistics = tunnel.getStatisticsAsync()
|
||||
for (i in 0 until binding.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
?: continue
|
||||
val publicKey = peer.item!!.publicKey
|
||||
val peerStats = statistics.peer(publicKey)
|
||||
if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) {
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
} else {
|
||||
peer.transferText.text = getString(R.string.transfer_rx_tx,
|
||||
peer.transferText.text = getString(
|
||||
R.string.transfer_rx_tx,
|
||||
QuantityFormatter.formatBytes(peerStats.rxBytes),
|
||||
QuantityFormatter.formatBytes(peerStats.txBytes))
|
||||
QuantityFormatter.formatBytes(peerStats.txBytes)
|
||||
)
|
||||
peer.transferLabel.visibility = View.VISIBLE
|
||||
peer.transferText.visibility = View.VISIBLE
|
||||
}
|
||||
@@ -135,7 +139,7 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
|
||||
} catch (e: Throwable) {
|
||||
for (i in 0 until binding.peersLayout.childCount) {
|
||||
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
|
||||
?: continue
|
||||
?: continue
|
||||
peer.transferLabel.visibility = View.GONE
|
||||
peer.transferText.visibility = View.GONE
|
||||
peer.latestHandshakeLabel.visibility = View.GONE
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
@@ -67,16 +66,14 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.config_editor, menu)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
|
||||
binding?.apply {
|
||||
@@ -103,8 +100,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
|
||||
val focusedView = activity.currentFocus
|
||||
if (focusedView != null) {
|
||||
val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
inputManager?.hideSoftInputFromWindow(
|
||||
focusedView.windowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS
|
||||
)
|
||||
}
|
||||
parentFragmentManager.popBackStackImmediate()
|
||||
|
||||
@@ -138,6 +137,7 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
|
||||
onTunnelCreated(null, e)
|
||||
}
|
||||
}
|
||||
|
||||
tunnel!!.name != binding!!.name -> {
|
||||
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||
try {
|
||||
@@ -147,6 +147,7 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
|
||||
onTunnelRenamed(tunnel!!, newConfig, e)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||
try {
|
||||
@@ -202,8 +203,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
|
||||
newTunnel: ObservableTunnel?) {
|
||||
override fun onSelectedTunnelChanged(
|
||||
oldTunnel: ObservableTunnel?,
|
||||
newTunnel: ObservableTunnel?
|
||||
) {
|
||||
tunnel = newTunnel
|
||||
if (binding == null) return
|
||||
binding!!.config = ConfigProxy()
|
||||
@@ -240,8 +243,10 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
|
||||
throwable: Throwable?) {
|
||||
private suspend fun onTunnelRenamed(
|
||||
renamedTunnel: ObservableTunnel, newConfig: Config,
|
||||
throwable: Throwable?
|
||||
) {
|
||||
val ctx = activity ?: Application.get()
|
||||
if (throwable == null) {
|
||||
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
|
||||
@@ -298,13 +303,15 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
|
||||
haveShownKeys = true
|
||||
showPrivateKey(edit)
|
||||
}
|
||||
|
||||
is BiometricAuthenticator.Result.Failure -> {
|
||||
Snackbar.make(
|
||||
binding!!.mainContainer,
|
||||
it.message,
|
||||
Snackbar.LENGTH_SHORT
|
||||
binding!!.mainContainer,
|
||||
it.message,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
is BiometricAuthenticator.Result.Cancelled -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ class TunnelListFragment : BaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private val snackbarUpdateShower = SnackbarUpdateShower(this)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (savedInstanceState != null) {
|
||||
@@ -91,8 +93,10 @@ class TunnelListFragment : BaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
binding = TunnelListFragmentBinding.inflate(inflater, container, false)
|
||||
val bottomSheet = AddTunnelsSheet()
|
||||
@@ -105,26 +109,29 @@ class TunnelListFragment : BaseFragment() {
|
||||
AddTunnelsSheet.REQUEST_CREATE -> {
|
||||
startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java))
|
||||
}
|
||||
|
||||
AddTunnelsSheet.REQUEST_IMPORT -> {
|
||||
tunnelFileImportResultLauncher.launch("*/*")
|
||||
}
|
||||
|
||||
AddTunnelsSheet.REQUEST_SCAN -> {
|
||||
qrImportResultLauncher.launch(ScanOptions()
|
||||
qrImportResultLauncher.launch(
|
||||
ScanOptions()
|
||||
.setOrientationLocked(false)
|
||||
.setBeepEnabled(false)
|
||||
.setPrompt(getString(R.string.qr_code_hint)))
|
||||
.setPrompt(getString(R.string.qr_code_hint))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
bottomSheet.showNow(childFragmentManager, "BOTTOM_SHEET")
|
||||
}
|
||||
executePendingBindings()
|
||||
snackbarUpdateShower.attach(mainContainer, createFab)
|
||||
}
|
||||
backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() }
|
||||
backPressedCallback?.isEnabled = false
|
||||
|
||||
SnackbarUpdateShower.attachToActivity(requireActivity(), binding?.mainContainer!!, binding?.createFab)
|
||||
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
@@ -191,8 +198,8 @@ class TunnelListFragment : BaseFragment() {
|
||||
val binding = binding
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(binding.createFab)
|
||||
.show()
|
||||
.setAnchorView(binding.createFab)
|
||||
.show()
|
||||
else
|
||||
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -234,6 +241,7 @@ class TunnelListFragment : BaseFragment() {
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_action_select_all -> {
|
||||
lifecycleScope.launch {
|
||||
val tunnels = Application.getTunnelManager().getTunnels()
|
||||
@@ -243,6 +251,7 @@ class TunnelListFragment : BaseFragment() {
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -308,7 +317,7 @@ class TunnelListFragment : BaseFragment() {
|
||||
private fun animateFab(view: View?, show: Boolean) {
|
||||
view ?: return
|
||||
val animation = AnimationUtils.loadAnimation(
|
||||
context, if (show) R.anim.scale_up else R.anim.scale_down
|
||||
context, if (show) R.anim.scale_up else R.anim.scale_down
|
||||
)
|
||||
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
|
||||
@@ -21,10 +21,10 @@ import kotlinx.coroutines.withContext
|
||||
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
||||
*/
|
||||
class ObservableTunnel internal constructor(
|
||||
private val manager: TunnelManager,
|
||||
private var name: String,
|
||||
config: Config?,
|
||||
state: Tunnel.State
|
||||
private val manager: TunnelManager,
|
||||
private var name: String,
|
||||
config: Config?,
|
||||
state: Tunnel.State
|
||||
) : BaseObservable(), Keyed<String>, Tunnel {
|
||||
override val key
|
||||
get() = name
|
||||
|
||||
@@ -140,7 +140,8 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||
if (previouslyRunning.isEmpty()) return
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }.awaitAll()
|
||||
tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }
|
||||
.awaitAll()
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import android.util.AttributeSet
|
||||
import android.widget.Toast
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.wireguard.android.BuildConfig
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.updater.Updater
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
|
||||
class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
@@ -23,7 +23,7 @@ class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(cont
|
||||
|
||||
override fun onClick() {
|
||||
/* Google Play Store forbids links to our donation page. */
|
||||
if (BuildConfig.IS_GOOGLE_PLAY) {
|
||||
if (Updater.installerIsGooglePlay(context)) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.donate_title)
|
||||
.setMessage(R.string.donate_google_play_disappointment)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.preference
|
||||
|
||||
import android.app.StatusBarManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.preference.Preference
|
||||
import com.wireguard.android.QuickTileService
|
||||
import com.wireguard.android.R
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
class QuickTilePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
|
||||
override fun getSummary() = context.getString(R.string.quick_settings_tile_add_summary)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.quick_settings_tile_add_title)
|
||||
|
||||
override fun onClick() {
|
||||
val statusBarManager = context.getSystemService(StatusBarManager::class.java)
|
||||
statusBarManager.requestAddTileService(
|
||||
ComponentName(context, QuickTileService::class.java),
|
||||
context.getString(R.string.quick_settings_tile_action),
|
||||
Icon.createWithResource(context, R.drawable.ic_tile),
|
||||
context.mainExecutor
|
||||
) {
|
||||
when (it) {
|
||||
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
|
||||
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED -> {
|
||||
parent?.removePreference(this)
|
||||
--preferenceManager.preferenceScreen.initialExpandedChildrenCount
|
||||
}
|
||||
StatusBarManager.TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE,
|
||||
StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
|
||||
StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
|
||||
StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
|
||||
StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
|
||||
StatusBarManager.TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE ->
|
||||
Toast.makeText(context, context.getString(R.string.quick_settings_tile_add_failure, it), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,14 +70,16 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
|
||||
val message = context.getString(R.string.zip_export_error, error)
|
||||
Log.e(TAG, message, e)
|
||||
Snackbar.make(
|
||||
activity.findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG).show()
|
||||
activity.findViewById(android.R.id.content),
|
||||
message, Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSummary() = if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
|
||||
override fun getSummary() =
|
||||
if (exportedFilePath == null) context.getString(R.string.zip_export_summary) else context.getString(R.string.zip_export_success, exportedFilePath)
|
||||
|
||||
override fun getTitle() = context.getString(R.string.zip_export_title)
|
||||
|
||||
@@ -91,13 +93,15 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
|
||||
isEnabled = false
|
||||
exportZip()
|
||||
}
|
||||
|
||||
is BiometricAuthenticator.Result.Failure -> {
|
||||
Snackbar.make(
|
||||
activity.findViewById(android.R.id.content),
|
||||
it.message,
|
||||
Snackbar.LENGTH_SHORT
|
||||
activity.findViewById(android.R.id.content),
|
||||
it.message,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
is BiometricAuthenticator.Result.Cancelled -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
|
||||
package com.wireguard.android.updater
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.wireguard.android.BuildConfig
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.util.ErrorMessages
|
||||
import com.wireguard.android.util.QuantityFormatter
|
||||
@@ -21,15 +24,20 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object SnackbarUpdateShower {
|
||||
private class SwapableSnackbar(activity: FragmentActivity, view: View, anchor: View?) {
|
||||
val actionSnackbar = makeSnackbar(activity, view, anchor)
|
||||
val statusSnackbar = makeSnackbar(activity, view, anchor)
|
||||
var showingAction: Boolean = false
|
||||
var showingStatus: Boolean = false
|
||||
class SnackbarUpdateShower(private val fragment: Fragment) {
|
||||
private var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
|
||||
private val intentLauncher = fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
lastUserIntervention?.markAsDone()
|
||||
}
|
||||
|
||||
private fun makeSnackbar(activity: FragmentActivity, view: View, anchor: View?): Snackbar {
|
||||
val snackbar = Snackbar.make(activity, view, "", Snackbar.LENGTH_INDEFINITE)
|
||||
private class SwapableSnackbar(fragment: Fragment, view: View, anchor: View?) {
|
||||
private val actionSnackbar = makeSnackbar(fragment, view, anchor)
|
||||
private val statusSnackbar = makeSnackbar(fragment, view, anchor)
|
||||
private var showingAction: Boolean = false
|
||||
private var showingStatus: Boolean = false
|
||||
|
||||
private fun makeSnackbar(fragment: Fragment, view: View, anchor: View?): Snackbar {
|
||||
val snackbar = Snackbar.make(fragment.requireContext(), view, "", Snackbar.LENGTH_INDEFINITE)
|
||||
if (anchor != null)
|
||||
snackbar.anchorView = anchor
|
||||
snackbar.setTextMaxLines(6)
|
||||
@@ -42,11 +50,10 @@ object SnackbarUpdateShower {
|
||||
override fun onDismissed(snackbar: Snackbar?, @DismissEvent event: Int) {
|
||||
super.onDismissed(snackbar, event)
|
||||
if (event == DISMISS_EVENT_MANUAL || event == DISMISS_EVENT_ACTION ||
|
||||
(snackbar == actionSnackbar && !showingAction) ||
|
||||
(snackbar == statusSnackbar && !showingStatus)
|
||||
(snackbar == actionSnackbar && !showingAction) || (snackbar == statusSnackbar && !showingStatus)
|
||||
)
|
||||
return
|
||||
activity.lifecycleScope.launch {
|
||||
fragment.lifecycleScope.launch {
|
||||
delay(5.seconds)
|
||||
snackbar?.show()
|
||||
}
|
||||
@@ -88,17 +95,9 @@ object SnackbarUpdateShower {
|
||||
}
|
||||
}
|
||||
|
||||
fun attachToActivity(activity: FragmentActivity, view: View, anchor: View?) {
|
||||
if (BuildConfig.IS_GOOGLE_PLAY)
|
||||
return
|
||||
|
||||
val snackbar = SwapableSnackbar(activity, view, anchor)
|
||||
val context = activity.applicationContext
|
||||
|
||||
var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
|
||||
val intentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
lastUserIntervention?.markAsDone()
|
||||
}
|
||||
fun attach(view: View, anchor: View?) {
|
||||
val snackbar = SwapableSnackbar(fragment, view, anchor)
|
||||
val context = fragment.requireContext()
|
||||
|
||||
Updater.state.onEach { progress ->
|
||||
when (progress) {
|
||||
@@ -106,10 +105,7 @@ object SnackbarUpdateShower {
|
||||
snackbar.dismiss()
|
||||
|
||||
is Updater.Progress.Available ->
|
||||
snackbar.showAction(
|
||||
context.getString(R.string.updater_avalable),
|
||||
context.getString(R.string.updater_action)
|
||||
) {
|
||||
snackbar.showAction(context.getString(R.string.updater_avalable), context.getString(R.string.updater_action)) {
|
||||
progress.update()
|
||||
}
|
||||
|
||||
@@ -145,16 +141,33 @@ object SnackbarUpdateShower {
|
||||
}
|
||||
|
||||
is Updater.Progress.Failure -> {
|
||||
snackbar.showText(
|
||||
context.getString(
|
||||
R.string.updater_failure,
|
||||
ErrorMessages[progress.error]
|
||||
)
|
||||
)
|
||||
snackbar.showText(context.getString(R.string.updater_failure, ErrorMessages[progress.error]))
|
||||
delay(5.seconds)
|
||||
progress.retry()
|
||||
}
|
||||
|
||||
is Updater.Progress.Corrupt -> {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.updater_corrupt_title)
|
||||
.setMessage(R.string.updater_corrupt_message)
|
||||
.setPositiveButton(R.string.updater_corrupt_navigate) { _, _ ->
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(progress.downloadUrl)
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: Throwable) {
|
||||
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}.setCancelable(false).setOnDismissListener {
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
intent.addCategory(Intent.CATEGORY_HOME)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
System.exit(0)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}.launchIn(activity.lifecycleScope)
|
||||
}.launchIn(fragment.lifecycleScope)
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@
|
||||
*/
|
||||
package com.wireguard.android.updater
|
||||
|
||||
import android.Manifest
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
@@ -19,6 +21,7 @@ import com.wireguard.android.Application
|
||||
import com.wireguard.android.BuildConfig
|
||||
import com.wireguard.android.activity.MainActivity
|
||||
import com.wireguard.android.util.UserKnobs
|
||||
import com.wireguard.android.util.applicationScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -44,22 +47,37 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object Updater {
|
||||
private const val TAG = "WireGuard/Updater"
|
||||
private const val LATEST_VERSION_URL =
|
||||
"https://download.wireguard.com/android-client/latest.sig"
|
||||
private const val APK_PATH_URL = "https://download.wireguard.com/android-client/%s"
|
||||
private val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID.removeSuffix(".debug") + "-"
|
||||
private const val UPDATE_URL_FMT = "https://download.wireguard.com/android-client/%s"
|
||||
private const val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID + "-"
|
||||
private const val APK_NAME_SUFFIX = ".apk"
|
||||
private const val RELEASE_PUBLIC_KEY_BASE64 =
|
||||
"RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
|
||||
private val CURRENT_VERSION = Version(BuildConfig.VERSION_NAME.removeSuffix("-debug"))
|
||||
private val LATEST_FILE = if (BuildConfig.DEBUG) "latest-debug.sig" else "latest.sig"
|
||||
private val RELEASE_PUBLIC_KEY_BASE64 =
|
||||
if (BuildConfig.DEBUG) "RWTKFCNaLimMkuolGwAw12XT6sx+nnS7KE1wmhJ7YHXvAPedtPR/rofU"
|
||||
else "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
|
||||
|
||||
private val updaterScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
private fun installer(context: Context): String = try {
|
||||
val packageName = context.packageName
|
||||
val pm = context.packageManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getInstallerPackageName(packageName) ?: ""
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
""
|
||||
}
|
||||
|
||||
fun installerIsGooglePlay(context: Context): Boolean = installer(context) == "com.android.vending"
|
||||
|
||||
sealed class Progress {
|
||||
object Complete : Progress()
|
||||
class Available(val version: String) : Progress() {
|
||||
fun update() {
|
||||
Application.getCoroutineScope().launch {
|
||||
applicationScope.launch {
|
||||
UserKnobs.setUpdaterNewerVersionConsented(version)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +101,7 @@ object Updater {
|
||||
}
|
||||
|
||||
fun markAsDone() {
|
||||
Application.getCoroutineScope().launch {
|
||||
applicationScope.launch {
|
||||
if (installerActive())
|
||||
return@launch
|
||||
delay(7.seconds)
|
||||
@@ -101,6 +119,11 @@ object Updater {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Corrupt(private val betterFile: String?) : Progress() {
|
||||
val downloadUrl: String
|
||||
get() = UPDATE_URL_FMT.format(betterFile ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
private val mutableState = MutableStateFlow<Progress>(Progress.Complete)
|
||||
@@ -200,7 +223,7 @@ object Updater {
|
||||
}
|
||||
|
||||
private fun checkForUpdates(): Update? {
|
||||
val connection = URL(LATEST_VERSION_URL).openConnection() as HttpURLConnection
|
||||
val connection = URL(UPDATE_URL_FMT.format(LATEST_FILE)).openConnection() as HttpURLConnection
|
||||
connection.setRequestProperty("User-Agent", Application.USER_AGENT)
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK)
|
||||
@@ -219,12 +242,7 @@ object Updater {
|
||||
val receiver = InstallReceiver()
|
||||
val context = Application.get().applicationContext
|
||||
val pendingIntent = withContext(Dispatchers.Main) {
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
receiver,
|
||||
IntentFilter(receiver.sessionId),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
ContextCompat.registerReceiver(context, receiver, IntentFilter(receiver.sessionId), ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
@@ -241,24 +259,20 @@ object Updater {
|
||||
}
|
||||
|
||||
emitProgress(Progress.Downloading(0UL, 0UL), true)
|
||||
val connection =
|
||||
URL(APK_PATH_URL.format(update.fileName)).openConnection() as HttpURLConnection
|
||||
val connection = URL(UPDATE_URL_FMT.format(update.fileName)).openConnection() as HttpURLConnection
|
||||
connection.setRequestProperty("User-Agent", Application.USER_AGENT)
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK)
|
||||
throw IOException("Update could not be fetched: ${connection.responseCode}")
|
||||
|
||||
var downloadedByteLen: ULong = 0UL
|
||||
val totalByteLen =
|
||||
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong()
|
||||
.toULong()
|
||||
val totalByteLen = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong().toULong()
|
||||
val fileBytes = ByteArray(1024 * 32 /* 32 KiB */)
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
|
||||
|
||||
val installer = context.packageManager.packageInstaller
|
||||
val params =
|
||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
params.setAppPackageName(context.packageName) /* Enforces updates; disallows new apps. */
|
||||
@@ -316,25 +330,17 @@ object Updater {
|
||||
if (sessionId != intent.action)
|
||||
return
|
||||
|
||||
when (val status =
|
||||
intent.getIntExtra(
|
||||
PackageInstaller.EXTRA_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE_INVALID
|
||||
)) {
|
||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE_INVALID)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
|
||||
val userIntervention = IntentCompat.getParcelableExtra(
|
||||
intent,
|
||||
Intent.EXTRA_INTENT,
|
||||
Intent::class.java
|
||||
)!!
|
||||
Application.getCoroutineScope().launch {
|
||||
val userIntervention = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)!!
|
||||
applicationScope.launch {
|
||||
emitProgress(Progress.NeedsUserIntervention(userIntervention, id))
|
||||
}
|
||||
}
|
||||
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
Application.getCoroutineScope().launch {
|
||||
applicationScope.launch {
|
||||
emitProgress(Progress.Complete)
|
||||
}
|
||||
context.applicationContext.unregisterReceiver(this)
|
||||
@@ -346,10 +352,8 @@ object Updater {
|
||||
context.applicationContext.packageManager.packageInstaller.abandonSession(id)
|
||||
} catch (_: SecurityException) {
|
||||
}
|
||||
val message =
|
||||
intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
?: "Installation error $status"
|
||||
Application.getCoroutineScope().launch {
|
||||
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status"
|
||||
applicationScope.launch {
|
||||
val e = Exception(message)
|
||||
Log.e(TAG, "Update failure", e)
|
||||
emitProgress(Progress.Failure(e))
|
||||
@@ -361,13 +365,31 @@ object Updater {
|
||||
}
|
||||
|
||||
fun monitorForUpdates() {
|
||||
if (BuildConfig.IS_GOOGLE_PLAY)
|
||||
val context = Application.get()
|
||||
|
||||
if (installerIsGooglePlay(context))
|
||||
return
|
||||
|
||||
if (!if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
|
||||
} else {
|
||||
context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
|
||||
}.requestedPermissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES)
|
||||
) {
|
||||
updaterScope.launch {
|
||||
val update = try {
|
||||
checkForUpdates()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
emitProgress(Progress.Corrupt(update?.fileName))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
updaterScope.launch {
|
||||
if (UserKnobs.updaterNewerVersionSeen.firstOrNull()
|
||||
?.let { Version(it) > CURRENT_VERSION } == true
|
||||
)
|
||||
if (UserKnobs.updaterNewerVersionSeen.firstOrNull()?.let { Version(it) > CURRENT_VERSION } == true)
|
||||
return@launch
|
||||
|
||||
var waitTime = 15
|
||||
@@ -387,41 +409,28 @@ object Updater {
|
||||
}
|
||||
|
||||
UserKnobs.updaterNewerVersionSeen.onEach { ver ->
|
||||
if (ver != null && Version(ver) > CURRENT_VERSION && UserKnobs.updaterNewerVersionConsented.firstOrNull()
|
||||
?.let { Version(it) > CURRENT_VERSION } != true
|
||||
if (
|
||||
ver != null &&
|
||||
Version(ver) > CURRENT_VERSION &&
|
||||
UserKnobs.updaterNewerVersionConsented.firstOrNull()?.let { Version(it) > CURRENT_VERSION } != true
|
||||
)
|
||||
emitProgress(Progress.Available(ver))
|
||||
}.launchIn(Application.getCoroutineScope())
|
||||
}.launchIn(applicationScope)
|
||||
|
||||
UserKnobs.updaterNewerVersionConsented.onEach { ver ->
|
||||
if (ver != null && Version(ver) > CURRENT_VERSION)
|
||||
updaterScope.launch {
|
||||
downloadAndUpdateWrapErrors()
|
||||
}
|
||||
}.launchIn(Application.getCoroutineScope())
|
||||
}.launchIn(applicationScope)
|
||||
}
|
||||
|
||||
class AppUpdatedReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (BuildConfig.IS_GOOGLE_PLAY)
|
||||
return
|
||||
|
||||
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED)
|
||||
return
|
||||
|
||||
val installer = try {
|
||||
val packageName = context.packageName
|
||||
val pm = context.packageManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getInstallerPackageName(packageName) ?: ""
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
""
|
||||
}
|
||||
if (installer != context.packageName)
|
||||
if (installer(context) != context.packageName)
|
||||
return
|
||||
|
||||
/* TODO: does not work because of restrictions placed on broadcast receivers. */
|
||||
|
||||
@@ -13,5 +13,5 @@ object AdminKnobs {
|
||||
private val restrictions: RestrictionsManager? = Application.get().getSystemService()
|
||||
val disableConfigExport: Boolean
|
||||
get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
|
||||
?: false
|
||||
?: false
|
||||
}
|
||||
|
||||
@@ -31,25 +31,29 @@ object BiometricAuthenticator {
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
@StringRes dialogTitleRes: Int,
|
||||
fragment: Fragment,
|
||||
callback: (Result) -> Unit
|
||||
@StringRes dialogTitleRes: Int,
|
||||
fragment: Fragment,
|
||||
callback: (Result) -> Unit
|
||||
) {
|
||||
val authCallback = object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
|
||||
callback(when (errorCode) {
|
||||
BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
|
||||
Result.Cancelled
|
||||
callback(
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
|
||||
Result.Cancelled
|
||||
}
|
||||
|
||||
BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
|
||||
BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
|
||||
Result.HardwareUnavailableOrDisabled
|
||||
}
|
||||
|
||||
else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
|
||||
}
|
||||
BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
|
||||
BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
|
||||
Result.HardwareUnavailableOrDisabled
|
||||
}
|
||||
else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
@@ -64,9 +68,9 @@ object BiometricAuthenticator {
|
||||
}
|
||||
val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(fragment.getString(dialogTitleRes))
|
||||
.setAllowedAuthenticators(allowedAuthenticators)
|
||||
.build()
|
||||
.setTitle(fragment.getString(dialogTitleRes))
|
||||
.setAllowedAuthenticators(allowedAuthenticators)
|
||||
.build()
|
||||
if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
} else {
|
||||
|
||||
@@ -46,9 +46,9 @@ class DownloadsFileSaver(private val context: ComponentActivity) {
|
||||
contentValues.put(MediaColumns.DISPLAY_NAME, name)
|
||||
contentValues.put(MediaColumns.MIME_TYPE, mimeType)
|
||||
val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
val contentStream = contentResolver.openOutputStream(contentUri)
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
?: throw IOException(context.getString(R.string.create_downloads_file_error))
|
||||
@Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
|
||||
var path: String? = null
|
||||
if (cursor != null) {
|
||||
|
||||
@@ -22,47 +22,47 @@ import java.net.InetAddress
|
||||
|
||||
object ErrorMessages {
|
||||
private val BCE_REASON_MAP = mapOf(
|
||||
BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key,
|
||||
BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number,
|
||||
BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value,
|
||||
BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute,
|
||||
BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section,
|
||||
BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error,
|
||||
BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute,
|
||||
BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section
|
||||
BadConfigException.Reason.INVALID_KEY to R.string.bad_config_reason_invalid_key,
|
||||
BadConfigException.Reason.INVALID_NUMBER to R.string.bad_config_reason_invalid_number,
|
||||
BadConfigException.Reason.INVALID_VALUE to R.string.bad_config_reason_invalid_value,
|
||||
BadConfigException.Reason.MISSING_ATTRIBUTE to R.string.bad_config_reason_missing_attribute,
|
||||
BadConfigException.Reason.MISSING_SECTION to R.string.bad_config_reason_missing_section,
|
||||
BadConfigException.Reason.SYNTAX_ERROR to R.string.bad_config_reason_syntax_error,
|
||||
BadConfigException.Reason.UNKNOWN_ATTRIBUTE to R.string.bad_config_reason_unknown_attribute,
|
||||
BadConfigException.Reason.UNKNOWN_SECTION to R.string.bad_config_reason_unknown_section
|
||||
)
|
||||
private val BE_REASON_MAP = mapOf(
|
||||
BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error,
|
||||
BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error,
|
||||
BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error,
|
||||
BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error,
|
||||
BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error,
|
||||
BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error,
|
||||
BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error,
|
||||
BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure
|
||||
BackendException.Reason.UNKNOWN_KERNEL_MODULE_NAME to R.string.module_version_error,
|
||||
BackendException.Reason.WG_QUICK_CONFIG_ERROR_CODE to R.string.tunnel_config_error,
|
||||
BackendException.Reason.TUNNEL_MISSING_CONFIG to R.string.no_config_error,
|
||||
BackendException.Reason.VPN_NOT_AUTHORIZED to R.string.vpn_not_authorized_error,
|
||||
BackendException.Reason.UNABLE_TO_START_VPN to R.string.vpn_start_error,
|
||||
BackendException.Reason.TUN_CREATION_ERROR to R.string.tun_create_error,
|
||||
BackendException.Reason.GO_ACTIVATION_ERROR_CODE to R.string.tunnel_on_error,
|
||||
BackendException.Reason.DNS_RESOLUTION_FAILURE to R.string.tunnel_dns_failure
|
||||
)
|
||||
private val KFE_FORMAT_MAP = mapOf(
|
||||
Key.Format.BASE64 to R.string.key_length_explanation_base64,
|
||||
Key.Format.BINARY to R.string.key_length_explanation_binary,
|
||||
Key.Format.HEX to R.string.key_length_explanation_hex
|
||||
Key.Format.BASE64 to R.string.key_length_explanation_base64,
|
||||
Key.Format.BINARY to R.string.key_length_explanation_binary,
|
||||
Key.Format.HEX to R.string.key_length_explanation_hex
|
||||
)
|
||||
private val KFE_TYPE_MAP = mapOf(
|
||||
KeyFormatException.Type.CONTENTS to R.string.key_contents_error,
|
||||
KeyFormatException.Type.LENGTH to R.string.key_length_error
|
||||
KeyFormatException.Type.CONTENTS to R.string.key_contents_error,
|
||||
KeyFormatException.Type.LENGTH to R.string.key_length_error
|
||||
)
|
||||
private val PE_CLASS_MAP = mapOf(
|
||||
InetAddress::class.java to R.string.parse_error_inet_address,
|
||||
InetEndpoint::class.java to R.string.parse_error_inet_endpoint,
|
||||
InetNetwork::class.java to R.string.parse_error_inet_network,
|
||||
Int::class.java to R.string.parse_error_integer
|
||||
InetAddress::class.java to R.string.parse_error_inet_address,
|
||||
InetEndpoint::class.java to R.string.parse_error_inet_endpoint,
|
||||
InetNetwork::class.java to R.string.parse_error_inet_network,
|
||||
Int::class.java to R.string.parse_error_integer
|
||||
)
|
||||
private val RSE_REASON_MAP = mapOf(
|
||||
RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root,
|
||||
RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error,
|
||||
RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error,
|
||||
RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error,
|
||||
RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error,
|
||||
RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error
|
||||
RootShellException.Reason.NO_ROOT_ACCESS to R.string.error_root,
|
||||
RootShellException.Reason.SHELL_MARKER_COUNT_ERROR to R.string.shell_marker_count_error,
|
||||
RootShellException.Reason.SHELL_EXIT_STATUS_READ_ERROR to R.string.shell_exit_status_read_error,
|
||||
RootShellException.Reason.SHELL_START_ERROR to R.string.shell_start_error,
|
||||
RootShellException.Reason.CREATE_BIN_DIR_ERROR to R.string.create_bin_dir_error,
|
||||
RootShellException.Reason.CREATE_TEMP_DIR_ERROR to R.string.create_temp_dir_error
|
||||
)
|
||||
|
||||
operator fun get(throwable: Throwable?): String {
|
||||
@@ -80,21 +80,27 @@ object ErrorMessages {
|
||||
val explanation = getBadConfigExceptionExplanation(resources, rootCause)
|
||||
resources.getString(R.string.bad_config_error, reason, context) + explanation
|
||||
}
|
||||
|
||||
rootCause is BackendException -> {
|
||||
resources.getString(BE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
|
||||
}
|
||||
|
||||
rootCause is RootShellException -> {
|
||||
resources.getString(RSE_REASON_MAP.getValue(rootCause.reason), *rootCause.format)
|
||||
}
|
||||
|
||||
rootCause is NotFoundException -> {
|
||||
resources.getString(R.string.error_no_qr_found)
|
||||
}
|
||||
|
||||
rootCause is ChecksumException -> {
|
||||
resources.getString(R.string.error_qr_checksum)
|
||||
}
|
||||
|
||||
rootCause.localizedMessage != null -> {
|
||||
rootCause.localizedMessage!!
|
||||
}
|
||||
|
||||
else -> {
|
||||
val errorType = rootCause.javaClass.simpleName
|
||||
resources.getString(R.string.generic_error, errorType)
|
||||
@@ -102,8 +108,10 @@ object ErrorMessages {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBadConfigExceptionExplanation(resources: Resources,
|
||||
bce: BadConfigException): String {
|
||||
private fun getBadConfigExceptionExplanation(
|
||||
resources: Resources,
|
||||
bce: BadConfigException
|
||||
): String {
|
||||
if (bce.cause is KeyFormatException) {
|
||||
val kfe = bce.cause as KeyFormatException?
|
||||
if (kfe!!.type == KeyFormatException.Type.LENGTH) return resources.getString(KFE_FORMAT_MAP.getValue(kfe.format))
|
||||
@@ -120,8 +128,10 @@ object ErrorMessages {
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun getBadConfigExceptionReason(resources: Resources,
|
||||
bce: BadConfigException): String {
|
||||
private fun getBadConfigExceptionReason(
|
||||
resources: Resources,
|
||||
bce: BadConfigException
|
||||
): String {
|
||||
if (bce.cause is KeyFormatException) {
|
||||
val kfe = bce.cause as KeyFormatException?
|
||||
return resources.getString(KFE_TYPE_MAP.getValue(kfe!!.type))
|
||||
@@ -137,7 +147,8 @@ object ErrorMessages {
|
||||
var cause = throwable
|
||||
while (cause.cause != null) {
|
||||
if (cause is BadConfigException || cause is BackendException ||
|
||||
cause is RootShellException) break
|
||||
cause is RootShellException
|
||||
) break
|
||||
val nextCause = cause.cause!!
|
||||
if (nextCause is RemoteException) break
|
||||
cause = nextCause
|
||||
|
||||
@@ -25,7 +25,7 @@ val Any.applicationScope: CoroutineScope
|
||||
|
||||
val Preference.activity: SettingsActivity
|
||||
get() = context as? SettingsActivity
|
||||
?: throw IllegalStateException("Failed to resolve SettingsActivity")
|
||||
?: throw IllegalStateException("Failed to resolve SettingsActivity")
|
||||
|
||||
val Preference.lifecycleScope: CoroutineScope
|
||||
get() = activity.lifecycleScope
|
||||
|
||||
@@ -55,11 +55,13 @@ class QrCodeFromFileScanner(
|
||||
multFactor = originalWidth.toFloat() / originalHeight.toFloat()
|
||||
newWidth = (newHeight * multFactor).toInt()
|
||||
}
|
||||
|
||||
originalWidth > originalHeight -> {
|
||||
newWidth = scaledSize
|
||||
multFactor = originalHeight.toFloat() / originalWidth.toFloat()
|
||||
newHeight = (newWidth * multFactor).toInt()
|
||||
}
|
||||
|
||||
originalHeight == originalWidth -> {
|
||||
newHeight = scaledSize
|
||||
newWidth = scaledSize
|
||||
|
||||
@@ -15,7 +15,6 @@ import com.wireguard.android.Application
|
||||
import com.wireguard.android.R
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
object QuantityFormatter {
|
||||
fun formatBytes(bytes: Long): String {
|
||||
|
||||
@@ -24,7 +24,6 @@ import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.ArrayList
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@@ -135,12 +134,16 @@ object TunnelImporter {
|
||||
message = context.getString(R.string.import_success, tunnels[0].name)
|
||||
else if (tunnels.isEmpty() && throwables.size == 1)
|
||||
else if (throwables.isEmpty())
|
||||
message = context.resources.getQuantityString(R.plurals.import_total_success,
|
||||
tunnels.size, tunnels.size)
|
||||
message = context.resources.getQuantityString(
|
||||
R.plurals.import_total_success,
|
||||
tunnels.size, tunnels.size
|
||||
)
|
||||
else if (!throwables.isEmpty())
|
||||
message = context.resources.getQuantityString(R.plurals.import_partial_success,
|
||||
tunnels.size + throwables.size,
|
||||
tunnels.size, tunnels.size + throwables.size)
|
||||
message = context.resources.getQuantityString(
|
||||
R.plurals.import_partial_success,
|
||||
tunnels.size + throwables.size,
|
||||
tunnels.size, tunnels.size + throwables.size
|
||||
)
|
||||
|
||||
messageCallback(message)
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ class ConfigProxy : Parcelable {
|
||||
val resolvedPeers: MutableCollection<Peer> = ArrayList()
|
||||
peers.forEach { resolvedPeers.add(it.resolve()) }
|
||||
return Config.Builder()
|
||||
.setInterface(`interface`.resolve())
|
||||
.addPeers(resolvedPeers)
|
||||
.build()
|
||||
.setInterface(`interface`.resolve())
|
||||
.addPeers(resolvedPeers)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
|
||||
@@ -16,8 +16,6 @@ import com.wireguard.config.Attribute
|
||||
import com.wireguard.config.BadConfigException
|
||||
import com.wireguard.config.Peer
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.ArrayList
|
||||
import java.util.LinkedHashSet
|
||||
|
||||
class PeerProxy : BaseObservable, Parcelable {
|
||||
private val dnsRoutes: MutableList<String?> = ArrayList()
|
||||
@@ -240,24 +238,32 @@ class PeerProxy : BaseObservable, Parcelable {
|
||||
peerProxy.setTotalPeers(sender.size)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(sender: ObservableList<PeerProxy?>,
|
||||
positionStart: Int, itemCount: Int) {
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<PeerProxy?>,
|
||||
positionStart: Int, itemCount: Int
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(sender: ObservableList<PeerProxy?>,
|
||||
positionStart: Int, itemCount: Int) {
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<PeerProxy?>,
|
||||
positionStart: Int, itemCount: Int
|
||||
) {
|
||||
onChanged(sender)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(sender: ObservableList<PeerProxy?>,
|
||||
fromPosition: Int, toPosition: Int,
|
||||
itemCount: Int) {
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<PeerProxy?>,
|
||||
fromPosition: Int, toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(sender: ObservableList<PeerProxy?>,
|
||||
positionStart: Int, itemCount: Int) {
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<PeerProxy?>,
|
||||
positionStart: Int, itemCount: Int
|
||||
) {
|
||||
onChanged(sender)
|
||||
}
|
||||
}
|
||||
@@ -276,12 +282,12 @@ class PeerProxy : BaseObservable, Parcelable {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<PeerProxy> = PeerProxyCreator()
|
||||
private val IPV4_PUBLIC_NETWORKS = setOf(
|
||||
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
||||
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
||||
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
||||
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
||||
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
||||
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
||||
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
||||
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
||||
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
||||
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
||||
)
|
||||
private val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ import com.wireguard.crypto.Key
|
||||
* InputFilter for entering WireGuard private/public keys encoded with base64.
|
||||
*/
|
||||
class KeyInputFilter : InputFilter {
|
||||
override fun filter(source: CharSequence,
|
||||
sStart: Int, sEnd: Int,
|
||||
dest: Spanned,
|
||||
dStart: Int, dEnd: Int): CharSequence? {
|
||||
override fun filter(
|
||||
source: CharSequence,
|
||||
sStart: Int, sEnd: Int,
|
||||
dest: Spanned,
|
||||
dStart: Int, dEnd: Int
|
||||
): CharSequence? {
|
||||
var replacement: SpannableStringBuilder? = null
|
||||
var rIndex = 0
|
||||
val dLength = dest.length
|
||||
@@ -26,8 +28,9 @@ class KeyInputFilter : InputFilter {
|
||||
// Restrict characters to the base64 character set.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if ((dIndex + 1 < Key.Format.BASE64.length && isAllowed(c) ||
|
||||
dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
|
||||
dLength + (sIndex - sStart) < Key.Format.BASE64.length) {
|
||||
dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
|
||||
dLength + (sIndex - sStart) < Key.Format.BASE64.length
|
||||
) {
|
||||
++rIndex
|
||||
} else {
|
||||
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
|
||||
|
||||
@@ -11,10 +11,10 @@ import android.widget.RelativeLayout
|
||||
import com.wireguard.android.R
|
||||
|
||||
class MultiselectableRelativeLayout @JvmOverloads constructor(
|
||||
context: Context? = null,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
defStyleRes: Int = 0
|
||||
context: Context? = null,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
defStyleRes: Int = 0
|
||||
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
|
||||
private var multiselected = false
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ import com.wireguard.android.backend.Tunnel
|
||||
* InputFilter for entering WireGuard configuration names (Linux interface names).
|
||||
*/
|
||||
class NameInputFilter : InputFilter {
|
||||
override fun filter(source: CharSequence,
|
||||
sStart: Int, sEnd: Int,
|
||||
dest: Spanned,
|
||||
dStart: Int, dEnd: Int): CharSequence? {
|
||||
override fun filter(
|
||||
source: CharSequence,
|
||||
sStart: Int, sEnd: Int,
|
||||
dest: Spanned,
|
||||
dStart: Int, dEnd: Int
|
||||
): CharSequence? {
|
||||
var replacement: SpannableStringBuilder? = null
|
||||
var rIndex = 0
|
||||
val dLength = dest.length
|
||||
@@ -26,7 +28,8 @@ class NameInputFilter : InputFilter {
|
||||
// Restrict characters to those valid in interfaces.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if (dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c) &&
|
||||
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
|
||||
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH
|
||||
) {
|
||||
++rIndex
|
||||
} else {
|
||||
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
|
||||
|
||||
@@ -35,10 +35,10 @@ class SlashDrawable(private val mDrawable: Drawable) : Drawable() {
|
||||
val radiusX = scale(CORNER_RADIUS, width)
|
||||
val radiusY = scale(CORNER_RADIUS, height)
|
||||
updateRect(
|
||||
scale(LEFT, width),
|
||||
scale(TOP, height),
|
||||
scale(RIGHT, width),
|
||||
scale(TOP + mCurrentSlashLength, height)
|
||||
scale(LEFT, width),
|
||||
scale(TOP, height),
|
||||
scale(RIGHT, width),
|
||||
scale(TOP + mCurrentSlashLength, height)
|
||||
)
|
||||
mPath.reset()
|
||||
// Draw the slash vertically
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
app:state_multiselected="true">
|
||||
<color android:color="?attr/colorSurfaceVariant" />
|
||||
</item>
|
||||
<item
|
||||
android:state_activated="true">
|
||||
<item android:state_activated="true">
|
||||
<color android:color="?attr/colorControlHighlight" />
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.widget.NameInputFilter" />
|
||||
</data>
|
||||
|
||||
@@ -24,7 +25,8 @@
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textNoSuggestions|textVisiblePassword"
|
||||
app:filter="@{NameInputFilter.newInstance()}">
|
||||
<requestFocus/>
|
||||
|
||||
<requestFocus />
|
||||
</com.google.android.material.textfield.TextInputEditText>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
@@ -60,11 +60,11 @@
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:layout_marginStart="@dimen/tunnel_list_placeholder_margin"
|
||||
android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="@dimen/tunnel_list_placeholder_margin"
|
||||
android:layout_marginEnd="@dimen/tunnel_list_placeholder_margin"
|
||||
android:text="@string/tunnel_list_placeholder"
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
unqualifiedResLocale=en-US
|
||||
@@ -119,6 +119,7 @@
|
||||
<string name="error_down">Fehler beim Abschalten des Tunnels: %s</string>
|
||||
<string name="error_fetching_apps">Fehler beim Abrufen der App-Liste: %s</string>
|
||||
<string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string>
|
||||
<string name="error_prepare">Fehler beim Vorbereiten des Tunnels: %s</string>
|
||||
<string name="error_up">Fehler beim Starten des Tunnels: %s</string>
|
||||
<string name="exclude_private_ips">Private IPs ausschließen</string>
|
||||
<string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string>
|
||||
@@ -138,6 +139,8 @@
|
||||
<string name="key_length_explanation_base64">: WireGuard base64-Schlüssel müssen 44 Zeichen enthalten (32 Bytes)</string>
|
||||
<string name="key_length_explanation_binary">: WireGuard-Schlüssel müssen 32 Bytes groß sein</string>
|
||||
<string name="key_length_explanation_hex">: WireGuard Hex-Schlüssel müssen 64 Zeichen (32 Bytes) groß sein</string>
|
||||
<string name="latest_handshake">Letzter Handshake</string>
|
||||
<string name="latest_handshake_ago">vor %s</string>
|
||||
<string name="listen_port">Eingangs-Port</string>
|
||||
<string name="log_export_error">Konnte Protokoll nicht exportieren: %s</string>
|
||||
<string name="log_export_subject">WireGuard Android Protokolldatei</string>
|
||||
@@ -216,6 +219,7 @@
|
||||
<string name="tunnel_create_success">Tunnel „%s “ erfolgreich erstellt</string>
|
||||
<string name="tunnel_error_already_exists">Tunnel „%s“ existiert bereits</string>
|
||||
<string name="tunnel_error_invalid_name">Ungültiger Name</string>
|
||||
<string name="tunnel_list_placeholder">Füge einen Tunnel mit der Schaltfläche unten hinzu</string>
|
||||
<string name="tunnel_name">Tunnelname</string>
|
||||
<string name="tunnel_on_error">Tunnel kann nicht eingeschaltet werden (wgTurnOn gab %d zurück)</string>
|
||||
<string name="tunnel_dns_failure">DNS-Hostname kann nicht aufgelöst werden: „%s“</string>
|
||||
@@ -224,6 +228,13 @@
|
||||
<string name="type_name_go_userspace">Go userspace</string>
|
||||
<string name="type_name_kernel_module">Kernelmodul</string>
|
||||
<string name="unknown_error">Unbekannter Fehler</string>
|
||||
<string name="updater_avalable">Ein Anwendungsupdate ist verfügbar. Bitte jetzt aktualisieren.</string>
|
||||
<string name="updater_action">Download & Update</string>
|
||||
<string name="updater_rechecking">Update-Metadaten abrufen…</string>
|
||||
<string name="updater_download_progress">Update wird heruntergeladen: %1$s / %2$s (%3$.2f%%)</string>
|
||||
<string name="updater_download_progress_nototal">Update wird heruntergeladen: %s</string>
|
||||
<string name="updater_installing">Installiere Update…</string>
|
||||
<string name="updater_failure">Fehler beim Aktualisieren: %s. Versuche es in Kürze erneut…</string>
|
||||
<string name="version_summary">%1$s backend %2$s</string>
|
||||
<string name="version_summary_checking">Überprüfe %s Backend-Version</string>
|
||||
<string name="version_summary_unknown">Unbekannte %s Version</string>
|
||||
|
||||
@@ -229,6 +229,13 @@ Aitäh veelkord sinu panuse eest.</string>
|
||||
<string name="type_name_go_userspace">Go kasutajamaa</string>
|
||||
<string name="type_name_kernel_module">Tuumamoodul</string>
|
||||
<string name="unknown_error">Tundmatu viga</string>
|
||||
<string name="updater_avalable">Rakenduse uuendus on saadaval. Palun uuenda nüüd.</string>
|
||||
<string name="updater_action">Laadi alla ja uuenda</string>
|
||||
<string name="updater_rechecking">Uuenduse andmete laadimine…</string>
|
||||
<string name="updater_download_progress">Uuenduse allalaadimine: %1$s / %2$s (%3$.2f%%)</string>
|
||||
<string name="updater_download_progress_nototal">Uuenduse allalaadimine: %s</string>
|
||||
<string name="updater_installing">Uuenduse paigaldamine…</string>
|
||||
<string name="updater_failure">Uuendamine ebaõnnestus: %s. Uus katse hetke pärast…</string>
|
||||
<string name="version_summary">%1$s taustsüsteem %2$s</string>
|
||||
<string name="version_summary_checking">Kontrollin %s taustsüsteemi versiooni</string>
|
||||
<string name="version_summary_unknown">Tundmatu %s versioon</string>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="WireGuardTheme" parent="Theme.Material3.Dark">
|
||||
<item name="colorPrimary">@color/md_theme_dark_primary</item>
|
||||
<item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
|
||||
|
||||
@@ -254,6 +254,13 @@
|
||||
<string name="type_name_go_userspace">Пользовательское пространство Go</string>
|
||||
<string name="type_name_kernel_module">Модуль ядра</string>
|
||||
<string name="unknown_error">Неизвестная ошибка</string>
|
||||
<string name="updater_avalable">Доступно обновление приложения. Пожалуйста, обновите.</string>
|
||||
<string name="updater_action">Загрузить и установить</string>
|
||||
<string name="updater_rechecking">Получение метаданных обновления…</string>
|
||||
<string name="updater_download_progress">Загрузка обновления: %1$s / %2$s (%3$.2f%%)</string>
|
||||
<string name="updater_download_progress_nototal">Загрузка обновления: %s</string>
|
||||
<string name="updater_installing">Установка обновления…</string>
|
||||
<string name="updater_failure">Ошибка обновления: %s. Повторите попытку…</string>
|
||||
<string name="version_summary">%1$s бэкенд %2$s</string>
|
||||
<string name="version_summary_checking">Проверка версии бэкэнда %s</string>
|
||||
<string name="version_summary_unknown">Неизвестная версия %s</string>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="AppTheme" parent="AppThemeBase">
|
||||
<item name="android:statusBarColor">?android:colorBackground</item>
|
||||
<item name="android:windowLightStatusBar">@bool/light_status_bar</item>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="AppTheme" parent="AppThemeBase">
|
||||
<item name="android:statusBarColor">?android:colorBackground</item>
|
||||
<item name="android:windowLightStatusBar">@bool/light_status_bar</item>
|
||||
|
||||
@@ -215,6 +215,13 @@
|
||||
<string name="type_name_go_userspace">Go userspace</string>
|
||||
<string name="type_name_kernel_module">Kernel module</string>
|
||||
<string name="unknown_error">未知错误</string>
|
||||
<string name="updater_avalable">有可用的应用程序更新。请立即更新。</string>
|
||||
<string name="updater_action">下载 & 更新</string>
|
||||
<string name="updater_rechecking">正获取更新元数据…</string>
|
||||
<string name="updater_download_progress">正在下载更新: %1$s / %2$s (%3$.2f%%)</string>
|
||||
<string name="updater_download_progress_nototal">正在下载更新: %s</string>
|
||||
<string name="updater_installing">安装更新中...</string>
|
||||
<string name="updater_failure">更新失败: %s。不久后将重试…</string>
|
||||
<string name="version_summary">%1$s backend %2$s</string>
|
||||
<string name="version_summary_checking">正在检查 %s backend 版本</string>
|
||||
<string name="version_summary_unknown">未知的 %s 版本</string>
|
||||
|
||||
@@ -185,6 +185,10 @@
|
||||
<string name="private_key">Private key</string>
|
||||
<string name="public_key">Public key</string>
|
||||
<string name="qr_code_hint">Tip: generate with `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="quick_settings_tile_add_title">Add tile to quick settings panel</string>
|
||||
<string name="quick_settings_tile_add_summary">The shortcut tile toggles the most recent tunnel</string>
|
||||
<string name="quick_settings_tile_add_failure">Unable to add shortcut tile: error %d</string>
|
||||
<string name="quick_settings_tile_action">Toggle tunnel</string>
|
||||
<string name="restore_on_boot_summary_off">Will not bring up enabled tunnels at boot</string>
|
||||
<string name="restore_on_boot_summary_on">Will bring up enabled tunnels at boot</string>
|
||||
<string name="restore_on_boot_title">Restore on boot</string>
|
||||
@@ -236,6 +240,9 @@
|
||||
<string name="updater_download_progress_nototal">Downloading update: %s</string>
|
||||
<string name="updater_installing">Installing update…</string>
|
||||
<string name="updater_failure">Update failure: %s. Will retry momentarily…</string>
|
||||
<string name="updater_corrupt_title">Application Corrupt</string>
|
||||
<string name="updater_corrupt_message">This application is corrupt. Please re-download the APK from the website linked below. After, uninstall this application, and reinstall it from downloaded APK.</string>
|
||||
<string name="updater_corrupt_navigate">Open Website</string>
|
||||
<string name="version_summary">%1$s backend %2$s</string>
|
||||
<string name="version_summary_checking">Checking %s backend version</string>
|
||||
<string name="version_summary_unknown">Unknown %s version</string>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="WireGuardTheme" parent="Theme.Material3.Light">
|
||||
<item name="colorPrimary">@color/md_theme_light_primary</item>
|
||||
<item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
android:summaryOn="@string/restore_on_boot_summary_on"
|
||||
android:title="@string/restore_on_boot_title" />
|
||||
<com.wireguard.android.preference.ZipExporterPreference android:key="zip_exporter" />
|
||||
<com.wireguard.android.preference.QuickTilePreference android:key="quick_tile" />
|
||||
<Preference
|
||||
android:key="log_viewer"
|
||||
android:singleLineTitle="false"
|
||||
@@ -40,6 +41,5 @@
|
||||
android:summaryOff="@string/allow_remote_control_intents_summary_off"
|
||||
android:summaryOn="@string/allow_remote_control_intents_summary_on"
|
||||
android:title="@string/allow_remote_control_intents_title" />
|
||||
<com.wireguard.android.preference.DonatePreference
|
||||
android:singleLineTitle="false" />
|
||||
<com.wireguard.android.preference.DonatePreference android:singleLineTitle="false" />
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user