aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/notify/PushChannels.kt
blob: e8619a57c8706986d84799674f6f6f3aee663fba (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
package org.meshbay.client.notify

import android.app.Activity
import android.app.NotificationManager
import android.os.Build
import org.json.JSONArray
import org.json.JSONObject
import org.unifiedpush.android.connector.UnifiedPush

/**
 * Notifications on this phone (§11.3), with nothing to install.
 *
 * Turned on, the phone fetches what is new every quarter of an hour (`PollJob`).
 * If a UnifiedPush distributor is already on the phone — ntfy, or an application
 * that carries one — it is used as well, and notifications arrive at once; if it
 * refuses or goes away, the phone simply goes back to fetching. The page gives
 * the hub whatever this side ends up with. Phone-only: the desktop has no
 * counterpart, and its preload has no such channels.
 *
 * Whether a notification is wanted at all — every one turned off, or one group
 * muted — is decided on the hub, which then creates nothing, so nothing is
 * pushed or fetched. Nothing here second-guesses it.
 */
class PushChannels(
    private val activity: Activity,
    private val state: PushState,
    private val channelNames: () -> Map<String, String>,
) {
    fun handles(channel: String) = channel.startsWith("push:")

    fun call(channel: String, args: JSONArray): Any? = when (channel) {
        "push:status" -> status()
        "push:enable" -> enable()
        "push:disable" -> { disable(); status() }
        "push:remember" -> {
            state.remember(args.optString(0, ""), args.optString(1, ""), args.optString(2, ""), args.optString(3, ""))
            Poller.schedule(activity, pushed = state.endpoint != null)
            status()
        }
        else -> throw org.meshbay.client.bridge.Refused("Refused: no such channel")
    }

    private fun distributors() = UnifiedPush.getDistributors(activity).any { it != activity.packageName }

    private fun status(): JSONObject = state.status()
        .put("distributors", distributors())
        .put("permitted", activity.getSystemService(NotificationManager::class.java).areNotificationsEnabled())

    private fun enable(): JSONObject {
        state.requested()
        Notifier.ensureChannels(activity, channelNames())
        if (!distributors()) state.fellBack("NO_DISTRIBUTOR")
        activity.runOnUiThread {
            if (Build.VERSION.SDK_INT >= 33) {
                activity.requestPermissions(arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), PERMISSION_REQUEST)
            }
            // Only when one is installed: the system's chooser for a person who
            // has none would be a screen about something they never asked for.
            if (distributors()) UnifiedPush.tryUseDefaultDistributor(activity) { found ->
                if (found) UnifiedPush.register(activity, messageForDistributor = "MeshBay")
                else state.fellBack("NO_DISTRIBUTOR")
            }
        }
        return status()
    }

    private fun disable() {
        Poller.cancel(activity)
        try { UnifiedPush.removeDistributor(activity) } catch (e: Exception) { /* none was saved */ }
        state.cleared()
    }

    companion object {
        private const val PERMISSION_REQUEST = 4207
    }
}