1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
package org.meshbay.client.notify
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import org.json.JSONObject
import org.meshbay.client.MainActivity
import org.meshbay.client.R
/**
* A notification from the hub, pushed or fetched, drawn by the system.
*
* The text is the hub's own line ("… posted in …") — the hub never holds a
* message, so neither does a push. A conversation is one entry per group,
* replaced as it goes on, as it is one row on the hub.
*/
object Notifier {
const val CHANNEL_CHAT = "chat"
const val CHANNEL_OTHER = "account"
const val EXTRA_LINK = "org.meshbay.client.LINK"
private val LINK = Regex("^#/[A-Za-z0-9/_-]{0,200}$")
data class Shown(val channel: String, val tag: String, val title: String, val link: String?,
val id: Long, val createdAt: String)
/**
* What to draw for a payload, or null for one that is not ours to draw.
* It was decrypted with this phone's key or fetched from the signed-in hub
* with this phone's secret; it is still checked like any input.
*/
fun parse(content: ByteArray): Shown? {
val o = try { JSONObject(String(content, Charsets.UTF_8)) } catch (e: Exception) { return null }
if (o.optInt("v", 0) != 1) return null
val kind = o.optString("kind", "").take(32)
val title = o.optString("title", "").take(256).ifBlank { return null }
val group = if (o.isNull("group_id")) "" else o.optString("group_id", "").take(64)
val link = (if (o.isNull("link")) null else o.optString("link", null))?.takeIf { LINK.matches(it) }
val chat = kind == "chat_message" && group.isNotEmpty()
val id = o.optLong("id", 0)
val createdAt = o.optString("created_at", "").take(40)
val tag = if (chat) "chat:$group" else "n:$id"
return Shown(if (chat) CHANNEL_CHAT else CHANNEL_OTHER, tag, title, link, id, createdAt)
}
/** Draw it unless it was already drawn — pushed, then fetched, is one line. */
fun deliver(context: Context, state: PushState, shown: Shown) {
if (state.firstSight(shown.id, shown.createdAt)) show(context, shown)
}
/**
* The two channels, named in the person's language. Called with names when
* the page turns push on; from the receiver with none, only to make sure a
* channel exists, so a localized name is never overwritten by the fallback.
*/
fun ensureChannels(context: Context, names: Map<String, String>? = null) {
val nm = context.getSystemService(NotificationManager::class.java)
for ((id, fallback, importance) in listOf(
Triple(CHANNEL_CHAT, "Messages", NotificationManager.IMPORTANCE_DEFAULT),
Triple(CHANNEL_OTHER, "Invitations and account", NotificationManager.IMPORTANCE_DEFAULT),
)) {
if (names == null && nm.getNotificationChannel(id) != null) continue
nm.createNotificationChannel(NotificationChannel(id, names?.get(id) ?: fallback, importance))
}
}
fun show(context: Context, shown: Shown) {
ensureChannels(context)
val open = Intent(context, MainActivity::class.java)
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
shown.link?.let { open.putExtra(EXTRA_LINK, it) }
val pending = PendingIntent.getActivity(context, shown.tag.hashCode(), open,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
val n = Notification.Builder(context, shown.channel)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("MeshBay")
.setContentText(shown.title)
.setStyle(Notification.BigTextStyle().bigText(shown.title))
.setContentIntent(pending)
.setAutoCancel(true)
.setCategory(if (shown.channel == CHANNEL_CHAT) Notification.CATEGORY_MESSAGE else Notification.CATEGORY_SOCIAL)
.build()
context.getSystemService(NotificationManager::class.java).notify(shown.tag, 1, n)
}
/** A link from a notification the shell itself drew, checked again on the way in. */
fun linkOf(intent: Intent?): String? = intent?.getStringExtra(EXTRA_LINK)?.takeIf { LINK.matches(it) }
}
|