Skip to content

Commit

Permalink
Merge pull request #487 from metabrainz/dev
Browse files Browse the repository at this point in the history
Dev to main
  • Loading branch information
07jasjeet authored Sep 26, 2024
2 parents 14814ce + ef5a0a9 commit b7d585f
Show file tree
Hide file tree
Showing 19 changed files with 452 additions and 138 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ dependencies {
// Dependency Injection
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.startup.runtime)
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/org/listenbrainz/android/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import org.listenbrainz.android.repository.listenservicemanager.ListenServiceManager
import org.listenbrainz.android.repository.listenservicemanager.ListenServiceManagerImpl
import org.listenbrainz.android.repository.preferences.AppPreferences
import org.listenbrainz.android.repository.preferences.AppPreferencesImpl
import org.listenbrainz.android.service.BrainzPlayerServiceConnection
Expand All @@ -19,6 +22,16 @@ object AppModule {
@Provides
fun providesAppPreferences(@ApplicationContext context: Context) : AppPreferences =
AppPreferencesImpl(context)

@Singleton
@Provides
fun providesListenServiceManager(
workManager: WorkManager,
appPreferences: AppPreferences,
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher,
@ApplicationContext context: Context
): ListenServiceManager =
ListenServiceManagerImpl(workManager, appPreferences, defaultDispatcher, context)

@Provides
fun providesWorkManager(@ApplicationContext context: Context): WorkManager =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,4 @@ abstract class RemotePlayerRepositoryModule {

@Binds
abstract fun bindsRemotePlayerRepository(repository: RemotePlaybackHandlerImpl?): RemotePlaybackHandler?


@Binds
abstract fun bindsListenServiceManager(listenServiceManager: ListenServiceManagerImpl): ListenServiceManager
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ package org.listenbrainz.android.repository.listenservicemanager
import android.media.MediaMetadata
import android.media.session.PlaybackState
import android.service.notification.StatusBarNotification
import org.listenbrainz.android.util.ListenSubmissionState
import javax.inject.Singleton

@Singleton
interface ListenServiceManager {

val listenSubmissionState: ListenSubmissionState

fun onMetadataChanged(metadata: MediaMetadata?, player: String)

fun onPlaybackStateChanged(state: PlaybackState?)

fun onNotificationPosted(sbn: StatusBarNotification?)
fun onNotificationPosted(sbn: StatusBarNotification?, mediaPlaying: Boolean)

fun onNotificationRemoved(sbn: StatusBarNotification?)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.app.Notification
import android.content.Context
import android.media.MediaMetadata
import android.media.session.PlaybackState
import android.os.Handler
import android.os.Looper
import android.service.notification.StatusBarNotification
import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -15,27 +17,29 @@ import org.listenbrainz.android.di.DefaultDispatcher
import org.listenbrainz.android.model.PlayingTrack
import org.listenbrainz.android.model.PlayingTrack.Companion.toPlayingTrack
import org.listenbrainz.android.repository.preferences.AppPreferences
import org.listenbrainz.android.util.JobQueue
import org.listenbrainz.android.util.ListenSubmissionState
import org.listenbrainz.android.util.ListenSubmissionState.Companion.extractTitle
import org.listenbrainz.android.util.Log
import javax.inject.Inject
import javax.inject.Singleton

/** The sole responsibility of this layer is to maintain mutual exclusion between [onMetadataChanged] and
* [onNotificationPosted], filter out repetitive submissions and handle changes in settings which concern
* listen scrobbing.
* listening.
*
* FUTURE: Call notification popups here as well.*/
@Singleton
class ListenServiceManagerImpl @Inject constructor(
workManager: WorkManager,
private val appPreferences: AppPreferences,
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
@ApplicationContext private val context: Context
): ListenServiceManager {

//private val handler: Handler by lazy { Handler(Looper.getMainLooper()) }
private val jobQueue: JobQueue by lazy { JobQueue(defaultDispatcher) }
private val listenSubmissionState = ListenSubmissionState(jobQueue, workManager, context)
private val handler: Handler = Handler(Looper.getMainLooper())
override val listenSubmissionState = ListenSubmissionState(workManager, context)
//private val jobQueue: JobQueue by lazy { JobQueue(defaultDispatcher) }
//private val listenSubmissionState = ListenSubmissionState(jobQueue, workManager, context)
private val scope = MainScope()

/** Used to avoid repetitive submissions.*/
Expand Down Expand Up @@ -70,7 +74,7 @@ class ListenServiceManagerImpl @Inject constructor(
}

override fun onMetadataChanged(metadata: MediaMetadata?, player: String) {
jobQueue.post {
handler.post {
if (!isListeningAllowed) return@post
if (metadata == null) return@post

Expand All @@ -96,48 +100,67 @@ class ListenServiceManagerImpl @Inject constructor(

override fun onPlaybackStateChanged(state: PlaybackState?) {
// No need of this right now.
/*scope.launch {
timerMutex.withLock {
listenSubmissionState.toggleTimer(state?.state)
}
}*/
//listenSubmissionState.alertPlaybackStateChanged()
}

/** NOTE FOR FUTURE USE: When onNotificationPosted is called twice within 300..600ms delay, it usually
* means the track has been changed.*/
override fun onNotificationPosted(sbn: StatusBarNotification?) {
jobQueue.post {
override fun onNotificationPosted(sbn: StatusBarNotification?, mediaPlaying: Boolean) {
handler.post {
if (!isListeningAllowed) return@post

// Only CATEGORY_TRANSPORT contain media player metadata.
if (sbn?.notification?.category != Notification.CATEGORY_TRANSPORT) return@post
if (sbn?.notification?.category != Notification.CATEGORY_TRANSPORT) {
Log.d("Notification category is ${sbn?.notification?.category} not transport")
return@post
}


val newTrack = PlayingTrack(
title = sbn.notification.extras.getString(Notification.EXTRA_TITLE)
?: return@post,
artist = sbn.notification.extras.getString(Notification.EXTRA_TEXT)
?: return@post,
title = sbn.notification.extras.getCharSequence(Notification.EXTRA_TITLE)?.toString()
?: sbn.notification.extras.getString(Notification.EXTRA_TITLE)
//?: (sbn.notification.extras.get(Notification.EXTRA_TITLE) as? SpannableString)?.toString()
//?: sbn.notification.extras.getCharSequence(Notification.EXTRA_TITLE)?.toString()
?: run {
Log.d("Notification title is null")
return@post
},
artist = sbn.notification.extras.getCharSequence(Notification.EXTRA_TEXT)?.toString()
?: sbn.notification.extras.getString(Notification.EXTRA_TEXT)
//?: (sbn.notification.extras.get(Notification.EXTRA_TEXT) as? SpannableString)?.toString()
//?: sbn.notification.extras.getCharSequence(Notification.EXTRA_TEXT)?.toString()
?: run {
Log.d("Notification artist is null")
return@post
},
pkgName = sbn.packageName,
timestamp = sbn.notification.`when`
timestamp = sbn.notification.`when`.let {
if (it == 0L) System.currentTimeMillis() else it
}
)

// Avoid repetitive submissions
with(listenSubmissionState) {
if (
/*if (
newTrack.pkgName == playingTrack.pkgName
&& newTrack.timestamp in lastNotificationPostTs..lastNotificationPostTs + NOTI_SUBMISSION_TIMEOUT_INTERVAL
&& newTrack.title == playingTrack.title
) return@post
) {
Log.d("Repetitive listen, dismissing.")
return@post
}*/

// Check for whitelisted apps
if (sbn.packageName !in whitelist) return@post
if (sbn.packageName !in whitelist) {
Log.d("Package ${sbn.packageName} not in whitelist, dismissing.")
return@post
}

lastNotificationPostTs = newTrack.timestamp

// Alert submission state
alertMediaNotificationUpdate(newTrack)
alertMediaNotificationUpdate(newTrack, mediaPlaying)
}
Log.e("NOTI")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package org.listenbrainz.android.service
import android.content.ComponentName
import android.content.Context
import android.media.MediaMetadata
import android.os.Looper
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Pause
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.core.os.HandlerCompat
import androidx.work.WorkManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -120,8 +122,7 @@ class BrainzPlayerServiceConnection(
runBlocking { appPreferences.isListeningAllowed.get() }
) return

listenSubmissionState.alertPlaybackStateChanged()

listenSubmissionState.alertPlaybackStateChanged(state?.isPlaying == true)
}

override fun onRepeatModeChanged(repeatMode: Int) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ class ListenSubmissionService : NotificationListenerService() {
@Inject
lateinit var serviceManager: ListenServiceManager

private var sessionListener: ListenSessionListener? = null
private var listenServiceComponent: ComponentName? = null
private val scope = MainScope()

private var _sessionListener: ListenSessionListener? = null
private val sessionListener: ListenSessionListener
get() = _sessionListener!!

private var listenServiceComponent: ComponentName? = null
private var isConnected = false

private val nm: NotificationManager? by lazy {
val manager = ContextCompat.getSystemService(this, NotificationManager::class.java)
Expand All @@ -45,41 +50,55 @@ class ListenSubmissionService : NotificationListenerService() {
Log.e("MediaSessionManager is not available in this context.")
manager
}

override fun onCreate() {
super.onCreate()
initialize()

override fun onListenerConnected() {
// Called more times than onListenerDisconnected for some reason.
if (!isConnected) {
initialize()
isConnected = true
}
}

override fun onListenerDisconnected() {
if (isConnected) {
destroy()
Log.d("onListenerDisconnected: Listen Service paused.")
isConnected = false
}
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = Service.START_STICKY

private fun initialize() {
Log.d("Initializing Listener Service")
sessionListener = ListenSessionListener(appPreferences, serviceManager, scope)
_sessionListener = ListenSessionListener(appPreferences, serviceManager, scope)
listenServiceComponent = ComponentName(this, this.javaClass)
createNotificationChannel()

try {
sessionManager?.addOnActiveSessionsChangedListener(sessionListener!!, listenServiceComponent)
sessionManager?.addOnActiveSessionsChangedListener(sessionListener, listenServiceComponent)
} catch (e: SecurityException) {
Log.e(message = "Could not add session listener due to security exception: ${e.message}")
} catch (e: Exception) {
Log.e(message = "Could not add session listener: ${e.message}")
}
}

override fun onDestroy() {
private fun destroy() {
deleteNotificationChannel()
sessionListener?.clearSessions()
sessionListener?.let { sessionManager?.removeOnActiveSessionsChangedListener(it) }
sessionListener.clearSessions()
sessionListener.let { sessionManager?.removeOnActiveSessionsChangedListener(it) }
}

override fun onDestroy() {
serviceManager.close()
scope.cancel()
Log.d("onDestroy: Listen Scrobble Service stopped.")
Log.d("onDestroy: Listen Service stopped.")
super.onDestroy()
}

override fun onNotificationPosted(sbn: StatusBarNotification?) {
serviceManager.onNotificationPosted(sbn)
serviceManager.onNotificationPosted(sbn, sessionListener.isMediaPlaying)
}

override fun onNotificationRemoved(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,12 @@ class ListenSubmissionWorker @AssistedInject constructor(
if (inputData.getString("TYPE") == "single"){
// We don't want to submit playing nows later.
if (response.error?.ordinal == ResponseError.BAD_REQUEST.ordinal) {
Log.d("Submission failed, not saving listen because metadata is faulty.")
Log.e(
"Submission failed, not saving listen because metadata is faulty."
+ "\n Server response: ${response.error.toast()}" + "\n POST Request Body: $body"
)
} else {
Log.d("Submission failed, listen saved.")
Log.e("Submission failed, listen saved.")
pendingListensDao.addListen(listen)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ private fun TrackListCard(
.fillMaxWidth()
.padding(23.dp)){
Column {
Text("Tracklist", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp))
Text("Tracklist", color = ListenBrainzTheme.colorScheme.text, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp))
Spacer(modifier = Modifier.height(20.dp))
trackList.map {
ListenCardSmall(trackName = it?.name ?: "", artists = it?.artists ?: listOf(), coverArtUrl = uiState.coverArt, goToArtistPage = {}){}
Expand Down Expand Up @@ -241,7 +241,7 @@ private fun TopListenersCard(
.fillMaxWidth()
.padding(23.dp)){
Column {
Text("Top listeners", color = ListenBrainzTheme.colorScheme.textColor, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp))
Text("Top listeners", color = ListenBrainzTheme.colorScheme.text, style = MaterialTheme.typography.bodyMedium.copy(fontSize = 25.sp))
Spacer(modifier = Modifier.height(20.dp))
topListeners.map {
ArtistCard(artistName = it?.userName ?: "", listenCountLabel = formatNumber(it?.listenCount ?: 0)) {
Expand Down
Loading

0 comments on commit b7d585f

Please sign in to comment.