From 4220fadf469284a43cd80dc905d0bd472613b0f9 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:23:59 +0530 Subject: [PATCH 1/5] Bump dependencies, add strict mode to app and fix codebase --- app/build.gradle | 20 ++--- .../listenbrainz/android/application/App.kt | 77 ++++++++++++++++++- .../ui/screens/dashboard/DashboardActivity.kt | 19 ++++- .../listenbrainz/android/util/Constants.kt | 9 ++- app/src/main/res/values/strings.xml | 28 +++++++ build.gradle | 8 +- sharedTest/build.gradle | 4 +- 7 files changed, 142 insertions(+), 23 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f2b4bb53..5e42ec83 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -123,7 +123,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2' - implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0-alpha02' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0-beta01' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.browser:browser:1.6.0' @@ -145,7 +145,7 @@ dependencies { //Image downloading and Caching library implementation 'com.github.bumptech.glide:glide:4.16.0' implementation "com.github.bumptech.glide:compose:1.0.0-alpha.3" - implementation 'io.coil-kt:coil-compose:2.4.0' + implementation 'io.coil-kt:coil-compose:2.5.0' implementation 'com.caverock:androidsvg-aar:1.4' ksp 'com.github.bumptech.glide:compiler:4.16.0' @@ -153,24 +153,24 @@ dependencies { implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" //Design Setup - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.10.0' implementation 'com.airbnb.android:lottie:6.1.0' implementation 'com.github.akshaaatt:Onboarding:1.1.2' implementation 'com.github.akshaaatt:Share-Android:1.0.0' - implementation 'androidx.hilt:hilt-navigation-compose:1.0.0' + implementation 'androidx.hilt:hilt-navigation-compose:1.1.0' implementation 'com.airbnb.android:lottie-compose:6.1.0' //Dagger-Hilt implementation("com.google.dagger:hilt-android:$hilt_version") kapt("com.google.dagger:hilt-android-compiler:$hilt_version") - kapt("androidx.hilt:hilt-compiler:1.0.0") - implementation 'androidx.hilt:hilt-work:1.0.0' + kapt('androidx.hilt:hilt-compiler:1.1.0') + implementation 'androidx.hilt:hilt-work:1.1.0' implementation "androidx.startup:startup-runtime:1.1.1" androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version" kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version" //Jetpack Compose - implementation platform('androidx.compose:compose-bom:2023.09.02') + implementation platform('androidx.compose:compose-bom:2023.10.01') implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling' @@ -191,7 +191,7 @@ dependencies { implementation files('./lib/spotify-app-remote-release-0.7.2.aar') // HTML Parser for retrieving token - implementation "org.jsoup:jsoup:1.16.1" + implementation 'org.jsoup:jsoup:1.16.2' //Socket IO implementation ('io.socket:socket.io-client:2.1.0') { @@ -207,11 +207,11 @@ dependencies { testImplementation 'androidx.arch.core:core-testing:2.2.0' // Mockito framework - testImplementation 'org.mockito:mockito-core:5.5.0' + testImplementation 'org.mockito:mockito-core:5.7.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:5.1.0' debugImplementation "androidx.test:monitor:1.6.1" // Solves "class PlatformTestStorageRegistery not found" error for ui tests. - debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.3" + debugImplementation 'androidx.compose.ui:ui-test-manifest:1.5.4' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" androidTestImplementation "androidx.work:work-testing:$work_version" diff --git a/app/src/main/java/org/listenbrainz/android/application/App.kt b/app/src/main/java/org/listenbrainz/android/application/App.kt index c2a55e5f..a92c7678 100644 --- a/app/src/main/java/org/listenbrainz/android/application/App.kt +++ b/app/src/main/java/org/listenbrainz/android/application/App.kt @@ -1,7 +1,12 @@ package org.listenbrainz.android.application import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Intent +import android.os.Build +import android.os.StrictMode +import androidx.core.content.ContextCompat import androidx.hilt.work.HiltWorkerFactory import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner @@ -9,8 +14,11 @@ import androidx.work.Configuration import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import org.listenbrainz.android.BuildConfig +import org.listenbrainz.android.R import org.listenbrainz.android.repository.preferences.AppPreferences import org.listenbrainz.android.service.ListenScrobbleService +import org.listenbrainz.android.util.Constants import javax.inject.Inject @HiltAndroidApp @@ -24,6 +32,11 @@ class App : Application(), Configuration.Provider { override fun onCreate() { super.onCreate() + + if (BuildConfig.DEBUG) { + enableStrictMode() + } + context = this MainScope().launch { @@ -35,8 +48,70 @@ class App : Application(), Configuration.Provider { startListenService() } } + + createChannels() } - + + private fun createChannels() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + return + + val nm = ContextCompat.getSystemService(this, NotificationManager::class.java)!! + + val channels = nm.notificationChannels + + // delete old channels, if they exist + if (channels?.any { it.id == "foreground" } == true) { + channels.forEach { nm.deleteNotificationChannel(it.id) } + } + + nm.createNotificationChannel( + NotificationChannel( + Constants.Strings.CHANNEL_NOTI_SCROBBLING, + getString(R.string.state_scrobbling), NotificationManager.IMPORTANCE_LOW + ) + ) + nm.createNotificationChannel( + NotificationChannel( + Constants.Strings.CHANNEL_NOTI_SCR_ERR, + getString(R.string.channel_err), NotificationManager.IMPORTANCE_MIN + ) + ) + nm.createNotificationChannel( + NotificationChannel( + Constants.Strings.CHANNEL_NOTI_NEW_APP, + getString(R.string.new_player, getString(R.string.new_app)), + NotificationManager.IMPORTANCE_LOW + ) + ) + nm.createNotificationChannel( + NotificationChannel( + Constants.Strings.CHANNEL_NOTI_PENDING, + getString(R.string.pending_scrobbles), NotificationManager.IMPORTANCE_MIN + ) + ) + } + + private fun enableStrictMode() { + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .penaltyFlashScreen() + .build() + ) + StrictMode.setVmPolicy( + StrictMode.VmPolicy.Builder() + .detectActivityLeaks() + .detectFileUriExposure() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() + .detectLeakedSqlLiteObjects() + .penaltyLog() + .build() + ) + } + override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder() .setWorkerFactory(workerFactory) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/dashboard/DashboardActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/dashboard/DashboardActivity.kt index cd2b44fe..d9c9e4e5 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/dashboard/DashboardActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/dashboard/DashboardActivity.kt @@ -1,5 +1,7 @@ package org.listenbrainz.android.ui.screens.dashboard +import android.app.ActivityManager +import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult @@ -26,6 +28,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.listenbrainz.android.application.App import org.listenbrainz.android.model.PermissionStatus +import org.listenbrainz.android.service.ListenScrobbleService import org.listenbrainz.android.ui.components.DialogLB import org.listenbrainz.android.ui.navigation.AppNavigation import org.listenbrainz.android.ui.navigation.BottomNavigationBar @@ -194,9 +197,21 @@ class DashboardActivity : ComponentActivity() { override fun onResume() { super.onResume() lifecycleScope.launch(Dispatchers.Main) { - if(dashBoardViewModel.isNotificationListenerServiceAllowed()) { - App.startListenService() + if (dashBoardViewModel.isNotificationListenerServiceAllowed()) { + if (!isServiceRunning(ListenScrobbleService::class.java)) { + App.startListenService() + } + } + } + } + + private fun isServiceRunning(serviceClass: Class<*>): Boolean { + val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + for (service in manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.name == service.service.className) { + return true } } + return false } } diff --git a/app/src/main/java/org/listenbrainz/android/util/Constants.kt b/app/src/main/java/org/listenbrainz/android/util/Constants.kt index f072425a..966a6528 100644 --- a/app/src/main/java/org/listenbrainz/android/util/Constants.kt +++ b/app/src/main/java/org/listenbrainz/android/util/Constants.kt @@ -14,10 +14,6 @@ object Constants { const val LISTENBRAINZ_API_BASE_URL = "https://api.listenbrainz.org/1/" const val ONBOARDING = "onboarding_lb" const val ABOUT_URL = "https://listenbrainz.org/about" - - object Headers { - const val AUTHORIZATION = "Authorization" - } object Strings { const val TIMESTAMP = "timestamp" @@ -35,6 +31,11 @@ object Constants { const val REFRESH_TOKEN = "refresh_token" const val STATUS_LOGGED_IN = 1 const val STATUS_LOGGED_OUT = 0 + + const val CHANNEL_NOTI_SCROBBLING = "noti_scrobbling" + const val CHANNEL_NOTI_SCR_ERR = "noti_scrobble_errors" + const val CHANNEL_NOTI_NEW_APP = "noti_new_app" + const val CHANNEL_NOTI_PENDING = "noti_pending_scrobbles" } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d695347f..5577fea4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -230,6 +230,34 @@ tt_yim_statistics_parent SettingsActivity + + Now scrobbling… + Scrobbled + Unscrobble + Unscrobbled + Unrecognised artist + (Tap to edit) + Recently Scrobbled + %1$s detected + Keep scrobbling this app? + No songs here yet… + Scrobble errors + New app + No apps are enabled + %1$d more + Pending Scrobbles + Pending scrobble + Processing pending scrobbles + %1$s top scrobbles + You acknowledge and agree that your contributed reviews to CritiqueBrainz are licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) license. You agree to license your work under this license. You represent and warrant that you own or control all rights in and to the work, that nothing in the work infringes the rights of any third-party, and that you have the permission to use and to license the work under the selected Creative Commons license. Finally, you give the MetaBrainz Foundation permission to license this content for commercial use outside of Creative Commons licenses in order to support the operations of the organization. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5077ae61..b409601e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,10 +3,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { ext { kotlin_version = '1.9.10' - navigationVersion = '2.7.3' - hilt_version = '2.48' - compose_version = '1.5.2' - room_version = '2.5.2' + navigationVersion = '2.7.5' + hilt_version = '2.48.1' + compose_version = '1.5.4' + room_version = '2.6.0' accompanist_version = '0.32.0' work_version = "2.8.1" exoplayer_version = '2.19.1' diff --git a/sharedTest/build.gradle b/sharedTest/build.gradle index eb629d12..e238eedd 100644 --- a/sharedTest/build.gradle +++ b/sharedTest/build.gradle @@ -30,7 +30,7 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.10.0' //Web Service Setup implementation 'com.google.code.gson:gson:2.10.1' @@ -43,7 +43,7 @@ dependencies { implementation 'com.squareup.okhttp3:mockwebserver:5.0.0-alpha.11' implementation 'androidx.arch.core:core-testing:2.2.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' - implementation "androidx.room:room-testing:2.5.2" + implementation 'androidx.room:room-testing:2.6.0' debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" implementation 'androidx.test:runner:1.5.2' From 3917339291b96741b0b9a3cfa0d6f6249288263d Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Sun, 12 Nov 2023 18:21:13 +0530 Subject: [PATCH 2/5] Cleanup code --- .../android/service/ListenScrobbleService.kt | 46 +++++++--- .../android/service/ListenSubmissionWorker.kt | 81 ++++++++++-------- .../ui/screens/dashboard/DashboardActivity.kt | 13 +-- .../listenbrainz/android/util/Constants.kt | 1 + .../android/util/ListenSessionListener.kt | 9 ++ .../android/util/ListenSubmissionState.kt | 1 - .../org/listenbrainz/android/util/Utils.kt | 83 +++++++++++++++++++ build.gradle | 2 +- 8 files changed, 173 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/service/ListenScrobbleService.kt b/app/src/main/java/org/listenbrainz/android/service/ListenScrobbleService.kt index 0fd1d624..d294e42a 100644 --- a/app/src/main/java/org/listenbrainz/android/service/ListenScrobbleService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/ListenScrobbleService.kt @@ -1,11 +1,15 @@ package org.listenbrainz.android.service +import android.app.NotificationChannel +import android.app.NotificationManager import android.app.Service import android.content.ComponentName import android.content.Intent import android.media.session.MediaSessionManager +import android.os.Build import android.service.notification.NotificationListenerService import android.service.notification.StatusBarNotification +import androidx.core.content.ContextCompat import androidx.work.WorkManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -13,8 +17,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import org.listenbrainz.android.repository.listens.ListensRepository import org.listenbrainz.android.repository.preferences.AppPreferences +import org.listenbrainz.android.util.Constants.Strings.CHANNEL_ID import org.listenbrainz.android.util.ListenSessionListener import org.listenbrainz.android.util.Log.d +import org.listenbrainz.android.util.Log.e import javax.inject.Inject @AndroidEntryPoint @@ -33,6 +39,12 @@ class ListenScrobbleService : NotificationListenerService() { private var sessionListener: ListenSessionListener? = null private var listenServiceComponent: ComponentName? = null private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val nm by lazy { + ContextCompat.getSystemService( + this, + NotificationManager::class.java + )!! + } override fun onCreate() { super.onCreate() @@ -46,22 +58,15 @@ class ListenScrobbleService : NotificationListenerService() { sessionManager = applicationContext.getSystemService(MEDIA_SESSION_SERVICE) as MediaSessionManager sessionListener = ListenSessionListener(appPreferences, workManager, scope) listenServiceComponent = ComponentName(this, this.javaClass) - + createNotificationChannel() + try { - sessionManager?.addOnActiveSessionsChangedListener( - sessionListener!!, - listenServiceComponent - ) + sessionManager?.addOnActiveSessionsChangedListener(sessionListener!!, listenServiceComponent) + } catch (e: SecurityException) { + e("Could not add session listener due to security exception: ${e.message}") } catch (e: Exception) { - e.printStackTrace() - // Remove orphan entries. - try { - sessionManager?.removeOnActiveSessionsChangedListener(sessionListener!!) - } catch (e: Exception) { - e.printStackTrace() - } + e("Could not add session listener: ${e.message}") } - } override fun onDestroy() { @@ -78,4 +83,19 @@ class ListenScrobbleService : NotificationListenerService() { override fun onNotificationRemoved(sbn: StatusBarNotification) { super.onNotificationRemoved(sbn) } + + companion object { + private const val CHANNEL_NAME = "Scrobbling" + private const val CHANNEL_DESCRIPTION = "Shows notifications when a song is played" + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance).apply { + description = CHANNEL_DESCRIPTION + } + nm.createNotificationChannel(channel) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/ListenSubmissionWorker.kt b/app/src/main/java/org/listenbrainz/android/service/ListenSubmissionWorker.kt index 43db9391..2ef01629 100644 --- a/app/src/main/java/org/listenbrainz/android/service/ListenSubmissionWorker.kt +++ b/app/src/main/java/org/listenbrainz/android/service/ListenSubmissionWorker.kt @@ -56,7 +56,10 @@ class ListenSubmissionWorker @AssistedInject constructor( // Our listen to submit val listen = ListenSubmitBody.Payload( - timestamp = if(inputData.getString("TYPE") == ListenType.SINGLE.code) inputData.getLong(Constants.Strings.TIMESTAMP, 0) else null, + timestamp = when (ListenType.SINGLE.code) { + inputData.getString("TYPE") -> inputData.getLong(Constants.Strings.TIMESTAMP, 0) + else -> null + }, metadata = metadata ) @@ -69,45 +72,51 @@ class ListenSubmissionWorker @AssistedInject constructor( repository.submitListen(token, body) } - return if (response.status == Resource.Status.SUCCESS){ - d("Listen submitted.") - - // Means conditions are met. Work manager automatically manages internet state. - val pendingListens = pendingListensDao.getPendingListens() - - if (pendingListens.isNotEmpty()) { - - val submission = withContext(Dispatchers.IO){ - repository.submitListen( - token, - ListenSubmitBody().apply { - listenType = "import" - addListens(listensList = pendingListens) + return when (response.status) { + Resource.Status.SUCCESS -> { + d("Listen submitted.") + + // Means conditions are met. Work manager automatically manages internet state. + val pendingListens = pendingListensDao.getPendingListens() + + if (pendingListens.isNotEmpty()) { + + val submission = withContext(Dispatchers.IO){ + repository.submitListen( + token, + ListenSubmitBody().apply { + listenType = "import" + addListens(listensList = pendingListens) + } + + ) + } + + when (submission.status) { + Resource.Status.SUCCESS -> { + // Empty all pending listens. + d("Pending listens submitted.") + pendingListensDao.deleteAllPendingListens() } - - ) - } - - if (submission.status == Resource.Status.SUCCESS){ - // Empty all pending listens. - d("Pending listens submitted.") - pendingListensDao.deleteAllPendingListens() - } else { - w("Could not submit pending listens.") + else -> { + w("Could not submit pending listens.") + } + } } + + Result.success() + } - - Result.success() - - } else { - // In case of failure, we add this listen to pending list. - if (inputData.getString("TYPE") == "single"){ - // We don't want to submit playing nows later. - d("Submission failed, listen saved.") - pendingListensDao.addListen(listen) + else -> { + // In case of failure, we add this listen to pending list. + if (inputData.getString("TYPE") == "single"){ + // We don't want to submit playing nows later. + d("Submission failed, listen saved.") + pendingListensDao.addListen(listen) + } + + Result.failure() } - - Result.failure() } } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/dashboard/DashboardActivity.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/dashboard/DashboardActivity.kt index d9c9e4e5..b282a5f2 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/dashboard/DashboardActivity.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/dashboard/DashboardActivity.kt @@ -1,7 +1,5 @@ package org.listenbrainz.android.ui.screens.dashboard -import android.app.ActivityManager -import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult @@ -37,6 +35,7 @@ import org.listenbrainz.android.ui.screens.brainzplayer.BrainzPlayerBackDropScre import org.listenbrainz.android.ui.screens.search.SearchScreen import org.listenbrainz.android.ui.screens.search.rememberSearchBarState import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.util.Utils.isServiceRunning import org.listenbrainz.android.viewmodel.DashBoardViewModel @AndroidEntryPoint @@ -204,14 +203,4 @@ class DashboardActivity : ComponentActivity() { } } } - - private fun isServiceRunning(serviceClass: Class<*>): Boolean { - val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - for (service in manager.getRunningServices(Integer.MAX_VALUE)) { - if (serviceClass.name == service.service.className) { - return true - } - } - return false - } } diff --git a/app/src/main/java/org/listenbrainz/android/util/Constants.kt b/app/src/main/java/org/listenbrainz/android/util/Constants.kt index 966a6528..75d17a88 100644 --- a/app/src/main/java/org/listenbrainz/android/util/Constants.kt +++ b/app/src/main/java/org/listenbrainz/android/util/Constants.kt @@ -36,6 +36,7 @@ object Constants { const val CHANNEL_NOTI_SCR_ERR = "noti_scrobble_errors" const val CHANNEL_NOTI_NEW_APP = "noti_new_app" const val CHANNEL_NOTI_PENDING = "noti_pending_scrobbles" + const val CHANNEL_ID = "listen_scrobble_channel" } } diff --git a/app/src/main/java/org/listenbrainz/android/util/ListenSessionListener.kt b/app/src/main/java/org/listenbrainz/android/util/ListenSessionListener.kt index e98d427f..970b178a 100644 --- a/app/src/main/java/org/listenbrainz/android/util/ListenSessionListener.kt +++ b/app/src/main/java/org/listenbrainz/android/util/ListenSessionListener.kt @@ -15,9 +15,11 @@ import org.listenbrainz.android.util.Log.d class ListenSessionListener(val appPreferences: AppPreferences, val workManager: WorkManager, private val serviceScope: CoroutineScope) : OnActiveSessionsChangedListener { private val activeSessions: MutableMap = HashMap() + private var controllers: List? = null @Synchronized override fun onActiveSessionsChanged(controllers: List?) { + this.controllers = controllers d("onActiveSessionsChanged: EXECUTED") if (controllers == null) return clearSessions() @@ -60,6 +62,13 @@ class ListenSessionListener(val appPreferences: AppPreferences, val workManager: activeSessions.clear() } + fun isMediaPlaying() = + controllers?.any { + it.playbackState?.state == PlaybackState.STATE_PLAYING && + !it.metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST).isNullOrEmpty() && + !it.metadata?.getString(MediaMetadata.METADATA_KEY_TITLE).isNullOrEmpty() + } ?: false + private inner class ListenCallback(private val player: String) : MediaController.Callback() { val listenSubmissionState: ListenSubmissionState = ListenSubmissionState() diff --git a/app/src/main/java/org/listenbrainz/android/util/ListenSubmissionState.kt b/app/src/main/java/org/listenbrainz/android/util/ListenSubmissionState.kt index 960c55a4..e095713b 100644 --- a/app/src/main/java/org/listenbrainz/android/util/ListenSubmissionState.kt +++ b/app/src/main/java/org/listenbrainz/android/util/ListenSubmissionState.kt @@ -11,7 +11,6 @@ import com.dariobrux.kotimer.interfaces.OnTimerListener import org.listenbrainz.android.model.ListenType import org.listenbrainz.android.service.ListenSubmissionWorker - class ListenSubmissionState( private var artist: String? = null, private var title: String? = null, diff --git a/app/src/main/java/org/listenbrainz/android/util/Utils.kt b/app/src/main/java/org/listenbrainz/android/util/Utils.kt index febc65ab..185116d2 100644 --- a/app/src/main/java/org/listenbrainz/android/util/Utils.kt +++ b/app/src/main/java/org/listenbrainz/android/util/Utils.kt @@ -1,5 +1,7 @@ package org.listenbrainz.android.util +import android.app.ActivityManager +import android.app.NotificationManager import android.content.ActivityNotFoundException import android.content.ContentValues import android.content.Context @@ -7,6 +9,9 @@ import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Icon import android.media.MediaScannerConnection import android.net.Uri import android.os.Build @@ -15,12 +20,16 @@ import android.provider.MediaStore import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.annotation.RequiresApi import androidx.annotation.WorkerThread +import androidx.core.app.NotificationCompat import kotlinx.coroutines.Dispatchers import okhttp3.* import org.listenbrainz.android.R import org.listenbrainz.android.model.ResponseError import org.listenbrainz.android.model.ResponseError.Companion.getError +import org.listenbrainz.android.service.ListenScrobbleService +import org.listenbrainz.android.util.Constants.Strings.CHANNEL_ID import org.listenbrainz.android.util.Log.e import retrofit2.Response import java.io.* @@ -125,6 +134,80 @@ object Utils { return null } + fun Context.isServiceRunning(serviceClass: Class<*>): Boolean { + val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + for (service in manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.name == service.service.className) { + return true + } + } + return false + } + + fun notifyScrobble(songTitle: String, artistName: String, albumArt: Bitmap?, nm: NotificationManager, context: Context) { + val notificationBuilder = NotificationCompat.Builder(context, + CHANNEL_ID + ) + .setContentTitle(songTitle) + .setContentText(artistName) + .setSmallIcon(R.drawable.ic_listenbrainz_logo_no_text) + .setLargeIcon(albumArt) // Set the album art here + .setPriority(NotificationCompat.PRIORITY_LOW) + .setAutoCancel(false) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + + try { + nm.notify(0, notificationBuilder.build()) + } catch (e: RuntimeException) { + e("Error showing notification") + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun Icon.toBitmap(context: Context): Bitmap? { + val drawable = this.loadDrawable(context) + return if (drawable is BitmapDrawable) { + drawable.bitmap + } else { + val bitmap = drawable?.let { + Bitmap.createBitmap( + it.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } + val canvas = bitmap?.let { Canvas(it) } + if (canvas != null) { + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + } + bitmap + } + } + + fun scrobbleFromNotiExtractMeta(titleStr: String, formatStr: String): Pair? { + val tpos = formatStr.indexOf("%1\$s") + val apos = formatStr.indexOf("%2\$s") + val regex = formatStr.replace("(", "\\(") + .replace(")", "\\)") + .replace("%1\$s", "(.*)") + .replace("%2\$s", "(.*)") + return try { + val m = regex.toRegex().find(titleStr)!! + val g = m.groupValues + if (g.size != 3) + throw IllegalArgumentException("group size != 3") + if (tpos > apos) + g[1] to g[2] + else + g[2] to g[1] + + } catch (e: Exception) { + print("err in $titleStr $formatStr") + null + } + } + fun stringFromAsset(context: Context, asset: String?): String { return try { val input = context.resources.assets.open(asset!!) diff --git a/build.gradle b/build.gradle index b409601e..c58a74d5 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.2' + classpath 'com.android.tools.build:gradle:8.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } From 955c93350bccc796f97ccccce2063c055572d758 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Mon, 13 Nov 2023 02:46:10 +0530 Subject: [PATCH 3/5] Add logger --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 11 ++++ .../listenbrainz/android/application/App.kt | 22 ++++++++ .../ui/screens/settings/SettingsScreen.kt | 54 +++++++++++++++++++ .../java/org/listenbrainz/android/util/Log.kt | 14 ++--- app/src/main/res/xml/provider_paths.xml | 6 +++ 6 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 app/src/main/res/xml/provider_paths.xml diff --git a/app/build.gradle b/app/build.gradle index 5e42ec83..1ebcbd0f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -250,4 +250,5 @@ dependencies { // Third party libraries implementation 'com.github.dariobrux:Timer:1.1.0' implementation 'com.github.a914-gowtham:compose-ratingbar:1.3.4' + implementation 'com.github.akshaaatt:Logger-Android:1.0.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 63b5efca..f674c569 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -107,6 +107,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/application/App.kt b/app/src/main/java/org/listenbrainz/android/application/App.kt index a92c7678..671a0b16 100644 --- a/app/src/main/java/org/listenbrainz/android/application/App.kt +++ b/app/src/main/java/org/listenbrainz/android/application/App.kt @@ -11,6 +11,8 @@ import androidx.hilt.work.HiltWorkerFactory import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.Configuration +import com.limurse.logger.Logger +import com.limurse.logger.config.Config import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch @@ -32,6 +34,15 @@ class App : Application(), Configuration.Provider { override fun onCreate() { super.onCreate() + val logDirectory = applicationContext.getExternalFilesDir(null)?.path.orEmpty() + val config = Config.Builder(logDirectory) + .setDefaultTag(Constants.TAG) + .setLogcatEnable(true) + .setDataFormatterPattern("dd-MM-yyyy-HH:mm:ss") + .setStartupData(collectStartupData()) + .build() + + Logger.init(config) if (BuildConfig.DEBUG) { enableStrictMode() @@ -52,6 +63,17 @@ class App : Application(), Configuration.Provider { createChannels() } + private fun collectStartupData(): Map = mapOf( + "App Version" to System.currentTimeMillis().toString(), + "Device Application Id" to BuildConfig.APPLICATION_ID, + "Device Version Code" to BuildConfig.VERSION_CODE.toString(), + "Device Version Name" to BuildConfig.VERSION_NAME, + "Device Build Type" to BuildConfig.BUILD_TYPE, + "Device" to Build.DEVICE, + "Device SDK" to Build.VERSION.SDK_INT.toString(), + "Device Manufacturer" to Build.MANUFACTURER + ) + private fun createChannels() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt index 0d167e8f..0d3347a4 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt @@ -46,7 +46,10 @@ import androidx.compose.ui.unit.sp import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle +import com.limurse.logger.Logger +import com.limurse.logger.util.FileIntent import kotlinx.coroutines.launch +import org.listenbrainz.android.BuildConfig import org.listenbrainz.android.R import org.listenbrainz.android.model.UiMode import org.listenbrainz.android.ui.components.Switch @@ -264,6 +267,57 @@ fun SettingsScreen( Divider(thickness = 1.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(18.dp) + .clickable { + Logger.apply { + compressLogsInZipFile { zipFile -> + zipFile?.let { + FileIntent.fromFile( + context, + it, + BuildConfig.APPLICATION_ID + )?.let { intent -> + intent.putExtra(Intent.EXTRA_SUBJECT, "Log File") + try { + context.startActivity(Intent.createChooser(intent, "Email logs...")) + } catch (e: java.lang.Exception) { + e(throwable = e) + } + } + } + } + } + } + , + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = "Report an issue", + color = when { + viewModel.appPreferences.isNotificationServiceAllowed -> MaterialTheme.colorScheme.onSurface + else -> Color(0xFF949494) + } + ) + + Text( + text = "Submit app logs for further investigation", + lineHeight = 18.sp, + fontSize = 12.sp, + color = Color(0xFF949494), + modifier = Modifier + .padding(top = 6.dp) + .width(240.dp) + ) + } + } + + Divider(thickness = 1.dp) + Row( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/org/listenbrainz/android/util/Log.kt b/app/src/main/java/org/listenbrainz/android/util/Log.kt index c87c940a..1878ff21 100644 --- a/app/src/main/java/org/listenbrainz/android/util/Log.kt +++ b/app/src/main/java/org/listenbrainz/android/util/Log.kt @@ -1,24 +1,18 @@ package org.listenbrainz.android.util -import android.util.Log +import com.limurse.logger.Logger object Log { - private const val TAG = Constants.TAG - fun e(message: String) { - Log.e(TAG, message) + Logger.e(msg = message) } fun d(message: String) { - Log.d(TAG, message) - } - - fun v(message: String) { - Log.v(TAG, message) + Logger.d(msg = message) } fun w(message: String) { - Log.w(TAG, message) + Logger.w(msg = message) } } \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000..0106c4d1 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From 8540f0cb0fe0dff855e8b56d5e16425ea881d92a Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Mon, 13 Nov 2023 03:00:43 +0530 Subject: [PATCH 4/5] Fix email template --- .../android/ui/screens/settings/SettingsScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt index 0d3347a4..7a09c9ba 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.FileProvider import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle @@ -280,7 +281,11 @@ fun SettingsScreen( it, BuildConfig.APPLICATION_ID )?.let { intent -> - intent.putExtra(Intent.EXTRA_SUBJECT, "Log File") + intent.putExtra(Intent.EXTRA_SUBJECT, "Log Files") + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("android@metabrainz.org")) + intent.putExtra(Intent.EXTRA_TEXT, "Please find the attached log files.") + intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", zipFile)) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) try { context.startActivity(Intent.createChooser(intent, "Email logs...")) } catch (e: java.lang.Exception) { From 0f37a3bc5cc7584aaf9588846da3dfa6611ae8dc Mon Sep 17 00:00:00 2001 From: Akshat Tiwari <51470769+akshaaatt@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:55:11 +0530 Subject: [PATCH 5/5] Update email --- .../listenbrainz/android/ui/screens/settings/SettingsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt index 7a09c9ba..9c5cfcdb 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/settings/SettingsScreen.kt @@ -282,7 +282,7 @@ fun SettingsScreen( BuildConfig.APPLICATION_ID )?.let { intent -> intent.putExtra(Intent.EXTRA_SUBJECT, "Log Files") - intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("android@metabrainz.org")) + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("mobile@metabrainz.org")) intent.putExtra(Intent.EXTRA_TEXT, "Please find the attached log files.") intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", zipFile)) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)