Skip to content

Commit

Permalink
Merge pull request #373 from metabrainz/dev
Browse files Browse the repository at this point in the history
Dev to Main
  • Loading branch information
akshaaatt authored Feb 27, 2024
2 parents ba65b11 + e3193b5 commit ef6a331
Show file tree
Hide file tree
Showing 43 changed files with 1,012 additions and 300 deletions.
13 changes: 5 additions & 8 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id 'dagger.hilt.android.plugin'
id "io.sentry.android.gradle" version "4.2.0"
id "io.sentry.android.gradle" version "4.3.0"
}

def keystorePropertiesFile = rootProject.file("keystore.properties")
Expand All @@ -31,8 +31,8 @@ android {
applicationId 'org.listenbrainz.android'
minSdk 21
targetSdk 34
versionCode 50
versionName "2.6.0"
versionCode 51
versionName "2.6.1"
multiDexEnabled true
testInstrumentationRunner "org.listenbrainz.android.di.CustomTestRunner" // "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down Expand Up @@ -203,7 +203,7 @@ dependencies {
implementation 'app.cash.turbine:turbine:1.0.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.12'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0'

// Mockito framework
Expand All @@ -225,9 +225,6 @@ dependencies {
testImplementation project(path: ':sharedTest')
androidTestImplementation project(path: ':sharedTest')

//ViewPager
implementation "com.google.accompanist:accompanist-pager:$accompanist_version"

//Exoplayer
api "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
api "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
Expand All @@ -252,5 +249,5 @@ dependencies {
implementation 'com.github.akshaaatt:Logger-Android:1.0.0'

//Charting Library (Vico)
implementation("com.patrykandpatrick.vico:compose:1.13.1")
implementation("com.patrykandpatrick.vico:compose:1.14.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.listenbrainz.android.model

import androidx.compose.runtime.Stable
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import org.listenbrainz.android.util.Resource
import org.listenbrainz.android.util.Utils.error
Expand Down Expand Up @@ -69,12 +70,15 @@ enum class ResponseError(val genericToast: String, var actualResponse: String? =

/** Parsing server response into [ApiError] class. Consider using specific functions like [getSocialResponseError], etc. for each repository if
* returning errors is the sole motive.*/
fun <T> parseError(response: Response<T>) : ApiError =
fun <T> parseError(response: Response<T>) : ApiError = try {
Gson().fromJson(
/* json = */ response.error(),
/* typeOfT = */ object : TypeToken<ApiError>() {}.type
)

} catch (e: JsonSyntaxException) {
// Server doesn't return error in expected format.
ApiError(response.code())
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.listenbrainz.android.model

data class SocialUiState(
val error: ResponseError? = null
val error: ResponseError? = null,
val successMsgId : Int? = null
)
17 changes: 17 additions & 0 deletions app/src/main/java/org/listenbrainz/android/model/Song.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,22 @@ data class Song (
albumArt = "",
discNumber = 0L
)

fun preview(): Song =
Song(
mediaID = 0L,
title = "Title",
trackNumber = 0,
year = 2024,
duration = 30000L,
dateModified = 0L,
artistId = 0L,
artist = "Artist",
uri = "",
albumID = 0L,
album = "Album",
albumArt = "",
discNumber = 0L
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ interface AlbumDao {
@Query(value = "SELECT * FROM ALBUMS WHERE albumId = :albumId")
fun getAlbumEntity(albumId: Long): Flow<AlbumEntity>

@Query(value = "SELECT * FROM ALBUMS")
@Query(value = "SELECT * FROM ALBUMS ORDER BY `title`")
fun getAlbumEntities(): Flow<List<AlbumEntity>>

@Query(value = "SELECT * FROM ALBUMS")
@Query(value = "SELECT * FROM ALBUMS ORDER BY `title`")
fun getAlbumEntitiesAsList(): List<AlbumEntity>

@Insert(onConflict = OnConflictStrategy.IGNORE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ interface ArtistDao {
@Query(value = "SELECT * FROM ARTISTS WHERE artistID = :artistID")
fun getArtistEntity(artistID: String) : Flow<ArtistEntity>

@Query(value = "SELECT * FROM ARTISTS")
@Query(value = "SELECT * FROM ARTISTS ORDER BY `name`")
fun getArtistEntities() : Flow<List<ArtistEntity>>

@Query(value = "SELECT * FROM ARTISTS")
@Query(value = "SELECT * FROM ARTISTS ORDER BY `name`")
fun getArtistEntitiesAsList() : List<ArtistEntity>

@Insert(onConflict = OnConflictStrategy.NONE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ interface SongDao {
value = "SELECT * FROM SONGS WHERE mediaID = :mediaId ")
fun getSongEntity(mediaId: String) : Flow<SongEntity>

@Query(value = "SELECT * FROM SONGS")
@Query(value = "SELECT * FROM SONGS ORDER BY `title`")
fun getSongEntities() : Flow<List<SongEntity>>

@Query(value = "SELECT * FROM SONGS")
@Query(value = "SELECT * FROM SONGS ORDER BY `title`")
fun getSongEntitiesAsList() : List<SongEntity>

@Insert(onConflict = OnConflictStrategy.IGNORE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import androidx.annotation.DrawableRes
import androidx.compose.foundation.text.ClickableText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
Expand All @@ -24,15 +25,15 @@ import org.listenbrainz.android.ui.screens.feed.events.RecordingRecommendationFe
import org.listenbrainz.android.ui.screens.feed.events.ReviewFeedLayout
import org.listenbrainz.android.ui.screens.feed.events.UnknownFeedLayout
import org.listenbrainz.android.ui.theme.ListenBrainzTheme
import org.listenbrainz.android.util.Log.d
import org.listenbrainz.android.util.Log.w
import org.listenbrainz.android.util.Log
import org.listenbrainz.android.util.TypeConverter
import org.listenbrainz.android.util.Utils.getArticle

/**
* @param icon Feed icon for the event, **must** be of width 19 dp.
* @param isDeletable Can only delete our (user's) recommendations and pins.
* @param isHideable Can only hide followed user's events and notifications.*/
@Immutable
enum class FeedEventType (
val type: String,
@DrawableRes val icon: Int,
Expand Down Expand Up @@ -281,11 +282,11 @@ enum class FeedEventType (
annotatedLinkString
.getStringAnnotations(charOffset, charOffset)
.firstOrNull()?.let { stringAnnotation ->
d(stringAnnotation.item)
Log.d(stringAnnotation.item)
uriHandler.openUri(stringAnnotation.item)
}
} catch (e: ActivityNotFoundException) {
w("MyFeed: Notification link invalid.")
Log.w("MyFeed: Notification link invalid.")
e.printStackTrace()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ interface ListenServiceManager {
fun onNotificationPosted(sbn: StatusBarNotification?)

fun onNotificationRemoved(sbn: StatusBarNotification?)

fun close()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ 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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
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

/** The sole responsibility of this layer is to maintain mutual exclusion between [onMetadataChanged] and
Expand All @@ -27,11 +29,13 @@ import javax.inject.Inject
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 listenSubmissionState = ListenSubmissionState(workManager, context)
//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 scope = MainScope()

/** Used to avoid repetitive submissions.*/
Expand All @@ -44,7 +48,7 @@ class ListenServiceManagerImpl @Inject constructor(

init {
with(scope) {
launch(Dispatchers.Default) {
launch(defaultDispatcher) {
appPreferences.listeningWhitelist.getFlow().collect {
whitelist = it
// Discard current listen if the controller/package has been removed from whitelist.
Expand All @@ -53,7 +57,7 @@ class ListenServiceManagerImpl @Inject constructor(
}
}
}
launch(Dispatchers.Default) {
launch(defaultDispatcher) {
appPreferences.isListeningAllowed.getFlow().collect {
isListeningAllowed = it
// Immediately discard current listen if "Send Listens" option has been turned off.
Expand All @@ -66,16 +70,16 @@ class ListenServiceManagerImpl @Inject constructor(
}

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

val newTimestamp = System.currentTimeMillis()
with(listenSubmissionState) {

// Repetitive submissions blocker
if (playingTrack.isCallbackTrack() &&
newTimestamp in lastCallbackTs..lastCallbackTs + CALLBACK_SUBMISSION_TIMEOUT_INTERVAL
if (playingTrack.isCallbackTrack()
&& newTimestamp in lastCallbackTs..lastCallbackTs + CALLBACK_SUBMISSION_TIMEOUT_INTERVAL
&& metadata.extractTitle() == playingTrack.title
) return@post

Expand All @@ -86,7 +90,7 @@ class ListenServiceManagerImpl @Inject constructor(

onControllerCallback(newTrack)
}
// Log.e("META")
Log.e("META")
}
}

Expand All @@ -102,7 +106,7 @@ class ListenServiceManagerImpl @Inject constructor(
/** 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?) {
handler.post {
jobQueue.post {
if (!isListeningAllowed) return@post

// Only CATEGORY_TRANSPORT contain media player metadata.
Expand All @@ -119,8 +123,9 @@ class ListenServiceManagerImpl @Inject constructor(

// Avoid repetitive submissions
with(listenSubmissionState) {
if (newTrack.timestamp in lastNotificationPostTs..lastNotificationPostTs + NOTI_SUBMISSION_TIMEOUT_INTERVAL
&& newTrack.pkgName == playingTrack.pkgName
if (
newTrack.pkgName == playingTrack.pkgName
&& newTrack.timestamp in lastNotificationPostTs..lastNotificationPostTs + NOTI_SUBMISSION_TIMEOUT_INTERVAL
&& newTrack.title == playingTrack.title
) return@post

Expand All @@ -132,7 +137,7 @@ class ListenServiceManagerImpl @Inject constructor(
// Alert submission state
alertMediaNotificationUpdate(newTrack)
}
// Log.e("NOTI")
Log.e("NOTI")
}
}

Expand All @@ -148,6 +153,11 @@ class ListenServiceManagerImpl @Inject constructor(
}
}

override fun close() {
//handler.cancel()
scope.cancel()
}

companion object {
/** Because some notification posts are repetitive and in close proximity to each other, these variables
* are used to mitigate those cases.*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.app.Service.STOP_FOREGROUND_DETACH
import android.os.Build
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import org.listenbrainz.android.util.Log.e
import org.listenbrainz.android.util.Log

class BrainzPlayerEventListener(
private val brainzPlayerService : BrainzPlayerService
Expand All @@ -22,6 +22,6 @@ class BrainzPlayerEventListener(

override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
e("BrainzPlayer error")
Log.e("BrainzPlayer error")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.material.icons.rounded.PlayArrow
import androidx.work.WorkManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.runBlocking
import org.listenbrainz.android.BuildConfig
import org.listenbrainz.android.model.PlayingTrack.Companion.toPlayingTrack
import org.listenbrainz.android.model.RepeatMode
Expand Down Expand Up @@ -97,7 +98,9 @@ class BrainzPlayerServiceConnection(
}
}
private inner class MediaControllerCallback(context: Context) : MediaControllerCompat.Callback() {
val listenSubmissionState: ListenSubmissionState = ListenSubmissionState(workManager, context)
val listenSubmissionState: ListenSubmissionState by lazy {
ListenSubmissionState(workManager = workManager, context = context)
}

override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
_playbackState.value = state ?: EMPTY_PLAYBACK_STATE
Expand All @@ -112,7 +115,10 @@ class BrainzPlayerServiceConnection(
previousPlaybackState = state?.isPlaying == true

// Cutout point for normal bp and bp submitter
if (appPreferences.isNotificationServiceAllowed) return
if (
appPreferences.isNotificationServiceAllowed &&
runBlocking { appPreferences.isListeningAllowed.get() }
) return

listenSubmissionState.alertPlaybackStateChanged()

Expand Down
Loading

0 comments on commit ef6a331

Please sign in to comment.