Use one notification for two services

This commit is contained in:
Mygod
2018-01-21 12:11:35 -08:00
parent d8b4a772ee
commit ac82a9446c
6 changed files with 89 additions and 66 deletions

View File

@@ -1,13 +1,8 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.annotation.TargetApi
import android.app.Application import android.app.Application
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build
import android.os.Handler import android.os.Handler
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.support.annotation.StringRes import android.support.annotation.StringRes
@@ -23,26 +18,12 @@ class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
app = this app = this
updateNotificationChannels() ServiceNotification.updateNotificationChannels()
} }
override fun onConfigurationChanged(newConfig: Configuration?) { override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
updateNotificationChannels() ServiceNotification.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
}
} }
val handler = Handler() val handler = Handler()

View File

@@ -79,7 +79,7 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
val ip = neighbour?.ip val ip = neighbour?.ip
val icon get() = TetherType.ofInterface(iface, p2pInterface).icon 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) { val description get() = when (neighbour?.state) {
IpNeighbour.State.INCOMPLETE, null -> "Connecting to $iface" IpNeighbour.State.INCOMPLETE, null -> "Connecting to $iface"
IpNeighbour.State.VALID -> "Connected to $iface" IpNeighbour.State.VALID -> "Connected to $iface"

View File

@@ -1,6 +1,5 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -13,8 +12,6 @@ import android.os.Binder
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.support.annotation.StringRes 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.support.v4.content.LocalBroadcastManager
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
@@ -26,8 +23,6 @@ import java.util.regex.Pattern
class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Callback { class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Callback {
companion object { companion object {
const val CHANNEL = "repeater"
const val CHANNEL_ID = 1
const val ACTION_STATUS_CHANGED = "be.mygod.vpnhotspot.RepeaterService.STATUS_CHANGED" const val ACTION_STATUS_CHANGED = "be.mygod.vpnhotspot.RepeaterService.STATUS_CHANGED"
const val KEY_NET_ID = "netId" const val KEY_NET_ID = "netId"
private const val TAG = "RepeaterService" private const val TAG = "RepeaterService"
@@ -304,19 +299,8 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
} }
} }
private fun showNotification(group: WifiP2pGroup? = null) { private fun showNotification(group: WifiP2pGroup? = null) = ServiceNotification.startForeground(this,
val builder = NotificationCompat.Builder(this, CHANNEL) if (group == null) emptyMap() else mapOf(Pair(group.`interface`, group.clientList?.size ?: 0)))
.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 removeGroup() { private fun removeGroup() {
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { 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() Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
routing = null routing = null
status = Status.IDLE status = Status.IDLE
stopForeground(true) ServiceNotification.stopForeground(this)
stopSelf() stopSelf()
} }

View File

@@ -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<Service, Map<String, Int>>()
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<String, Int>) {
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")
}
}
}

View File

@@ -1,12 +1,8 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.app.Notification
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat
import android.support.v4.content.LocalBroadcastManager import android.support.v4.content.LocalBroadcastManager
import android.widget.Toast import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
@@ -14,8 +10,6 @@ import be.mygod.vpnhotspot.net.*
class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Callback { class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Callback {
companion object { companion object {
const val CHANNEL = "tethering"
const val CHANNEL_ID = 2
const val EXTRA_ADD_INTERFACE = "interface.add" const val EXTRA_ADD_INTERFACE = "interface.add"
const val EXTRA_REMOVE_INTERFACE = "interface.remove" const val EXTRA_REMOVE_INTERFACE = "interface.remove"
} }
@@ -46,7 +40,7 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
private fun updateRoutings() { private fun updateRoutings() {
if (routings.isEmpty()) { if (routings.isEmpty()) {
unregisterReceiver() unregisterReceiver()
stopForeground(true) ServiceNotification.stopForeground(this)
stopSelf() stopSelf()
} else { } else {
val upstream = upstream val upstream = upstream
@@ -106,23 +100,8 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
this.neighbours = neighbours.values.toList() this.neighbours = neighbours.values.toList()
} }
override fun postIpNeighbourAvailable() { override fun postIpNeighbourAvailable() {
val builder = NotificationCompat.Builder(this, CHANNEL) val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) -> neighbours.size }
.setWhen(0) ServiceNotification.startForeground(this, routings.keys.associate { Pair(it, sizeLookup[it] ?: 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())
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -51,12 +51,15 @@
<string name="settings_misc_donate">Donate</string> <string name="settings_misc_donate">Donate</string>
<string name="settings_misc_donate_summary">I love money</string> <string name="settings_misc_donate_summary">I love money</string>
<string name="notification_channel_repeater">Repeater Service</string>
<string name="notification_channel_tethering">VPN Tethering Service</string> <string name="notification_channel_tethering">VPN Tethering Service</string>
<plurals name="notification_connected_devices"> <plurals name="notification_connected_devices">
<item quantity="one">%d device connected to %s</item> <item quantity="one">%d device connected to %s</item>
<item quantity="other">%d devices connected to %s</item> <item quantity="other">%d devices connected to %s</item>
</plurals> </plurals>
<plurals name="notification_interfaces">
<item quantity="one">%d interface</item>
<item quantity="other">%d interfaces</item>
</plurals>
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string> <string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
<string name="noisy_su_failure">Something went wrong, please check logcat.</string> <string name="noisy_su_failure">Something went wrong, please check logcat.</string>