ui: add service/notification abstractions
Add additional abstractions for service foreground states and their notifications.
This commit is contained in:
parent
a63b3791d2
commit
caa755c12f
8 changed files with 188 additions and 117 deletions
|
@ -17,34 +17,20 @@
|
|||
|
||||
package org.oxycblt.auxio.music.system
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.ui.system.ServiceNotification
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||
|
||||
/** The notification responsible for showing the indexer state. */
|
||||
class IndexingNotification(private val context: Context) :
|
||||
NotificationCompat.Builder(context, CHANNEL_ID) {
|
||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||
|
||||
ServiceNotification(context, INDEXER_CHANNEL) {
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.info_indexer_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW)
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
setSmallIcon(R.drawable.ic_indexer_24)
|
||||
setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
setShowWhen(false)
|
||||
|
@ -56,9 +42,8 @@ class IndexingNotification(private val context: Context) :
|
|||
setProgress(0, 0, true)
|
||||
}
|
||||
|
||||
fun renotify() {
|
||||
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
|
||||
}
|
||||
override val code: Int
|
||||
get() = IntegerTable.INDEXER_NOTIFICATION_CODE
|
||||
|
||||
fun updateIndexingState(indexing: Indexer.Indexing): Boolean {
|
||||
when (indexing) {
|
||||
|
@ -82,27 +67,11 @@ class IndexingNotification(private val context: Context) :
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
|
||||
}
|
||||
}
|
||||
|
||||
/** The notification responsible for showing the indexer state. */
|
||||
class ObservingNotification(context: Context) : NotificationCompat.Builder(context, CHANNEL_ID) {
|
||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||
|
||||
class ObservingNotification(context: Context) : ServiceNotification(context, INDEXER_CHANNEL) {
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.info_indexer_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW)
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
setSmallIcon(R.drawable.ic_indexer_24)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
setShowWhen(false)
|
||||
|
@ -113,11 +82,12 @@ class ObservingNotification(context: Context) : NotificationCompat.Builder(conte
|
|||
setContentText(context.getString(R.string.lbl_observing_desc))
|
||||
}
|
||||
|
||||
fun renotify() {
|
||||
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
|
||||
}
|
||||
override val code: Int
|
||||
get() = IntegerTable.INDEXER_NOTIFICATION_CODE
|
||||
}
|
||||
|
||||
private val INDEXER_CHANNEL =
|
||||
ServiceNotification.ChannelInfo(
|
||||
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER",
|
||||
R.string.info_indexer_channel_name,
|
||||
NotificationManager.IMPORTANCE_LOW)
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package org.oxycblt.auxio.music.system
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.database.ContentObserver
|
||||
|
@ -25,17 +24,16 @@ import android.os.Handler
|
|||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.app.ServiceCompat
|
||||
import coil.imageLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.system.ForegroundManager
|
||||
import org.oxycblt.auxio.util.contentResolverSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -49,8 +47,6 @@ import org.oxycblt.auxio.util.logD
|
|||
* boilerplate you skip is not worth the insanity of androidx.
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Add abstractions for services. notifications, and generations
|
||||
*/
|
||||
class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||
private val indexer = Indexer.getInstance()
|
||||
|
@ -63,19 +59,20 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private lateinit var settings: Settings
|
||||
|
||||
private var isForeground = false
|
||||
private lateinit var foregroundManager: ForegroundManager
|
||||
private lateinit var indexingNotification: IndexingNotification
|
||||
private lateinit var observingNotification: ObservingNotification
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
settings = Settings(this, this)
|
||||
indexerContentObserver = SystemContentObserver()
|
||||
|
||||
foregroundManager = ForegroundManager(this)
|
||||
indexingNotification = IndexingNotification(this)
|
||||
observingNotification = ObservingNotification(this)
|
||||
|
||||
settings = Settings(this, this)
|
||||
indexerContentObserver = SystemContentObserver()
|
||||
|
||||
indexer.registerController(this)
|
||||
if (musicStore.library == null && indexer.isIndeterminate) {
|
||||
logD("No library present and no previous response, indexing music now")
|
||||
|
@ -92,6 +89,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
foregroundManager.release()
|
||||
|
||||
// De-initialize the components first to prevent stray reloading events
|
||||
settings.release()
|
||||
indexerContentObserver.release()
|
||||
|
@ -162,9 +161,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
// notification when initially starting, we will not update the notification
|
||||
// unless it indicates that we have changed it.
|
||||
val changed = indexingNotification.updateIndexingState(state)
|
||||
if (!tryStartForeground(indexingNotification.build()) && changed) {
|
||||
if (!foregroundManager.tryStartForeground(indexingNotification) && changed) {
|
||||
logD("Notification changed, re-posting notification")
|
||||
indexingNotification.renotify()
|
||||
indexingNotification.post()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,34 +175,14 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
// we can go foreground later.
|
||||
// 2. If a non-foreground service is killed, the app will probably still be alive,
|
||||
// and thus the music library will not be updated at all.
|
||||
if (!tryStartForeground(observingNotification.build())) {
|
||||
observingNotification.renotify()
|
||||
if (!foregroundManager.tryStartForeground(observingNotification)) {
|
||||
observingNotification.post()
|
||||
}
|
||||
} else {
|
||||
tryStopForeground()
|
||||
foregroundManager.tryStopForeground()
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryStartForeground(notification: Notification): Boolean {
|
||||
if (isForeground) {
|
||||
return false
|
||||
}
|
||||
|
||||
startForeground(IntegerTable.INDEXER_NOTIFICATION_CODE, notification)
|
||||
isForeground = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun tryStopForeground() {
|
||||
if (!isForeground) {
|
||||
return
|
||||
}
|
||||
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
isForeground = false
|
||||
}
|
||||
|
||||
// --- SETTING CALLBACKS ---
|
||||
|
||||
override fun onSettingChanged(key: String) {
|
||||
|
|
|
@ -423,8 +423,10 @@ class PlaybackStateManager private constructor() {
|
|||
isPlaying = false
|
||||
notifyNewPlayback()
|
||||
|
||||
// Controller may have reloaded the media item, re-seek to the previous position
|
||||
seekTo(oldPosition)
|
||||
if (index > -1) {
|
||||
// Controller may have reloaded the media item, re-seek to the previous position
|
||||
seekTo(oldPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeStateImpl() =
|
||||
|
|
|
@ -43,6 +43,9 @@ import org.oxycblt.auxio.util.logD
|
|||
* using something like MediaSessionConnector is more or less impossible.
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Update textual metadata first, then cover metadata later. Janky, yes, but also resolves
|
||||
* some coherency issues.
|
||||
*/
|
||||
class MediaSessionComponent(
|
||||
private val context: Context,
|
||||
|
@ -54,12 +57,13 @@ class MediaSessionComponent(
|
|||
PlaybackStateManager.Callback,
|
||||
Settings.Callback {
|
||||
interface Callback {
|
||||
fun onPostNotification(notification: NotificationComponent)
|
||||
fun onPostNotification(notification: NotificationComponent?)
|
||||
}
|
||||
|
||||
val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true }
|
||||
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val settings = Settings(context, this)
|
||||
val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true }
|
||||
private val notification = NotificationComponent(context, mediaSession.sessionToken)
|
||||
private val provider = BitmapProvider(context)
|
||||
|
||||
|
@ -98,6 +102,7 @@ class MediaSessionComponent(
|
|||
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
|
||||
if (song == null) {
|
||||
mediaSession.setMetadata(emptyMetadata)
|
||||
callback.onPostNotification(null)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
package org.oxycblt.auxio.playback.system
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
@ -31,7 +30,7 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.ui.system.ServiceNotification
|
||||
import org.oxycblt.auxio.util.newBroadcastPendingIntent
|
||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||
|
||||
|
@ -43,20 +42,8 @@ import org.oxycblt.auxio.util.newMainPendingIntent
|
|||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
class NotificationComponent(private val context: Context, sessionToken: MediaSessionCompat.Token) :
|
||||
NotificationCompat.Builder(context, CHANNEL_ID) {
|
||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||
|
||||
ServiceNotification(context, CHANNEL_INFO) {
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.info_playback_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT)
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
setSmallIcon(R.drawable.ic_auxio_24)
|
||||
setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
||||
setShowWhen(false)
|
||||
|
@ -75,9 +62,8 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
|||
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
|
||||
}
|
||||
|
||||
fun renotify() {
|
||||
notificationManager.notify(IntegerTable.PLAYBACK_NOTIFICATION_CODE, build())
|
||||
}
|
||||
override val code: Int
|
||||
get() = IntegerTable.PLAYBACK_NOTIFICATION_CODE
|
||||
|
||||
// --- STATE FUNCTIONS ---
|
||||
|
||||
|
@ -164,6 +150,10 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK"
|
||||
val CHANNEL_INFO =
|
||||
ChannelInfo(
|
||||
id = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK",
|
||||
nameRes = R.string.info_playback_channel_name,
|
||||
importance = NotificationManager.IMPORTANCE_LOW)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import android.content.Intent
|
|||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
|
@ -45,7 +44,6 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||
|
@ -53,6 +51,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
|||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.system.ForegroundManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.widgets.WidgetComponent
|
||||
import org.oxycblt.auxio.widgets.WidgetProvider
|
||||
|
@ -93,7 +92,7 @@ class PlaybackService :
|
|||
private lateinit var settings: Settings
|
||||
|
||||
// State
|
||||
private var isForeground = false
|
||||
private lateinit var foregroundManager: ForegroundManager
|
||||
private var hasPlayed = false
|
||||
|
||||
// Coroutines
|
||||
|
@ -106,6 +105,8 @@ class PlaybackService :
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
foregroundManager = ForegroundManager(this)
|
||||
|
||||
// --- PLAYER SETUP ---
|
||||
|
||||
replayGainProcessor = ReplayGainAudioProcessor(this)
|
||||
|
@ -163,8 +164,7 @@ class PlaybackService :
|
|||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
isForeground = false
|
||||
foregroundManager.release()
|
||||
|
||||
// Pause just in case this destruction was unexpected.
|
||||
playbackManager.isPlaying = false
|
||||
|
@ -272,14 +272,17 @@ class PlaybackService :
|
|||
player.playWhenReady = isPlaying
|
||||
}
|
||||
|
||||
override fun onPostNotification(notification: NotificationComponent) {
|
||||
if (hasPlayed && playbackManager.song != null) {
|
||||
if (!isForeground) {
|
||||
startForeground(IntegerTable.PLAYBACK_NOTIFICATION_CODE, notification.build())
|
||||
isForeground = true
|
||||
} else {
|
||||
// If we are already in foreground just update the notification
|
||||
notification.renotify()
|
||||
override fun onPostNotification(notification: NotificationComponent?) {
|
||||
if (notification == null) {
|
||||
// This case is only here if I ever need to move foreground stopping from
|
||||
// the player code to the notification code.
|
||||
logD("No notification, ignoring")
|
||||
return
|
||||
}
|
||||
|
||||
if (hasPlayed) {
|
||||
if (!foregroundManager.tryStartForeground(notification)) {
|
||||
notification.post()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -329,10 +332,11 @@ class PlaybackService :
|
|||
|
||||
/** Stop the foreground state and hide the notification */
|
||||
private fun stopAndSave() {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
isForeground = false
|
||||
saveScope.launch {
|
||||
playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
|
||||
if (foregroundManager.tryStopForeground()) {
|
||||
logD("Saving playback state")
|
||||
saveScope.launch {
|
||||
playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.system
|
||||
|
||||
import android.app.Service
|
||||
import androidx.core.app.ServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* Wrapper to create consistent behavior regarding a service's foreground state.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ForegroundManager(private val service: Service) {
|
||||
private var isForeground = false
|
||||
|
||||
fun release() {
|
||||
tryStopForeground()
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to enter a foreground state. Returns false if already in foreground, returns true
|
||||
* if state was entered.
|
||||
*/
|
||||
fun tryStartForeground(notification: ServiceNotification): Boolean {
|
||||
if (isForeground) {
|
||||
return false
|
||||
}
|
||||
|
||||
logD("Starting foreground state")
|
||||
|
||||
service.startForeground(notification.code, notification.build())
|
||||
isForeground = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to stop a foreground state. Returns false if already in backend, returns true
|
||||
* if state was stopped.
|
||||
*/
|
||||
fun tryStopForeground(): Boolean {
|
||||
if (!isForeground) {
|
||||
return false
|
||||
}
|
||||
|
||||
logD("Stopping foreground state")
|
||||
|
||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
isForeground = false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.system
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
|
||||
/**
|
||||
* Wrapper around [NotificationCompat.Builder] that automates parts of the notification setup.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class ServiceNotification(context: Context, info: ChannelInfo) :
|
||||
NotificationCompat.Builder(context, info.id) {
|
||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
||||
val channel =
|
||||
NotificationChannel(info.id, context.getString(info.nameRes), info.importance)
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
abstract val code: Int
|
||||
|
||||
fun post() {
|
||||
notificationManager.notify(code, build())
|
||||
}
|
||||
|
||||
data class ChannelInfo(val id: String, @StringRes val nameRes: Int, val importance: Int)
|
||||
}
|
Loading…
Reference in a new issue