Skip to content

Commit 9e56e20

Browse files
authored
Merge pull request #415 from openziti/vpn-service-avoid-getting-killed
VpnService: avoid getting killed
2 parents 43dbad5 + 88ad6bd commit 9e56e20

File tree

4 files changed

+110
-20
lines changed

4 files changed

+110
-20
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<uses-permission android:name="android.permission.INTERNET" />
88
<uses-permission android:name="android.permission.VIBRATE" />
99
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
10+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"
11+
tools:ignore="ForegroundServicesPolicy" />
12+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
1013

1114
<application
1215
android:name=".ZitiMobileEdgeApp"
@@ -27,7 +30,10 @@
2730
android:name=".ZitiVPNService"
2831
android:enabled="true"
2932
android:exported="true"
30-
android:permission="android.permission.BIND_VPN_SERVICE">
33+
android:permission="android.permission.BIND_VPN_SERVICE"
34+
android:foregroundServiceType="systemExempted"
35+
tools:ignore="ForegroundServicePermission,VpnServicePolicy"
36+
>
3137
<intent-filter>
3238
<action android:name="android.net.VpnService" />
3339
</intent-filter>

app/src/main/java/org/openziti/mobile/ZitiVPNService.kt

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@
44

55
package org.openziti.mobile
66

7+
import android.Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED
8+
import android.app.Notification
9+
import android.app.NotificationChannel
10+
import android.app.NotificationManager
711
import android.app.PendingIntent
812
import android.content.Intent
13+
import android.content.pm.ServiceInfo
914
import android.net.ConnectivityManager
1015
import android.net.LinkProperties
1116
import android.net.Network
1217
import android.net.NetworkCapabilities
1318
import android.net.NetworkRequest
1419
import android.net.VpnService
1520
import android.os.Binder
21+
import android.os.Build
1622
import android.os.IBinder
1723
import android.system.OsConstants
24+
import androidx.core.app.NotificationCompat
1825
import kotlinx.coroutines.CoroutineScope
1926
import kotlinx.coroutines.Job
2027
import kotlinx.coroutines.SupervisorJob
@@ -140,6 +147,11 @@ class ZitiVPNService : VpnService(), CoroutineScope {
140147
}
141148
}
142149

150+
override fun onTimeout(startId: Int, fgsType: Int) {
151+
super.onTimeout(startId, fgsType)
152+
Log.i("onTimeout($startId, $fgsType)")
153+
}
154+
143155
override fun onCreate() {
144156
Log.i("onCreate()")
145157
ZitiMobileEdgeApp.vpnService = this
@@ -153,6 +165,10 @@ class ZitiVPNService : VpnService(), CoroutineScope {
153165
connMgr.registerNetworkCallback(netReq, networkMonitor)
154166
connMgr.addDefaultNetworkActiveListener(netListener)
155167

168+
runCatching { markForeground() }.onFailure { it
169+
Log.w(it, "failed to mark service foreground")
170+
}
171+
156172
monitor = launch {
157173
launch {
158174
Log.i("monitoring route updates")
@@ -189,13 +205,13 @@ class ZitiVPNService : VpnService(), CoroutineScope {
189205
}.onSuccess {
190206
Log.i("tunnel $cmd success")
191207
}.onFailure {
192-
Log.w("exception during tunnel $cmd", it)
208+
Log.w(it, "exception during tunnel $cmd")
193209
}
194210
}
195211
}
196212

197213
monitor.invokeOnCompletion {
198-
Log.i("monitor stopped", it)
214+
Log.i(it, "monitor stopped")
199215
}
200216
}
201217

@@ -216,15 +232,13 @@ class ZitiVPNService : VpnService(), CoroutineScope {
216232
Log.i("monitor=$monitor")
217233
val action = intent?.action
218234
when (action) {
219-
SERVICE_INTERFACE,
220-
START -> tunnelState.value = "start"
221-
222-
RESTART -> tunnelState.value = "restart"
223-
224-
STOP -> tunnelState.value = "stop"
225-
226-
else -> Log.wtf("what is your intent? $intent")
235+
STOP -> {
236+
tunnelState.value = STOP
237+
return START_NOT_STICKY
238+
}
227239

240+
RESTART -> tunnelState.value = RESTART
241+
else -> tunnelState.value = START
228242
}
229243
return START_STICKY
230244
}
@@ -244,7 +258,9 @@ class ZitiVPNService : VpnService(), CoroutineScope {
244258
allowFamily(OsConstants.AF_INET)
245259
allowFamily(OsConstants.AF_INET6)
246260
allowBypass()
247-
setMetered(metered)
261+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
262+
setMetered(metered)
263+
}
248264

249265
val range = runBlocking { model.zitiRange.first().toRoute() }
250266
val size = if (range.address is Inet6Address) 128 else 32
@@ -256,7 +272,7 @@ class ZitiVPNService : VpnService(), CoroutineScope {
256272
route.runCatching {
257273
addRoute(route.address, route.bits)
258274
}.onFailure {
259-
Log.e("invalid route[$route]", it)
275+
Log.e(it, "invalid route[$route]")
260276
}
261277
}
262278
setUnderlyingNetworks(null)
@@ -302,4 +318,36 @@ class ZitiVPNService : VpnService(), CoroutineScope {
302318
fun isVPNActive() = tun.isActive()
303319
fun getUptime(): Duration = tun.getUptime().toJavaDuration()
304320
}
321+
322+
private fun markForeground() {
323+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
324+
startForeground(1, createNotification())
325+
} else {
326+
startForeground(1, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED)
327+
}
328+
}
329+
330+
private fun createNotification(): Notification {
331+
val channelId = "Ziti Mobile Edge"
332+
val channel = NotificationChannel(
333+
channelId,
334+
"Firewall Status",
335+
NotificationManager.IMPORTANCE_DEFAULT
336+
)
337+
val manager = getSystemService(NotificationManager::class.java)
338+
manager.createNotificationChannel(channel)
339+
340+
val pendingIntent = PendingIntent.getActivity(
341+
this, 0, Intent(this, ZitiMobileEdgeActivity::class.java),
342+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
343+
)
344+
345+
return NotificationCompat.Builder(this, channelId)
346+
.setContentTitle("Ziti Mobile Edge")
347+
.setContentText("Ziti is active")
348+
.setSmallIcon(R.drawable.z)
349+
.setContentIntent(pendingIntent)
350+
.build()
351+
}
352+
305353
}

app/src/main/java/org/openziti/mobile/debug/DebugInfo.kt

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package org.openziti.mobile.debug
66

77
import android.app.ActivityManager
8-
import android.app.ApplicationExitInfo
98
import android.content.Context
109
import android.content.Intent
1110
import android.os.Build
@@ -23,15 +22,14 @@ import java.net.URI
2322
import java.security.KeyStore.PrivateKeyEntry
2423
import java.security.KeyStore.TrustedCertificateEntry
2524
import java.security.cert.X509Certificate
26-
import java.text.SimpleDateFormat
2725
import java.time.Instant
2826
import java.time.LocalDateTime
2927
import java.time.format.DateTimeFormatter
3028
import java.util.concurrent.CompletableFuture
3129
import java.util.zip.ZipEntry
3230
import java.util.zip.ZipOutputStream
3331

34-
sealed class DebugInfo {
32+
sealed class DebugInfo(val wrap: Boolean = false) {
3533
abstract val names: Iterable<String>
3634
abstract fun dump(name: String, output: Writer = StringWriter()): Writer
3735

@@ -128,7 +126,31 @@ sealed class DebugInfo {
128126
}
129127
}
130128

131-
data object ZitiDumpInfo: DebugInfo() {
129+
data object ExitInfo: DebugInfo(wrap = true) {
130+
override val names = listOf("Last Exit")
131+
override fun dump(name: String, output: Writer) = output.apply {
132+
with(zme.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) {
133+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
134+
getHistoricalProcessExitReasons(null, 0, 1).firstOrNull()?.let {
135+
val ts = Instant.ofEpochMilli(it.timestamp)
136+
appendLine("last exit: $ts")
137+
appendLine()
138+
appendLine("pid = ${it.pid}")
139+
appendLine("reason = ${it.reason}")
140+
appendLine("status = ${it.status}")
141+
appendLine("description = ${it.description}")
142+
appendLine()
143+
appendLine(it.toString())
144+
145+
}
146+
} else {
147+
appendLine("not supported on Android ${Build.VERSION.SDK_INT}")
148+
}
149+
}
150+
}
151+
}
152+
153+
data object ZitiDumpInfo: DebugInfo(wrap = true) {
132154
override val names: Iterable<String>
133155
get() =
134156
zme.model.identities().value?.map{it.zitiID} ?: emptyList()
@@ -145,7 +167,6 @@ sealed class DebugInfo {
145167
it.printStackTrace(PrintWriter(output))
146168
}
147169
}
148-
149170
}
150171

151172
companion object {
@@ -155,6 +176,7 @@ sealed class DebugInfo {
155176
}
156177
val providers = listOf(
157178
AppInfoProvider,
179+
ExitInfo,
158180
MemoryInfo,
159181
LogCatProvider,
160182
KeystoreInfo,
@@ -220,7 +242,7 @@ sealed class DebugInfo {
220242
getHistoricalProcessExitReasons(null, 0, 10)
221243
.forEachIndexed { idx, it ->
222244
val ts = Instant.ofEpochMilli(it.timestamp)
223-
val label = "crashdumps/$idx-crash-${fmt.format(ts)}"
245+
val label = "terminations/$idx-exit-${fmt.format(ts)}"
224246
zip.putNextEntry(
225247
ZipEntry("$label/info").apply {
226248
time = it.timestamp

app/src/main/java/org/openziti/mobile/debug/DebugInfoFragment.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
package org.openziti.mobile.debug
66

7+
import android.graphics.text.LineBreaker
8+
import android.os.Build
79
import android.os.Bundle
10+
import android.text.method.ScrollingMovementMethod
811
import android.view.LayoutInflater
912
import android.view.View
1013
import android.view.ViewGroup
@@ -25,13 +28,15 @@ class DebugInfoFragment : Fragment() {
2528
// onDestroyView.
2629
private val binding get() = _binding!!
2730

31+
private var wrap = false
2832
override fun onCreate(savedInstanceState: Bundle?) {
2933
super.onCreate(savedInstanceState)
3034

3135
pageViewModel = ViewModelProvider(this)[PageViewModel::class.java]
3236
val dia = activity as DebugInfoActivity
3337
arguments?.getString(SECTION_ARG)?.let { name ->
3438
val info = dia.getSectionProvider(name)
39+
wrap = info?.wrap ?: false
3540
pageViewModel.setText(info?.dump(name)?.toString() ?: "nothing to see")
3641
}
3742
}
@@ -45,7 +50,16 @@ class DebugInfoFragment : Fragment() {
4550
val root = binding.root
4651

4752
val textView: TextView = binding.sectionLabel
48-
textView.setHorizontallyScrolling(true)
53+
textView.movementMethod = ScrollingMovementMethod()
54+
textView.setHorizontallyScrolling(!wrap)
55+
textView.scrollIndicators = View.SCROLL_INDICATOR_BOTTOM or View.SCROLL_INDICATOR_RIGHT
56+
textView.setLines(100)
57+
if (wrap) {
58+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
59+
textView.breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE
60+
}
61+
}
62+
4963
pageViewModel.text.observe(viewLifecycleOwner, { textView.text = it })
5064
return root
5165
}

0 commit comments

Comments
 (0)