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))
}
}
}
|