Skip to content

package de.crysxd.octoapp

Yamac Kocovalioop0popp0 requested to merge release/1.5 into release/1.8

import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent import android.media.RingtoneManager import android.os.Build import android.os.IBinder import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import de.crysxd.octoapp.base.di.Injector import de.crysxd.octoapp.base.usecase.FormatDurationUseCase import de.crysxd.octoapp.octoprint.models.socket.Event import de.crysxd.octoapp.octoprint.models.socket.Message import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber

const val ACTION_STOP = "stop" const val DISCONNECT_IF_NO_MESSAGE_FOR_MS = 60_000L const val RETRY_DELAY = 1_000L const val RETRY_COUNT = 3L

class PrintNotificationService : Service() {

companion object {
    const val NOTIFICATION_ID = 3249
    private val isNotificationEnabled get() = Injector.get().octoPreferences().isPrintNotificationEnabled

    fun start(context: Context) {
        if (isNotificationEnabled) {
            val intent = Intent(context, PrintNotificationService::class.java)
            context.startService(intent)
        }
    }

    fun stop(context: Context) {
        val intent = Intent(context, PrintNotificationService::class.java)
        context.stopService(intent)
    }
}

private val coroutineJob = Job()
private var markDisconnectedJob: Job? = null
private val eventFlow = Injector.get().octoPrintProvider().eventFlow("notification-service")
private val openAppRequestCode = 3249
private val maxProgress = 100
private val notificationChannelId = "print_progress"
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
private val formatDurationUseCase: FormatDurationUseCase = Injector.get().formatDurationUseCase()
private var lastEta: String = ""
private var didSeePrintBeingActive = false

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

override fun onCreate() {
    super.onCreate()
    if (isNotificationEnabled) {
        Timber.i("Creating notification service")
        GlobalScope.launch(coroutineJob) {
            eventFlow.onEach {
                onEventReceived(it)
            }.retry(RETRY_COUNT) {
                delay(RETRY_DELAY)
                true
            }.catch {
                Timber.e(it)
            }.collect()
        }

        GlobalScope.launch(coroutineJob) {
            Injector.get().octoPreferences().updatedFlow.collectLatest {
                if (!isNotificationEnabled) {
                    Timber.i("Service disabled, stopping self")
                    stopSelf()
                }
            }
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
        }

        startForeground(NOTIFICATION_ID, createInitialNotification())
    } else {
        Timber.i("Notification service disabled, skipping creation")
        stopSelf()
    }
}

override fun onDestroy() {
    super.onDestroy()
    Timber.i("Destroying notification service")
    notificationManager.cancel(NOTIFICATION_ID)
    coroutineJob.cancel()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if (intent?.action == ACTION_STOP) {
        stopSelf()
    }

    return super.onStartCommand(intent, flags, startId)
}

private fun onEventReceived(event: Event) {
    GlobalScope.launch {
        try {
            when (event) {
                is Event.Disconnected -> createDisconnectedNotification()
                is Event.Connected -> createInitialNotification()
                is Event.MessageReceived -> {
                    (event.message as? Message.CurrentMessage)?.let { message ->
                        // Schedule transition into disconnected state if no message was received for a set timeout
                        markDisconnectedAfterDelay()

                        // Check if still printing
                        val flags = message.state?.flags
                        Timber.v(message.toString())
                        if (flags == null || !listOf(flags.printing, flags.paused, flags.pausing, flags.cancelling).any { it }) {
                            if (message.progress?.completion?.toInt() == maxProgress && didSeePrintBeingActive) {
                                didSeePrintBeingActive = false
                                Timber.i("Print done, showing notification")
                                val name = message.job?.file?.display
                                notificationManager.notify((3242..4637).random(), createCompletedNotification(name))
                            }

                            Timber.i("Not printing, stopping self")
                            stopSelf()
                            return@let null
                        }

                        // Update notification
                        didSeePrintBeingActive = true
                        message.progress?.let {
                            val progress = it.completion.toInt()
                            val left = formatDurationUseCase.execute(it.printTimeLeft.toLong())

                            lastEta = getString(R.string.print_eta_x, Injector.get().formatEtaUseCase().execute(it.printTimeLeft))
                            val detail = getString(R.string.notification_printing_message, progress, left)
                            val title = getString(
                                when {
                                    flags.pausing -> R.string.notification_pausing_title
                                    flags.paused -> R.string.notification_paused_title
                                    flags.cancelling -> R.string.notification_cancelling_title
                                    else -> R.string.notification_printing_title
                                }
                            )

                            createProgressNotification(progress, title, detail)
                        }
                    }
                }
                else -> null
            }?.let {
                Timber.v("Updating notification")
                notificationManager.notify(NOTIFICATION_ID, it)
            }
        } catch (e: Exception) {
            Timber.e(e)
        }
    }
}

@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
    notificationManager.createNotificationChannel(
        NotificationChannel(
            notificationChannelId,
            getString(R.string.notification_channel_print_progress),
            NotificationManager.IMPORTANCE_HIGH
        )
    )
}

private fun markDisconnectedAfterDelay() {
    markDisconnectedJob?.cancel()
    markDisconnectedJob = GlobalScope.launch(coroutineJob) {
        delay(DISCONNECT_IF_NO_MESSAGE_FOR_MS)
        notificationManager.notify(NOTIFICATION_ID, createDisconnectedNotification())
    }
}

private fun createProgressNotification(progress: Int, title: String, status: String) = createNotificationBuilder()
    .setContentTitle(title)
    .setContentText(status)
    .setProgress(maxProgress, progress, false)
    .setOngoing(true)
    .addCloseAction()
    .setNotificationSilent()
    .build()

private fun createCompletedNotification(name: String?) = createNotificationBuilder()
    .setContentTitle(getString(R.string.notification_print_done_title))
    .apply {
        name?.let {
            setContentText(it)
        }
    }
    .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
    .build()

private fun createDisconnectedNotification() = createNotificationBuilder()
    .setContentTitle(getString(R.string.notification_printing_lost_connection_message))
    .setContentText(lastEta)
    .setProgress(maxProgress, 0, true)
    .addCloseAction()
    .setOngoing(false)
    .setNotificationSilent()
    .build()

private fun createInitialNotification() = createNotificationBuilder()
    .setContentTitle(getString(R.string.notification_printing_title))
    .setProgress(maxProgress, 0, true)
    .setOngoing(true)
    .addCloseAction()
    .setNotificationSilent()
    .build()

private fun NotificationCompat.Builder.addCloseAction() = addAction(
    NotificationCompat.Action.Builder(
        null,
        getString(R.string.close),
        PendingIntent.getService(
            this@PrintNotificationService,
            0,
            Intent(this@PrintNotificationService, PrintNotificationService::class.java).setAction(ACTION_STOP),
            0
        )
    ).build()
)

private fun createNotificationBuilder() = NotificationCompat.Builder(this, notificationChannelId)
    .setColorized(true)
    .setColor(ContextCompat.getColor(this, R.color.primary_light))
    .setSmallIcon(R.drawable.ic_notification_default)
    .setContentIntent(createStartAppPendingIntent())

private fun createStartAppPendingIntent() = PendingIntent.getActivity(
    this,
    openAppRequestCode,
    Intent(this, MainActivity::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT
)

}

Merge request reports