aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/main/java/com/wireguard/android/QuickTileService.kt
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/QuickTileService.kt')
-rw-r--r--ui/src/main/java/com/wireguard/android/QuickTileService.kt187
1 files changed, 187 insertions, 0 deletions
diff --git a/ui/src/main/java/com/wireguard/android/QuickTileService.kt b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
new file mode 100644
index 00000000..9be2cc2b
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Build
+import android.os.IBinder
+import android.provider.Settings
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.databinding.Observable
+import androidx.databinding.Observable.OnPropertyChangedCallback
+import com.wireguard.android.activity.MainActivity
+import com.wireguard.android.activity.TunnelToggleActivity
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.applicationScope
+import com.wireguard.android.widget.SlashDrawable
+import kotlinx.coroutines.launch
+
+/**
+ * Service that maintains the application's custom Quick Settings tile. This service is bound by the
+ * system framework as necessary to update the appearance of the tile in the system UI, and to
+ * forward click events to the application.
+ */
+@RequiresApi(Build.VERSION_CODES.N)
+class QuickTileService : TileService() {
+ private val onStateChangedCallback = OnStateChangedCallback()
+ private val onTunnelChangedCallback = OnTunnelChangedCallback()
+ private var iconOff: Icon? = null
+ private var iconOn: Icon? = null
+ private var tunnel: ObservableTunnel? = null
+
+ /* This works around an annoying unsolved frameworks bug some people are hitting. */
+ override fun onBind(intent: Intent): IBinder? {
+ var ret: IBinder? = null
+ try {
+ ret = super.onBind(intent)
+ } catch (e: Throwable) {
+ Log.d(TAG, "Failed to bind to TileService", e)
+ }
+ return ret
+ }
+
+ override fun onClick() {
+ when (val tunnel = tunnel) {
+ null -> {
+ val intent = Intent(this, MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startActivityAndCollapse(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE))
+ } else {
+ @Suppress("DEPRECATION")
+ startActivityAndCollapse(intent)
+ }
+ }
+ else -> {
+ unlockAndRun {
+ applicationScope.launch {
+ try {
+ tunnel.setStateAsync(Tunnel.State.TOGGLE)
+ updateTile()
+ } catch (_: Throwable) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !Settings.canDrawOverlays(this@QuickTileService)) {
+ val permissionIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
+ permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivityAndCollapse(PendingIntent.getActivity(this@QuickTileService, 0, permissionIntent, PendingIntent.FLAG_IMMUTABLE))
+ return@launch
+ }
+ val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
+ toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(toggleIntent)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCreate() {
+ isAdded = true
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ iconOn = Icon.createWithResource(this, R.drawable.ic_tile)
+ iconOff = iconOn
+ return
+ }
+ val icon = SlashDrawable(resources.getDrawable(R.drawable.ic_tile, Application.get().theme))
+ icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */
+ icon.setSlashed(false)
+ var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
+ var c = Canvas(b)
+ icon.setBounds(0, 0, c.width, c.height)
+ icon.draw(c)
+ iconOn = Icon.createWithBitmap(b)
+ icon.setSlashed(true)
+ b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
+ c = Canvas(b)
+ icon.setBounds(0, 0, c.width, c.height)
+ icon.draw(c)
+ iconOff = Icon.createWithBitmap(b)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ isAdded = false
+ }
+
+ override fun onStartListening() {
+ Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback)
+ tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
+ updateTile()
+ }
+
+ override fun onStopListening() {
+ tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
+ Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback)
+ }
+
+ override fun onTileAdded() {
+ isAdded = true
+ }
+
+ override fun onTileRemoved() {
+ isAdded = false
+ }
+
+ private fun updateTile() {
+ // Update the tunnel.
+ val newTunnel = Application.getTunnelManager().lastUsedTunnel
+ if (newTunnel != tunnel) {
+ tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
+ tunnel = newTunnel
+ tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
+ }
+ // Update the tile contents.
+ val tile = qsTile ?: return
+
+ when (val tunnel = tunnel) {
+ null -> {
+ tile.label = getString(R.string.app_name)
+ tile.state = Tile.STATE_INACTIVE
+ tile.icon = iconOff
+ }
+ else -> {
+ tile.label = tunnel.name
+ tile.state = if (tunnel.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
+ tile.icon = if (tunnel.state == Tunnel.State.UP) iconOn else iconOff
+ }
+ }
+ tile.updateTile()
+ }
+
+ private inner class OnStateChangedCallback : OnPropertyChangedCallback() {
+ override fun onPropertyChanged(sender: Observable, propertyId: Int) {
+ if (sender != tunnel) {
+ sender.removeOnPropertyChangedCallback(this)
+ return
+ }
+ if (propertyId != 0 && propertyId != BR.state)
+ return
+ updateTile()
+ }
+ }
+
+ private inner class OnTunnelChangedCallback : OnPropertyChangedCallback() {
+ override fun onPropertyChanged(sender: Observable, propertyId: Int) {
+ if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
+ return
+ updateTile()
+ }
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/QuickTileService"
+ var isAdded: Boolean = false
+ private set
+ }
+}