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.EntryPoint16import dagger.hilt.InstallIn17import dagger.hilt.android.AndroidEntryPoint18import dagger.hilt.android.EntryPointAccessors19import dagger.hilt.components.SingletonComponent20import com.peachpasswords.vault.VaultCrypto21import com.peachpasswords.vault.VaultRepository22import kotlinx.coroutines.CoroutineScope23import kotlinx.coroutines.SupervisorJob24import kotlinx.coroutines.cancel25import javax.inject.Inject26272829303132333435363738394041424344@AndroidEntryPoint45class PeachSyncService : Service() {4647 @Inject lateinit var cloudRelayListener: CloudRelayReconnectListener48 @Inject lateinit var vault: VaultRepository49 @Inject lateinit var deviceStore: PairedDeviceStore50 @Inject lateinit var pendingVouchedStore: PendingVouchedDeviceStore51 @Inject lateinit var syncAlertStore: SyncAlertStore52 @Inject lateinit var syncOutcomes: SyncOutcomeStore5354 private val scope = CoroutineScope(SupervisorJob())5556 override fun onBind(intent: Intent?): IBinder? = null5758 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {59 60 61 62 63 64 65 if (!startOrReportBlocked(syncOutcomes::report) {66 startForeground(NOTIFICATION_ID, buildNotification())67 }68 ) {69 stopSelf()70 return START_NOT_STICKY71 }72 startReconnectListener()73 74 75 76 77 cloudRelayListener.requestReconcile()78 79 80 81 return START_STICKY82 }8384 override fun onDestroy() {85 cloudRelayListener.stop()86 scope.cancel()87 super.onDestroy()88 }8990 919293949596979899100101102103 private fun startReconnectListener() {104 val master = vault.syncMasterKey() ?: return105 try {106 val keys = deriveLocalSyncKeypair(master, getOrCreateDeviceId(this))107 val identity = buildLocalIdentity(this, master)108 cloudRelayListener.start(scope, keys.secretKey, keys.publicKey, identity) {109 if (!vault.isUnlocked.value) null else VaultSyncAdapter(110 repo = vault,111 ourDeviceId = getOrCreateDeviceId(this),112 pairedDevices = deviceStore,113 pendingVouched = pendingVouchedStore,114 syncAlerts = syncAlertStore,115 )116 }117 } finally {118 VaultCrypto.zero(master)119 }120 }121122 private fun buildNotification(): Notification {123 val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager124 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {125 manager.createNotificationChannel(126 NotificationChannel(CHANNEL_ID, "Peach sync", NotificationManager.IMPORTANCE_LOW).apply {127 description = "Shown while Peach is reachable for encrypted device sync."128 setShowBadge(false)129 },130 )131 }132 val open = PendingIntent.getActivity(133 this,134 0,135 Intent(this, MainActivity::class.java),136 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,137 )138 return NotificationCompat.Builder(this, CHANNEL_ID)139 .setContentTitle("Peach sync is active")140 .setContentText("Your other devices can reach this phone for encrypted sync.")141 .setSmallIcon(R.drawable.ic_launcher_monochrome)142 .setOngoing(true)143 .setPriority(NotificationCompat.PRIORITY_LOW)144 .setContentIntent(open)145 .build()146 }147148 companion object {149 private const val CHANNEL_ID = "peach_sync"150 private const val NOTIFICATION_ID = 4201151152 fun start(context: Context) {153 val intent = Intent(context, PeachSyncService::class.java)154 startOrReportBlocked({ context.reportSyncOutcome(it) }) {155 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {156 context.startForegroundService(intent)157 } else {158 context.startService(intent)159 }160 }161 }162163 fun stop(context: Context) {164 context.stopService(Intent(context, PeachSyncService::class.java))165 }166 }167}168169170171172173174175176177178179180181internal fun startOrReportBlocked(182 report: (SyncOutcomeSummary) -> Unit,183 start: () -> Unit,184): Boolean = try {185 start()186 true187} catch (blocked: IllegalStateException) {188 report(189 SyncOutcomeSummary(190 reason = SyncOutcomeReason.FOREGROUND_START_BLOCKED,191 detail = blocked.message,192 ),193 )194 false195} catch (error: Exception) {196 197 198 report(SyncOutcomeSummary(reason = SyncOutcomeReason.RUN_FAILED, detail = error.message))199 false200}201202203204205206207208private fun Context.reportSyncOutcome(summary: SyncOutcomeSummary) {209 runCatching {210 EntryPointAccessors211 .fromApplication(applicationContext, SyncOutcomeEntryPoint::class.java)212 .syncOutcomes()213 .report(summary)214 }215}216217@EntryPoint218@InstallIn(SingletonComponent::class)219internal interface SyncOutcomeEntryPoint {220 fun syncOutcomes(): SyncOutcomeStore221}