/* * Copyright (c) 2024 Auxio Project * AuxioService.kt is part of Auxio. * * 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 . */ package org.oxycblt.auxio import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle import android.os.IBinder import android.support.v4.media.MediaBrowserCompat.MediaItem import androidx.annotation.StringRes import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.ServiceCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.music.service.MusicServiceFragment import org.oxycblt.auxio.playback.service.PlaybackServiceFragment @AndroidEntryPoint class AuxioService : MediaBrowserServiceCompat(), ForegroundListener, MusicServiceFragment.Invalidator { @Inject lateinit var playbackFragmentFactory: PlaybackServiceFragment.Factory private lateinit var playbackFragment: PlaybackServiceFragment @Inject lateinit var musicFragmentFactory: MusicServiceFragment.Factory private lateinit var musicFragment: MusicServiceFragment @SuppressLint("WrongConstant") override fun onCreate() { super.onCreate() playbackFragment = playbackFragmentFactory.create(this, this) sessionToken = playbackFragment.attach() musicFragment = musicFragmentFactory.create(this, this, this) musicFragment.attach() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // TODO: Start command occurring from a foreign service basically implies a detached // service, we might need more handling here. onHandleForeground(intent) return super.onStartCommand(intent, flags, startId) } override fun onBind(intent: Intent): IBinder? { onHandleForeground(intent) return super.onBind(intent) } private fun onHandleForeground(intent: Intent?) { val startId = intent?.getIntExtra(INTENT_KEY_START_ID, -1) ?: -1 musicFragment.start() playbackFragment.start(startId) } override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) playbackFragment.handleTaskRemoved() } override fun onDestroy() { super.onDestroy() musicFragment.release() playbackFragment.release() sessionToken = null } override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): BrowserRoot { val maximumRootChildLimit = rootHints?.getInt(MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, 4) ?: 4 return musicFragment.getRoot(maximumRootChildLimit) } override fun onLoadItem(itemId: String, result: Result) { musicFragment.getItem(itemId, result) } override fun onLoadChildren(parentId: String, result: Result>) { musicFragment.getChildren(parentId, result) } override fun onSearch(query: String, extras: Bundle?, result: Result>) { musicFragment.search(query, result) } override fun updateForeground(change: ForegroundListener.Change) { val mediaNotification = playbackFragment.notification if (mediaNotification != null) { if (change == ForegroundListener.Change.MEDIA_SESSION) { startForeground(mediaNotification.code, mediaNotification.build()) } // Nothing changed, but don't show anything music related since we can always // index during playback. } else { musicFragment.createNotification { if (it != null) { startForeground(it.code, it.build()) isForeground = true } else { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) isForeground = false } } } } override fun invalidateMusic(mediaId: String) { notifyChildrenChanged(mediaId) } companion object { var isForeground = false private set // This is only meant for Auxio to internally ensure that it's state management will work. const val INTENT_KEY_START_ID = BuildConfig.APPLICATION_ID + ".service.START_ID" } } interface ForegroundListener { fun updateForeground(change: Change) enum class Change { MEDIA_SESSION, INDEXER } } /** * Wrapper around [NotificationCompat.Builder] intended for use for [NotificationCompat]s that * signal a Service's ongoing foreground state. * * @author Alexander Capehart (OxygenCobalt) */ abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo) : NotificationCompat.Builder(context, info.id) { private val notificationManager = NotificationManagerCompat.from(context) init { // Set up the notification channel. Foreground notifications are non-substantial, and // thus make no sense to have lights, vibration, or lead to a notification badge. val channel = NotificationChannelCompat.Builder(info.id, NotificationManagerCompat.IMPORTANCE_LOW) .setName(context.getString(info.nameRes)) .setLightsEnabled(false) .setVibrationEnabled(false) .setShowBadge(false) .build() notificationManager.createNotificationChannel(channel) } /** * The code used to identify this notification. * * @see NotificationManagerCompat.notify */ abstract val code: Int /** * Reduced representation of a [NotificationChannelCompat]. * * @param id The ID of the channel. * @param nameRes A string resource ID corresponding to the human-readable name of this channel. */ data class ChannelInfo(val id: String, @StringRes val nameRes: Int) }