From ac82a9446c6a615234dd13ca6fa5b7c3ac28dcf7 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 21 Jan 2018 12:11:35 -0800 Subject: [PATCH] Use one notification for two services --- .../src/main/java/be/mygod/vpnhotspot/App.kt | 23 +----- .../be/mygod/vpnhotspot/RepeaterFragment.kt | 2 +- .../be/mygod/vpnhotspot/RepeaterService.kt | 22 +----- .../mygod/vpnhotspot/ServiceNotification.kt | 76 +++++++++++++++++++ .../be/mygod/vpnhotspot/TetheringService.kt | 27 +------ mobile/src/main/res/values/strings.xml | 5 +- 6 files changed, 89 insertions(+), 66 deletions(-) create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt index 0444e7ec..bb583f32 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt @@ -1,13 +1,8 @@ package be.mygod.vpnhotspot -import android.annotation.TargetApi import android.app.Application -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.SharedPreferences import android.content.res.Configuration -import android.os.Build import android.os.Handler import android.preference.PreferenceManager import android.support.annotation.StringRes @@ -23,26 +18,12 @@ class App : Application() { override fun onCreate() { super.onCreate() app = this - updateNotificationChannels() + ServiceNotification.updateNotificationChannels() } override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) - updateNotificationChannels() - } - - private fun updateNotificationChannels() { - if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) { - val nm = getSystemService(NotificationManager::class.java) - val tethering = NotificationChannel(TetheringService.CHANNEL, - getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW) - tethering.lockscreenVisibility = Notification.VISIBILITY_PUBLIC - nm.createNotificationChannels(listOf( - NotificationChannel(RepeaterService.CHANNEL, getText(R.string.notification_channel_repeater), - NotificationManager.IMPORTANCE_LOW), tethering - )) - nm.deleteNotificationChannel("hotspot") // remove old service channel - } + ServiceNotification.updateNotificationChannels() } val handler = Handler() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt index 6c76a17f..91645114 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt @@ -79,7 +79,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL val ip = neighbour?.ip val icon get() = TetherType.ofInterface(iface, p2pInterface).icon - val title get() = listOf(ip, mac).filter { !it.isNullOrEmpty() }.joinToString(", ") + val title get() = listOf(ip, mac).filter { !it.isNullOrEmpty() }.joinToString() val description get() = when (neighbour?.state) { IpNeighbour.State.INCOMPLETE, null -> "Connecting to $iface" IpNeighbour.State.VALID -> "Connected to $iface" diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index eadac39f..5779db45 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -1,6 +1,5 @@ package be.mygod.vpnhotspot -import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent @@ -13,8 +12,6 @@ import android.os.Binder import android.os.Handler import android.os.Looper import android.support.annotation.StringRes -import android.support.v4.app.NotificationCompat -import android.support.v4.content.ContextCompat import android.support.v4.content.LocalBroadcastManager import android.util.Log import android.widget.Toast @@ -26,8 +23,6 @@ import java.util.regex.Pattern class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Callback { companion object { - const val CHANNEL = "repeater" - const val CHANNEL_ID = 1 const val ACTION_STATUS_CHANGED = "be.mygod.vpnhotspot.RepeaterService.STATUS_CHANGED" const val KEY_NET_ID = "netId" private const val TAG = "RepeaterService" @@ -304,19 +299,8 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca } } - private fun showNotification(group: WifiP2pGroup? = null) { - val builder = NotificationCompat.Builder(this, CHANNEL) - .setWhen(0) - .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) - .setContentTitle(group?.networkName ?: ssid ?: getString(R.string.repeater_connecting)) - .setSmallIcon(R.drawable.ic_device_wifi_tethering) - .setContentIntent(PendingIntent.getActivity(this, 0, - Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)) - val size = group?.clientList?.size ?: 0 - if (size != 0) builder.setContentText(resources.getQuantityString(R.plurals.notification_connected_devices, - size, size, group!!.`interface`)) - startForeground(CHANNEL_ID, builder.build()) - } + private fun showNotification(group: WifiP2pGroup? = null) = ServiceNotification.startForeground(this, + if (group == null) emptyMap() else mapOf(Pair(group.`interface`, group.clientList?.size ?: 0))) private fun removeGroup() { p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { @@ -345,7 +329,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show() routing = null status = Status.IDLE - stopForeground(true) + ServiceNotification.stopForeground(this) stopSelf() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt b/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt new file mode 100644 index 00000000..8e5fdaef --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/ServiceNotification.kt @@ -0,0 +1,76 @@ +package be.mygod.vpnhotspot + +import android.annotation.TargetApi +import android.app.* +import android.content.Context +import android.content.Intent +import android.os.Build +import android.support.v4.app.NotificationCompat +import android.support.v4.content.ContextCompat +import be.mygod.vpnhotspot.App.Companion.app + +object ServiceNotification { + private const val CHANNEL = "tethering" + private const val CHANNEL_ID = 1 + + private val deviceCountsMap = HashMap>() + private val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + private fun buildNotification(context: Context): Notification { + val builder = NotificationCompat.Builder(context, CHANNEL) + .setWhen(0) + .setColor(ContextCompat.getColor(context, R.color.colorPrimary)) + .setContentTitle("VPN tethering active") + .setSmallIcon(R.drawable.ic_device_wifi_tethering) + .setContentIntent(PendingIntent.getActivity(context, 0, + Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + val deviceCounts = deviceCountsMap.values.flatMap { it.entries }.sortedBy { it.key } + return when (deviceCounts.size) { + 0 -> builder.build() + 1 -> { + val (dev, size) = deviceCounts.single() + builder.setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices, + size, size, dev)) + .build() + } + else -> { + val deviceCount = deviceCounts.sumBy { it.value } + NotificationCompat.BigTextStyle(builder + .setContentText(context.resources.getQuantityString(R.plurals.notification_connected_devices, + deviceCount, deviceCount, + context.resources.getQuantityString(R.plurals.notification_interfaces, + deviceCounts.size, deviceCounts.size)))) + .bigText(deviceCounts.joinToString("\n") { (dev, size) -> + context.resources.getQuantityString(R.plurals.notification_connected_devices, + size, size, dev) + }) + .build() + } + } + } + + fun startForeground(service: Service, deviceCounts: Map) { + deviceCountsMap[service] = deviceCounts + service.startForeground(CHANNEL_ID, buildNotification(service)) + } + fun stopForeground(service: Service) { + deviceCountsMap.remove(service) + if (deviceCountsMap.isEmpty()) service.stopForeground(true) else { + service.stopForeground(false) + manager.notify(CHANNEL_ID, buildNotification(service)) + } + } + + fun updateNotificationChannels() { + if (Build.VERSION.SDK_INT >= 26) @TargetApi(26) { + val tethering = NotificationChannel(CHANNEL, + app.getText(R.string.notification_channel_tethering), NotificationManager.IMPORTANCE_LOW) + tethering.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + manager.createNotificationChannel(tethering) + // remove old service channels + manager.deleteNotificationChannel("hotspot") + manager.deleteNotificationChannel("repeater") + } + } +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt index f56535d9..77a054ce 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt @@ -1,12 +1,8 @@ package be.mygod.vpnhotspot -import android.app.Notification -import android.app.PendingIntent import android.app.Service import android.content.Intent import android.os.Binder -import android.support.v4.app.NotificationCompat -import android.support.v4.content.ContextCompat import android.support.v4.content.LocalBroadcastManager import android.widget.Toast import be.mygod.vpnhotspot.App.Companion.app @@ -14,8 +10,6 @@ import be.mygod.vpnhotspot.net.* class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Callback { companion object { - const val CHANNEL = "tethering" - const val CHANNEL_ID = 2 const val EXTRA_ADD_INTERFACE = "interface.add" const val EXTRA_REMOVE_INTERFACE = "interface.remove" } @@ -46,7 +40,7 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call private fun updateRoutings() { if (routings.isEmpty()) { unregisterReceiver() - stopForeground(true) + ServiceNotification.stopForeground(this) stopSelf() } else { val upstream = upstream @@ -106,23 +100,8 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call this.neighbours = neighbours.values.toList() } override fun postIpNeighbourAvailable() { - val builder = NotificationCompat.Builder(this, CHANNEL) - .setWhen(0) - .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) - .setContentTitle("VPN tethering active") - .setSmallIcon(R.drawable.ic_device_wifi_tethering) - .setContentIntent(PendingIntent.getActivity(this, 0, - Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)) - .setVisibility(Notification.VISIBILITY_PUBLIC) - val content = neighbours.groupBy { it.dev } - .filter { (dev, _) -> routings.contains(dev) } - .mapValues { (_, neighbours) -> neighbours.size } - .toList() - .joinToString(", ") { (dev, size) -> - resources.getQuantityString(R.plurals.notification_connected_devices, size, size, dev) - } - if (content.isNotEmpty()) builder.setContentText(content) - startForeground(CHANNEL_ID, builder.build()) + val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) -> neighbours.size } + ServiceNotification.startForeground(this, routings.keys.associate { Pair(it, sizeLookup[it] ?: 0) }) } override fun onDestroy() { diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index ef02f132..15c7b69a 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -51,12 +51,15 @@ Donate I love money - Repeater Service VPN Tethering Service %d device connected to %s %d devices connected to %s + + %d interface + %d interfaces + Fatal: Downstream interface not found Something went wrong, please check logcat.