/** * Copyright (C) 2018 Jonas Lochmann * Copyright (C) 2018 Sebastian Kappes * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see //www.gnu.org/licenses/>. */ package com.redirectapps.tvkill import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.arch.lifecycle.LiveData import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.Observer import android.content.Context import android.content.Intent import android.graphics.BitmapFactory import android.os.* import android.preference.PreferenceManager.getDefaultSharedPreferences import android.support.v4.app.NotificationCompat import android.support.v4.os.CancellationSignal import com.redirectapps.tvkill.widget.UpdateWidget import java.io.Serializable import java.util.concurrent.Executors class TransmitService : Service() { // helper method for starting companion object { private const val EXTRA_REQUEST = "request" private const val NOTIFICATION_ID = 1 const val NOTIFICATION_CHANNEL = "report running in background" private val handler = Handler(Looper.getMainLooper()) private val isAppInForeground = MutableLiveData() val status = MutableLiveData() init { status.value = null isAppInForeground.value = false } val subscribeIfRunning = object : LiveData() { override fun onActive() { super.onActive() isAppInForeground.value = true } override fun onInactive() { super.onInactive() isAppInForeground.value = false } } fun executeRequest(request: TransmitServiceRequest, context: Context) { context.startService(buildIntent(request, context)) } fun buildIntent(request: TransmitServiceRequest, context: Context): Intent { return Intent(context, TransmitService::class.java) .putExtra(EXTRA_REQUEST, request) } } private var verboseInformation: Boolean = false // detection if bound (used for showing/ hiding notification) private var cancel = CancellationSignal() private val executor = Executors.newSingleThreadExecutor() private lateinit var wakeLock: PowerManager.WakeLock private var isNotificationVisible = false private lateinit var notificationBuilder: NotificationCompat.Builder private lateinit var notificationManager: NotificationManager private val statusObserver = Observer { updateNotification() UpdateWidget.updateAllWidgets(this) } private var stopped = false private var pendingRequests = 0 override fun onCreate() { super.onCreate() val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "TransmitService") val cancelIntent = buildIntent(TransmitServiceCancelRequest, this) val pendingCancelIntent = PendingIntent.getService(this, PendingIntents.NOTIFICATION_CANCEL, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT) notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL) .setOngoing(true) .setAutoCancel(false) .setPriority(NotificationCompat.PRIORITY_MAX) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_power_settings_new_white_48dp) .setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)) // this is set later (depending on the status) // .setContentTitle(getString(R.string.mode_running)) .setOnlyAlertOnce(true) .setProgress(100, 0, true) .addAction(R.drawable.ic_clear_black_48dp, getString(R.string.stop), pendingCancelIntent) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // setup notification channel (system ignores it if already registered) val channel = NotificationChannel( NOTIFICATION_CHANNEL, getString(R.string.toast_transmission_initiated), NotificationManager.IMPORTANCE_DEFAULT ) channel.setSound(null, null) channel.vibrationPattern = null channel.setShowBadge(false) channel.enableLights(false) notificationManager.createNotificationChannel(channel) } status.observeForever(statusObserver) isAppInForeground.observeForever { updateNotification() } wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/) } override fun onDestroy() { super.onDestroy() wakeLock.release() status.removeObserver(statusObserver) cancel() status.value = null stopped = true UpdateWidget.updateAllWidgets(this) stopForeground(true) //Dismiss the progress dialog (if present) if (MainActivity.progressDialog != null) { try { MainActivity.progressDialog.dismiss() } catch (e: IllegalArgumentException) { //On Android 8.1, the OS apparently sometimes throws this exception due to some internal bug (this is not our fault) e.printStackTrace() } MainActivity.progressDialog = null } } override fun onBind(intent: Intent?): IBinder? { return null } // managing of current running things override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val request = intent!!.getSerializableExtra(EXTRA_REQUEST) as TransmitServiceRequest val cancel = this.cancel if (request is TransmitServiceSendRequest) { // there is no lock because the selected executor only executes one task per time pendingRequests++ executor.submit { fun execute() { if (request.brandName == null) { if (request.action == TransmitServiceAction.Off) { verboseInformation = getDefaultSharedPreferences(this).getBoolean("show_verbose", false) // check if additional patterns should be transmitted var depth = 1 if (Settings.with(this).additionalPatterns.value!!) { depth = BrandContainer.allBrands.map { it.patterns.size }.max()!! } val numOfPatterns = BrandContainer.allBrands.sumBy { Math.min(it.patterns.size, depth) } var transmittedPatterns = 0 // transmit all patterns for (i in 0 until depth) { for (brand in BrandContainer.allBrands) { if (cancel.isCanceled) { break } if (i < brand.patterns.size) { if (!request.forever) { status.postValue(TransmitServiceStatus( request, TransmitServiceProgress(transmittedPatterns++, numOfPatterns) )) } brand.patterns[i].send(this) Brand.wait(this) } } } } else if (request.action == TransmitServiceAction.Mute) { var transmittedPatterns = 0 val numOfPatterns = BrandContainer.allBrands.size for (brand in BrandContainer.allBrands) { if (cancel.isCanceled) { break } if (!request.forever) { status.postValue(TransmitServiceStatus( request, TransmitServiceProgress(transmittedPatterns++, numOfPatterns) )) } brand.mute(this) } } else { throw IllegalStateException() } } else { val brand = BrandContainer.brandByDesignation[request.brandName] ?: throw IllegalStateException() when { request.action == TransmitServiceAction.Off -> brand.kill(this) request.action == TransmitServiceAction.Mute -> brand.mute(this) else -> throw IllegalStateException() } } } try { status.postValue(TransmitServiceStatus(request, null)) // inform about this request if (request.forever) { while (!cancel.isCanceled) { execute() } } else { execute() } } finally { handler.post { if (--pendingRequests == 0) { status.value = null // nothing is running stopSelf() } else { // status will be changed very soon } } } } } else if (request is TransmitServiceCancelRequest) { cancel() } else { throw IllegalStateException() } return START_NOT_STICKY } private fun cancel() { cancel.cancel() // create a new signal for next cancelling cancel = CancellationSignal() } private fun updateNotification() { if (stopped) { return } val request = status.value val appRunning = isAppInForeground.value if (appRunning!!) { if (isNotificationVisible) { stopForeground(true) isNotificationVisible = false } } else { if (request == null) return if (request.request.forever) { notificationBuilder.setContentTitle(getString(R.string.mode_running)) notificationBuilder.setProgress(100, 0, true) } else { notificationBuilder.setContentTitle(getString(R.string.toast_transmission_initiated)) if (request.progress != null) { notificationBuilder.setProgress(request.progress.max, request.progress.current, false) //Also update the progress dialog (if present) if (MainActivity.progressDialog != null) { MainActivity.progressDialog.max = request.progress.max MainActivity.progressDialog.progress = request.progress.current + 1 if (verboseInformation) try { MainActivity.progressDialog.setProgressNumberFormat(BrandContainer.allBrands[request.progress.current].designation.capitalize() + " (%1d/%2d)") } catch (e: ArrayIndexOutOfBoundsException) { //There is no obvious reason why this exception should occur, but, according to crash reports from Google Play, it does happen. e.printStackTrace() } } } } if (isNotificationVisible) { notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) } else { startForeground(NOTIFICATION_ID, notificationBuilder.build()) isNotificationVisible = true } } } } sealed class TransmitServiceRequest : Serializable class TransmitServiceSendRequest(val action: TransmitServiceAction, val forever: Boolean, val brandName: String?) : TransmitServiceRequest() object TransmitServiceCancelRequest : TransmitServiceRequest() enum class TransmitServiceAction { Off, Mute } data class TransmitServiceProgress(val current: Int, val max: Int) class TransmitServiceStatus(val request: TransmitServiceSendRequest, val progress: TransmitServiceProgress?)