diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e71f2f497..3219b5ee4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,7 +11,6 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.kotlinParcelize)
- alias(libs.plugins.kapt)
alias(libs.plugins.ksp)
alias(libs.plugins.protobuf)
alias(libs.plugins.googleServices) apply false
@@ -98,6 +97,10 @@ android {
buildConfigField("String", "BUILD_TIME", "\"${buildTime()}\"")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas")
+ }
}
testOptions {
@@ -205,18 +208,6 @@ dependencies {
implementation(projects.changelog)
- testImplementation(libs.androidx.arch.core.testing)
- testImplementation(libs.androidx.test.core)
- testImplementation(libs.androidx.test.runner)
- testImplementation(libs.androidx.test.junit)
- testImplementation(libs.androidx.test.truth)
- testImplementation(libs.bundles.androidx.test.espresso)
- testImplementation(libs.truth)
- testImplementation(libs.koin.test)
- testImplementation(libs.kotlin.coroutines.test)
- testImplementation(libs.mockk)
- testImplementation(libs.robolectric)
-
implementation(libs.androidx.annotation)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
@@ -233,23 +224,34 @@ dependencies {
implementation(libs.androidx.legacy.support.v4)
implementation(libs.androidx.legacy.support.v13)
implementation(libs.bundles.coroutines)
+ implementation(libs.bundles.androidx.room)
implementation(libs.bundles.coil)
implementation(libs.bundles.koin)
implementation(libs.google.material)
implementation(libs.google.protobuf.javalite)
+ implementation(libs.squareup.moshi.lib)
implementation(libs.squareup.okio)
+ implementation(libs.squareup.okhttp)
implementation(libs.timber)
- implementation(libs.bundles.dbflow)
- implementation(libs.bundles.jackson)
- implementation(libs.kotlin.stdlib)
- implementation(libs.kotlin.reflect)
- implementation(libs.rxandroid)
- implementation(libs.rxjava)
- implementation(libs.rxkotlin)
- implementation(libs.rxrelay)
+ ksp(libs.androidx.room.compiler)
+ ksp(libs.squareup.moshi.codegen)
- kapt(libs.dbflow.processor)
+ testImplementation(libs.androidx.arch.core.testing)
+ testImplementation(libs.androidx.room.testing)
+ testImplementation(libs.androidx.test.core)
+ testImplementation(libs.androidx.test.runner)
+ testImplementation(libs.androidx.test.junit)
+ testImplementation(libs.androidx.test.truth)
+ testImplementation(libs.bundles.androidx.test.espresso)
+ testImplementation(libs.androidx.paging.common.ktx)
+ testImplementation(libs.androidx.paging.testing)
+ testImplementation(libs.turbine)
+ testImplementation(libs.truth)
+ testImplementation(libs.koin.test)
+ testImplementation(libs.kotlin.coroutines.test)
+ testImplementation(libs.mockk)
+ testImplementation(libs.robolectric)
debugImplementation(libs.squareup.leakcanary)
debugImplementation(libs.androidx.fragment.testing)
@@ -365,7 +367,8 @@ tasks {
doLast {
if (!project.file("google-services.json").exists()) {
throw GradleException(
- "You need a google-services.json file to run this project. Please refer to the CONTRIBUTING.md file for details."
+ "You need a google-services.json file to run this project." +
+ " Please refer to the CONTRIBUTING.md file for details."
)
}
}
@@ -379,12 +382,6 @@ tasks {
}
}
-kotlin {
- sourceSets.all {
- languageSettings.enableLanguageFeature("ExplicitBackingFields")
- }
-}
-
configurations.all {
resolutionStrategy {
force("com.google.code.findbugs:jsr305:3.0.2")
diff --git a/app/schemas/com.kelsos.mbrc.data.Database/3.json b/app/schemas/com.kelsos.mbrc.data.Database/3.json
new file mode 100644
index 000000000..db36f22cb
--- /dev/null
+++ b/app/schemas/com.kelsos.mbrc.data.Database/3.json
@@ -0,0 +1,378 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "b169004076611f9b3ad422f67163745c",
+ "entities": [
+ {
+ "tableName": "genre",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`genre` TEXT, `count` INTEGER, `date_added` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "genre",
+ "columnName": "genre",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "count",
+ "columnName": "count",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateAdded",
+ "columnName": "date_added",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "artist",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `count` INTEGER, `date_added` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "count",
+ "columnName": "count",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateAdded",
+ "columnName": "date_added",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "album",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `album` TEXT, `cover` TEXT, `date_added` INTEGER, `count` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "album",
+ "columnName": "album",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "cover",
+ "columnName": "cover",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateAdded",
+ "columnName": "date_added",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "count",
+ "columnName": "count",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "track",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`artist` TEXT, `title` TEXT, `src` TEXT, `trackno` INTEGER, `disc` INTEGER, `album_artist` TEXT, `album` TEXT, `genre` TEXT, `date_added` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "src",
+ "columnName": "src",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "trackno",
+ "columnName": "trackno",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "disc",
+ "columnName": "disc",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "albumArtist",
+ "columnName": "album_artist",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "album",
+ "columnName": "album",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "genre",
+ "columnName": "genre",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateAdded",
+ "columnName": "date_added",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "now_playing",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT, `artist` TEXT, `path` TEXT, `position` INTEGER, `date_added` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "path",
+ "columnName": "path",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateAdded",
+ "columnName": "date_added",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "playlists",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT, `url` TEXT, `date_added` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateAdded",
+ "columnName": "date_added",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "radio_station",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT, `url` TEXT, `date_added` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateAdded",
+ "columnName": "date_added",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "settings",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT, `port` INTEGER, `name` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "port",
+ "columnName": "port",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b169004076611f9b3ad422f67163745c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8e7ec56eb..bdb498d80 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -53,11 +53,11 @@
android:value=".features.player.PlayerActivity" />
-
+
-
+
-
+
() }
- single { ObjectMapper().registerKotlinModule() }
-
+ single { Moshi.Builder().build() }
singleOf(::LibrarySyncUseCaseImpl) { bind() }
singleOf(::ApiBase)
singleOf(::RequestManagerImpl) { bind() }
@@ -186,7 +156,6 @@ val appModule =
singleOf(::GenreRepositoryImpl) { bind() }
singleOf(::ArtistRepositoryImpl) { bind() }
- singleOf(::LocalArtistDataSourceImpl) { bind() }
singleOf(::AlbumRepositoryImpl) { bind() }
singleOf(::TrackRepositoryImpl) { bind() }
@@ -195,21 +164,6 @@ val appModule =
singleOf(::RadioRepositoryImpl) { bind() }
singleOf(::ConnectionRepositoryImpl) { bind() }
- singleOf(::LocalGenreDataSource)
- singleOf(::RemoteGenreDataSource)
- singleOf(::LocalArtistDataSourceImpl) { bind() }
- singleOf(::RemoteArtistDataSource)
- singleOf(::LocalAlbumDataSource)
- singleOf(::RemoteAlbumDataSource)
- singleOf(::LocalTrackDataSource)
- singleOf(::RemoteTrackDataSource)
- singleOf(::LocalPlaylistDataSource)
- singleOf(::RemotePlaylistDataSource)
- singleOf(::LocalNowPlayingDataSource)
- singleOf(::RemoteNowPlayingDataSource)
- singleOf(::LocalRadioDataSource)
- singleOf(::RemoteRadioDataSource)
-
singleOf(::GenreEntryAdapter)
singleOf(::ArtistEntryAdapter)
@@ -222,26 +176,29 @@ val appModule =
singleOf(::RemoteBroadcastReceiver)
singleOf(::SettingsManagerImpl) { bind() }
singleOf(::ServiceCheckerImpl) { bind() }
- singleOf(::ModelCacheImpl) { bind() }
- singleOf(::MainDataModel)
- singleOf(::ModelInitializer)
- singleOf(::ConnectionModel)
- singleOf(::LyricsModel)
- singleOf(::RemoteController)
- singleOf(::SocketService)
+ singleOf(::PlayingTrackCacheImpl) { bind() }
singleOf(::SocketActivityChecker)
singleOf(::CoverCache)
- singleOf(::ProtocolHandler)
singleOf(::NotificationModel)
+ singleOf(::AppNotificationManagerImpl) { bind() }
+ singleOf(::CommandFactoryImpl) { bind() }
+ singleOf(::UserActionUseCaseImpl) { bind() }
+ singleOf(::VolumeModifyUseCaseImpl) { bind() }
+ singleOf(::AppStateManager)
+ singleOf(::AppState)
+ singleOf(::ClientConnectionManagerImpl) { bind() }
+ singleOf(::ClientInformationStoreImpl) { bind() }
+ singleOf(::ClientConnectionUseCaseImpl) { bind() }
+ singleOf(::MessageHandlerImpl) { bind() }
+ singleOf(::MessageQueueImpl) { bind() }
+ singleOf(::UiMessageQueueImpl) { bind() }
+ singleOf(::ConnectionState)
+ singleOf(::SerializationAdapterImpl) { bind() }
+ singleOf(::DeserializationAdapterImpl) { bind() }
+ singleOf(::PluginUpdateCheckUseCaseImpl) { bind() }
factoryOf(::BasicSettingsHelper)
- factoryOf(::ReduceVolumeOnRingCommand)
- factoryOf(::HandshakeCompletionActions)
- factoryOf(::NotifyNotAllowedCommand)
- factoryOf(::ProtocolRequest)
- factoryOf(::VersionCheckCommand)
- factoryOf(::ProcessUserAction)
factoryOf(::UpdateNowPlayingTrack)
factoryOf(::UpdateCover)
factoryOf(::UpdateRating)
@@ -260,27 +217,13 @@ val appModule =
factoryOf(::UpdatePluginVersionCommand)
factoryOf(::ProtocolPingHandle)
factoryOf(::SimpleLogCommand)
- factoryOf(::RestartConnectionCommand)
- factoryOf(::CancelNotificationCommand)
- factoryOf(::SessionNotificationManager)
factoryOf(::RemoteSessionManager)
factoryOf(::RemoteVolumeProvider)
- factoryOf(::InitiateConnectionCommand)
- factoryOf(::TerminateConnectionCommand)
- factoryOf(::StartDiscoveryCommand)
- factoryOf(::RemoteServiceDiscovery)
- factoryOf(::KeyVolumeUpCommand)
- factoryOf(::KeyVolumeDownCommand)
- factoryOf(::SocketDataAvailableCommand)
- factoryOf(::ConnectionStatusChangedCommand)
- factoryOf(::HandleHandshake)
- factoryOf(::TerminateServiceCommand)
+ factoryOf(::ProtocolVersionUpdate)
+ singleOf(::RemoteServiceDiscoveryImpl) { bind() }
singleOf(::WidgetUpdaterImpl) { bind() }
- single(named("main")) { AndroidSchedulers.mainThread() }
- single(named("io")) { Schedulers.io() }
-
single {
val database =
Executors
@@ -304,83 +247,94 @@ val appModule =
)
}
- singleOf(::ArtistAlbumsPresenterImpl) { bind() }
+ single {
+ Room
+ .databaseBuilder(get(), Database::class.java, Database.NAME)
+ .build()
+ }
+ single { get().genreDao() }
+ single { get().artistDao() }
+ single { get().albumDao() }
+ single { get().trackDao() }
+ single { get().nowPlayingDao() }
+ single { get().playlistDao() }
+ single { get().radioStationDao() }
+ single { get().connectionDao() }
scope {
- scopedOf(::MiniControlPresenterImpl) { bind() }
+ viewModelOf(::MiniControlViewModel)
}
scope {
- scopedOf(::PlayerViewPresenterImpl) { bind() }
- scoped { ProgressSeekerHelper(get(named("main"))) }
+ viewModelOf(::PlayerViewModel)
}
scope {
- scopedOf(::LyricsPresenterImpl) { bind() }
+ viewModelOf(::LyricsViewModel)
}
scope {
- scopedOf(::LibraryPresenterImpl) { bind() }
scopedOf(::LibrarySearchModel)
- scopedOf(::PopupActionHandler)
}
scope {
- scopedOf(::BrowseGenrePresenterImpl) { bind() }
+ viewModelOf(::BrowseGenreViewModel)
scopedOf(::GenreEntryAdapter)
}
scope {
- scopedOf(::BrowseArtistPresenterImpl) { bind() }
+ viewModelOf(::BrowseArtistViewModel)
scopedOf(::ArtistEntryAdapter)
}
scope {
- scopedOf(::BrowseAlbumPresenterImpl) { bind() }
+ viewModelOf(::BrowseAlbumViewModel)
scopedOf(::AlbumEntryAdapter)
}
scope {
- scopedOf(::BrowseTrackPresenterImpl) { bind() }
+ viewModelOf(::BrowseTrackViewModel)
scopedOf(::TrackEntryAdapter)
}
scope {
- scopedOf(::GenreArtistsPresenterImpl) { bind() }
+ viewModelOf(::GenreArtistsViewModel)
scopedOf(::ArtistEntryAdapter)
- scopedOf(::PopupActionHandler)
}
scope {
- scopedOf(::ArtistAlbumsPresenterImpl) { bind() }
+ viewModelOf(::ArtistAlbumsViewModel)
scopedOf(::AlbumEntryAdapter)
- scopedOf(::PopupActionHandler)
}
scope {
- scopedOf(::AlbumTracksPresenterImpl) { bind() }
+ viewModelOf(::AlbumTracksViewModel)
scopedOf(::TrackEntryAdapter)
- scopedOf(::PopupActionHandler)
}
scope {
- scopedOf(::NowPlayingPresenterImpl) { bind() }
+ viewModelOf(::NowPlayingViewModel)
scopedOf(::NowPlayingAdapter)
+ scopedOf(::MoveManagerImpl) { bind() }
}
scope {
- scopedOf(::PlaylistPresenterImpl) { bind() }
+ viewModelOf(::PlaylistViewModel)
scopedOf(::PlaylistAdapter)
}
scope {
- scopedOf(::ConnectionManagerPresenterImpl) { bind() }
+ viewModelOf(::ConnectionManagerViewModel)
}
scope {
- scopedOf(::RadioPresenterImpl) { bind() }
+ viewModelOf(::RadioViewModel)
scopedOf(::RadioAdapter)
}
+ scope {
+ viewModelOf(::RatingDialogViewModel)
+ }
+
viewModelOf(::OutputSelectionViewModel)
}
diff --git a/app/src/main/java/com/kelsos/mbrc/BaseActivity.kt b/app/src/main/java/com/kelsos/mbrc/BaseActivity.kt
index 412cb7c3c..fdcd16156 100644
--- a/app/src/main/java/com/kelsos/mbrc/BaseActivity.kt
+++ b/app/src/main/java/com/kelsos/mbrc/BaseActivity.kt
@@ -11,22 +11,20 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorRes
+import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.navigation.NavigationView
-import com.google.android.material.snackbar.Snackbar
-import com.kelsos.mbrc.annotations.Connection
-import com.kelsos.mbrc.constants.UserInputEventType
-import com.kelsos.mbrc.events.MessageEvent
-import com.kelsos.mbrc.events.bus.RxBus
-import com.kelsos.mbrc.events.ui.ConnectionStatusChangeEvent
-import com.kelsos.mbrc.events.ui.NotifyUser
-import com.kelsos.mbrc.events.ui.RequestConnectionStateEvent
+import com.kelsos.mbrc.common.state.ConnectionState
+import com.kelsos.mbrc.common.state.ConnectionStatus
import com.kelsos.mbrc.features.help.HelpFeedbackActivity
import com.kelsos.mbrc.features.library.LibraryActivity
import com.kelsos.mbrc.features.lyrics.LyricsActivity
@@ -36,19 +34,25 @@ import com.kelsos.mbrc.features.player.PlayerActivity
import com.kelsos.mbrc.features.playlists.PlaylistActivity
import com.kelsos.mbrc.features.radio.RadioActivity
import com.kelsos.mbrc.features.settings.SettingsActivity
+import com.kelsos.mbrc.networking.ClientConnectionUseCase
+import com.kelsos.mbrc.networking.protocol.VolumeModifyUseCase
import com.kelsos.mbrc.platform.RemoteService
import com.kelsos.mbrc.platform.ServiceChecker
+import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.scope.ScopeActivity
import timber.log.Timber
private const val NAVIGATION_DELAY = 250L
-abstract class BaseActivity :
- ScopeActivity(),
+abstract class BaseActivity(
+ @LayoutRes val contentLayoutId: Int,
+) : ScopeActivity(contentLayoutId),
NavigationView.OnNavigationItemSelectedListener {
- private val bus: RxBus by inject()
private val serviceChecker: ServiceChecker by inject()
+ private val connectionUseCase: ClientConnectionUseCase by inject()
+ private val volumeModifyUseCase: VolumeModifyUseCase by inject()
+ private val connectionState: ConnectionState by inject()
private lateinit var toolbar: MaterialToolbar
private lateinit var drawer: DrawerLayout
@@ -65,14 +69,46 @@ abstract class BaseActivity :
private fun onConnectLongClick(): Boolean {
Timber.v("Connect long pressed")
serviceChecker.startServiceIfNotRunning()
- bus.post(MessageEvent(UserInputEventType.RESET_CONNECTION))
+ connectionUseCase.connect()
return true
}
protected fun onConnectClick() {
Timber.v("Attempting to connect")
serviceChecker.startServiceIfNotRunning()
- bus.post(MessageEvent(UserInputEventType.START_CONNECTION))
+ connectionUseCase.connect()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setupBackButtonHandler()
+
+ toolbar = findViewById(R.id.toolbar)
+ drawer = findViewById(R.id.drawer_layout)
+ navigationView = findViewById(R.id.nav_view)
+
+ setSupportActionBar(toolbar)
+
+ toggle =
+ ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close)
+ drawer.addDrawerListener(toggle!!)
+ drawer.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START)
+ toggle!!.syncState()
+ navigationView.setNavigationItemSelectedListener(this)
+
+ val header = navigationView.getHeaderView(0)
+ connectText = header.findViewById(R.id.nav_connect_text)
+ connect = header.findViewById(R.id.connect_button)
+ connect!!.setOnClickListener { this.onConnectClick() }
+ connect!!.setOnLongClickListener { this.onConnectLongClick() }
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setHomeButtonEnabled(true)
+ navigationView.setCheckedItem(active())
+ serviceChecker.startServiceIfNotRunning()
+
+ observeFlows()
}
override fun onDestroy() {
@@ -100,37 +136,31 @@ abstract class BaseActivity :
else -> super.onKeyUp(keyCode, event)
}
- private fun onConnection(event: ConnectionStatusChangeEvent) {
+ private fun updateConnectionState(event: ConnectionStatus) {
Timber.v("Handling new connection status %s", event.status)
@StringRes val resId: Int
@ColorRes val colorId: Int
- if (event.status == Connection.OFF) {
- resId = R.string.drawer_connection_status_off
- colorId = R.color.black
- } else if (event.status == Connection.ON) {
- resId = R.string.drawer_connection_status_on
- colorId = R.color.accent
- } else if (event.status == Connection.ACTIVE) {
- resId = R.string.drawer_connection_status_active
- colorId = R.color.power_on
- } else {
- resId = R.string.drawer_connection_status_off
- colorId = R.color.black
+ when (event) {
+ ConnectionStatus.Offline -> {
+ resId = R.string.drawer_connection_status_off
+ colorId = R.color.black
+ }
+
+ ConnectionStatus.Authenticating -> {
+ resId = R.string.drawer_connection_status_on
+ colorId = R.color.accent
+ }
+
+ ConnectionStatus.Connected -> {
+ resId = R.string.drawer_connection_status_active
+ colorId = R.color.power_on
+ }
}
connectText!!.setText(resId)
connect!!.setColorFilter(ContextCompat.getColor(this, colorId))
- isConnected = event.status == Connection.ACTIVE
- }
-
- private fun handleUserNotification(event: NotifyUser) {
- val message = if (event.isFromResource) getString(event.resId) else event.message
-
- val focus = currentFocus
- if (focus != null) {
- Snackbar.make(focus, message, Snackbar.LENGTH_SHORT).show()
- }
+ isConnected = event == ConnectionStatus.Connected
}
override fun onKeyDown(
@@ -140,12 +170,16 @@ abstract class BaseActivity :
val result =
when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
- bus.post(MessageEvent(UserInputEventType.KEY_VOLUME_UP))
+ lifecycleScope.launch {
+ volumeModifyUseCase.increment()
+ }
true
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
- bus.post(MessageEvent(UserInputEventType.KEY_VOLUME_DOWN))
+ lifecycleScope.launch {
+ volumeModifyUseCase.decrement()
+ }
true
}
@@ -221,11 +255,17 @@ abstract class BaseActivity :
}
}
- /**
- * Should be called after injections and setContentView.
- */
- fun setup() {
- Timber.v("Initializing base activity")
+ private fun observeFlows() {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ connectionState.connection.collect { event ->
+ updateConnectionState(event)
+ }
+ }
+ }
+ }
+
+ private fun setupBackButtonHandler() {
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
@@ -238,46 +278,6 @@ abstract class BaseActivity :
}
},
)
- toolbar = findViewById(R.id.toolbar)
- drawer = findViewById(R.id.drawer_layout)
- navigationView = findViewById(R.id.nav_view)
-
- setSupportActionBar(toolbar)
-
- toggle =
- ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close)
- drawer.addDrawerListener(toggle!!)
- drawer.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START)
- toggle!!.syncState()
- navigationView.setNavigationItemSelectedListener(this)
-
- val header = navigationView.getHeaderView(0)
- connectText = header.findViewById(R.id.nav_connect_text)
- connect = header.findViewById(R.id.connect_button)
- connect!!.setOnClickListener { this.onConnectClick() }
- connect!!.setOnLongClickListener { this.onConnectLongClick() }
-
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setHomeButtonEnabled(true)
- navigationView.setCheckedItem(active())
- serviceChecker.startServiceIfNotRunning()
- }
-
- override fun onResume() {
- super.onResume()
- this.bus.register(this, NotifyUser::class.java, { this.handleUserNotification(it) }, true)
- this.bus.register(
- this,
- ConnectionStatusChangeEvent::class.java,
- { this.onConnection(it) },
- true,
- )
- this.bus.post(RequestConnectionStateEvent())
- }
-
- override fun onPause() {
- super.onPause()
- this.bus.unregister(this)
}
companion object {
diff --git a/app/src/main/java/com/kelsos/mbrc/UpdateRequiredActivity.kt b/app/src/main/java/com/kelsos/mbrc/UpdateRequiredActivity.kt
index 4e73e4220..006e30806 100644
--- a/app/src/main/java/com/kelsos/mbrc/UpdateRequiredActivity.kt
+++ b/app/src/main/java/com/kelsos/mbrc/UpdateRequiredActivity.kt
@@ -17,12 +17,12 @@ class UpdateRequiredActivity : AppCompatActivity() {
window.sharedElementEnterTransition =
MaterialContainerTransform().apply {
addTarget(android.R.id.content)
- duration = 300L
+ duration = ENTER_DURATION
}
window.sharedElementReturnTransition =
MaterialContainerTransform().apply {
addTarget(android.R.id.content)
- duration = 250L
+ duration = RETURN_DURATION
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_update_required)
@@ -36,5 +36,7 @@ class UpdateRequiredActivity : AppCompatActivity() {
companion object {
const val VERSION: String = "version"
+ const val ENTER_DURATION = 300L
+ const val RETURN_DURATION = 250L
}
}
diff --git a/app/src/main/java/com/kelsos/mbrc/annotations/Connection.kt b/app/src/main/java/com/kelsos/mbrc/annotations/Connection.kt
deleted file mode 100644
index b659462cc..000000000
--- a/app/src/main/java/com/kelsos/mbrc/annotations/Connection.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.kelsos.mbrc.annotations
-
-import androidx.annotation.IntDef
-
-object Connection {
- const val OFF = 0
- const val ON = 1
- const val ACTIVE = 2
-
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(OFF, ON, ACTIVE)
- annotation class Status
-} // no instance
diff --git a/app/src/main/java/com/kelsos/mbrc/annotations/PlayerState.kt b/app/src/main/java/com/kelsos/mbrc/annotations/PlayerState.kt
deleted file mode 100644
index c022e4271..000000000
--- a/app/src/main/java/com/kelsos/mbrc/annotations/PlayerState.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.kelsos.mbrc.annotations
-
-import androidx.annotation.StringDef
-
-object PlayerState {
- const val PLAYING = "playing"
- const val PAUSED = "paused"
- const val STOPPED = "stopped"
- const val UNDEFINED = "undefined"
-
- @Retention(AnnotationRetention.SOURCE)
- @StringDef(PAUSED, PLAYING, STOPPED, UNDEFINED)
- annotation class State
-} // no instance
diff --git a/app/src/main/java/com/kelsos/mbrc/annotations/Queue.kt b/app/src/main/java/com/kelsos/mbrc/annotations/Queue.kt
deleted file mode 100644
index 7cf089bc5..000000000
--- a/app/src/main/java/com/kelsos/mbrc/annotations/Queue.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.kelsos.mbrc.annotations
-
-import androidx.annotation.StringDef
-
-object Queue {
- @StringDef(NEXT, LAST, NOW, ADD_ALL, PROFILE)
- @Retention(AnnotationRetention.SOURCE)
- annotation class Action
-
- const val PROFILE = "profile"
- const val NEXT = "next"
- const val LAST = "last"
- const val NOW = "now"
- const val ADD_ALL = "add-all"
- const val PLAY_ALBUM = "play-album"
- const val PLAY_ARTIST = "play-artist"
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/annotations/Repeat.kt b/app/src/main/java/com/kelsos/mbrc/annotations/Repeat.kt
deleted file mode 100644
index 52fbb5ff1..000000000
--- a/app/src/main/java/com/kelsos/mbrc/annotations/Repeat.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.kelsos.mbrc.annotations
-
-import androidx.annotation.StringDef
-
-object Repeat {
- const val ALL = "all"
- const val NONE = "none"
- const val ONE = "one"
-
- @StringDef(ALL, NONE, ONE)
- @Retention(AnnotationRetention.SOURCE)
- annotation class Mode
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/annotations/Search.kt b/app/src/main/java/com/kelsos/mbrc/annotations/Search.kt
deleted file mode 100644
index 5c3f515ab..000000000
--- a/app/src/main/java/com/kelsos/mbrc/annotations/Search.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.kelsos.mbrc.annotations
-
-object Search {
- const val SECTION_GENRE = 0
- const val SECTION_ARTIST = 1
- const val SECTION_ALBUM = 2
- const val SECTION_TRACK = 3
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/annotations/SocketAction.kt b/app/src/main/java/com/kelsos/mbrc/annotations/SocketAction.kt
deleted file mode 100644
index 5aa21e018..000000000
--- a/app/src/main/java/com/kelsos/mbrc/annotations/SocketAction.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.kelsos.mbrc.annotations
-
-import androidx.annotation.IntDef
-
-object SocketAction {
- const val RESET = 1
- const val START = 2
- const val RETRY = 3
- const val TERMINATE = 4
- const val STOP = 5
-
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(RESET, START, RETRY, TERMINATE, STOP)
- annotation class Action
-
- fun name(
- @Action action: Int,
- ): String =
- when (action) {
- RESET -> "Reset"
- START -> "Start"
- RETRY -> "Retry"
- TERMINATE -> "Terminate"
- STOP -> "Stop"
- else -> throw IllegalArgumentException("action $action is not recognised")
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/CancelNotificationCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/CancelNotificationCommand.kt
deleted file mode 100644
index 5cc5648f1..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/CancelNotificationCommand.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-import com.kelsos.mbrc.platform.mediasession.SessionNotificationManager
-
-class CancelNotificationCommand(
- private val sessionNotificationManager: SessionNotificationManager,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- sessionNotificationManager.cancelNotification(SessionNotificationManager.NOW_PLAYING_PLACEHOLDER)
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/ConnectionStatusChangedCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/ConnectionStatusChangedCommand.kt
deleted file mode 100644
index 02a9f8cdb..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/ConnectionStatusChangedCommand.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.common.state.ConnectionModel
-import com.kelsos.mbrc.networking.client.SocketMessage
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.Protocol
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-import com.kelsos.mbrc.platform.mediasession.SessionNotificationManager
-
-class ConnectionStatusChangedCommand(
- private val model: ConnectionModel,
- private val service: SocketService,
- private val sessionNotificationManager: SessionNotificationManager,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- model.setConnectionState(message.dataString)
-
- if (model.isConnectionActive) {
- service.sendData(SocketMessage.create(Protocol.PLAYER, "Android"))
- } else {
- sessionNotificationManager.cancelNotification(SessionNotificationManager.NOW_PLAYING_PLACEHOLDER)
- }
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/HandleHandshake.kt b/app/src/main/java/com/kelsos/mbrc/commands/HandleHandshake.kt
deleted file mode 100644
index 7622dd412..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/HandleHandshake.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.common.state.ConnectionModel
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolHandler
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class HandleHandshake(
- private val handler: ProtocolHandler,
- private val model: ConnectionModel,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- if (!(message.data as Boolean)) {
- handler.resetHandshake()
- model.setHandShakeDone(false)
- }
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/InitiateConnectionCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/InitiateConnectionCommand.kt
deleted file mode 100644
index 584d64b0a..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/InitiateConnectionCommand.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.annotations.SocketAction.START
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class InitiateConnectionCommand(
- private val socketService: SocketService,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- socketService.socketManager(START)
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/KeyVolumeDownCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/KeyVolumeDownCommand.kt
deleted file mode 100644
index 5b3cc7a73..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/KeyVolumeDownCommand.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.common.state.MainDataModel
-import com.kelsos.mbrc.data.UserAction
-import com.kelsos.mbrc.events.MessageEvent
-import com.kelsos.mbrc.events.bus.RxBus
-import com.kelsos.mbrc.networking.protocol.Protocol
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class KeyVolumeDownCommand(
- private val model: MainDataModel,
- private val bus: RxBus,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- if (model.volume >= 10) {
- val mod = model.volume % 10
- val volume: Int
-
- if (mod == 0) {
- volume = model.volume - 10
- } else if (mod < 5) {
- volume = model.volume - (10 + mod)
- } else {
- volume = model.volume - mod
- }
-
- bus.post(MessageEvent.action(UserAction(Protocol.PLAYER_VOLUME, volume)))
- }
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/KeyVolumeUpCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/KeyVolumeUpCommand.kt
deleted file mode 100644
index 9ec372b0c..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/KeyVolumeUpCommand.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.common.state.MainDataModel
-import com.kelsos.mbrc.data.UserAction
-import com.kelsos.mbrc.events.MessageEvent
-import com.kelsos.mbrc.events.bus.RxBus
-import com.kelsos.mbrc.networking.protocol.Protocol
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class KeyVolumeUpCommand(
- private val model: MainDataModel,
- private val bus: RxBus,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- val volume: Int =
- if (model.volume <= 90) {
- val mod = model.volume % 10
-
- when {
- mod == 0 -> model.volume + 10
- mod < 5 -> model.volume + (10 - mod)
- else -> model.volume + (20 - mod)
- }
- } else {
- 100
- }
-
- bus.post(MessageEvent.action(UserAction(Protocol.PLAYER_VOLUME, volume)))
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/ProcessUserAction.kt b/app/src/main/java/com/kelsos/mbrc/commands/ProcessUserAction.kt
deleted file mode 100644
index a99117302..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/ProcessUserAction.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.data.UserAction
-import com.kelsos.mbrc.networking.client.SocketMessage
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class ProcessUserAction(
- private val socket: SocketService,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- socket.sendData(
- SocketMessage.create(
- (message.data as UserAction).context,
- (message.data as UserAction).data,
- ),
- )
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/ProtocolRequest.kt b/app/src/main/java/com/kelsos/mbrc/commands/ProtocolRequest.kt
deleted file mode 100644
index 022415a10..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/ProtocolRequest.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.networking.client.SocketMessage
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.Protocol
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-import com.kelsos.mbrc.networking.protocol.ProtocolPayload
-
-class ProtocolRequest(
- private val socket: SocketService,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- val payload = ProtocolPayload()
- payload.noBroadcast = false
- payload.protocolVersion = Protocol.PROTOCOL_VERSION_NUMBER
- socket.sendData(SocketMessage.create(Protocol.PROTOCOL_TAG, payload))
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/ReduceVolumeOnRingCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/ReduceVolumeOnRingCommand.kt
deleted file mode 100644
index 087811f1b..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/ReduceVolumeOnRingCommand.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.common.state.MainDataModel
-import com.kelsos.mbrc.networking.client.SocketMessage
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.Protocol
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class ReduceVolumeOnRingCommand(
- private val model: MainDataModel,
- private val service: SocketService,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- if (model.isMute || model.volume == 0) {
- return
- }
- service.sendData(SocketMessage.create(Protocol.PLAYER_VOLUME, (model.volume * 0.2).toInt()))
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/RestartConnectionCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/RestartConnectionCommand.kt
deleted file mode 100644
index cfde4f318..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/RestartConnectionCommand.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.annotations.SocketAction.RESET
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class RestartConnectionCommand(
- private val socket: SocketService,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- socket.socketManager(RESET)
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/SocketDataAvailableCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/SocketDataAvailableCommand.kt
deleted file mode 100644
index 99948733a..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/SocketDataAvailableCommand.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolHandler
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class SocketDataAvailableCommand(
- private val handler: ProtocolHandler,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- handler.preProcessIncoming(message.dataString)
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/StartDiscoveryCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/StartDiscoveryCommand.kt
deleted file mode 100644
index ab3625ba3..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/StartDiscoveryCommand.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.networking.discovery.RemoteServiceDiscovery
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class StartDiscoveryCommand(
- private val discovery: RemoteServiceDiscovery,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- discovery.startDiscovery()
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/TerminateConnectionCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/TerminateConnectionCommand.kt
deleted file mode 100644
index 8c890c834..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/TerminateConnectionCommand.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.kelsos.mbrc.annotations.SocketAction.TERMINATE
-import com.kelsos.mbrc.common.state.ConnectionModel
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class TerminateConnectionCommand(
- private val service: SocketService,
- private val model: ConnectionModel,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- model.setHandShakeDone(false)
- model.setConnectionState("false")
- service.socketManager(TERMINATE)
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/TerminateServiceCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/TerminateServiceCommand.kt
deleted file mode 100644
index e0514715f..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/TerminateServiceCommand.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import android.app.Application
-import android.content.Intent
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-import com.kelsos.mbrc.platform.RemoteService
-
-class TerminateServiceCommand(
- private val application: Application,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- if (RemoteService.serviceStopping) {
- return
- }
- application.run {
- stopService(Intent(this, RemoteService::class.java))
- }
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/VersionCheckCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/VersionCheckCommand.kt
deleted file mode 100644
index 901737299..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/VersionCheckCommand.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.kelsos.mbrc.commands
-
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.kelsos.mbrc.common.state.MainDataModel
-import com.kelsos.mbrc.constants.ProtocolEventType
-import com.kelsos.mbrc.events.MessageEvent
-import com.kelsos.mbrc.events.bus.RxBus
-import com.kelsos.mbrc.features.settings.SettingsManager
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-import timber.log.Timber
-import java.io.IOException
-import java.net.URL
-import java.time.Instant
-import java.time.temporal.ChronoUnit
-
-class VersionCheckCommand(
- private val model: MainDataModel,
- private val mapper: ObjectMapper,
- private val manager: SettingsManager,
- private val bus: RxBus,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- val now = Instant.now()
-
- if (check(MINIMUM_REQUIRED)) {
- val next = getNextCheck(true)
- if (next.isAfter(now)) {
- Timber.d("Next update required check is @ $next")
- return
- }
- bus.post(MessageEvent(ProtocolEventType.PLUGIN_UPDATE_REQUIRED))
- model.minimumRequired = MINIMUM_REQUIRED
- model.pluginUpdateRequired = true
- manager.setLastUpdated(now, true)
- return
- }
-
- if (!manager.isPluginUpdateCheckEnabled()) {
- return
- }
-
- val nextCheck = getNextCheck()
-
- if (nextCheck.isAfter(now)) {
- Timber.d("Next update check after @ $nextCheck")
- return
- }
-
- val jsonNode: JsonNode
- try {
- jsonNode = mapper.readValue(URL(CHECK_URL), JsonNode::class.java)
- } catch (e1: IOException) {
- Timber.d(e1, "While reading json node")
- return
- }
-
- val expected = jsonNode.path("tag_name").asText().replace("v", "")
-
- val found = model.pluginVersion
- if (expected != found && check(expected)) {
- model.pluginUpdateAvailable = true
- bus.post(MessageEvent(ProtocolEventType.PLUGIN_UPDATE_AVAILABLE))
- }
-
- manager.setLastUpdated(now, false)
- Timber.d("Checked for plugin update @ $now. Found: $found expected: $expected")
- }
-
- private fun getNextCheck(required: Boolean = false): Instant {
- val lastUpdated = manager.getLastUpdated(required)
- val days = if (required) 1L else 2L
- return lastUpdated.plus(days, ChronoUnit.DAYS)
- }
-
- private fun check(suggestedVersion: String): Boolean {
- val currentVersion = model.pluginVersion.toVersionArray()
- val latestVersion = suggestedVersion.toVersionArray()
-
- var i = 0
- val currentSize = currentVersion.size
- val latestSize = latestVersion.size
- while (i < currentSize && i < latestSize && currentVersion[i] == latestVersion[i]) {
- i++
- }
-
- if (i < currentSize && i < latestSize) {
- val diff = currentVersion[i].compareTo(latestVersion[i])
- return diff < 0
- }
-
- return false
- }
-
- companion object {
- private const val CHECK_URL =
- "https://api.github.com/repos/musicbeeremote/plugin/releases/latest"
- private const val MINIMUM_REQUIRED = "1.4.0"
- }
-}
-
-fun String.toVersionArray(): Array =
- split("\\.".toRegex())
- .dropLastWhile(String::isEmpty)
- .take(3)
- .map { it.toInt() }
- .toTypedArray()
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/visual/HandshakeCompletionActions.kt b/app/src/main/java/com/kelsos/mbrc/commands/visual/HandshakeCompletionActions.kt
deleted file mode 100644
index f2567f4fa..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/visual/HandshakeCompletionActions.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.kelsos.mbrc.commands.visual
-
-import com.kelsos.mbrc.common.state.ConnectionModel
-import com.kelsos.mbrc.common.state.MainDataModel
-import com.kelsos.mbrc.features.library.LibrarySyncUseCase
-import com.kelsos.mbrc.networking.client.SocketMessage
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.Protocol
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-import rx.Observable
-import timber.log.Timber
-import java.util.concurrent.TimeUnit
-
-class HandshakeCompletionActions(
- private val service: SocketService,
- private val model: MainDataModel,
- private val connectionModel: ConnectionModel,
- private val syncInteractor: LibrarySyncUseCase,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- val isComplete = message.data as Boolean
- connectionModel.setHandShakeDone(isComplete)
-
- if (!isComplete) {
- return
- }
-
- if (model.pluginProtocol > 2) {
- Timber.v("Sending init request")
- service.sendData(SocketMessage.create(Protocol.INIT))
- service.sendData(SocketMessage.create(Protocol.PLUGIN_VERSION))
- } else {
- Timber.v("Preparing to send requests for state")
-
- val messages = ArrayList()
- messages.add(SocketMessage.create(Protocol.NOW_PLAYING_COVER))
- messages.add(SocketMessage.create(Protocol.PLAYER_STATUS))
- messages.add(SocketMessage.create(Protocol.NOW_PLAYING_TRACK))
- messages.add(SocketMessage.create(Protocol.NOW_PLAYING_LYRICS))
- messages.add(SocketMessage.create(Protocol.NOW_PLAYING_POSITION))
- messages.add(SocketMessage.create(Protocol.PLUGIN_VERSION))
-
- val totalMessages = messages.size
- Observable
- .interval(150, TimeUnit.MILLISECONDS)
- .take(totalMessages)
- .subscribe({ service.sendData(messages.removeAt(0)) }) {
- Timber.v(it, "Failure while sending the init messages")
- }
- }
-
- syncInteractor.sync(true)
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/commands/visual/NotifyNotAllowedCommand.kt b/app/src/main/java/com/kelsos/mbrc/commands/visual/NotifyNotAllowedCommand.kt
deleted file mode 100644
index bb771654c..000000000
--- a/app/src/main/java/com/kelsos/mbrc/commands/visual/NotifyNotAllowedCommand.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.kelsos.mbrc.commands.visual
-
-import com.kelsos.mbrc.R
-import com.kelsos.mbrc.annotations.SocketAction.STOP
-import com.kelsos.mbrc.common.state.ConnectionModel
-import com.kelsos.mbrc.events.bus.RxBus
-import com.kelsos.mbrc.events.ui.NotifyUser
-import com.kelsos.mbrc.networking.client.SocketService
-import com.kelsos.mbrc.networking.protocol.ProtocolAction
-import com.kelsos.mbrc.networking.protocol.ProtocolHandler
-import com.kelsos.mbrc.networking.protocol.ProtocolMessage
-
-class NotifyNotAllowedCommand(
- private val socketService: SocketService,
- private val model: ConnectionModel,
- private val handler: ProtocolHandler,
- private val bus: RxBus,
-) : ProtocolAction {
- override fun execute(message: ProtocolMessage) {
- bus.post(NotifyUser(R.string.notification_not_allowed))
- socketService.socketManager(STOP)
- model.setConnectionState("false")
- handler.resetHandshake()
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/data/LocalDataSource.kt b/app/src/main/java/com/kelsos/mbrc/common/data/LocalDataSource.kt
deleted file mode 100644
index ef44fb0f2..000000000
--- a/app/src/main/java/com/kelsos/mbrc/common/data/LocalDataSource.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.kelsos.mbrc.common.data
-
-import com.kelsos.mbrc.data.Data
-import com.raizlabs.android.dbflow.list.FlowCursorList
-
-interface LocalDataSource {
- suspend fun deleteAll()
-
- suspend fun saveAll(list: List)
-
- suspend fun loadAllCursor(): FlowCursorList
-
- suspend fun search(term: String): FlowCursorList
-
- suspend fun isEmpty(): Boolean
-
- suspend fun count(): Long
-
- suspend fun removePreviousEntries(epoch: Long)
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/data/RemoteDataSource.kt b/app/src/main/java/com/kelsos/mbrc/common/data/RemoteDataSource.kt
deleted file mode 100644
index 53a2473bd..000000000
--- a/app/src/main/java/com/kelsos/mbrc/common/data/RemoteDataSource.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.kelsos.mbrc.common.data
-
-import kotlinx.coroutines.flow.Flow
-
-interface RemoteDataSource {
- /**
- * Retrieves all the available data from a remote data source
- */
- suspend fun fetch(): Flow>
-
- companion object {
- const val LIMIT = 800
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/data/Repository.kt b/app/src/main/java/com/kelsos/mbrc/common/data/Repository.kt
index 03ba9084d..5fe9ad361 100644
--- a/app/src/main/java/com/kelsos/mbrc/common/data/Repository.kt
+++ b/app/src/main/java/com/kelsos/mbrc/common/data/Repository.kt
@@ -1,18 +1,20 @@
package com.kelsos.mbrc.common.data
-import com.kelsos.mbrc.data.Data
-import com.raizlabs.android.dbflow.list.FlowCursorList
+import androidx.paging.PagingData
+import kotlinx.coroutines.flow.Flow
-interface Repository {
- suspend fun getAllCursor(): FlowCursorList
+typealias Progress = suspend (current: Int, total: Int) -> Unit
- suspend fun getAndSaveRemote(): FlowCursorList
+interface Repository {
+ fun getAll(): Flow>
- suspend fun getRemote()
+ suspend fun getRemote(progress: Progress? = null)
- suspend fun search(term: String): FlowCursorList
-
- suspend fun cacheIsEmpty(): Boolean
+ fun search(term: String): Flow>
suspend fun count(): Long
+
+ suspend fun getById(id: Long): T?
}
+
+suspend fun Repository.cacheIsEmpty(): Boolean = count() == 0L
diff --git a/app/src/main/java/com/kelsos/mbrc/common/mvp/BasePresenter.kt b/app/src/main/java/com/kelsos/mbrc/common/mvp/BasePresenter.kt
deleted file mode 100644
index 2957284f7..000000000
--- a/app/src/main/java/com/kelsos/mbrc/common/mvp/BasePresenter.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.kelsos.mbrc.common.mvp
-
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancelChildren
-import rx.Subscription
-import rx.subscriptions.CompositeSubscription
-import kotlin.coroutines.CoroutineContext
-
-open class BasePresenter(
- private val dispatcher: CoroutineDispatcher = Dispatchers.Main,
-) : Presenter,
- LifecycleOwner {
- var view: T? = null
- private set
-
- private val compositeSubscription = CompositeSubscription()
- private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
- private lateinit var job: Job
- protected lateinit var scope: CoroutineScope
-
- private val coroutineContext: CoroutineContext
- get() = job + dispatcher
-
- override val isAttached: Boolean
- get() = view != null
-
- override fun attach(view: T) {
- this.view = view
- job = SupervisorJob()
- scope = CoroutineScope(coroutineContext)
- lifecycleRegistry.currentState = Lifecycle.State.CREATED
- lifecycleRegistry.currentState = Lifecycle.State.STARTED
- }
-
- override fun detach() {
- lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
- this.view = null
- compositeSubscription.clear()
- job.cancelChildren()
- }
-
- protected fun addSubscription(subscription: Subscription) {
- this.compositeSubscription.add(subscription)
- }
-
- fun checkIfAttached() {
- if (!isAttached) {
- throw ViewNotAttachedException()
- }
- }
-
- protected class ViewNotAttachedException :
- RuntimeException("Please call Presenter.attach(BaseView) before calling a method on the presenter")
-
- override val lifecycle: Lifecycle
- get() = lifecycleRegistry
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/mvp/BaseView.kt b/app/src/main/java/com/kelsos/mbrc/common/mvp/BaseView.kt
deleted file mode 100644
index b8a327ebb..000000000
--- a/app/src/main/java/com/kelsos/mbrc/common/mvp/BaseView.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.kelsos.mbrc.common.mvp
-
-interface BaseView
diff --git a/app/src/main/java/com/kelsos/mbrc/common/mvp/Presenter.kt b/app/src/main/java/com/kelsos/mbrc/common/mvp/Presenter.kt
deleted file mode 100644
index 6f72a76ed..000000000
--- a/app/src/main/java/com/kelsos/mbrc/common/mvp/Presenter.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.kelsos.mbrc.common.mvp
-
-interface Presenter {
- fun attach(view: T)
-
- fun detach()
-
- val isAttached: Boolean
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/mvvm/BaseViewModel.kt b/app/src/main/java/com/kelsos/mbrc/common/mvvm/BaseViewModel.kt
new file mode 100644
index 000000000..9d0c84ea6
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/mvvm/BaseViewModel.kt
@@ -0,0 +1,16 @@
+package com.kelsos.mbrc.common.mvvm
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+interface UiMessageBase
+
+open class BaseViewModel : ViewModel() {
+ private val backingEvents = MutableSharedFlow()
+ val events: Flow = backingEvents
+
+ protected suspend fun emit(uiMessage: T) {
+ backingEvents.emit(uiMessage)
+ }
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/AppState.kt b/app/src/main/java/com/kelsos/mbrc/common/state/AppState.kt
new file mode 100644
index 000000000..47f083d46
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/AppState.kt
@@ -0,0 +1,11 @@
+package com.kelsos.mbrc.common.state
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class AppState {
+ val playerStatus = MutableStateFlow(PlayerStatusModel())
+ val playingTrack = MutableStateFlow(PlayingTrack())
+ val playingTrackRating = MutableStateFlow(TrackRating())
+ val playingPosition = MutableStateFlow(PlayingPosition())
+ val lyrics = MutableStateFlow(emptyList())
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/AppStateManager.kt b/app/src/main/java/com/kelsos/mbrc/common/state/AppStateManager.kt
new file mode 100644
index 000000000..72deef3c3
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/AppStateManager.kt
@@ -0,0 +1,135 @@
+package com.kelsos.mbrc.common.state
+
+import com.kelsos.mbrc.common.utilities.AppCoroutineDispatchers
+import com.kelsos.mbrc.platform.mediasession.AppNotificationManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import java.util.*
+import kotlin.concurrent.fixedRateTimer
+
+typealias StateHandler = (Boolean) -> Unit
+
+@OptIn(FlowPreview::class)
+class AppStateManager(
+ private val appState: AppState,
+ private val connectionState: ConnectionState,
+ private val notifications: AppNotificationManager,
+ private val trackCache: PlayingTrackCache,
+ private val dispatchers: AppCoroutineDispatchers,
+) {
+ private var isRunning = false
+ private var stateHandler: StateHandler? = null
+ private var job = SupervisorJob()
+ private var scope = CoroutineScope(job + dispatchers.io)
+ var timer: Timer? = null
+
+ init {
+ scope.launch {
+ val track = trackCache.restoreInfo()
+ appState.playingTrack.emit(track)
+ }
+ }
+
+ fun start() {
+ if (isRunning) {
+ Timber.v("state manager is already running")
+ return
+ }
+
+ isRunning = true
+
+ if (job.isCancelled || job.isCompleted) {
+ job = SupervisorJob()
+ scope = CoroutineScope(job + dispatchers.io)
+ }
+
+ val playingPosition = appState.playingPosition
+ val debouncedPlayerState =
+ appState.playerStatus
+ .map { it.state }
+ .distinctUntilChanged { old, new -> old == new }
+ .debounce(PLAYER_STATE_DEBOUNCE_MS)
+
+ scope.launch {
+ debouncedPlayerState.collect { state ->
+ stateHandler?.invoke(state == PlayerState.Playing)
+ val currentPosition = playingPosition.map { it.current }.distinctUntilChanged().first()
+ notifications.updateState(state, currentPosition)
+ Timber.v("ooo my $state")
+ if (state == PlayerState.Playing) {
+ startPositionUpdater()
+ } else {
+ stopPositionUpdater()
+ }
+ }
+ }
+
+ scope.launch {
+ appState.playingTrack.collect { playingTrack ->
+ notifications.updatePlayingTrack(playingTrack)
+ trackCache.persistInfo(playingTrack)
+ }
+ }
+
+ scope.launch {
+ connectionState.connection.collect { connection ->
+ notifications.connectionStateChanged(connection == ConnectionStatus.Connected)
+ }
+ }
+
+ scope.launch {
+ playingPosition.collect { playingPosition ->
+ val playerState = appState.playerStatus.map { it.state }.first()
+ notifications.updateState(playerState, playingPosition.current)
+ }
+ }
+ }
+
+ fun stop() {
+ job.cancel()
+ notifications.cancel()
+ stopPositionUpdater()
+ isRunning = false
+ }
+
+ private fun startPositionUpdater() {
+ stopPositionUpdater()
+ timer =
+ fixedRateTimer("progress", period = UPDATE_PERIOD_MS) {
+ updatePosition()
+ }
+ }
+
+ private fun stopPositionUpdater() {
+ timer?.cancel()
+ timer?.purge()
+ }
+
+ private fun updatePosition() {
+ scope.launch {
+ val playingPosition = appState.playingPosition
+ val position = playingPosition.first()
+ val current = (position.current + UPDATE_PERIOD_MS).coerceAtMost(position.total)
+ if (current == position.current) {
+ return@launch
+ }
+ playingPosition.emit(position.copy(current = current))
+ }
+ }
+
+ fun setStateHandler(stateHandler: StateHandler? = null) {
+ this.stateHandler = stateHandler
+ }
+
+ companion object {
+ private const val PLAYER_STATE_DEBOUNCE_MS = 600L
+ private const val UPDATE_PERIOD_MS = 1000L
+ }
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionModel.kt b/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionModel.kt
deleted file mode 100644
index d62f41ca6..000000000
--- a/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionModel.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.kelsos.mbrc.common.state
-
-import com.kelsos.mbrc.annotations.Connection
-import com.kelsos.mbrc.annotations.Connection.Status
-import com.kelsos.mbrc.events.bus.RxBus
-import com.kelsos.mbrc.events.ui.ConnectionStatusChangeEvent
-import com.kelsos.mbrc.events.ui.RequestConnectionStateEvent
-
-class ConnectionModel(
- private val bus: RxBus,
-) {
- var isConnectionActive: Boolean = false
- private set
- private var isHandShakeDone: Boolean = false
-
- init {
- isConnectionActive = false
- isHandShakeDone = false
- this.bus.register(this, RequestConnectionStateEvent::class.java) { notifyState() }
- }
-
- val connection: Int
- @Status
- get() {
- if (isConnectionActive && isHandShakeDone) {
- return Connection.ACTIVE
- } else if (isConnectionActive) {
- return Connection.ON
- }
-
- return Connection.OFF
- }
-
- fun setConnectionState(connectionActive: String) {
- this.isConnectionActive = java.lang.Boolean.parseBoolean(connectionActive)
- notifyState()
- }
-
- private fun notifyState() {
- bus.post(ConnectionStatusChangeEvent.create(connection))
- }
-
- fun setHandShakeDone(handShakeDone: Boolean) {
- this.isHandShakeDone = handShakeDone
- notifyState()
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionState.kt b/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionState.kt
new file mode 100644
index 000000000..811fe5fa2
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionState.kt
@@ -0,0 +1,7 @@
+package com.kelsos.mbrc.common.state
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class ConnectionState {
+ val connection = MutableStateFlow(ConnectionStatus.Offline)
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionStatus.kt b/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionStatus.kt
new file mode 100644
index 000000000..3e5e6d352
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/ConnectionStatus.kt
@@ -0,0 +1,11 @@
+package com.kelsos.mbrc.common.state
+
+sealed class ConnectionStatus(
+ val status: String,
+) {
+ object Offline : ConnectionStatus("Offline")
+
+ object Authenticating : ConnectionStatus("Authenticating")
+
+ object Connected : ConnectionStatus("Connected")
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/LfmRating.kt b/app/src/main/java/com/kelsos/mbrc/common/state/LfmRating.kt
new file mode 100644
index 000000000..3c7f7abf5
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/LfmRating.kt
@@ -0,0 +1,21 @@
+package com.kelsos.mbrc.common.state
+
+sealed class LfmRating {
+ data object Loved : LfmRating()
+
+ data object Banned : LfmRating()
+
+ data object Normal : LfmRating()
+
+ companion object {
+ private const val LOVE = "Love"
+ private const val BAN = "Ban"
+
+ fun fromString(value: String?): LfmRating =
+ when (value) {
+ LOVE -> Loved
+ BAN -> Banned
+ else -> Normal
+ }
+ }
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/MainDataModel.kt b/app/src/main/java/com/kelsos/mbrc/common/state/MainDataModel.kt
deleted file mode 100644
index 9833de47c..000000000
--- a/app/src/main/java/com/kelsos/mbrc/common/state/MainDataModel.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.kelsos.mbrc.common.state
-
-import com.kelsos.mbrc.annotations.PlayerState
-import com.kelsos.mbrc.annotations.PlayerState.State
-import com.kelsos.mbrc.annotations.Repeat
-import com.kelsos.mbrc.annotations.Repeat.Mode
-import com.kelsos.mbrc.constants.Const
-import com.kelsos.mbrc.events.ui.ShuffleChange
-import com.kelsos.mbrc.events.ui.ShuffleChange.ShuffleState
-import com.kelsos.mbrc.features.player.LfmStatus
-import com.kelsos.mbrc.features.player.TrackInfo
-import com.kelsos.mbrc.networking.protocol.Protocol
-
-class MainDataModel {
- var pluginUpdateAvailable: Boolean = false
- var pluginUpdateRequired: Boolean = false
- var minimumRequired: String = ""
- var position: Long = 0
- var duration: Long = 0
- var trackInfo: TrackInfo = TrackInfo()
- var coverPath: String = ""
- var rating: Float = 0f
- var volume: Int = 0
-
- @ShuffleState
- var shuffle: String = ShuffleChange.OFF
- var isScrobblingEnabled: Boolean = false
- var isMute: Boolean = false
- var lfmStatus: LfmStatus = LfmStatus.NORMAL
- private set
- var apiOutOfDate: Boolean = false
- private set
-
- var pluginVersion: String = "1.0.0"
- set(value) {
- if (value.isEmpty()) {
- return
- }
- field = value.substring(0, value.lastIndexOf('.'))
- }
-
- var pluginProtocol: Int = 2
- set(value) {
- field = value
- if (value < Protocol.PROTOCOL_VERSION_NUMBER) {
- apiOutOfDate = true
- }
- }
-
- @State
- var playState: String = PlayerState.UNDEFINED
- set(value) {
- @State val newState: String =
- when {
- Const.PLAYING.equals(value, ignoreCase = true) -> PlayerState.PLAYING
- Const.STOPPED.equals(value, ignoreCase = true) -> PlayerState.STOPPED
- Const.PAUSED.equals(value, ignoreCase = true) -> PlayerState.PAUSED
- else -> PlayerState.UNDEFINED
- }
- field = newState
- }
-
- @Mode
- var repeat: String
- private set
-
- init {
- repeat = Repeat.NONE
- rating = 0f
-
- lfmStatus = LfmStatus.NORMAL
- pluginVersion = Const.EMPTY
- }
-
- fun setLfmRating(rating: String) {
- lfmStatus =
- when (rating) {
- "Love" -> LfmStatus.LOVED
- "Ban" -> LfmStatus.BANNED
- else -> LfmStatus.NORMAL
- }
- }
-
- fun setRepeatState(repeat: String) {
- this.repeat =
- when {
- Protocol.ALL.equals(repeat, ignoreCase = true) -> Repeat.ALL
- Protocol.ONE.equals(repeat, ignoreCase = true) -> Repeat.ONE
- else -> Repeat.NONE
- }
- }
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/NowPlayingTrack.kt b/app/src/main/java/com/kelsos/mbrc/common/state/NowPlayingTrack.kt
new file mode 100644
index 000000000..a747426db
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/NowPlayingTrack.kt
@@ -0,0 +1,18 @@
+package com.kelsos.mbrc.common.state
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class NowPlayingTrack(
+ @Json(name = "artist")
+ val artist: String,
+ @Json(name = "album")
+ val album: String,
+ @Json(name = "title")
+ val title: String,
+ @Json(name = "year")
+ val year: String,
+ @Json(name = "path")
+ val path: String,
+)
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/PlayerState.kt b/app/src/main/java/com/kelsos/mbrc/common/state/PlayerState.kt
new file mode 100644
index 000000000..763d98b89
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/PlayerState.kt
@@ -0,0 +1,28 @@
+package com.kelsos.mbrc.common.state
+
+sealed class PlayerState(
+ val state: String,
+) {
+ object Playing : PlayerState(PLAYING)
+
+ object Paused : PlayerState(PAUSED)
+
+ object Stopped : PlayerState(STOPPED)
+
+ object Undefined : PlayerState(UNDEFINED)
+
+ companion object {
+ const val PLAYING = "playing"
+ const val PAUSED = "paused"
+ const val STOPPED = "stopped"
+ const val UNDEFINED = "undefined"
+
+ fun fromString(state: String?): PlayerState =
+ when (state?.lowercase()) {
+ PLAYING -> Playing
+ PAUSED -> Paused
+ STOPPED -> Stopped
+ else -> Undefined
+ }
+ }
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/PlayerStatus.kt b/app/src/main/java/com/kelsos/mbrc/common/state/PlayerStatus.kt
new file mode 100644
index 000000000..ea0ddaf50
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/PlayerStatus.kt
@@ -0,0 +1,21 @@
+package com.kelsos.mbrc.common.state
+
+import com.kelsos.mbrc.networking.protocol.Protocol
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class PlayerStatus(
+ @Json(name = Protocol.PLAYER_MUTE)
+ val mute: Boolean,
+ @Json(name = Protocol.PLAYER_STATE)
+ val playState: String,
+ @Json(name = Protocol.PLAYER_REPEAT)
+ val repeat: String,
+ @Json(name = Protocol.PLAYER_SHUFFLE)
+ val shuffle: String,
+ @Json(name = Protocol.PLAYER_SCROBBLE)
+ val scrobbling: Boolean,
+ @Json(name = Protocol.PLAYER_VOLUME)
+ val volume: Int,
+)
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/PlayerStatusModel.kt b/app/src/main/java/com/kelsos/mbrc/common/state/PlayerStatusModel.kt
new file mode 100644
index 000000000..c0eb91e95
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/PlayerStatusModel.kt
@@ -0,0 +1,13 @@
+package com.kelsos.mbrc.common.state
+
+import androidx.annotation.IntRange
+
+data class PlayerStatusModel(
+ @get:IntRange(from = 0, to = 100)
+ val volume: Int = 0,
+ val mute: Boolean = false,
+ val shuffle: ShuffleMode = ShuffleMode.Off,
+ val scrobbling: Boolean = false,
+ val repeat: Repeat = Repeat.None,
+ val state: PlayerState = PlayerState.Undefined,
+)
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/PlayingPosition.kt b/app/src/main/java/com/kelsos/mbrc/common/state/PlayingPosition.kt
new file mode 100644
index 000000000..ce7f41005
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/PlayingPosition.kt
@@ -0,0 +1,23 @@
+package com.kelsos.mbrc.common.state
+
+data class PlayingPosition(
+ val current: Duration = 0,
+ val total: Duration = 0,
+) {
+ val totalMinutes get() = total.toMinutes()
+ val currentMinutes get() = current.toMinutes()
+
+ fun progress(): String = "$currentMinutes / $totalMinutes"
+}
+
+typealias Duration = Long
+
+private const val SECONDS_IN_MINUTE = 60
+private const val MILLIS_IN_SECONDS = 1000
+
+fun Duration.toMinutes(): String {
+ val inSeconds = this / MILLIS_IN_SECONDS
+ val minutes = inSeconds / SECONDS_IN_MINUTE
+ val seconds = inSeconds % SECONDS_IN_MINUTE
+ return "%02d:%02d".format(minutes, seconds)
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/PlayingTrack.kt b/app/src/main/java/com/kelsos/mbrc/common/state/PlayingTrack.kt
new file mode 100644
index 000000000..f339d3bf6
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/PlayingTrack.kt
@@ -0,0 +1,49 @@
+package com.kelsos.mbrc.common.state
+
+import android.os.Parcel
+import android.os.Parcelable
+
+data class PlayingTrack(
+ val artist: String = "",
+ val title: String = "",
+ val album: String = "",
+ val year: String = "",
+ val path: String = "",
+ val coverUrl: String = "",
+ val duration: Long = 0,
+) : Parcelable {
+ companion object {
+ @JvmField
+ val CREATOR: Parcelable.Creator =
+ object : Parcelable.Creator {
+ override fun createFromParcel(source: Parcel): PlayingTrack = PlayingTrack(source)
+
+ override fun newArray(size: Int): Array = arrayOfNulls(size)
+ }
+ }
+
+ constructor(source: Parcel) : this(
+ source.readString() ?: "",
+ source.readString() ?: "",
+ source.readString() ?: "",
+ source.readString() ?: "",
+ source.readString() ?: "",
+ source.readString() ?: "",
+ source.readLong(),
+ )
+
+ override fun describeContents() = 0
+
+ override fun writeToParcel(
+ dest: Parcel,
+ flags: Int,
+ ) {
+ dest.writeString(artist)
+ dest.writeString(title)
+ dest.writeString(album)
+ dest.writeString(year)
+ dest.writeString(path)
+ dest.writeString(coverUrl)
+ dest.writeLong(duration)
+ }
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/features/player/ModelCache.kt b/app/src/main/java/com/kelsos/mbrc/common/state/PlayingTrackCache.kt
similarity index 58%
rename from app/src/main/java/com/kelsos/mbrc/features/player/ModelCache.kt
rename to app/src/main/java/com/kelsos/mbrc/common/state/PlayingTrackCache.kt
index 055542c86..1efddcd0e 100644
--- a/app/src/main/java/com/kelsos/mbrc/features/player/ModelCache.kt
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/PlayingTrackCache.kt
@@ -1,9 +1,12 @@
-package com.kelsos.mbrc.features.player
+package com.kelsos.mbrc.common.state
import android.app.Application
import android.content.Context
+import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
+import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
+import com.google.protobuf.InvalidProtocolBufferException
import com.kelsos.mbrc.common.utilities.AppCoroutineDispatchers
import com.kelsos.mbrc.store.Store
import com.kelsos.mbrc.store.Track
@@ -13,39 +16,51 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
internal val Context.cacheDataStore: DataStore by dataStore(
fileName = "cache_store.db",
serializer = PlayerStateSerializer,
)
-class ModelCacheImpl(
+interface PlayingTrackCache {
+ suspend fun persistInfo(playingTrack: PlayingTrack)
+
+ suspend fun restoreInfo(): PlayingTrack
+
+ suspend fun persistCover(cover: String)
+
+ suspend fun restoreCover(): String
+}
+
+class PlayingTrackCacheImpl(
private val context: Application,
private val dispatchers: AppCoroutineDispatchers,
-) : ModelCache {
+) : PlayingTrackCache {
private val storeFlow: Flow =
context.cacheDataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
- Timber.Forest.e(exception, "Error reading sort order preferences.")
+ Timber.e(exception, "Error reading sort order preferences.")
emit(Store.getDefaultInstance())
} else {
throw exception
}
}
- override suspend fun persistInfo(trackInfo: TrackInfo) =
+ override suspend fun persistInfo(playingTrack: PlayingTrack) =
withContext(dispatchers.io) {
context.cacheDataStore.updateData { store ->
val track =
Track
.newBuilder()
- .setAlbum(trackInfo.album)
- .setArtist(trackInfo.artist)
- .setPath(trackInfo.path)
- .setTitle(trackInfo.title)
- .setYear(trackInfo.year)
+ .setAlbum(playingTrack.album)
+ .setArtist(playingTrack.artist)
+ .setPath(playingTrack.path)
+ .setTitle(playingTrack.title)
+ .setYear(playingTrack.year)
.build()
store
@@ -56,11 +71,11 @@ class ModelCacheImpl(
return@withContext
}
- override suspend fun restoreInfo(): TrackInfo =
+ override suspend fun restoreInfo(): PlayingTrack =
withContext(dispatchers.io) {
val track = storeFlow.first().track
- return@withContext TrackInfo(
+ return@withContext PlayingTrack(
track.artist,
track.title,
track.album,
@@ -78,12 +93,20 @@ class ModelCacheImpl(
override suspend fun restoreCover(): String = storeFlow.first().cover
}
-interface ModelCache {
- suspend fun persistInfo(trackInfo: TrackInfo)
-
- suspend fun restoreInfo(): TrackInfo
+object PlayerStateSerializer : Serializer {
+ override suspend fun readFrom(input: InputStream): Store {
+ try {
+ return Store.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", exception)
+ }
+ }
- suspend fun persistCover(cover: String)
+ override suspend fun writeTo(
+ t: Store,
+ output: OutputStream,
+ ) = t.writeTo(output)
- suspend fun restoreCover(): String
+ override val defaultValue: Store
+ get() = Store.getDefaultInstance()
}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/Repeat.kt b/app/src/main/java/com/kelsos/mbrc/common/state/Repeat.kt
new file mode 100644
index 000000000..96c296c6f
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/Repeat.kt
@@ -0,0 +1,24 @@
+package com.kelsos.mbrc.common.state
+
+sealed class Repeat(
+ val mode: String,
+) {
+ object All : Repeat(ALL)
+
+ object None : Repeat(NONE)
+
+ object One : Repeat(ONE)
+
+ companion object {
+ const val ALL = "all"
+ const val NONE = "none"
+ const val ONE = "one"
+
+ fun fromString(mode: String?): Repeat =
+ when (mode?.lowercase()) {
+ ALL -> All
+ ONE -> One
+ else -> None
+ }
+ }
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/ShuffleMode.kt b/app/src/main/java/com/kelsos/mbrc/common/state/ShuffleMode.kt
new file mode 100644
index 000000000..7338b702a
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/ShuffleMode.kt
@@ -0,0 +1,24 @@
+package com.kelsos.mbrc.common.state
+
+sealed class ShuffleMode(
+ val mode: String,
+) {
+ data object Off : ShuffleMode(OFF)
+
+ data object AutoDJ : ShuffleMode(AUTO_DJ)
+
+ data object Shuffle : ShuffleMode(SHUFFLE)
+
+ companion object {
+ const val OFF = "off"
+ const val AUTO_DJ = "autodj"
+ const val SHUFFLE = "shuffle"
+
+ fun fromString(string: String?): ShuffleMode =
+ when (string?.lowercase()) {
+ AUTO_DJ -> AutoDJ
+ SHUFFLE -> Shuffle
+ else -> Off
+ }
+ }
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/state/TrackRating.kt b/app/src/main/java/com/kelsos/mbrc/common/state/TrackRating.kt
new file mode 100644
index 000000000..b069bfc6d
--- /dev/null
+++ b/app/src/main/java/com/kelsos/mbrc/common/state/TrackRating.kt
@@ -0,0 +1,8 @@
+package com.kelsos.mbrc.common.state
+
+data class TrackRating(
+ val lfmRating: LfmRating = LfmRating.Normal,
+ val rating: Float = 0f,
+) {
+ fun isFavorite(): Boolean = lfmRating == LfmRating.Loved
+}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/ui/CircleImageView.kt b/app/src/main/java/com/kelsos/mbrc/common/ui/CircleImageView.kt
index 6d993bbd7..bb340192c 100644
--- a/app/src/main/java/com/kelsos/mbrc/common/ui/CircleImageView.kt
+++ b/app/src/main/java/com/kelsos/mbrc/common/ui/CircleImageView.kt
@@ -36,6 +36,7 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import com.kelsos.mbrc.R
+import timber.log.Timber
class CircleImageView : AppCompatImageView {
private val mDrawableRect = RectF()
@@ -112,15 +113,11 @@ class CircleImageView : AppCompatImageView {
override fun getScaleType(): ScaleType = SCALE_TYPE
override fun setScaleType(scaleType: ScaleType) {
- if (scaleType != SCALE_TYPE) {
- throw IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType))
- }
+ require(scaleType == SCALE_TYPE) { "ScaleType $scaleType not supported." }
}
override fun setAdjustViewBounds(adjustViewBounds: Boolean) {
- if (adjustViewBounds) {
- throw IllegalArgumentException("adjustViewBounds not supported.")
- }
+ require(!adjustViewBounds) { "adjustViewBounds not supported." }
}
override fun onDraw(canvas: Canvas) {
@@ -272,22 +269,19 @@ class CircleImageView : AppCompatImageView {
}
try {
- val bitmap: Bitmap
-
- if (drawable is ColorDrawable) {
- bitmap =
- Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG)
- } else {
- bitmap =
+ val bitmap: Bitmap =
+ if (drawable is ColorDrawable) {
+ Bitmap.createBitmap(COLOR_DRAWABLE_DIMENSION, COLOR_DRAWABLE_DIMENSION, BITMAP_CONFIG)
+ } else {
Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, BITMAP_CONFIG)
- }
+ }
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
- } catch (e: Exception) {
- e.printStackTrace()
+ } catch (e: IllegalArgumentException) {
+ Timber.e(e)
return null
}
}
@@ -372,16 +366,16 @@ class CircleImageView : AppCompatImageView {
if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
scale = mDrawableRect.height() / mBitmapHeight.toFloat()
- dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f
+ dx = (mDrawableRect.width() - mBitmapWidth * scale) * HALF
} else {
scale = mDrawableRect.width() / mBitmapWidth.toFloat()
- dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f
+ dy = (mDrawableRect.height() - mBitmapHeight * scale) * HALF
}
mShaderMatrix.setScale(scale, scale)
mShaderMatrix.postTranslate(
- (dx + 0.5f).toInt() + mDrawableRect.left,
- (dy + 0.5f).toInt() + mDrawableRect.top,
+ (dx + HALF).toInt() + mDrawableRect.left,
+ (dy + HALF).toInt() + mDrawableRect.top,
)
mBitmapShader!!.setLocalMatrix(mShaderMatrix)
@@ -391,11 +385,12 @@ class CircleImageView : AppCompatImageView {
private val SCALE_TYPE = ScaleType.CENTER_CROP
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
- private val COLORDRAWABLE_DIMENSION = 2
+ private const val COLOR_DRAWABLE_DIMENSION = 2
- private val DEFAULT_BORDER_WIDTH = 0
- private val DEFAULT_BORDER_COLOR = Color.BLACK
- private val DEFAULT_FILL_COLOR = Color.TRANSPARENT
- private val DEFAULT_BORDER_OVERLAY = false
+ private const val DEFAULT_BORDER_WIDTH = 0
+ private const val DEFAULT_BORDER_COLOR = Color.BLACK
+ private const val DEFAULT_FILL_COLOR = Color.TRANSPARENT
+ private const val DEFAULT_BORDER_OVERLAY = false
+ private const val HALF = 0.5f
}
}
diff --git a/app/src/main/java/com/kelsos/mbrc/common/utilities/Helpers.kt b/app/src/main/java/com/kelsos/mbrc/common/utilities/Helpers.kt
index 7fcd5fbc1..f347432e1 100644
--- a/app/src/main/java/com/kelsos/mbrc/common/utilities/Helpers.kt
+++ b/app/src/main/java/com/kelsos/mbrc/common/utilities/Helpers.kt
@@ -1,5 +1,35 @@
package com.kelsos.mbrc.common.utilities
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.paging.PagingSource
+import androidx.paging.map
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import java.time.Instant
+
+fun paged(
+ pagingSourceFactory: () -> PagingSource,
+ transform: (value: T) -> I,
+): Flow> {
+ val config =
+ PagingConfig(
+ enablePlaceholders = true,
+ pageSize = 60,
+ maxSize = 200,
+ )
+ return Pager(
+ config,
+ pagingSourceFactory = pagingSourceFactory,
+ ).flow.map { data -> data.map(transform) }
+}
+
+/**
+ * [Instant.getEpochSecond] for [Instant.now]
+ */
+fun epoch(): Long = Instant.now().epochSecond
+
inline fun whenNotNull(
p1: T1?,
p2: T2?,
diff --git a/app/src/main/java/com/kelsos/mbrc/common/utilities/RemoteUtils.kt b/app/src/main/java/com/kelsos/mbrc/common/utilities/RemoteUtils.kt
index b7894eec8..48a73257c 100644
--- a/app/src/main/java/com/kelsos/mbrc/common/utilities/RemoteUtils.kt
+++ b/app/src/main/java/com/kelsos/mbrc/common/utilities/RemoteUtils.kt
@@ -1,48 +1,20 @@
package com.kelsos.mbrc.common.utilities
-import android.content.Context
-import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
-import androidx.core.content.pm.PackageInfoCompat
-import java.security.MessageDigest
+import com.kelsos.mbrc.BuildConfig
object RemoteUtils {
- @Throws(PackageManager.NameNotFoundException::class)
- fun Context.getVersion(): String? = packageManager.getPackageInfo(packageName, 0).versionName
-
- @Throws(PackageManager.NameNotFoundException::class)
- fun Context.getVersionCode(): Long = PackageInfoCompat.getLongVersionCode(packageManager.getPackageInfo(packageName, 0))
-
- fun coverBitmapSync(coverPath: String): Bitmap? {
- return try {
- val options = BitmapFactory.Options()
- options.inPreferredConfig = Bitmap.Config.RGB_565
- return BitmapFactory.decodeFile(coverPath, options)
- } catch (e: Exception) {
- null
+ const val VERSION: String = BuildConfig.VERSION_NAME
+ const val VERSION_CODE: Int = BuildConfig.VERSION_CODE
+
+ fun loadBitmap(path: String): Result =
+ runCatching {
+ BitmapFactory.decodeFile(
+ path,
+ BitmapFactory.Options().apply {
+ inPreferredConfig = Bitmap.Config.RGB_565
+ },
+ )
}
- }
-
- fun sha1(input: String) = hashString("SHA-1", input)
-
- private fun hashString(
- type: String,
- input: String,
- ): String {
- val hexChars = "0123456789ABCDEF"
- val bytes =
- MessageDigest
- .getInstance(type)
- .digest(input.toByteArray())
- val result = StringBuilder(bytes.size * 2)
-
- bytes.forEach {
- val i = it.toInt()
- result.append(hexChars[i shr 4 and 0x0f])
- result.append(hexChars[i and 0x0f])
- }
-
- return result.toString()
- }
}
diff --git a/app/src/main/java/com/kelsos/mbrc/constants/ApplicationEvents.kt b/app/src/main/java/com/kelsos/mbrc/constants/ApplicationEvents.kt
deleted file mode 100644
index 74e6bf402..000000000
--- a/app/src/main/java/com/kelsos/mbrc/constants/ApplicationEvents.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.kelsos.mbrc.constants
-
-object ApplicationEvents {
- const val SOCKET_DATA_AVAILABLE = "SocketDataAvailable"
- const val SOCKET_STATUS_CHANGED = "SocketStatusChanged"
- const val SOCKET_HANDSHAKE_UPDATE = "SocketHandshakeUpdate"
- const val TERMINATE_SERVICE = "TerminateService"
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/constants/Const.kt b/app/src/main/java/com/kelsos/mbrc/constants/Const.kt
index ad050faf6..77b44f2cc 100644
--- a/app/src/main/java/com/kelsos/mbrc/constants/Const.kt
+++ b/app/src/main/java/com/kelsos/mbrc/constants/Const.kt
@@ -1,13 +1,6 @@
package com.kelsos.mbrc.constants
object Const {
- const val EMPTY = ""
- const val LYRICS_NEWLINE = "\r\n|\n"
- const val DATA = "data"
- const val TOGGLE = "toggle"
- const val PLAYING = "playing"
- const val PAUSED = "paused"
- const val STOPPED = "stopped"
const val NEWLINE = "\r\n"
const val UTF_8 = "UTF-8"
}
diff --git a/app/src/main/java/com/kelsos/mbrc/constants/ProtocolEventType.kt b/app/src/main/java/com/kelsos/mbrc/constants/ProtocolEventType.kt
deleted file mode 100644
index ceeec70e1..000000000
--- a/app/src/main/java/com/kelsos/mbrc/constants/ProtocolEventType.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.kelsos.mbrc.constants
-
-object ProtocolEventType {
- const val INITIATE_PROTOCOL_REQUEST = "InitiateProtocolRequest"
- const val REDUCE_VOLUME = "ReduceVolume"
- const val HANDSHAKE_COMPLETE = "HandshakeComplete"
- const val INFORM_CLIENT_NOT_ALLOWED = "InformClientNotAllowed"
- const val PLUGIN_UPDATE_AVAILABLE = "PluginUpdateAvailable"
- const val PLUGIN_UPDATE_REQUIRED = "PluginUpdateRequired"
- const val USER_ACTION = "UserAction"
- const val PLUGIN_VERSION_CHECK = "PluginVersionCheck"
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/constants/UserInputEventType.kt b/app/src/main/java/com/kelsos/mbrc/constants/UserInputEventType.kt
deleted file mode 100644
index 7713ad6a3..000000000
--- a/app/src/main/java/com/kelsos/mbrc/constants/UserInputEventType.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.kelsos.mbrc.constants
-
-object UserInputEventType {
- const val START_CONNECTION = "StartConnection"
- const val SETTINGS_CHANGED = "SettingsChanged"
- const val RESET_CONNECTION = "ResetConnection"
- const val CANCEL_NOTIFICATION = "CancelNotification"
- const val START_DISCOVERY = "StartDiscovery"
- const val KEY_VOLUME_UP = "KeyVolumeUp"
- const val KEY_VOLUME_DOWN = "KeyVolumeDown"
- const val TERMINATE_CONNECTION = "TerminateConnection"
-}
diff --git a/app/src/main/java/com/kelsos/mbrc/data/Data.kt b/app/src/main/java/com/kelsos/mbrc/data/Data.kt
deleted file mode 100644
index 7979c974b..000000000
--- a/app/src/main/java/com/kelsos/mbrc/data/Data.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.kelsos.mbrc.data
-
-interface Data
diff --git a/app/src/main/java/com/kelsos/mbrc/data/Database.kt b/app/src/main/java/com/kelsos/mbrc/data/Database.kt
index 3924d8ba6..0005a79ab 100644
--- a/app/src/main/java/com/kelsos/mbrc/data/Database.kt
+++ b/app/src/main/java/com/kelsos/mbrc/data/Database.kt
@@ -1,73 +1,57 @@
package com.kelsos.mbrc.data
-import com.kelsos.mbrc.features.library.Album
-import com.kelsos.mbrc.features.library.Artist
-import com.kelsos.mbrc.features.library.Genre
-import com.kelsos.mbrc.features.library.Track
-import com.kelsos.mbrc.features.nowplaying.NowPlaying
-import com.kelsos.mbrc.features.playlists.Playlist
-import com.raizlabs.android.dbflow.annotation.Migration
-import com.raizlabs.android.dbflow.sql.SQLiteType
-import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration
-import com.raizlabs.android.dbflow.annotation.Database as Db
-
-@Db(version = Database.VERSION, name = Database.NAME)
-object Database {
- const val VERSION = 3
- const val NAME = "cache"
-
- @Migration(version = 3, database = Database::class)
- class Migration3Genre(
- table: Class,
- ) : AlterTableMigration(table) {
- override fun onPreMigrate() {
- addColumn(SQLiteType.INTEGER, "date_added")
- }
- }
-
- @Migration(version = 3, database = Database::class)
- class Migration3Artist(
- table: Class,
- ) : AlterTableMigration(table) {
- override fun onPreMigrate() {
- addColumn(SQLiteType.INTEGER, "date_added")
- }
- }
-
- @Migration(version = 3, database = Database::class)
- class Migration3Album(
- table: Class,
- ) : AlterTableMigration(table) {
- override fun onPreMigrate() {
- addColumn(SQLiteType.TEXT, "cover")
- addColumn(SQLiteType.INTEGER, "date_added")
- }
- }
-
- @Migration(version = 3, database = Database::class)
- class Migration3Track(
- table: Class