226 lines
7.5 KiB
Kotlin
226 lines
7.5 KiB
Kotlin
/*
|
||
* MediaPlayerService.kt
|
||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||
*
|
||
* Distributed under terms of the GNU GPLv3 license.
|
||
*/
|
||
|
||
package org.moire.ultrasonic.service
|
||
|
||
import android.annotation.SuppressLint
|
||
import android.app.Notification
|
||
import android.app.NotificationChannel
|
||
import android.app.NotificationManager
|
||
import android.app.PendingIntent
|
||
import android.app.Service
|
||
import android.content.Intent
|
||
import android.os.Build
|
||
import android.os.IBinder
|
||
import android.support.v4.media.session.MediaSessionCompat
|
||
import androidx.core.app.NotificationCompat
|
||
import androidx.core.app.NotificationManagerCompat
|
||
import org.koin.android.ext.android.inject
|
||
import org.moire.ultrasonic.R
|
||
import org.moire.ultrasonic.activity.NavigationActivity
|
||
import org.moire.ultrasonic.app.UApp
|
||
import org.moire.ultrasonic.util.Constants
|
||
import org.moire.ultrasonic.util.SimpleServiceBinder
|
||
import org.moire.ultrasonic.util.Util
|
||
import timber.log.Timber
|
||
|
||
/**
|
||
* Android Foreground service which is used to download tracks even when the app is not visible
|
||
*
|
||
* "A foreground service is a service that the user is
|
||
* actively aware of and isn’t a candidate for the system to kill when low on memory."
|
||
*
|
||
* TODO: Migrate this to use the Media3 DownloadHelper
|
||
*/
|
||
class DownloadService : Service() {
|
||
private val binder: IBinder = SimpleServiceBinder(this)
|
||
|
||
private val downloader by inject<Downloader>()
|
||
|
||
private var mediaSession: MediaSessionCompat? = null
|
||
|
||
private var isInForeground = false
|
||
|
||
override fun onBind(intent: Intent): IBinder {
|
||
return binder
|
||
}
|
||
|
||
override fun onCreate() {
|
||
super.onCreate()
|
||
|
||
// Create Notification Channel
|
||
createNotificationChannel()
|
||
updateNotification()
|
||
|
||
instance = this
|
||
Timber.i("DownloadService created")
|
||
}
|
||
|
||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||
super.onStartCommand(intent, flags, startId)
|
||
return START_NOT_STICKY
|
||
}
|
||
|
||
override fun onDestroy() {
|
||
super.onDestroy()
|
||
instance = null
|
||
try {
|
||
downloader.stop()
|
||
|
||
mediaSession?.release()
|
||
mediaSession = null
|
||
} catch (ignored: Throwable) {
|
||
}
|
||
Timber.i("DownloadService stopped")
|
||
}
|
||
|
||
fun notifyDownloaderStopped() {
|
||
isInForeground = false
|
||
stopForeground(true)
|
||
stopSelf()
|
||
}
|
||
|
||
private fun createNotificationChannel() {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
|
||
// The suggested importance of a startForeground service notification is IMPORTANCE_LOW
|
||
val channel = NotificationChannel(
|
||
NOTIFICATION_CHANNEL_ID,
|
||
NOTIFICATION_CHANNEL_NAME,
|
||
NotificationManager.IMPORTANCE_LOW
|
||
)
|
||
|
||
channel.lightColor = android.R.color.holo_blue_dark
|
||
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||
channel.setShowBadge(false)
|
||
|
||
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||
manager.createNotificationChannel(channel)
|
||
}
|
||
}
|
||
|
||
// We should use a single notification builder, otherwise the notification may not be updated
|
||
// Set some values that never change
|
||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||
NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||
.setSmallIcon(R.drawable.ic_stat_ultrasonic)
|
||
.setAutoCancel(false)
|
||
.setOngoing(true)
|
||
.setOnlyAlertOnce(true)
|
||
.setWhen(System.currentTimeMillis())
|
||
.setShowWhen(false)
|
||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||
.setContentIntent(getPendingIntentForContent())
|
||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||
}
|
||
|
||
private fun updateNotification() {
|
||
|
||
val notification = buildForegroundNotification()
|
||
|
||
if (isInForeground) {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||
manager.notify(NOTIFICATION_ID, notification)
|
||
} else {
|
||
val manager = NotificationManagerCompat.from(this)
|
||
manager.notify(NOTIFICATION_ID, notification)
|
||
}
|
||
Timber.v("Updated notification")
|
||
} else {
|
||
startForeground(NOTIFICATION_ID, notification)
|
||
isInForeground = true
|
||
Timber.v("Created Foreground notification")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* This method builds a notification, reusing the Notification Builder if possible
|
||
*/
|
||
@Suppress("SpreadOperator")
|
||
private fun buildForegroundNotification(): Notification {
|
||
|
||
if (downloader.started) {
|
||
// No song is playing, but Ultrasonic is downloading files
|
||
notificationBuilder.setContentTitle(
|
||
getString(R.string.notification_downloading_title)
|
||
)
|
||
}
|
||
|
||
return notificationBuilder.build()
|
||
}
|
||
|
||
@SuppressLint("UnspecifiedImmutableFlag")
|
||
private fun getPendingIntentForContent(): PendingIntent {
|
||
val intent = Intent(this, NavigationActivity::class.java)
|
||
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||
val flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
|
||
return PendingIntent.getActivity(this, 0, intent, flags)
|
||
}
|
||
|
||
@Suppress("MagicNumber")
|
||
companion object {
|
||
|
||
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"
|
||
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"
|
||
private const val NOTIFICATION_ID = 3033
|
||
|
||
@Volatile
|
||
private var instance: DownloadService? = null
|
||
private val instanceLock = Any()
|
||
|
||
@JvmStatic
|
||
fun getInstance(): DownloadService? {
|
||
val context = UApp.applicationContext()
|
||
// Try for twenty times to retrieve a running service,
|
||
// sleep 100 millis between each try,
|
||
// and run the block that creates a service only synchronized.
|
||
for (i in 0..19) {
|
||
if (instance != null) return instance
|
||
synchronized(instanceLock) {
|
||
if (instance != null) return instance
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
context.startForegroundService(
|
||
Intent(context, DownloadService::class.java)
|
||
)
|
||
} else {
|
||
context.startService(Intent(context, DownloadService::class.java))
|
||
}
|
||
}
|
||
Util.sleepQuietly(100L)
|
||
}
|
||
return instance
|
||
}
|
||
|
||
@JvmStatic
|
||
val runningInstance: DownloadService?
|
||
get() {
|
||
synchronized(instanceLock) { return instance }
|
||
}
|
||
|
||
@JvmStatic
|
||
fun executeOnStartedMediaPlayerService(
|
||
taskToExecute: (DownloadService) -> Unit
|
||
) {
|
||
|
||
val t: Thread = object : Thread() {
|
||
override fun run() {
|
||
val instance = getInstance()
|
||
if (instance == null) {
|
||
Timber.e("ExecuteOnStarted.. failed to get a DownloadService instance!")
|
||
return
|
||
} else {
|
||
taskToExecute(instance)
|
||
}
|
||
}
|
||
}
|
||
t.start()
|
||
}
|
||
}
|
||
}
|