PeachSyncService.kt

3.6 KB · raw

1package com.peachpasswords.sync23import android.app.Notification4import android.app.NotificationChannel5import android.app.NotificationManager6import android.app.PendingIntent7import android.app.Service8import android.content.Context9import android.content.Intent10import android.os.Build11import android.os.IBinder12import androidx.core.app.NotificationCompat13import com.peachpasswords.MainActivity14import com.peachpasswords.R15import dagger.hilt.android.AndroidEntryPoint16import kotlinx.coroutines.CoroutineScope17import kotlinx.coroutines.SupervisorJob18import kotlinx.coroutines.cancel1920/**21 * Keeps the pairing/sync listener reachable.22 *23 * A browser extension can never listen for a connection, so the phone has to be24 * the side that is dialable — and a phone that is only dialable while its app25 * is foregrounded is not dialable in any useful sense. Doze and App Standby26 * will take an ordinary background socket away. This is the same standard27 * pattern Syncthing and Tailscale use for exactly the same reason: a foreground28 * service with a visible, honest notification.29 *30 * The notification is deliberately plain about what it is. A persistent31 * notification the user does not understand gets the app force-stopped, which32 * is the one outcome that breaks sync entirely.33 */34@AndroidEntryPoint35class PeachSyncService : Service() {3637    private val scope = CoroutineScope(SupervisorJob())3839    override fun onBind(intent: Intent?): IBinder? = null4041    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {42        startForeground(NOTIFICATION_ID, buildNotification())43        // START_STICKY: if the system reclaims us under pressure, come back —44        // a listener that silently stays down looks identical to a peer that45        // has simply gone offline, and the user gets no signal either way.46        return START_STICKY47    }4849    override fun onDestroy() {50        scope.cancel()51        super.onDestroy()52    }5354    private fun buildNotification(): Notification {55        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager56        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {57            manager.createNotificationChannel(58                NotificationChannel(CHANNEL_ID, "Peach sync", NotificationManager.IMPORTANCE_LOW).apply {59                    description = "Shown while Peach is reachable for syncing on your network."60                    setShowBadge(false)61                },62            )63        }64        val open = PendingIntent.getActivity(65            this,66            0,67            Intent(this, MainActivity::class.java),68            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,69        )70        return NotificationCompat.Builder(this, CHANNEL_ID)71            .setContentTitle("Peach sync is active")72            .setContentText("Your other devices can reach this phone on your network.")73            .setSmallIcon(R.drawable.ic_launcher_monochrome)74            .setOngoing(true)75            .setPriority(NotificationCompat.PRIORITY_LOW)76            .setContentIntent(open)77            .build()78    }7980    companion object {81        private const val CHANNEL_ID = "peach_sync"82        private const val NOTIFICATION_ID = 42018384        fun start(context: Context) {85            val intent = Intent(context, PeachSyncService::class.java)86            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {87                context.startForegroundService(intent)88            } else {89                context.startService(intent)90            }91        }9293        fun stop(context: Context) {94            context.stopService(Intent(context, PeachSyncService::class.java))95        }96    }97}