aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/BackupService.kt
blob: b40d0064f10f36324490e1c3d70e5544e5ddf4e8 (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.photos

import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.wifi.WifiManager
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import org.meshbay.client.R

/**
 * Keeps a photo backup going with the screen off — the same pair as a cast
 * (CastService): this service holds the process, the CPU and the Wi-Fi, and
 * the shell keeps the WebView reported visible, because the sending happens in
 * the page and Chromium freezes a hidden page after 60 s.
 *
 * Its own service, of type dataSync, rather than a second reason on the cast
 * service: music can play during a backup, and each stops on its own.
 *
 * Started only from the page while the application is in front — a foreground
 * service cannot be started from the background on Android 12+ — and its
 * progress line is updated through the notification, which needs no start.
 */
class BackupService : Service() {
    private var wake: PowerManager.WakeLock? = null
    private var wifi: WifiManager.WifiLock? = null

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val n = notification(this, intent?.getStringExtra(EXTRA_TEXT) ?: "")
        if (Build.VERSION.SDK_INT >= 29) startForeground(ID, n, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
        else startForeground(ID, n)
        if (wake == null) {
            wake = getSystemService(PowerManager::class.java)
                .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "meshbay:backup").apply { acquire(MAX_HOLD_MS) }
            @Suppress("DEPRECATION")
            val mode = if (Build.VERSION.SDK_INT >= 29) WifiManager.WIFI_MODE_FULL_LOW_LATENCY else WifiManager.WIFI_MODE_FULL_HIGH_PERF
            wifi = (applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager)
                .createWifiLock(mode, "meshbay:backup").apply { acquire() }
        }
        return START_NOT_STICKY
    }

    /**
     * Android 15 gives dataSync six hours a day and then calls this; not
     * stopping here is a crash. The run carries on with the screen on, or at
     * the next opening, which is where an interrupted run goes anyway.
     */
    override fun onTimeout(startId: Int, fgsType: Int) {
        stopSelf()
    }

    override fun onDestroy() {
        wake?.let { if (it.isHeld) it.release() }
        wifi?.let { if (it.isHeld) it.release() }
        wake = null; wifi = null
        super.onDestroy()
    }

    companion object {
        private const val ID = 8
        const val EXTRA_TEXT = "text"
        private const val MAX_HOLD_MS = 6L * 3600 * 1000

        fun notification(context: Context, text: String): Notification {
            PhotoChannels.ensureChannel(context)
            val open = PendingIntent.getActivity(context, ID,
                context.packageManager.getLaunchIntentForPackage(context.packageName), PendingIntent.FLAG_IMMUTABLE)
            return Notification.Builder(context, PhotoChannels.CHANNEL)
                .setContentTitle("MeshBay")
                .setContentText(text)
                .setSmallIcon(R.drawable.ic_notify)
                .setContentIntent(open)
                .setOngoing(true)
                .setOnlyAlertOnce(true)
                .build()
        }

        /** The progress line of a running backup; nothing when none runs. */
        fun update(context: Context, text: String) {
            val nm = context.getSystemService(NotificationManager::class.java)
            if (nm.activeNotifications.any { it.id == ID }) nm.notify(ID, notification(context, text))
        }
    }
}