Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev to main #487

Merged
merged 14 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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