Compare commits
103 Commits
1.65.4-t7a
...
main
Author | SHA1 | Date |
---|---|---|
Andrea Gottardo | 840a31d74e | 4 days ago |
Andrea Gottardo | b6cacdfd6a | 4 days ago |
Andrea Gottardo | d702d2dab8 | 4 days ago |
Jonathan Nobels | 811641f538 | 1 week ago |
Andrea Gottardo | 9ae30c06bf | 1 week ago |
Andrea Gottardo | 793a83fdc6 | 1 week ago |
Andrea Gottardo | ea928ca971 | 1 week ago |
Andrea Gottardo | 8dc1a13f77 | 1 week ago |
Jonathan Nobels | 196944d168 | 2 weeks ago |
Jonathan Nobels | 0ff6be6345 | 2 weeks ago |
Jonathan Nobels | 634d51c20b | 2 weeks ago |
Fred Silberberg | 864cc35bd4 | 2 weeks ago |
Jonathan Nobels | 23805e9d00 | 2 weeks ago |
Jonathan Nobels | 5b121c1876 | 2 weeks ago |
Jonathan Nobels | 80864fec12 | 3 weeks ago |
Jonathan Nobels | ef21753763 | 3 weeks ago |
Jonathan Nobels | 0e82e54ffb | 3 weeks ago |
Jonathan Nobels | 64fca2a712 | 4 weeks ago |
Jonathan Nobels | a74e30d4e2 | 4 weeks ago |
Jonathan Nobels | 2788cf7ee5 | 4 weeks ago |
kari-ts | d7a87e868c | 4 weeks ago |
kari-ts | 15da8f3797 | 4 weeks ago |
kari-ts | 8f62f0da79 | 4 weeks ago |
kari-ts | cbc47791ad | 1 month ago |
kari-ts | a6fd8a8093 | 1 month ago |
kari-ts | 0df6c61eee | 1 month ago |
Andrea Gottardo | 75db9e64c8 | 1 month ago |
kari-ts | e826a173aa | 1 month ago |
kari-ts | a05829b3c0 | 1 month ago |
kari-ts | 72f35cd318 | 1 month ago |
kari-ts | 4fa86dbf03 | 1 month ago |
Jonathan Nobels | 77c2d924ee | 1 month ago |
Jonathan Nobels | b37492a547 | 1 month ago |
kari-ts | 999c6f2357 | 1 month ago |
Andrea Gottardo | 006b1e6852 | 1 month ago |
kari-ts | 32e29c4efd | 1 month ago |
kari-ts | 9aa3a840de | 1 month ago |
kari-ts | 0ff47f7ab5 | 1 month ago |
kari-ts | 12ad295706 | 1 month ago |
kari-ts | d842ccde22 | 1 month ago |
Andrea Gottardo | cbcc773b98 | 1 month ago |
Andrea Gottardo | cbc0035dfe | 2 months ago |
kari-ts | c47ead9412 | 2 months ago |
Percy Wegmann | 46cdbb7b9b | 2 months ago |
kari-ts | 5476288100 | 2 months ago |
kari-ts | a3b356a81c | 2 months ago |
Percy Wegmann | 411d7b2597 | 2 months ago |
Percy Wegmann | 59a88ffbab | 2 months ago |
kari-ts | f684bf696d | 2 months ago |
Percy Wegmann | 698fb868a7 | 2 months ago |
Andrea Gottardo | 82c17a4d1d | 2 months ago |
Jonathan Nobels | b615eb38b4 | 2 months ago |
Andrea Gottardo | 24d6cc7a08 | 2 months ago |
kari-ts | ec1dc8b0be | 2 months ago |
Percy Wegmann | edb3f5b0c5 | 2 months ago |
kari-ts | 7f66c373ea | 2 months ago |
kari-ts | 2d7d6e1357 | 2 months ago |
Jonathan Nobels | 45fd2e0661 | 2 months ago |
Percy Wegmann | 31b0ec8865 | 2 months ago |
Will Norris | 9703d48f1a | 2 months ago |
Jonathan Nobels | 17ad0c8cc0 | 2 months ago |
Jonathan Nobels | a2471d38cb | 2 months ago |
kari-ts | e6f6d35a99 | 2 months ago |
kari-ts | 5e3236260f | 2 months ago |
kari-ts | d330726ba1 | 2 months ago |
Andrea Gottardo | 0c0853a962 | 2 months ago |
James Tucker | 3f864b28c7 | 2 months ago |
kari-ts | 22c129ee1c | 2 months ago |
Andrea Gottardo | 427e2d29b4 | 2 months ago |
kari-ts | 1c0aef5418 | 2 months ago |
kari-ts | 39628be8a6 | 2 months ago |
Brad Fitzpatrick | 9dda2cc470 | 2 months ago |
kari-ts | a6bc2244b6 | 2 months ago |
kari-ts | 24dd83090c | 2 months ago |
kari-ts | ad3b6a5a64 | 2 months ago |
Percy Wegmann | 16fa0e9b9e | 2 months ago |
Andrea Gottardo | 88b0af2c9b | 2 months ago |
Andrea Gottardo | 7119424e32 | 2 months ago |
Jonathan Nobels | b06342629f | 2 months ago |
Percy Wegmann | 07d04ca750 | 2 months ago |
Percy Wegmann | 057e25c23d | 2 months ago |
Will Norris | a54ebf75ef | 2 months ago |
Jonathan Nobels | f4d2a277a5 | 2 months ago |
kari-ts | 75e2d8983b | 2 months ago |
kari-ts | bbb3c86fa8 | 2 months ago |
Percy Wegmann | bc8985126d | 2 months ago |
Brad Fitzpatrick | eb8d731a04 | 2 months ago |
kari-ts | 81acaef5b7 | 2 months ago |
kari-ts | 19177df1e2 | 2 months ago |
Praneet Loke | 6197cb9576 | 2 months ago |
kari-ts | 253c116f9b | 2 months ago |
Jonathan Nobels | 1c3af6713c | 2 months ago |
kari-ts | 39d1d0b3c3 | 2 months ago |
Andrea Gottardo | 56da7b66d0 | 2 months ago |
kari-ts | f95428f7fa | 2 months ago |
Percy Wegmann | 0c58841350 | 2 months ago |
Andrea Gottardo | 8a7148c085 | 2 months ago |
Jonathan Nobels | 372af99c53 | 2 months ago |
Andrea Gottardo | a73025b36f | 2 months ago |
Andrea Gottardo | 4d86c1a6f6 | 2 months ago |
Andrea Gottardo | a1d97baeb0 | 3 months ago |
Matt Drollette | 9533db44b7 | 3 months ago |
Andrea Gottardo | 44ac22c29d | 3 months ago |
@ -0,0 +1,36 @@
|
|||||||
|
name: go mod tidy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "release-branch/*"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-go-mod-tidy:
|
||||||
|
runs-on: [ubuntu-latest]
|
||||||
|
timeout-minutes: 8
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
cache: false
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Check 'go mod tidy' is clean
|
||||||
|
run: |
|
||||||
|
./tool/go mod tidy
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go mod tidy'."; exit 1)
|
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
|
distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
# Keep all classes with native methods
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep specific classes from Tink library
|
||||||
|
-keep class com.google.crypto.tink.** { *; }
|
||||||
|
|
||||||
|
# Ignore warnings about missing Error Prone annotations
|
||||||
|
-dontwarn com.google.errorprone.annotations.**
|
||||||
|
|
||||||
|
# Keep Error Prone annotations if referenced
|
||||||
|
-keep class com.google.errorprone.annotations.** { *; }
|
||||||
|
|
||||||
|
# Keep Google HTTP Client classes
|
||||||
|
-keep class com.google.api.client.http.** { *; }
|
||||||
|
-dontwarn com.google.api.client.http.**
|
||||||
|
|
||||||
|
# Keep Joda-Time classes
|
||||||
|
-keep class org.joda.time.** { *; }
|
||||||
|
-dontwarn org.joda.time.**
|
@ -1,28 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn;
|
|
||||||
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
public class Peer extends Fragment {
|
|
||||||
|
|
||||||
private static int resultOK = -1;
|
|
||||||
|
|
||||||
public class RequestCodes {
|
|
||||||
public static final int requestPrepareVPN = 1001;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (requestCode == RequestCodes.requestPrepareVPN) {
|
|
||||||
if (resultCode == resultOK) {
|
|
||||||
App.getApplication().startVPN();
|
|
||||||
} else {
|
|
||||||
App.getApplication().setWantRunning(false);
|
|
||||||
// notify VPN revoked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
package com.tailscale.ipn
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.tailscale.ipn.UninitializedApp.Companion.STATUS_CHANNEL_ID
|
||||||
|
import com.tailscale.ipn.UninitializedApp.Companion.STATUS_EXIT_NODE_FAILURE_NOTIFICATION_ID
|
||||||
|
import com.tailscale.ipn.ui.localapi.Client
|
||||||
|
import com.tailscale.ipn.ui.model.Ipn
|
||||||
|
import com.tailscale.ipn.ui.notifier.Notifier
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
|
||||||
|
class UseExitNodeWorker(
|
||||||
|
appContext: Context,
|
||||||
|
workerParams: WorkerParameters
|
||||||
|
) : CoroutineWorker(appContext, workerParams) {
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val app = UninitializedApp.get()
|
||||||
|
suspend fun runAndGetResult(): String? {
|
||||||
|
val exitNodeName = inputData.getString(EXIT_NODE_NAME)
|
||||||
|
|
||||||
|
val exitNodeId = if (exitNodeName.isNullOrEmpty()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
if (!app.isAbleToStartVPN()) {
|
||||||
|
return app.getString(R.string.vpn_is_not_ready_to_start)
|
||||||
|
}
|
||||||
|
|
||||||
|
val peers =
|
||||||
|
(Notifier.netmap.value
|
||||||
|
?: run { return@runAndGetResult app.getString(R.string.tailscale_is_not_setup) })
|
||||||
|
.Peers ?: run { return@runAndGetResult app.getString(R.string.no_peers_found) }
|
||||||
|
|
||||||
|
val filteredPeers = peers.filter {
|
||||||
|
it.displayName == exitNodeName
|
||||||
|
}.toList()
|
||||||
|
|
||||||
|
if (filteredPeers.isEmpty()) {
|
||||||
|
return app.getString(R.string.no_peers_with_name_found, exitNodeName)
|
||||||
|
} else if (filteredPeers.size > 1) {
|
||||||
|
return app.getString(R.string.multiple_peers_with_name_found, exitNodeName)
|
||||||
|
} else if (!filteredPeers[0].isExitNode) {
|
||||||
|
return app.getString(
|
||||||
|
R.string.peer_with_name_is_not_an_exit_node,
|
||||||
|
exitNodeName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredPeers[0].StableID
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowLanAccess = inputData.getBoolean(ALLOW_LAN_ACCESS, false)
|
||||||
|
val prefsOut = Ipn.MaskedPrefs()
|
||||||
|
prefsOut.ExitNodeID = exitNodeId
|
||||||
|
prefsOut.ExitNodeAllowLANAccess = allowLanAccess
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||||
|
var result: String? = null
|
||||||
|
Client(scope).editPrefs(prefsOut) {
|
||||||
|
result = if (it.isFailure) {
|
||||||
|
it.exceptionOrNull()?.message
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.coroutineContext[Job]?.join()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = runAndGetResult()
|
||||||
|
|
||||||
|
return if (result != null) {
|
||||||
|
val intent =
|
||||||
|
Intent(app, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
val pendingIntent: PendingIntent =
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
app, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(app, STATUS_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentTitle(app.getString(R.string.use_exit_node_intent_failed))
|
||||||
|
.setContentText(result)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
app.notifyStatus(notification)
|
||||||
|
|
||||||
|
Result.failure(Data.Builder().putString(ERROR_KEY, result).build())
|
||||||
|
} else {
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXIT_NODE_NAME = "EXIT_NODE_NAME"
|
||||||
|
const val ALLOW_LAN_ACCESS = "ALLOW_LAN_ACCESS"
|
||||||
|
const val ERROR_KEY = "error"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
class Health {
|
||||||
|
@Serializable
|
||||||
|
data class State(
|
||||||
|
// WarnableCode -> UnhealthyState or null
|
||||||
|
var Warnings: Map<String, UnhealthyState?>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UnhealthyState(
|
||||||
|
var WarnableCode: String,
|
||||||
|
var Severity: Severity,
|
||||||
|
var Title: String,
|
||||||
|
var Text: String,
|
||||||
|
var BrokenSince: String? = null,
|
||||||
|
var Args: Map<String, String>? = null,
|
||||||
|
var DependsOn: List<String>? = null, // an array of WarnableCodes this depends on
|
||||||
|
) {
|
||||||
|
fun hiddenByDependencies(currentWarnableCodes: Set<String>): Boolean {
|
||||||
|
return this.DependsOn?.let {
|
||||||
|
it.any { depWarnableCode -> currentWarnableCodes.contains(depWarnableCode) }
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class Severity {
|
||||||
|
high,
|
||||||
|
medium,
|
||||||
|
low
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.notifier
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.tailscale.ipn.App
|
||||||
|
import com.tailscale.ipn.R
|
||||||
|
import com.tailscale.ipn.UninitializedApp.Companion.notificationManager
|
||||||
|
import com.tailscale.ipn.ui.model.Health
|
||||||
|
import com.tailscale.ipn.ui.model.Health.UnhealthyState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
class HealthNotifier(
|
||||||
|
healthStateFlow: StateFlow<Health.State?>,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val HEALTH_CHANNEL_ID = "tailscale-health"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TAG = "Health"
|
||||||
|
private val ignoredWarnableCodes: Set<String> =
|
||||||
|
setOf(
|
||||||
|
// Ignored on Android because installing unstable takes quite some effort
|
||||||
|
"is-using-unstable-version",
|
||||||
|
|
||||||
|
// Ignored on Android because we already have a dedicated connected/not connected
|
||||||
|
// notification
|
||||||
|
"wantrunning-false")
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
healthStateFlow
|
||||||
|
.distinctUntilChanged { old, new -> old?.Warnings?.count() == new?.Warnings?.count() }
|
||||||
|
.debounce(5000)
|
||||||
|
.collect { health ->
|
||||||
|
Log.d(TAG, "Health updated: ${health?.Warnings?.keys?.sorted()}")
|
||||||
|
health?.Warnings?.let {
|
||||||
|
notifyHealthUpdated(it.values.mapNotNull { it }.toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentWarnings: MutableSet<String> = mutableSetOf()
|
||||||
|
|
||||||
|
private fun notifyHealthUpdated(warnings: Array<UnhealthyState>) {
|
||||||
|
val warningsBeforeAdd = currentWarnings
|
||||||
|
val currentWarnableCodes = warnings.map { it.WarnableCode }.toSet()
|
||||||
|
|
||||||
|
val addedWarnings: MutableSet<String> = mutableSetOf()
|
||||||
|
for (warning in warnings) {
|
||||||
|
if (ignoredWarnableCodes.contains(warning.WarnableCode)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addedWarnings.add(warning.WarnableCode)
|
||||||
|
|
||||||
|
if (this.currentWarnings.contains(warning.WarnableCode)) {
|
||||||
|
// Already notified, skip
|
||||||
|
continue
|
||||||
|
} else if (warning.hiddenByDependencies(currentWarnableCodes)) {
|
||||||
|
// Ignore this warning because a dependency is also unhealthy
|
||||||
|
Log.d(TAG, "Ignoring ${warning.WarnableCode} because of dependency")
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Adding health warning: ${warning.WarnableCode}")
|
||||||
|
this.currentWarnings.add(warning.WarnableCode)
|
||||||
|
this.sendNotification(warning.Title, warning.Text, warning.WarnableCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val warningsToDrop = warningsBeforeAdd.minus(addedWarnings)
|
||||||
|
if (warningsToDrop.isNotEmpty()) {
|
||||||
|
Log.d(TAG, "Dropping health warnings with codes $warningsToDrop")
|
||||||
|
this.removeNotifications(warningsToDrop)
|
||||||
|
}
|
||||||
|
currentWarnings.subtract(warningsToDrop)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendNotification(title: String, text: String, code: String) {
|
||||||
|
Log.d(TAG, "Sending notification for $code")
|
||||||
|
val notification =
|
||||||
|
NotificationCompat.Builder(App.get().applicationContext, HEALTH_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.build()
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
App.get().applicationContext, Manifest.permission.POST_NOTIFICATIONS) !=
|
||||||
|
PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Log.d(TAG, "Notification permission not granted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notificationManager.notify(code.hashCode(), notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeNotifications(codes: Set<String>) {
|
||||||
|
Log.d(TAG, "Removing notifications for $codes")
|
||||||
|
for (code in codes) {
|
||||||
|
notificationManager.cancel(code.hashCode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.util
|
||||||
|
|
||||||
|
import com.tailscale.ipn.ui.model.Ipn
|
||||||
|
|
||||||
|
class AdvertisedRoutesHelper {
|
||||||
|
companion object {
|
||||||
|
fun exitNodeOnFromPrefs(prefs: Ipn.Prefs): Boolean {
|
||||||
|
var v4 = false
|
||||||
|
var v6 = false
|
||||||
|
prefs.AdvertiseRoutes?.forEach {
|
||||||
|
if (it == "0.0.0.0/0") {
|
||||||
|
v4 = true
|
||||||
|
}
|
||||||
|
if (it == "::/0") {
|
||||||
|
v6 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v4 && v6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.util
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.tailscale.ipn.R
|
||||||
|
import com.tailscale.ipn.ui.theme.on
|
||||||
|
|
||||||
|
sealed class ConnectionMode {
|
||||||
|
class NotConnected : ConnectionMode()
|
||||||
|
|
||||||
|
class Derp(val relayName: String) : ConnectionMode()
|
||||||
|
|
||||||
|
class Direct : ConnectionMode()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun titleString(): String {
|
||||||
|
return when (this) {
|
||||||
|
is NotConnected -> stringResource(id = R.string.not_connected)
|
||||||
|
is Derp -> stringResource(R.string.relayed_connection, relayName)
|
||||||
|
is Direct -> stringResource(R.string.direct_connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contentKey(): String {
|
||||||
|
return when (this) {
|
||||||
|
is NotConnected -> "NotConnected"
|
||||||
|
is Derp -> "Derp($relayName)"
|
||||||
|
is Direct -> "Direct"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun iconDrawable(): Int {
|
||||||
|
return when (this) {
|
||||||
|
is NotConnected -> R.drawable.xmark_circle
|
||||||
|
is Derp -> R.drawable.link_off
|
||||||
|
is Direct -> R.drawable.link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun color(): Color {
|
||||||
|
return when (this) {
|
||||||
|
is NotConnected -> MaterialTheme.colorScheme.onPrimary
|
||||||
|
is Derp -> MaterialTheme.colorScheme.error
|
||||||
|
is Direct -> MaterialTheme.colorScheme.on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tailscale.ipn.R
|
||||||
|
import com.tailscale.ipn.ui.theme.listItem
|
||||||
|
import com.tailscale.ipn.ui.util.set
|
||||||
|
import com.tailscale.ipn.ui.viewModel.LoginWithAuthKeyViewModel
|
||||||
|
import com.tailscale.ipn.ui.viewModel.LoginWithCustomControlURLViewModel
|
||||||
|
|
||||||
|
data class LoginViewStrings(
|
||||||
|
var title: String,
|
||||||
|
var explanation: String,
|
||||||
|
var inputTitle: String,
|
||||||
|
var placeholder: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginWithCustomControlURLView(
|
||||||
|
onNavigateHome: BackNavigation,
|
||||||
|
backToSettings: BackNavigation,
|
||||||
|
viewModel: LoginWithCustomControlURLViewModel = LoginWithCustomControlURLViewModel()
|
||||||
|
) {
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
Header(
|
||||||
|
R.string.add_account,
|
||||||
|
onBack = backToSettings,
|
||||||
|
)
|
||||||
|
}) { innerPadding ->
|
||||||
|
val error by viewModel.errorDialog.collectAsState()
|
||||||
|
val strings =
|
||||||
|
LoginViewStrings(
|
||||||
|
title = stringResource(id = R.string.custom_control_menu),
|
||||||
|
explanation = stringResource(id = R.string.custom_control_menu_desc),
|
||||||
|
inputTitle = stringResource(id = R.string.custom_control_url_title),
|
||||||
|
placeholder = stringResource(id = R.string.custom_control_placeholder),
|
||||||
|
)
|
||||||
|
|
||||||
|
error?.let { ErrorDialog(type = it, action = { viewModel.errorDialog.set(null) }) }
|
||||||
|
|
||||||
|
LoginView(
|
||||||
|
innerPadding = innerPadding,
|
||||||
|
strings = strings,
|
||||||
|
onSubmitAction = { viewModel.setControlURL(it, onNavigateHome) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginWithAuthKeyView(
|
||||||
|
onNavigateHome: BackNavigation,
|
||||||
|
backToSettings: BackNavigation,
|
||||||
|
viewModel: LoginWithAuthKeyViewModel = LoginWithAuthKeyViewModel()
|
||||||
|
) {
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
Header(
|
||||||
|
R.string.add_account,
|
||||||
|
onBack = backToSettings,
|
||||||
|
)
|
||||||
|
}) { innerPadding ->
|
||||||
|
val error by viewModel.errorDialog.collectAsState()
|
||||||
|
val strings =
|
||||||
|
LoginViewStrings(
|
||||||
|
title = stringResource(id = R.string.auth_key_title),
|
||||||
|
explanation = stringResource(id = R.string.auth_key_explanation),
|
||||||
|
inputTitle = stringResource(id = R.string.auth_key_input_title),
|
||||||
|
placeholder = stringResource(id = R.string.auth_key_placeholder),
|
||||||
|
)
|
||||||
|
// Show the error overlay if need be
|
||||||
|
error?.let { ErrorDialog(type = it, action = { viewModel.errorDialog.set(null) }) }
|
||||||
|
|
||||||
|
LoginView(
|
||||||
|
innerPadding = innerPadding,
|
||||||
|
strings = strings,
|
||||||
|
onSubmitAction = { viewModel.setAuthKey(it, onNavigateHome) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginView(
|
||||||
|
innerPadding: PaddingValues = PaddingValues(16.dp),
|
||||||
|
strings: LoginViewStrings,
|
||||||
|
onSubmitAction: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var textVal by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(innerPadding)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surface)) {
|
||||||
|
ListItem(
|
||||||
|
colors = MaterialTheme.colorScheme.listItem,
|
||||||
|
headlineContent = { Text(text = strings.title) },
|
||||||
|
supportingContent = { Text(text = strings.explanation) })
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
colors = MaterialTheme.colorScheme.listItem,
|
||||||
|
headlineContent = { Text(text = strings.inputTitle) },
|
||||||
|
supportingContent = {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent),
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
value = textVal,
|
||||||
|
onValueChange = { textVal = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(strings.placeholder, style = MaterialTheme.typography.bodySmall)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
colors = MaterialTheme.colorScheme.listItem,
|
||||||
|
headlineContent = {
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Button(
|
||||||
|
onClick = { onSubmitAction(textVal) },
|
||||||
|
content = { Text(stringResource(id = R.string.add_account_short)) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.viewModel
|
||||||
|
|
||||||
|
import com.tailscale.ipn.ui.util.set
|
||||||
|
import com.tailscale.ipn.ui.view.ErrorDialogType
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
const val AUTH_KEY_LENGTH = 16
|
||||||
|
|
||||||
|
open class CustomLoginViewModel : IpnViewModel() {
|
||||||
|
val errorDialog: StateFlow<ErrorDialogType?> = MutableStateFlow(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginWithAuthKeyViewModel : CustomLoginViewModel() {
|
||||||
|
// Sets the auth key and invokes the login flow
|
||||||
|
fun setAuthKey(authKey: String, onSuccess: () -> Unit) {
|
||||||
|
// The most basic of checks for auth key syntax
|
||||||
|
if (authKey.isEmpty()) {
|
||||||
|
errorDialog.set(ErrorDialogType.INVALID_AUTH_KEY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loginWithAuthKey(authKey) {
|
||||||
|
it.onFailure { errorDialog.set(ErrorDialogType.ADD_PROFILE_FAILED) }
|
||||||
|
it.onSuccess { onSuccess() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginWithCustomControlURLViewModel : CustomLoginViewModel() {
|
||||||
|
// Sets the custom control URL and invokes the login flow
|
||||||
|
fun setControlURL(urlStr: String, onSuccess: () -> Unit) {
|
||||||
|
// Some basic checks that the entered URL is "reasonable". The underlying
|
||||||
|
// localAPIClient will use the default server if we give it a broken URL,
|
||||||
|
// but we can make sure we can construct a URL from the input string and
|
||||||
|
// ensure it has an http/https scheme
|
||||||
|
when (urlStr.startsWith("http") && urlStr.contains("://") && urlStr.length > 7) {
|
||||||
|
false -> {
|
||||||
|
errorDialog.set(ErrorDialogType.INVALID_CUSTOM_URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
true -> {
|
||||||
|
loginWithCustomControlURL(urlStr) {
|
||||||
|
it.onFailure { errorDialog.set(ErrorDialogType.ADD_PROFILE_FAILED) }
|
||||||
|
it.onSuccess { onSuccess() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.viewModel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.tailscale.ipn.App
|
||||||
|
import com.tailscale.ipn.R
|
||||||
|
import com.tailscale.ipn.ui.localapi.Client
|
||||||
|
import com.tailscale.ipn.ui.model.Tailcfg
|
||||||
|
import com.tailscale.ipn.ui.util.ConnectionMode
|
||||||
|
import com.tailscale.ipn.ui.util.set
|
||||||
|
import com.tailscale.ipn.ui.view.roundedString
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
class PingViewModelFactory(private val peer: Tailcfg.Node) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return PingViewModel() as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PingViewModel : ViewModel() {
|
||||||
|
private val TAG = PingViewModel::class.simpleName
|
||||||
|
|
||||||
|
// The timer ticks every second, for a maximum of 10 seconds, hence triggering 10 ping
|
||||||
|
// requests.
|
||||||
|
private val timer =
|
||||||
|
object : CountDownTimer(1000 * 10, 1000) {
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
sendPing()
|
||||||
|
fetchStatusAndUpdateConnectionMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
Log.d(TAG, "Ping timer terminated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The peer to ping.
|
||||||
|
var peer: StateFlow<Tailcfg.Node?> = MutableStateFlow(null)
|
||||||
|
// Whether we are using a relayed or direct connection. Will be NotConnected until the first
|
||||||
|
// PeerStatus value has been fetched. NotConnected is not surfaced to the user.
|
||||||
|
val connectionMode: StateFlow<ConnectionMode> = MutableStateFlow(ConnectionMode.NotConnected())
|
||||||
|
// An error message to display if any request fails. Non-null if an error message must be surfaced
|
||||||
|
// to the user. If a subsequent request succeeds, this property should be set to null again.
|
||||||
|
val errorMessage: StateFlow<String?> = MutableStateFlow(null)
|
||||||
|
// The last latency value in a human-readable format (e.g. "14.5 ms").
|
||||||
|
val lastLatencyValue: StateFlow<String> = MutableStateFlow("")
|
||||||
|
// A list of latency values over time in milliseconds. These are used to plot the latency
|
||||||
|
// values in the chart.
|
||||||
|
var latencyValues: StateFlow<List<Double>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
fun startPing(peer: Tailcfg.Node) {
|
||||||
|
this.peer.set(peer)
|
||||||
|
timer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleDismissal() {
|
||||||
|
timer.cancel()
|
||||||
|
this.peer.set(null)
|
||||||
|
this.connectionMode.set(ConnectionMode.NotConnected())
|
||||||
|
this.lastLatencyValue.set("")
|
||||||
|
this.latencyValues.set(emptyList())
|
||||||
|
this.errorMessage.set(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendPing asks the backend to send one ping to the peer and handles the response.
|
||||||
|
// It checks for any errors in the response Err field. If an error is present, it sets the
|
||||||
|
// errorMessage property to a non-null value and returns. If there is no error, it updates the
|
||||||
|
// lastLatencyValue property with the formatted latency, and adds the latency value to the
|
||||||
|
// latencyValues list.
|
||||||
|
private fun sendPing() {
|
||||||
|
peer.value?.let { peer ->
|
||||||
|
Client(viewModelScope).ping(peer) { response ->
|
||||||
|
response.onSuccess { pingResult ->
|
||||||
|
val error = pingResult.Err
|
||||||
|
if (error.isNotEmpty()) {
|
||||||
|
this.errorMessage.set(error.replaceFirstChar { it.uppercase() })
|
||||||
|
return@onSuccess
|
||||||
|
} else {
|
||||||
|
this.errorMessage.set(null)
|
||||||
|
val latency: Double = pingResult.LatencySeconds * 1000
|
||||||
|
this.lastLatencyValue.set("${latency.roundedString(1)} ms")
|
||||||
|
this.latencyValues.set(this.latencyValues.value + latency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.onFailure { error ->
|
||||||
|
val context: Context = App.get().applicationContext
|
||||||
|
val stringError = error.toString()
|
||||||
|
Log.d(TAG, "Ping request failed: $stringError")
|
||||||
|
if (stringError.contains("timeout")) {
|
||||||
|
this.errorMessage.set(
|
||||||
|
context.getString(
|
||||||
|
R.string.request_timed_out_make_sure_that_is_online, peer.ComputedName))
|
||||||
|
} else {
|
||||||
|
this.errorMessage.set(
|
||||||
|
context.getString(R.string.an_unknown_error_occurred_please_try_again))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchStatusAndUpdateConnectionMode fetches the PeerStatus for the peer and updates the
|
||||||
|
// connectionMode property as soon as a direct connection is finally established.
|
||||||
|
private fun fetchStatusAndUpdateConnectionMode() {
|
||||||
|
Client(viewModelScope).status { statusResult ->
|
||||||
|
statusResult.onSuccess { result ->
|
||||||
|
result.Peer?.let { map ->
|
||||||
|
map[peer.value?.Key]?.let { peerStatus ->
|
||||||
|
val curAddr = peerStatus.CurAddr.orEmpty()
|
||||||
|
val relay = peerStatus.Relay.orEmpty()
|
||||||
|
if (curAddr.isNotEmpty()) {
|
||||||
|
this.connectionMode.set(ConnectionMode.Direct())
|
||||||
|
} else if (relay.isNotEmpty()) {
|
||||||
|
this.connectionMode.set(ConnectionMode.Derp(relayName = relay.uppercase()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statusResult.onFailure { Log.d(TAG, "Failed to fetch status: $it") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,97 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn.ui.viewModel
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.tailscale.ipn.ui.localapi.Client
|
|
||||||
import com.tailscale.ipn.ui.model.Ipn
|
|
||||||
import com.tailscale.ipn.ui.notifier.Notifier
|
|
||||||
import com.tailscale.ipn.ui.util.LoadingIndicator
|
|
||||||
import com.tailscale.ipn.ui.util.set
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class RunExitNodeViewModelFactory() : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
return RunExitNodeViewModel() as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdvertisedRoutesHelper() {
|
|
||||||
companion object {
|
|
||||||
fun exitNodeOnFromPrefs(prefs: Ipn.Prefs): Boolean {
|
|
||||||
var v4 = false
|
|
||||||
var v6 = false
|
|
||||||
prefs.AdvertiseRoutes?.forEach {
|
|
||||||
if (it == "0.0.0.0/0") {
|
|
||||||
v4 = true
|
|
||||||
}
|
|
||||||
if (it == "::/0") {
|
|
||||||
v6 = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v4 && v6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RunExitNodeViewModel() : IpnViewModel() {
|
|
||||||
|
|
||||||
val isRunningExitNode: StateFlow<Boolean> = MutableStateFlow(false)
|
|
||||||
var lastPrefs: Ipn.Prefs? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
Notifier.prefs.stateIn(viewModelScope).collect { prefs ->
|
|
||||||
Log.d("RunExitNode", "prefs: AdvertiseRoutes=" + prefs?.AdvertiseRoutes.toString())
|
|
||||||
prefs?.let {
|
|
||||||
lastPrefs = it
|
|
||||||
isRunningExitNode.set(AdvertisedRoutesHelper.exitNodeOnFromPrefs(it))
|
|
||||||
} ?: run { isRunningExitNode.set(false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRunningExitNode(isOn: Boolean) {
|
|
||||||
LoadingIndicator.start()
|
|
||||||
lastPrefs?.let { currentPrefs ->
|
|
||||||
val newPrefs: Ipn.MaskedPrefs
|
|
||||||
if (isOn) {
|
|
||||||
newPrefs = setZeroRoutes(currentPrefs)
|
|
||||||
} else {
|
|
||||||
newPrefs = removeAllZeroRoutes(currentPrefs)
|
|
||||||
}
|
|
||||||
Client(viewModelScope).editPrefs(newPrefs) { result ->
|
|
||||||
LoadingIndicator.stop()
|
|
||||||
Log.d("RunExitNodeViewModel", "Edited prefs: $result")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setZeroRoutes(prefs: Ipn.Prefs): Ipn.MaskedPrefs {
|
|
||||||
val newRoutes = (removeAllZeroRoutes(prefs).AdvertiseRoutes ?: emptyList()).toMutableList()
|
|
||||||
newRoutes.add("0.0.0.0/0")
|
|
||||||
newRoutes.add("::/0")
|
|
||||||
val newPrefs = Ipn.MaskedPrefs()
|
|
||||||
newPrefs.AdvertiseRoutes = newRoutes
|
|
||||||
return newPrefs
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeAllZeroRoutes(prefs: Ipn.Prefs): Ipn.MaskedPrefs {
|
|
||||||
val newRoutes = emptyList<String>().toMutableList()
|
|
||||||
(prefs.AdvertiseRoutes ?: emptyList()).forEach {
|
|
||||||
if (it != "0.0.0.0/0" && it != "::/0") {
|
|
||||||
newRoutes.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val newPrefs = Ipn.MaskedPrefs()
|
|
||||||
newPrefs.AdvertiseRoutes = newRoutes
|
|
||||||
return newPrefs
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 641 B |
Binary file not shown.
Before Width: | Height: | Size: 406 B |
Binary file not shown.
Before Width: | Height: | Size: 879 B |
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB |
@ -1,41 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="200dp"
|
|
||||||
android:height="200dp"
|
|
||||||
android:viewportWidth="200"
|
|
||||||
android:viewportHeight="200">
|
|
||||||
<path
|
|
||||||
android:pathData="M0,0h200v200h-200z"
|
|
||||||
android:fillColor="#1F1E1E"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M50,62.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillAlpha="0.4"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M87.5,62.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillAlpha="0.4"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M125,62.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillAlpha="0.4"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M50,100a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M87.5,100a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M125,100a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M50,137.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillAlpha="0.4"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M87.5,137.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M125,137.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillAlpha="0.4"/>
|
|
||||||
</vector>
|
|
@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="200dp"
|
|
||||||
android:height="200dp"
|
|
||||||
android:viewportWidth="200"
|
|
||||||
android:viewportHeight="200">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:pathData="M50,62.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#cccccc"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M87.5,62.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#cccccc"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M125,62.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#cccccc"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M50,100a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#222222"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M87.5,100a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#222222"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M125,100a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#222222"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M50,137.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#cccccc"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M87.5,137.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#222222"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M125,137.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
|
||||||
android:fillColor="#cccccc"/>
|
|
||||||
</vector>
|
|
@ -0,0 +1,38 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="100dp"
|
||||||
|
android:height="100dp"
|
||||||
|
android:viewportWidth="100"
|
||||||
|
android:viewportHeight="100">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,12.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M37.5,12.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M75,12.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M-0,50a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M37.5,50a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M75,50a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M-0,87.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M37.5,87.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M75,87.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
</vector>
|
@ -0,0 +1,46 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="100dp"
|
||||||
|
android:height="100dp"
|
||||||
|
android:viewportWidth="100"
|
||||||
|
android:viewportHeight="100">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,12.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M37.5,12.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M75,12.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M-0,50a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:strokeAlpha="0.4"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M37.5,50a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:strokeAlpha="0.4"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M75,50a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:strokeAlpha="0.4"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M-0,87.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M37.5,87.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:strokeAlpha="0.4"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M75,87.5a12.5,12.5 0,1 0,25 0a12.5,12.5 0,1 0,-25 0z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.4"/>
|
||||||
|
</vector>
|
@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
|
||||||
|
|
||||||
|
</vector>
|
@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1 0,1.43 -0.98,2.63 -2.31,2.98l1.46,1.46C20.88,15.61 22,13.95 22,12c0,-2.76 -2.24,-5 -5,-5zM16,11h-2.19l2,2L16,13zM2,4.27l3.11,3.11C3.29,8.12 2,9.91 2,12c0,2.76 2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1 0,-1.59 1.21,-2.9 2.76,-3.07L8.73,11L8,11v2h2.73L13,15.27L13,17h1.73l4.01,4L20,19.74 3.27,3 2,4.27z"/>
|
||||||
|
|
||||||
|
</vector>
|
@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M480,840Q406,840 340.5,811.5Q275,783 226,734Q177,685 148.5,619.5Q120,554 120,480Q120,402 150,334Q180,266 234,217Q246,206 262.5,206.5Q279,207 290,218L508,436Q519,447 519,464Q519,481 508,492Q497,503 480,503Q463,503 452,492L264,304Q234,340 217,384.5Q200,429 200,480Q200,596 282,678Q364,760 480,760Q596,760 678,678Q760,596 760,480Q760,373 691.5,295.5Q623,218 520,204L520,240Q520,257 508.5,268.5Q497,280 480,280Q463,280 451.5,268.5Q440,257 440,240L440,160Q440,143 451.5,131.5Q463,120 480,120Q554,120 619.5,148.5Q685,177 734,226Q783,275 811.5,340.5Q840,406 840,480Q840,554 811.5,619.5Q783,685 734,734Q685,783 619.5,811.5Q554,840 480,840ZM280,520Q263,520 251.5,508.5Q240,497 240,480Q240,463 251.5,451.5Q263,440 280,440Q297,440 308.5,451.5Q320,463 320,480Q320,497 308.5,508.5Q297,520 280,520ZM480,720Q463,720 451.5,708.5Q440,697 440,680Q440,663 451.5,651.5Q463,640 480,640Q497,640 508.5,651.5Q520,663 520,680Q520,697 508.5,708.5Q497,720 480,720ZM680,520Q663,520 651.5,508.5Q640,497 640,480Q640,463 651.5,451.5Q663,440 680,440Q697,440 708.5,451.5Q720,463 720,480Q720,497 708.5,508.5Q697,520 680,520Z"/>
|
||||||
|
|
||||||
|
</vector>
|
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package android.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a mock class for the android.util.Log class. It is used to print log messages to the console.
|
||||||
|
*/
|
||||||
|
public class Log {
|
||||||
|
public static int d(String tag, String msg) {
|
||||||
|
System.out.println("DEBUG: " + tag + ": " + msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int i(String tag, String msg) {
|
||||||
|
System.out.println("INFO: " + tag + ": " + msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int w(String tag, String msg) {
|
||||||
|
System.out.println("WARN: " + tag + ": " + msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int e(String tag, String msg) {
|
||||||
|
System.out.println("ERROR: " + tag + ": " + msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailcale.ipn.ui.util
|
||||||
|
|
||||||
|
import com.tailscale.ipn.ui.util.TimeUtil
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class TimeUtilTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun durationInvalidMsUnits() {
|
||||||
|
val input = "5s10ms"
|
||||||
|
val actual = TimeUtil.duration(input)
|
||||||
|
assertNull("Should return null", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun durationInvalidUsUnits() {
|
||||||
|
val input = "5s10us"
|
||||||
|
val actual = TimeUtil.duration(input)
|
||||||
|
assertNull("Should return null", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun durationTestHappyPath() {
|
||||||
|
val input = arrayOf("1.0y1.0w1.0d1.0h1.0m1.0s", "1s", "1m", "1h", "1d", "1w", "1y")
|
||||||
|
val expectedSeconds =
|
||||||
|
arrayOf((31536000 + 604800 + 86400 + 3600 + 60 + 1), 1, 60, 3600, 86400, 604800, 31536000)
|
||||||
|
val expected = expectedSeconds.map { Duration.ofSeconds(it.toLong()) }
|
||||||
|
val actual = input.map { TimeUtil.duration(it) }
|
||||||
|
assertEquals("Incorrect conversion", expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBadDurationString() {
|
||||||
|
val input = "1..0y1.0w1.0d1.0h1.0m1.0s"
|
||||||
|
val actual = TimeUtil.duration(input)
|
||||||
|
assertNull("Should return null", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBadDInputString() {
|
||||||
|
val input = "1.0yy1.0w1.0d1.0h1.0m1.0s"
|
||||||
|
val actual = TimeUtil.duration(input)
|
||||||
|
assertNull("Should return null", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIgnoreFractionalSeconds() {
|
||||||
|
val input = "10.9s"
|
||||||
|
val expectedSeconds = 10
|
||||||
|
val expected = Duration.ofSeconds(expectedSeconds.toLong())
|
||||||
|
val actual = TimeUtil.duration(input)
|
||||||
|
assertEquals("Should return $expectedSeconds seconds", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = "1.9.22"
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "com.android.tools.build:gradle:8.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
flatDir {
|
|
||||||
dirs 'libs'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
android {
|
|
||||||
ndkVersion "23.1.7779620"
|
|
||||||
compileSdk 33
|
|
||||||
defaultConfig {
|
|
||||||
minSdkVersion 22
|
|
||||||
targetSdkVersion 33
|
|
||||||
versionCode 201
|
|
||||||
versionName "1.61.105-t7429e8912-g12210d3f26b"
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
flavorDimensions "version"
|
|
||||||
productFlavors {
|
|
||||||
fdroid {
|
|
||||||
// The fdroid flavor contains only free dependencies and is suitable
|
|
||||||
// for the F-Droid app store.
|
|
||||||
}
|
|
||||||
play {
|
|
||||||
// The play flavor contains all features and is for the Play Store.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
namespace 'com.tailscale.ipn'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "androidx.core:core:1.9.0"
|
|
||||||
implementation "androidx.browser:browser:1.5.0"
|
|
||||||
implementation "androidx.security:security-crypto:1.1.0-alpha06"
|
|
||||||
implementation "androidx.work:work-runtime:2.8.1"
|
|
||||||
implementation ':ipn@aar'
|
|
||||||
testImplementation "junit:junit:4.12"
|
|
||||||
|
|
||||||
// Non-free dependencies.
|
|
||||||
playImplementation 'com.google.android.gms:play-services-auth:20.7.0'
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
android.defaults.buildfeatures.buildconfig=true
|
|
||||||
android.nonFinalResIds=false
|
|
||||||
android.nonTransitiveRClass=false
|
|
||||||
android.useAndroidX=true
|
|
||||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
|
|
Binary file not shown.
@ -1,6 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
@ -1,183 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright 2015 the original author or authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
##
|
|
||||||
## Gradle start up script for UN*X
|
|
||||||
##
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
PRG="$0"
|
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
|
||||||
ls=`ls -ld "$PRG"`
|
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
|
||||||
PRG="$link"
|
|
||||||
else
|
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=`basename "$0"`
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx80m" "-Xms80m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD="maximum"
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "`uname`" in
|
|
||||||
CYGWIN* )
|
|
||||||
cygwin=true
|
|
||||||
;;
|
|
||||||
Darwin* )
|
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
|
||||||
else
|
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD="java"
|
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
|
||||||
if [ $? -eq 0 ] ; then
|
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
ulimit -n $MAX_FD
|
|
||||||
if [ $? -ne 0 ] ; then
|
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=`expr $i + 1`
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
0) set -- ;;
|
|
||||||
1) set -- "$args0" ;;
|
|
||||||
2) set -- "$args0" "$args1" ;;
|
|
||||||
3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Escape application args
|
|
||||||
save () {
|
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
|
||||||
echo " "
|
|
||||||
}
|
|
||||||
APP_ARGS=`save "$@"`
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
@ -1,103 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx80m" "-Xms80m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:init
|
|
||||||
@rem Get command-line arguments, handling Windows variants
|
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
|
||||||
|
|
||||||
:win9xME_args
|
|
||||||
@rem Slurp the command line arguments.
|
|
||||||
set CMD_LINE_ARGS=
|
|
||||||
set _SKIP=2
|
|
||||||
|
|
||||||
:win9xME_args_slurp
|
|
||||||
if "x%~1" == "x" goto execute
|
|
||||||
|
|
||||||
set CMD_LINE_ARGS=%*
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
|
||||||
exit /b 1
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
@ -1,85 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
|
||||||
|
|
||||||
<!-- Disable input emulation on ChromeOS -->
|
|
||||||
<uses-feature android:name="android.hardware.type.pc" android:required="false"/>
|
|
||||||
|
|
||||||
<!-- Signal support for Android TV -->
|
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
|
||||||
|
|
||||||
<application android:label="Tailscale" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:banner="@drawable/tv_banner"
|
|
||||||
android:name=".App" android:allowBackup="false">
|
|
||||||
<activity android:name="IPNActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/Theme.GioApp"
|
|
||||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
<data android:mimeType="application/*" />
|
|
||||||
<data android:mimeType="audio/*" />
|
|
||||||
<data android:mimeType="image/*" />
|
|
||||||
<data android:mimeType="message/*" />
|
|
||||||
<data android:mimeType="multipart/*" />
|
|
||||||
<data android:mimeType="text/*" />
|
|
||||||
<data android:mimeType="video/*" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="application/*" />
|
|
||||||
<data android:mimeType="audio/*" />
|
|
||||||
<data android:mimeType="image/*" />
|
|
||||||
<data android:mimeType="message/*" />
|
|
||||||
<data android:mimeType="multipart/*" />
|
|
||||||
<data android:mimeType="text/*" />
|
|
||||||
<data android:mimeType="video/*" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<receiver android:name="IPNReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.tailscale.ipn.CONNECT_VPN" />
|
|
||||||
<action android:name="com.tailscale.ipn.DISCONNECT_VPN" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<service android:name=".IPNService"
|
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.net.VpnService"/>
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<service
|
|
||||||
android:name=".QuickToggleService"
|
|
||||||
android:icon="@drawable/ic_tile"
|
|
||||||
android:label="@string/tile_name"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
@ -1,416 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.DownloadManager;
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.app.FragmentTransaction;
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.UiModeManager;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.Signature;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.net.LinkProperties;
|
|
||||||
import android.net.Network;
|
|
||||||
import android.net.NetworkInfo;
|
|
||||||
import android.net.NetworkRequest;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.net.VpnService;
|
|
||||||
import android.view.View;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
|
|
||||||
import java.lang.StringBuilder;
|
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InterfaceAddress;
|
|
||||||
import java.net.NetworkInterface;
|
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences;
|
|
||||||
import androidx.security.crypto.MasterKey;
|
|
||||||
|
|
||||||
import androidx.browser.customtabs.CustomTabsIntent;
|
|
||||||
|
|
||||||
import org.gioui.Gio;
|
|
||||||
|
|
||||||
public class App extends Application {
|
|
||||||
private static final String PEER_TAG = "peer";
|
|
||||||
|
|
||||||
static final String STATUS_CHANNEL_ID = "tailscale-status";
|
|
||||||
static final int STATUS_NOTIFICATION_ID = 1;
|
|
||||||
|
|
||||||
static final String NOTIFY_CHANNEL_ID = "tailscale-notify";
|
|
||||||
static final int NOTIFY_NOTIFICATION_ID = 2;
|
|
||||||
|
|
||||||
private static final String FILE_CHANNEL_ID = "tailscale-files";
|
|
||||||
private static final int FILE_NOTIFICATION_ID = 3;
|
|
||||||
|
|
||||||
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
private ConnectivityManager connectivityManager;
|
|
||||||
public DnsConfig dns = new DnsConfig();
|
|
||||||
public DnsConfig getDnsConfigObj() { return this.dns; }
|
|
||||||
|
|
||||||
@Override public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
// Load and initialize the Go library.
|
|
||||||
Gio.init(this);
|
|
||||||
|
|
||||||
this.connectivityManager = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
||||||
setAndRegisterNetworkCallbacks();
|
|
||||||
|
|
||||||
createNotificationChannel(NOTIFY_CHANNEL_ID, "Notifications", NotificationManagerCompat.IMPORTANCE_DEFAULT);
|
|
||||||
createNotificationChannel(STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW);
|
|
||||||
createNotificationChannel(FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is possible that
|
|
||||||
// this might return an unusuable network, eg a captive portal.
|
|
||||||
private void setAndRegisterNetworkCallbacks() {
|
|
||||||
connectivityManager.requestNetwork(dns.getDNSConfigNetworkRequest(), new ConnectivityManager.NetworkCallback(){
|
|
||||||
@Override
|
|
||||||
public void onAvailable(Network network){
|
|
||||||
super.onAvailable(network);
|
|
||||||
StringBuilder sb = new StringBuilder("");
|
|
||||||
LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
|
|
||||||
List<InetAddress> dnsList = linkProperties.getDnsServers();
|
|
||||||
for (InetAddress ip : dnsList) {
|
|
||||||
sb.append(ip.getHostAddress()).append(" ");
|
|
||||||
}
|
|
||||||
String searchDomains = linkProperties.getDomains();
|
|
||||||
if (searchDomains != null) {
|
|
||||||
sb.append("\n");
|
|
||||||
sb.append(searchDomains);
|
|
||||||
}
|
|
||||||
|
|
||||||
dns.updateDNSFromNetwork(sb.toString());
|
|
||||||
onDnsConfigChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLost(Network network) {
|
|
||||||
super.onLost(network);
|
|
||||||
onDnsConfigChanged();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startVPN() {
|
|
||||||
Intent intent = new Intent(this, IPNService.class);
|
|
||||||
intent.setAction(IPNService.ACTION_REQUEST_VPN);
|
|
||||||
startService(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopVPN() {
|
|
||||||
Intent intent = new Intent(this, IPNService.class);
|
|
||||||
intent.setAction(IPNService.ACTION_STOP_VPN);
|
|
||||||
startService(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// encryptToPref a byte array of data using the Jetpack Security
|
|
||||||
// library and writes it to a global encrypted preference store.
|
|
||||||
public void encryptToPref(String prefKey, String plaintext) throws IOException, GeneralSecurityException {
|
|
||||||
getEncryptedPrefs().edit().putString(prefKey, plaintext).commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// decryptFromPref decrypts a encrypted preference using the Jetpack Security
|
|
||||||
// library and returns the plaintext.
|
|
||||||
public String decryptFromPref(String prefKey) throws IOException, GeneralSecurityException {
|
|
||||||
return getEncryptedPrefs().getString(prefKey, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SharedPreferences getEncryptedPrefs() throws IOException, GeneralSecurityException {
|
|
||||||
MasterKey key = new MasterKey.Builder(this)
|
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return EncryptedSharedPreferences.create(
|
|
||||||
this,
|
|
||||||
"secret_shared_prefs",
|
|
||||||
key,
|
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean autoConnect = false;
|
|
||||||
public boolean vpnReady = false;
|
|
||||||
|
|
||||||
void setTileReady(boolean ready) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QuickToggleService.setReady(this, ready);
|
|
||||||
android.util.Log.d("App", "Set Tile Ready: " + ready + " " + autoConnect);
|
|
||||||
|
|
||||||
vpnReady = ready;
|
|
||||||
if (ready && autoConnect) {
|
|
||||||
startVPN();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setTileStatus(boolean status) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QuickToggleService.setStatus(this, status);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getHostname() {
|
|
||||||
String userConfiguredDeviceName = getUserConfiguredDeviceName();
|
|
||||||
if (!isEmpty(userConfiguredDeviceName)) return userConfiguredDeviceName;
|
|
||||||
|
|
||||||
return getModelName();
|
|
||||||
}
|
|
||||||
|
|
||||||
String getModelName() {
|
|
||||||
String manu = Build.MANUFACTURER;
|
|
||||||
String model = Build.MODEL;
|
|
||||||
// Strip manufacturer from model.
|
|
||||||
int idx = model.toLowerCase().indexOf(manu.toLowerCase());
|
|
||||||
if (idx != -1) {
|
|
||||||
model = model.substring(idx + manu.length());
|
|
||||||
model = model.trim();
|
|
||||||
}
|
|
||||||
return manu + " " + model;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getOSVersion() {
|
|
||||||
return Build.VERSION.RELEASE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get user defined nickname from Settings
|
|
||||||
// returns null if not available
|
|
||||||
private String getUserConfiguredDeviceName() {
|
|
||||||
String nameFromSystemDevice = Settings.Secure.getString(getContentResolver(), "device_name");
|
|
||||||
if (!isEmpty(nameFromSystemDevice)) return nameFromSystemDevice;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEmpty(String str) {
|
|
||||||
return str == null || str.length() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// attachPeer adds a Peer fragment for tracking the Activity
|
|
||||||
// lifecycle.
|
|
||||||
void attachPeer(Activity act) {
|
|
||||||
act.runOnUiThread(new Runnable() {
|
|
||||||
@Override public void run() {
|
|
||||||
FragmentTransaction ft = act.getFragmentManager().beginTransaction();
|
|
||||||
ft.add(new Peer(), PEER_TAG);
|
|
||||||
ft.commit();
|
|
||||||
act.getFragmentManager().executePendingTransactions();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isChromeOS() {
|
|
||||||
return getPackageManager().hasSystemFeature("android.hardware.type.pc");
|
|
||||||
}
|
|
||||||
|
|
||||||
void prepareVPN(Activity act, int reqCode) {
|
|
||||||
act.runOnUiThread(new Runnable() {
|
|
||||||
@Override public void run() {
|
|
||||||
Intent intent = VpnService.prepare(act);
|
|
||||||
if (intent == null) {
|
|
||||||
onVPNPrepared();
|
|
||||||
} else {
|
|
||||||
startActivityForResult(act, intent, reqCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static void startActivityForResult(Activity act, Intent intent, int request) {
|
|
||||||
Fragment f = act.getFragmentManager().findFragmentByTag(PEER_TAG);
|
|
||||||
f.startActivityForResult(intent, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showURL(Activity act, String url) {
|
|
||||||
act.runOnUiThread(new Runnable() {
|
|
||||||
@Override public void run() {
|
|
||||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
|
||||||
int headerColor = 0xff496495;
|
|
||||||
builder.setToolbarColor(headerColor);
|
|
||||||
CustomTabsIntent intent = builder.build();
|
|
||||||
intent.launchUrl(act, Uri.parse(url));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPackageSignatureFingerprint returns the first package signing certificate, if any.
|
|
||||||
byte[] getPackageCertificate() throws Exception {
|
|
||||||
PackageInfo info;
|
|
||||||
info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
|
|
||||||
for (Signature signature : info.signatures) {
|
|
||||||
return signature.toByteArray();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestWriteStoragePermission(Activity act) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
||||||
// We can write files without permission.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ContextCompat.checkSelfPermission(act, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
act.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, IPNActivity.WRITE_STORAGE_RESULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
String insertMedia(String name, String mimeType) throws IOException {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
ContentResolver resolver = getContentResolver();
|
|
||||||
ContentValues contentValues = new ContentValues();
|
|
||||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
|
|
||||||
if (!"".equals(mimeType)) {
|
|
||||||
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
|
|
||||||
}
|
|
||||||
Uri root = MediaStore.Files.getContentUri("external");
|
|
||||||
return resolver.insert(root, contentValues).toString();
|
|
||||||
} else {
|
|
||||||
File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
|
||||||
dir.mkdirs();
|
|
||||||
File f = new File(dir, name);
|
|
||||||
return Uri.fromFile(f).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int openUri(String uri, String mode) throws IOException {
|
|
||||||
ContentResolver resolver = getContentResolver();
|
|
||||||
return resolver.openFileDescriptor(Uri.parse(uri), mode).detachFd();
|
|
||||||
}
|
|
||||||
|
|
||||||
void deleteUri(String uri) {
|
|
||||||
ContentResolver resolver = getContentResolver();
|
|
||||||
resolver.delete(Uri.parse(uri), null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyFile(String uri, String msg) {
|
|
||||||
Intent viewIntent;
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
|
||||||
} else {
|
|
||||||
// uri is a file:// which is not allowed to be shared outside the app.
|
|
||||||
viewIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
|
|
||||||
}
|
|
||||||
PendingIntent pending = PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, FILE_CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setContentTitle("File received")
|
|
||||||
.setContentText(msg)
|
|
||||||
.setContentIntent(pending)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
|
||||||
nm.notify(FILE_NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createNotificationChannel(String id, String name, int importance) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
NotificationChannel channel = new NotificationChannel(id, name, importance);
|
|
||||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
|
||||||
nm.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
static native void onVPNPrepared();
|
|
||||||
private static native void onDnsConfigChanged();
|
|
||||||
static native void onShareIntent(int nfiles, int[] types, String[] mimes, String[] items, String[] names, long[] sizes);
|
|
||||||
static native void onWriteStorageGranted();
|
|
||||||
|
|
||||||
// Returns details of the interfaces in the system, encoded as a single string for ease
|
|
||||||
// of JNI transfer over to the Go environment.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
// rmnet_data0 10 2000 true false false false false | fe80::4059:dc16:7ed3:9c6e%rmnet_data0/64
|
|
||||||
// dummy0 3 1500 true false false false false | fe80::1450:5cff:fe13:f891%dummy0/64
|
|
||||||
// wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24
|
|
||||||
// r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64
|
|
||||||
// rmnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64
|
|
||||||
// r_rmnet_data1 22 1500 true false false false false | fe80::b6cd:5cb0:8ae6:fe92%r_rmnet_data1/64
|
|
||||||
// rmnet_data1 11 1500 true false false false false | fe80::51f2:ee00:edce:d68b%rmnet_data1/64
|
|
||||||
// lo 1 65536 true false true false false | ::1/128 127.0.0.1/8
|
|
||||||
// v4-rmnet_data2 68 1472 true true false true true | 192.0.0.4/32
|
|
||||||
//
|
|
||||||
// Where the fields are:
|
|
||||||
// name ifindex mtu isUp hasBroadcast isLoopback isPointToPoint hasMulticast | ip1/N ip2/N ip3/N;
|
|
||||||
String getInterfacesAsString() {
|
|
||||||
List<NetworkInterface> interfaces;
|
|
||||||
try {
|
|
||||||
interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder("");
|
|
||||||
for (NetworkInterface nif : interfaces) {
|
|
||||||
try {
|
|
||||||
// Android doesn't have a supportsBroadcast() but the Go net.Interface wants
|
|
||||||
// one, so we say the interface has broadcast if it has multicast.
|
|
||||||
sb.append(String.format(java.util.Locale.ROOT, "%s %d %d %b %b %b %b %b |", nif.getName(),
|
|
||||||
nif.getIndex(), nif.getMTU(), nif.isUp(), nif.supportsMulticast(),
|
|
||||||
nif.isLoopback(), nif.isPointToPoint(), nif.supportsMulticast()));
|
|
||||||
|
|
||||||
for (InterfaceAddress ia : nif.getInterfaceAddresses()) {
|
|
||||||
// InterfaceAddress == hostname + "/" + IP
|
|
||||||
String[] parts = ia.toString().split("/", 0);
|
|
||||||
if (parts.length > 1) {
|
|
||||||
sb.append(String.format(java.util.Locale.ROOT, "%s/%d ", parts[1], ia.getNetworkPrefixLength()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// TODO(dgentry) should log the exception not silently suppress it.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isTV() {
|
|
||||||
UiModeManager mm = (UiModeManager)getSystemService(UI_MODE_SERVICE);
|
|
||||||
return mm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn;
|
|
||||||
|
|
||||||
import android.net.NetworkCapabilities;
|
|
||||||
import android.net.NetworkRequest;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
// Tailscale DNS Config retrieval
|
|
||||||
//
|
|
||||||
// Tailscale's DNS support can either override the local DNS servers with a set of servers
|
|
||||||
// configured in the admin panel, or supplement the local DNS servers with additional
|
|
||||||
// servers for specific domains like example.com.beta.tailscale.net. In the non-override mode,
|
|
||||||
// we need to retrieve the current set of DNS servers from the platform. These will typically
|
|
||||||
// be the DNS servers received from DHCP.
|
|
||||||
//
|
|
||||||
// Importantly, after the Tailscale VPN comes up it will set a DNS server of 100.100.100.100
|
|
||||||
// but we still want to retrieve the underlying DNS servers received from DHCP. If we roam
|
|
||||||
// from Wi-Fi to LTE, we want the DNS servers received from LTE.
|
|
||||||
|
|
||||||
public class DnsConfig {
|
|
||||||
private String dnsConfigs;
|
|
||||||
|
|
||||||
// getDnsConfigAsString returns the current DNS configuration as a multiline string:
|
|
||||||
// line[0] DNS server addresses separated by spaces
|
|
||||||
// line[1] search domains separated by spaces
|
|
||||||
//
|
|
||||||
// For example:
|
|
||||||
// 8.8.8.8 8.8.4.4
|
|
||||||
// example.com
|
|
||||||
//
|
|
||||||
// an empty string means the current DNS configuration could not be retrieved.
|
|
||||||
String getDnsConfigAsString() {
|
|
||||||
return getDnsConfigs().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDnsConfigs(){
|
|
||||||
synchronized(this) {
|
|
||||||
return this.dnsConfigs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateDNSFromNetwork(String dnsConfigs){
|
|
||||||
synchronized(this) {
|
|
||||||
this.dnsConfigs = dnsConfigs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkRequest getDNSConfigNetworkRequest(){
|
|
||||||
// Request networks that are able to reach the Internet.
|
|
||||||
return new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.res.AssetFileDescriptor;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.provider.OpenableColumns;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import org.gioui.GioView;
|
|
||||||
|
|
||||||
public final class IPNActivity extends Activity {
|
|
||||||
final static int WRITE_STORAGE_RESULT = 1000;
|
|
||||||
|
|
||||||
private GioView view;
|
|
||||||
|
|
||||||
@Override public void onCreate(Bundle state) {
|
|
||||||
super.onCreate(state);
|
|
||||||
view = new GioView(this);
|
|
||||||
setContentView(view);
|
|
||||||
handleIntent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onNewIntent(Intent i) {
|
|
||||||
setIntent(i);
|
|
||||||
handleIntent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleIntent() {
|
|
||||||
Intent it = getIntent();
|
|
||||||
String act = it.getAction();
|
|
||||||
String[] texts;
|
|
||||||
Uri[] uris;
|
|
||||||
if (Intent.ACTION_SEND.equals(act)) {
|
|
||||||
uris = new Uri[]{it.getParcelableExtra(Intent.EXTRA_STREAM)};
|
|
||||||
texts = new String[]{it.getStringExtra(Intent.EXTRA_TEXT)};
|
|
||||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(act)) {
|
|
||||||
List<Uri> extraUris = it.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
|
||||||
uris = extraUris.toArray(new Uri[0]);
|
|
||||||
texts = new String[uris.length];
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String mime = it.getType();
|
|
||||||
int nitems = uris.length;
|
|
||||||
String[] items = new String[nitems];
|
|
||||||
String[] mimes = new String[nitems];
|
|
||||||
int[] types = new int[nitems];
|
|
||||||
String[] names = new String[nitems];
|
|
||||||
long[] sizes = new long[nitems];
|
|
||||||
int nfiles = 0;
|
|
||||||
for (int i = 0; i < uris.length; i++) {
|
|
||||||
String text = texts[i];
|
|
||||||
Uri uri = uris[i];
|
|
||||||
if (text != null) {
|
|
||||||
types[nfiles] = 1; // FileTypeText
|
|
||||||
names[nfiles] = "file.txt";
|
|
||||||
mimes[nfiles] = mime;
|
|
||||||
items[nfiles] = text;
|
|
||||||
// Determined by len(text) in Go to eliminate UTF-8 encoding differences.
|
|
||||||
sizes[nfiles] = 0;
|
|
||||||
nfiles++;
|
|
||||||
} else if (uri != null) {
|
|
||||||
Cursor c = getContentResolver().query(uri, null, null, null, null);
|
|
||||||
if (c == null) {
|
|
||||||
// Ignore files we have no permission to access.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
|
||||||
int sizeCol = c.getColumnIndex(OpenableColumns.SIZE);
|
|
||||||
c.moveToFirst();
|
|
||||||
String name = c.getString(nameCol);
|
|
||||||
long size = c.getLong(sizeCol);
|
|
||||||
types[nfiles] = 2; // FileTypeURI
|
|
||||||
mimes[nfiles] = mime;
|
|
||||||
items[nfiles] = uri.toString();
|
|
||||||
names[nfiles] = name;
|
|
||||||
sizes[nfiles] = size;
|
|
||||||
nfiles++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
App.onShareIntent(nfiles, types, mimes, items, names, sizes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onRequestPermissionsResult(int reqCode, String[] perms, int[] grants) {
|
|
||||||
switch (reqCode) {
|
|
||||||
case WRITE_STORAGE_RESULT:
|
|
||||||
if (grants.length > 0 && grants[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
App.onWriteStorageGranted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onDestroy() {
|
|
||||||
view.destroy();
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
view.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onStop() {
|
|
||||||
view.stop();
|
|
||||||
super.onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onConfigurationChanged(Configuration c) {
|
|
||||||
super.onConfigurationChanged(c);
|
|
||||||
view.configurationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onLowMemory() {
|
|
||||||
super.onLowMemory();
|
|
||||||
view.onLowMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onBackPressed() {
|
|
||||||
if (!view.backPressed())
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
|
|
||||||
public class IPNReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
public static final String INTENT_CONNECT_VPN = "com.tailscale.ipn.CONNECT_VPN";
|
|
||||||
public static final String INTENT_DISCONNECT_VPN = "com.tailscale.ipn.DISCONNECT_VPN";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
WorkManager workManager = WorkManager.getInstance(context);
|
|
||||||
|
|
||||||
// On the relevant action, start the relevant worker, which can stay active for longer than this receiver can.
|
|
||||||
if (intent.getAction() == INTENT_CONNECT_VPN) {
|
|
||||||
workManager.enqueue(new OneTimeWorkRequest.Builder(StartVPNWorker.class).build());
|
|
||||||
} else if (intent.getAction() == INTENT_DISCONNECT_VPN) {
|
|
||||||
workManager.enqueue(new OneTimeWorkRequest.Builder(StopVPNWorker.class).build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.net.VpnService;
|
|
||||||
import android.system.OsConstants;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
|
|
||||||
import org.gioui.GioActivity;
|
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
|
|
||||||
public class IPNService extends VpnService {
|
|
||||||
public static final String ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN";
|
|
||||||
public static final String ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN";
|
|
||||||
|
|
||||||
@Override public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
if (intent != null && ACTION_STOP_VPN.equals(intent.getAction())) {
|
|
||||||
((App)getApplicationContext()).autoConnect = false;
|
|
||||||
close();
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
if (intent != null && "android.net.VpnService".equals(intent.getAction())) {
|
|
||||||
// Start VPN and connect to it due to Always-on VPN
|
|
||||||
Intent i = new Intent(IPNReceiver.INTENT_CONNECT_VPN);
|
|
||||||
i.setPackage(getPackageName());
|
|
||||||
i.setClass(getApplicationContext(), com.tailscale.ipn.IPNReceiver.class);
|
|
||||||
sendBroadcast(i);
|
|
||||||
requestVPN();
|
|
||||||
connect();
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
requestVPN();
|
|
||||||
App app = ((App)getApplicationContext());
|
|
||||||
if (app.vpnReady && app.autoConnect) {
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void close() {
|
|
||||||
stopForeground(true);
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onDestroy() {
|
|
||||||
close();
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onRevoke() {
|
|
||||||
close();
|
|
||||||
super.onRevoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
private PendingIntent configIntent() {
|
|
||||||
return PendingIntent.getActivity(this, 0, new Intent(this, IPNActivity.class),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disallowApp(VpnService.Builder b, String name) {
|
|
||||||
try {
|
|
||||||
b.addDisallowedApplication(name);
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected VpnService.Builder newBuilder() {
|
|
||||||
VpnService.Builder b = new VpnService.Builder()
|
|
||||||
.setConfigureIntent(configIntent())
|
|
||||||
.allowFamily(OsConstants.AF_INET)
|
|
||||||
.allowFamily(OsConstants.AF_INET6);
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
|
||||||
b.setMetered(false); // Inherit the metered status from the underlying networks.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
|
||||||
b.setUnderlyingNetworks(null); // Use all available networks.
|
|
||||||
|
|
||||||
// RCS/Jibe https://github.com/tailscale/tailscale/issues/2322
|
|
||||||
this.disallowApp(b, "com.google.android.apps.messaging");
|
|
||||||
|
|
||||||
// Stadia https://github.com/tailscale/tailscale/issues/3460
|
|
||||||
this.disallowApp(b, "com.google.stadia.android");
|
|
||||||
|
|
||||||
// Android Auto https://github.com/tailscale/tailscale/issues/3828
|
|
||||||
this.disallowApp(b, "com.google.android.projection.gearhead");
|
|
||||||
|
|
||||||
// GoPro https://github.com/tailscale/tailscale/issues/2554
|
|
||||||
this.disallowApp(b, "com.gopro.smarty");
|
|
||||||
|
|
||||||
// Sonos https://github.com/tailscale/tailscale/issues/2548
|
|
||||||
this.disallowApp(b, "com.sonos.acr");
|
|
||||||
this.disallowApp(b, "com.sonos.acr2");
|
|
||||||
|
|
||||||
// Google Chromecast https://github.com/tailscale/tailscale/issues/3636
|
|
||||||
this.disallowApp(b, "com.google.android.apps.chromecast.app");
|
|
||||||
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notify(String title, String message) {
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(message)
|
|
||||||
.setContentIntent(configIntent())
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
|
||||||
nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateStatusNotification(String title, String message) {
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(message)
|
|
||||||
.setContentIntent(configIntent())
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW);
|
|
||||||
|
|
||||||
startForeground(App.STATUS_NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void requestVPN();
|
|
||||||
|
|
||||||
private native void disconnect();
|
|
||||||
private native void connect();
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
public class Peer extends Fragment {
|
|
||||||
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
onActivityResult0(getActivity(), requestCode, resultCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native void onActivityResult0(Activity act, int reqCode, int resCode);
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue