aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/notify/Notifier.kt
blob: 7e6fce67ac2e593b2d87684396a39039c21120fd (plain) (blame)
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) }
}