1package com.peachpasswords.sync23import android.app.Notification4import android.app.NotificationChannel5import android.app.NotificationManager6import android.app.PendingIntent7import android.app.Service8import android.content.BroadcastReceiver9import android.content.Context10import android.content.Intent11import android.os.Build12import android.os.IBinder13import androidx.core.app.NotificationCompat14import com.peachpasswords.MainActivity15import com.peachpasswords.R16import dagger.hilt.EntryPoint17import dagger.hilt.InstallIn18import dagger.hilt.android.AndroidEntryPoint19import dagger.hilt.android.EntryPointAccessors20import dagger.hilt.components.SingletonComponent21import com.peachpasswords.vault.VaultCrypto22import com.peachpasswords.vault.VaultRepository23import kotlinx.coroutines.CoroutineScope24import kotlinx.coroutines.SupervisorJob25import kotlinx.coroutines.cancel26import javax.inject.Inject27282930313233343536373839404142434445@AndroidEntryPoint46class PeachSyncService : Service() {4748 @Inject lateinit var cloudRelayListener: CloudRelayReconnectListener49 @Inject lateinit var vault: VaultRepository50 @Inject lateinit var deviceStore: PairedDeviceStore51 @Inject lateinit var pendingVouchedStore: PendingVouchedDeviceStore52 @Inject lateinit var syncAlertStore: SyncAlertStore53 @Inject lateinit var syncOutcomes: SyncOutcomeStore5455 private val scope = CoroutineScope(SupervisorJob())5657 override fun onBind(intent: Intent?): IBinder? = null5859 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {60 61 62 63 64 65 66 if (!startOrReportBlocked(syncOutcomes::report) {67 startForeground(SYNC_NOTIFICATION_ID, buildSyncNotification(this))68 }69 ) {70 stopSelf()71 return START_NOT_STICKY72 }73 startReconnectListener()74 75 76 77 78 cloudRelayListener.requestReconcile()79 80 81 82 return START_STICKY83 }8485 override fun onDestroy() {86 cloudRelayListener.stop()87 scope.cancel()88 super.onDestroy()89 }9091 9293949596979899100101102103104 private fun startReconnectListener() {105 val master = vault.syncMasterKey() ?: return106 try {107 val keys = deriveLocalSyncKeypair(master, getOrCreateDeviceId(this))108 val identity = buildLocalIdentity(this, master)109 cloudRelayListener.start(scope, keys.secretKey, keys.publicKey, identity) {110 if (!vault.isUnlocked.value) null else VaultSyncAdapter(111 repo = vault,112 ourDeviceId = getOrCreateDeviceId(this),113 pairedDevices = deviceStore,114 pendingVouched = pendingVouchedStore,115 syncAlerts = syncAlertStore,116 )117 }118 } finally {119 VaultCrypto.zero(master)120 }121 }122123 companion object {124 fun start(context: Context) {125 val intent = Intent(context, PeachSyncService::class.java)126 startOrReportBlocked({ context.reportSyncOutcome(it) }) {127 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {128 context.startForegroundService(intent)129 } else {130 context.startService(intent)131 }132 }133 }134135 fun stop(context: Context) {136 context.stopService(Intent(context, PeachSyncService::class.java))137 }138 }139}140141internal const val SYNC_NOTIFICATION_CHANNEL_ID = "peach_sync"142internal const val SYNC_NOTIFICATION_ID = 4201143internal const val ACTION_RESTORE_SYNC_NOTIFICATION =144 "com.peachpasswords.action.RESTORE_SYNC_NOTIFICATION"145146internal fun buildSyncNotification(context: Context): Notification {147 val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager148 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {149 manager.createNotificationChannel(150 NotificationChannel(151 SYNC_NOTIFICATION_CHANNEL_ID,152 "Peach sync",153 NotificationManager.IMPORTANCE_LOW,154 ).apply {155 description = "Shown while Peach is reachable for encrypted device sync."156 setShowBadge(false)157 },158 )159 }160 val open = PendingIntent.getActivity(161 context,162 0,163 Intent(context, MainActivity::class.java),164 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,165 )166 val restore = PendingIntent.getBroadcast(167 context,168 0,169 Intent(context, SyncNotificationDismissedReceiver::class.java)170 .setAction(ACTION_RESTORE_SYNC_NOTIFICATION),171 PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,172 )173 return NotificationCompat.Builder(context, SYNC_NOTIFICATION_CHANNEL_ID)174 .setContentTitle("Peach sync is active")175 .setContentText("Your other devices can reach this phone for encrypted sync.")176 .setSmallIcon(R.drawable.ic_launcher_monochrome)177 .setOngoing(true)178 .setPriority(NotificationCompat.PRIORITY_LOW)179 .setContentIntent(open)180 .setDeleteIntent(restore)181 .build()182 .also {183 184 185 186 187 it.flags = it.flags or Notification.FLAG_ONGOING_EVENT or Notification.FLAG_NO_CLEAR188 }189}190191internal fun shouldRestoreSyncNotification(action: String?, backgroundSyncEnabled: Boolean): Boolean =192 action == ACTION_RESTORE_SYNC_NOTIFICATION && backgroundSyncEnabled193194195196197198199200class SyncNotificationDismissedReceiver : BroadcastReceiver() {201 override fun onReceive(context: Context, intent: Intent?) {202 if (!shouldRestoreSyncNotification(203 intent?.action,204 BackgroundSyncPreference.isEnabled(context),205 )206 ) {207 return208 }209 val manager =210 context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager211 manager.notify(SYNC_NOTIFICATION_ID, buildSyncNotification(context))212 }213}214215216217218219220221222223224225226227internal fun startOrReportBlocked(228 report: (SyncOutcomeSummary) -> Unit,229 start: () -> Unit,230): Boolean = try {231 start()232 true233} catch (blocked: IllegalStateException) {234 report(235 SyncOutcomeSummary(236 reason = SyncOutcomeReason.FOREGROUND_START_BLOCKED,237 detail = blocked.message,238 ),239 )240 false241} catch (error: Exception) {242 243 244 report(SyncOutcomeSummary(reason = SyncOutcomeReason.RUN_FAILED, detail = error.message))245 false246}247248249250251252253254private fun Context.reportSyncOutcome(summary: SyncOutcomeSummary) {255 runCatching {256 EntryPointAccessors257 .fromApplication(applicationContext, SyncOutcomeEntryPoint::class.java)258 .syncOutcomes()259 .report(summary)260 }261}262263@EntryPoint264@InstallIn(SingletonComponent::class)265internal interface SyncOutcomeEntryPoint {266 fun syncOutcomes(): SyncOutcomeStore267}