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, - ) : AlterTableMigration(table) { - override fun onPreMigrate() { - addColumn(SQLiteType.INTEGER, "date_added") - } - } - - @Migration(version = 3, database = Database::class) - class Migration3NowPlaying( - table: Class, - ) : AlterTableMigration(table) { - override fun onPreMigrate() { - addColumn(SQLiteType.INTEGER, "date_added") - } - } - - @Migration(version = 3, database = Database::class) - class Migration3Playlist( - table: Class, - ) : AlterTableMigration(table) { - override fun onPreMigrate() { - addColumn(SQLiteType.INTEGER, "date_added") - } +import androidx.room.Database +import androidx.room.RoomDatabase +import com.kelsos.mbrc.data.Database.Companion.VERSION +import com.kelsos.mbrc.features.library.albums.AlbumDao +import com.kelsos.mbrc.features.library.albums.AlbumEntity +import com.kelsos.mbrc.features.library.artists.ArtistDao +import com.kelsos.mbrc.features.library.artists.ArtistEntity +import com.kelsos.mbrc.features.library.genres.GenreDao +import com.kelsos.mbrc.features.library.genres.GenreEntity +import com.kelsos.mbrc.features.library.tracks.TrackDao +import com.kelsos.mbrc.features.library.tracks.TrackEntity +import com.kelsos.mbrc.features.nowplaying.NowPlayingDao +import com.kelsos.mbrc.features.nowplaying.NowPlayingEntity +import com.kelsos.mbrc.features.playlists.PlaylistDao +import com.kelsos.mbrc.features.playlists.PlaylistEntity +import com.kelsos.mbrc.features.radio.RadioStationDao +import com.kelsos.mbrc.features.radio.RadioStationEntity +import com.kelsos.mbrc.features.settings.ConnectionDao +import com.kelsos.mbrc.features.settings.ConnectionSettingsEntity + +@Database( + entities = [ + GenreEntity::class, + ArtistEntity::class, + AlbumEntity::class, + TrackEntity::class, + NowPlayingEntity::class, + PlaylistEntity::class, + RadioStationEntity::class, + ConnectionSettingsEntity::class, + ], + version = VERSION, +) +abstract class Database : RoomDatabase() { + abstract fun genreDao(): GenreDao + + abstract fun artistDao(): ArtistDao + + abstract fun albumDao(): AlbumDao + + abstract fun trackDao(): TrackDao + + abstract fun nowPlayingDao(): NowPlayingDao + + abstract fun playlistDao(): PlaylistDao + + abstract fun radioStationDao(): RadioStationDao + + abstract fun connectionDao(): ConnectionDao + + companion object { + const val VERSION = 3 + const val NAME = "cache.db" } } diff --git a/app/src/main/java/com/kelsos/mbrc/data/DeserializationAdapter.kt b/app/src/main/java/com/kelsos/mbrc/data/DeserializationAdapter.kt new file mode 100644 index 000000000..70b5dd5a0 --- /dev/null +++ b/app/src/main/java/com/kelsos/mbrc/data/DeserializationAdapter.kt @@ -0,0 +1,50 @@ +package com.kelsos.mbrc.data + +import com.squareup.moshi.Moshi +import java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +interface DeserializationAdapter { + fun objectify( + line: String, + kClass: KClass, + ): T where T : Any + + fun objectify( + line: String, + type: ParameterizedType, + ): T where T : Any + + fun convertValue( + data: Any?, + kClass: KClass, + ): T where T : Any +} + +class DeserializationAdapterImpl( + private val moshi: Moshi, +) : DeserializationAdapter { + override fun objectify( + line: String, + type: ParameterizedType, + ): T { + val adapter = moshi.adapter(type) + return checkNotNull(adapter.fromJson(line)) { "what?" } + } + + override fun convertValue( + data: Any?, + kClass: KClass, + ): T { + val adapter = moshi.adapter(kClass.java) + return checkNotNull(adapter.fromJsonValue(data)) { "what?" } + } + + override fun objectify( + line: String, + kClass: KClass, + ): T { + val adapter = moshi.adapter(kClass.java) + return checkNotNull(adapter.fromJson(line)) { "what? " } + } +} diff --git a/app/src/main/java/com/kelsos/mbrc/data/SerializationAdapter.kt b/app/src/main/java/com/kelsos/mbrc/data/SerializationAdapter.kt new file mode 100644 index 000000000..316714161 --- /dev/null +++ b/app/src/main/java/com/kelsos/mbrc/data/SerializationAdapter.kt @@ -0,0 +1,17 @@ +package com.kelsos.mbrc.data + +import com.kelsos.mbrc.networking.client.SocketMessage +import com.squareup.moshi.Moshi + +interface SerializationAdapter { + fun stringify(message: SocketMessage): String +} + +class SerializationAdapterImpl( + private val moshi: Moshi, +) : SerializationAdapter { + override fun stringify(message: SocketMessage): String { + val adapter = moshi.adapter(SocketMessage::class.java) + return adapter.toJson(message) + } +} diff --git a/app/src/main/java/com/kelsos/mbrc/data/UserAction.kt b/app/src/main/java/com/kelsos/mbrc/data/UserAction.kt deleted file mode 100644 index 0ec25690b..000000000 --- a/app/src/main/java/com/kelsos/mbrc/data/UserAction.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.kelsos.mbrc.data - -class UserAction( - val context: String, - val data: Any, -) { - companion object { - fun create(context: String): UserAction = UserAction(context, true) - - fun create( - context: String, - data: Any, - ): UserAction = UserAction(context, data) - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/DefaultSettingsChangedEvent.kt b/app/src/main/java/com/kelsos/mbrc/events/DefaultSettingsChangedEvent.kt deleted file mode 100644 index 2ffd7a687..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/DefaultSettingsChangedEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.kelsos.mbrc.events - -class DefaultSettingsChangedEvent { - companion object { - fun create(): DefaultSettingsChangedEvent = DefaultSettingsChangedEvent() - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/MessageEvent.kt b/app/src/main/java/com/kelsos/mbrc/events/MessageEvent.kt deleted file mode 100644 index bf405be54..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/MessageEvent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.kelsos.mbrc.events - -import com.fasterxml.jackson.databind.node.TextNode -import com.kelsos.mbrc.constants.ProtocolEventType -import com.kelsos.mbrc.data.UserAction -import com.kelsos.mbrc.networking.protocol.ProtocolMessage - -data class MessageEvent( - override var type: String = "", - override var data: Any = "", -) : ProtocolMessage { - override val dataString: String - get() { - return when (data.javaClass) { - TextNode::class.java -> (data as TextNode).asText() - String::class.java -> data as String - else -> "" - } - } - - companion object { - fun action(data: UserAction): MessageEvent = MessageEvent(ProtocolEventType.USER_ACTION, data) - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/bus/RxBus.kt b/app/src/main/java/com/kelsos/mbrc/events/bus/RxBus.kt deleted file mode 100644 index 10908c819..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/bus/RxBus.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.kelsos.mbrc.events.bus - -import rx.Subscription - -interface RxBus { - fun register( - receiver: Any, - eventClass: Class, - onNext: (T) -> Unit, - ) - - fun register( - receiver: Any, - eventClass: Class, - onNext: (T) -> Unit, - main: Boolean, - ) - - fun unregister(receiver: Any) - - fun register( - eventClass: Class, - onNext: (T) -> Unit, - main: Boolean, - ): Subscription - - fun post(event: Any) -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/bus/RxBusImpl.kt b/app/src/main/java/com/kelsos/mbrc/events/bus/RxBusImpl.kt deleted file mode 100644 index 3173ca7f6..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/bus/RxBusImpl.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.kelsos.mbrc.events.bus - -import com.jakewharton.rxrelay.PublishRelay -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber -import java.util.LinkedList - -class RxBusImpl : RxBus { - init { - Timber.v("Injecting RxBus instance %s", this) - } - - private val serializedRelay = PublishRelay.create().toSerialized() - private val activeSubscriptions = HashMap>() - - @Suppress("UNCHECKED_CAST") - override fun register( - receiver: Any, - eventClass: Class, - onNext: (T) -> Unit, - ) { - //noinspection unchecked - val subscription = - serializedRelay - .filter { - it.javaClass == eventClass - }.map { obj -> obj as T } - .subscribe(onNext) - - updateSubscriptions(receiver, subscription) - } - - override fun register( - receiver: Any, - eventClass: Class, - onNext: (T) -> Unit, - main: Boolean, - ) { - val subscription = register(eventClass, onNext, true) - updateSubscriptions(receiver, subscription) - } - - private fun updateSubscriptions( - receiver: Any, - subscription: Subscription, - ) { - val subscriptions: MutableList = - activeSubscriptions[receiver] ?: LinkedList() - subscriptions.add(subscription) - activeSubscriptions[receiver] = subscriptions - } - - override fun unregister(receiver: Any) { - val subscriptions = activeSubscriptions.remove(receiver) - if (subscriptions != null) { - Observable.from(subscriptions).filter { !it.isUnsubscribed }.subscribe { it.unsubscribe() } - } - } - - @Suppress("UNCHECKED_CAST") - override fun register( - eventClass: Class, - onNext: (T) -> Unit, - main: Boolean, - ): Subscription { - //noinspection unchecked - val observable = serializedRelay.filter { it.javaClass == eventClass }.map { obj -> obj as T } - val scheduler = if (main) AndroidSchedulers.mainThread() else Schedulers.immediate() - return observable.onBackpressureBuffer().observeOn(scheduler).subscribe(onNext) - } - - override fun post(event: Any) { - serializedRelay.call(event) - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/ConnectionSettingsChanged.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/ConnectionSettingsChanged.kt deleted file mode 100644 index 4bf916223..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/ConnectionSettingsChanged.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class ConnectionSettingsChanged private constructor( - val defaultId: Long, -) { - companion object { - fun newInstance(defaultId: Long): ConnectionSettingsChanged = ConnectionSettingsChanged(defaultId) - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/ConnectionStatusChangeEvent.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/ConnectionStatusChangeEvent.kt deleted file mode 100644 index 98ee6b47a..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/ConnectionStatusChangeEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.kelsos.mbrc.annotations.Connection.Status - -class ConnectionStatusChangeEvent private constructor( - @Status val status: Int, -) { - companion object { - fun create( - @Status status: Int, - ): ConnectionStatusChangeEvent = ConnectionStatusChangeEvent(status) - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/CoverChangedEvent.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/CoverChangedEvent.kt deleted file mode 100644 index 0132c08db..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/CoverChangedEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class CoverChangedEvent( - val path: String = "", -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/DiscoveryStopped.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/DiscoveryStopped.kt deleted file mode 100644 index cb87decd1..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/DiscoveryStopped.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.kelsos.mbrc.networking.discovery.DiscoveryStop - -class DiscoveryStopped( - val reason: DiscoveryStop, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/LfmRatingChanged.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/LfmRatingChanged.kt deleted file mode 100644 index 0e32944c0..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/LfmRatingChanged.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.kelsos.mbrc.features.player.LfmStatus - -class LfmRatingChanged( - val status: LfmStatus, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/LibraryRefreshCompleteEvent.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/LibraryRefreshCompleteEvent.kt deleted file mode 100644 index b5bb6cf6e..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/LibraryRefreshCompleteEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class LibraryRefreshCompleteEvent diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/LyricsUpdatedEvent.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/LyricsUpdatedEvent.kt deleted file mode 100644 index aab9af1ff..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/LyricsUpdatedEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class LyricsUpdatedEvent( - val lyrics: String, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/NotifyUser.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/NotifyUser.kt deleted file mode 100644 index fe5514461..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/NotifyUser.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class NotifyUser { - val message: String - val resId: Int - var isFromResource: Boolean = false - private set - - constructor(message: String) { - this.message = message - this.isFromResource = false - this.resId = -1 - } - - constructor(resId: Int) { - this.resId = resId - this.isFromResource = true - this.message = "" - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/PlayStateChange.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/PlayStateChange.kt deleted file mode 100644 index fe4feb90c..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/PlayStateChange.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.kelsos.mbrc.annotations.PlayerState.State - -data class PlayStateChange( - @State val state: String, - val position: Long, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/RatingChanged.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/RatingChanged.kt deleted file mode 100644 index a8726cefe..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/RatingChanged.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class RatingChanged( - val rating: Float, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/RemoteClientMetaData.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/RemoteClientMetaData.kt deleted file mode 100644 index 53e4bef56..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/RemoteClientMetaData.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.kelsos.mbrc.features.player.TrackInfo - -data class RemoteClientMetaData( - val trackInfo: TrackInfo, - val coverPath: String = "", - val duration: Long, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/RepeatChange.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/RepeatChange.kt deleted file mode 100644 index b1d5857de..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/RepeatChange.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.kelsos.mbrc.annotations.Repeat.Mode - -class RepeatChange( - @Mode val mode: String, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/RequestConnectionStateEvent.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/RequestConnectionStateEvent.kt deleted file mode 100644 index 0566fa5aa..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/RequestConnectionStateEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class RequestConnectionStateEvent diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/ScrobbleChange.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/ScrobbleChange.kt deleted file mode 100644 index 9573ac8ca..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/ScrobbleChange.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class ScrobbleChange( - val isActive: Boolean, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/ShuffleChange.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/ShuffleChange.kt deleted file mode 100644 index 71473edd4..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/ShuffleChange.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import androidx.annotation.StringDef - -class ShuffleChange( - @ShuffleState val shuffleState: String, -) { - @StringDef(OFF, AUTODJ, SHUFFLE) - @Retention(AnnotationRetention.SOURCE) - annotation class ShuffleState - - companion object { - const val OFF = "off" - const val AUTODJ = "autodj" - const val SHUFFLE = "shuffle" - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/TrackInfoChangeEvent.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/TrackInfoChangeEvent.kt deleted file mode 100644 index bf489824e..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/TrackInfoChangeEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.kelsos.mbrc.features.player.TrackInfo - -data class TrackInfoChangeEvent( - val trackInfo: TrackInfo, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/TrackMoved.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/TrackMoved.kt deleted file mode 100644 index 1a22b1f6f..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/TrackMoved.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.fasterxml.jackson.databind.node.ObjectNode - -class TrackMoved( - node: ObjectNode, -) { - val isSuccess: Boolean = node.path("success").asBoolean() - val from: Int = node.path("from").asInt() - val to: Int = node.path("to").asInt() -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/TrackRemoval.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/TrackRemoval.kt deleted file mode 100644 index 0bbbc60ca..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/TrackRemoval.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.kelsos.mbrc.events.ui - -import com.fasterxml.jackson.databind.node.ObjectNode - -class TrackRemoval( - node: ObjectNode, -) { - val index: Int = node.path("index").asInt() - val isSuccess: Boolean = node.path("success").asBoolean() -} diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/UpdateDuration.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/UpdateDuration.kt deleted file mode 100644 index 0527cbf5f..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/UpdateDuration.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class UpdateDuration( - val position: Int, - val duration: Int, -) diff --git a/app/src/main/java/com/kelsos/mbrc/events/ui/VolumeChange.kt b/app/src/main/java/com/kelsos/mbrc/events/ui/VolumeChange.kt deleted file mode 100644 index 18332e884..000000000 --- a/app/src/main/java/com/kelsos/mbrc/events/ui/VolumeChange.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.kelsos.mbrc.events.ui - -class VolumeChange { - var volume: Int = 0 - private set - var isMute: Boolean = false - private set - - constructor(vol: Int) { - this.volume = vol - this.isMute = false - } - - constructor() { - this.volume = 0 - this.isMute = true - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/extensions/EnableHome.kt b/app/src/main/java/com/kelsos/mbrc/extensions/EnableHome.kt deleted file mode 100644 index 6d987535a..000000000 --- a/app/src/main/java/com/kelsos/mbrc/extensions/EnableHome.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.kelsos.mbrc.extensions - -import androidx.appcompat.app.ActionBar - -fun ActionBar.enableHome(title: String?) { - this.setDisplayHomeAsUpEnabled(true) - this.setDisplayShowHomeEnabled(true) - this.title = title ?: "" -} diff --git a/app/src/main/java/com/kelsos/mbrc/extensions/FileExtensions.kt b/app/src/main/java/com/kelsos/mbrc/extensions/FileExtensions.kt deleted file mode 100644 index f22feca81..000000000 --- a/app/src/main/java/com/kelsos/mbrc/extensions/FileExtensions.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.kelsos.mbrc.extensions - -import java.io.File -import java.io.FileInputStream -import java.security.MessageDigest - -fun File.md5(): String? { - try { - val fin = FileInputStream(this) - val md5er = MessageDigest.getInstance("MD5") - val buffer = ByteArray(1024) - var read: Int - do { - read = fin.read(buffer) - if (read > 0) { - md5er.update(buffer, 0, read) - } - } while (read != -1) - fin.close() - val digest = md5er.digest() ?: return null - var str = "" - digest.indices.forEach { - str += Integer.toString((digest[it].toInt() and 0xff) + 0x100, 16).substring(1) - } - return str - } catch (e: Exception) { - return null - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/extensions/StringExtensions.kt b/app/src/main/java/com/kelsos/mbrc/extensions/StringExtensions.kt deleted file mode 100644 index b42f9bec3..000000000 --- a/app/src/main/java/com/kelsos/mbrc/extensions/StringExtensions.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.kelsos.mbrc.extensions - -fun String.escapeLike(): String = this.replace("%", "_") diff --git a/app/src/main/java/com/kelsos/mbrc/features/dragsort/OnStartDragListener.kt b/app/src/main/java/com/kelsos/mbrc/features/dragsort/OnStartDragListener.kt index cae15f921..d70123739 100644 --- a/app/src/main/java/com/kelsos/mbrc/features/dragsort/OnStartDragListener.kt +++ b/app/src/main/java/com/kelsos/mbrc/features/dragsort/OnStartDragListener.kt @@ -4,4 +4,6 @@ import androidx.recyclerview.widget.RecyclerView interface OnStartDragListener { fun onStartDrag(viewHolder: RecyclerView.ViewHolder) + + fun onDragComplete() } diff --git a/app/src/main/java/com/kelsos/mbrc/features/dragsort/SimpleItemTouchHelper.kt b/app/src/main/java/com/kelsos/mbrc/features/dragsort/SimpleItemTouchHelper.kt index 4fac4bc3a..505543ebb 100644 --- a/app/src/main/java/com/kelsos/mbrc/features/dragsort/SimpleItemTouchHelper.kt +++ b/app/src/main/java/com/kelsos/mbrc/features/dragsort/SimpleItemTouchHelper.kt @@ -64,10 +64,8 @@ class SimpleItemTouchHelper( viewHolder: RecyclerView.ViewHolder?, actionState: Int, ) { - if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { - if (viewHolder is TouchHelperViewHolder) { - viewHolder.onItemSelected() - } + if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is TouchHelperViewHolder) { + viewHolder.onItemSelected() } super.onSelectedChanged(viewHolder, actionState) diff --git a/app/src/main/java/com/kelsos/mbrc/features/help/FeedbackFragment.kt b/app/src/main/java/com/kelsos/mbrc/features/help/FeedbackFragment.kt index 09f92fc9e..b8e52e339 100644 --- a/app/src/main/java/com/kelsos/mbrc/features/help/FeedbackFragment.kt +++ b/app/src/main/java/com/kelsos/mbrc/features/help/FeedbackFragment.kt @@ -12,12 +12,14 @@ import android.widget.CheckBox import android.widget.EditText import androidx.core.content.FileProvider import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.kelsos.mbrc.BuildConfig.APPLICATION_ID import com.kelsos.mbrc.R -import com.kelsos.mbrc.common.utilities.RemoteUtils.getVersion +import com.kelsos.mbrc.common.utilities.RemoteUtils.VERSION import com.kelsos.mbrc.logging.LogHelper -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File class FeedbackFragment : Fragment() { @@ -26,6 +28,8 @@ class FeedbackFragment : Fragment() { private lateinit var logInfo: CheckBox private lateinit var feedbackButton: Button + val logHelper = LogHelper() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -39,14 +43,9 @@ class FeedbackFragment : Fragment() { feedbackButton.setOnClickListener { onFeedbackButtonClicked() } - LogHelper - .logsExist(requireContext()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - logInfo.isEnabled = true - }) { - } + lifecycleScope.launch { + logInfo.isEnabled = logHelper.logsExist(filesDir = requireContext().filesDir) + } return view } @@ -61,7 +60,7 @@ class FeedbackFragment : Fragment() { if (deviceInfo.isChecked) { val device = Build.DEVICE val manufacturer = Build.MANUFACTURER - val appVersion = requireContext().getVersion() + val appVersion = VERSION val androidVersion = Build.VERSION.RELEASE feedbackText += @@ -79,15 +78,20 @@ class FeedbackFragment : Fragment() { return } - LogHelper - .zipLogs(requireContext()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - openChooser(feedbackText, it) - }) { - openChooser(feedbackText) + lifecycleScope.launch(Dispatchers.IO) { + val result = + runCatching { + logHelper.zipLogs(requireContext().filesDir, requireContext().externalCacheDir) + } + + withContext(Dispatchers.Main) { + if (result.isSuccess) { + openChooser(feedbackText, result.getOrNull()) + } else { + openChooser(feedbackText) + } } + } } private fun openChooser( diff --git a/app/src/main/java/com/kelsos/mbrc/features/help/HelpFragment.kt b/app/src/main/java/com/kelsos/mbrc/features/help/HelpFragment.kt index ef62b5a08..8e96f0ca9 100644 --- a/app/src/main/java/com/kelsos/mbrc/features/help/HelpFragment.kt +++ b/app/src/main/java/com/kelsos/mbrc/features/help/HelpFragment.kt @@ -1,6 +1,5 @@ package com.kelsos.mbrc.features.help -import android.content.pm.PackageManager import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -10,23 +9,14 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.fragment.app.Fragment import com.kelsos.mbrc.R -import com.kelsos.mbrc.common.utilities.RemoteUtils.getVersion -import timber.log.Timber +import com.kelsos.mbrc.common.utilities.RemoteUtils class HelpFragment : Fragment() { private lateinit var helpView: WebView override fun onStart() { super.onStart() - val url: String = - try { - String.format("https://mbrc.kelsos.net/help?version=%s", requireContext().getVersion()) - } catch (e: PackageManager.NameNotFoundException) { - Timber.v(e, "Failed to get version") - "https://mbrc.kelsos.net/help" - } - - helpView.loadUrl(url) + helpView.loadUrl("https://mbrc.kelsos.net/help?version=${RemoteUtils.VERSION}") } override fun onCreateView( diff --git a/app/src/main/java/com/kelsos/mbrc/features/library/Album.kt b/app/src/main/java/com/kelsos/mbrc/features/library/Album.kt deleted file mode 100644 index 70d4fe246..000000000 --- a/app/src/main/java/com/kelsos/mbrc/features/library/Album.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.kelsos.mbrc.features.library - -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonProperty -import com.kelsos.mbrc.common.utilities.RemoteUtils.sha1 -import com.kelsos.mbrc.data.Data -import com.kelsos.mbrc.data.Database -import com.raizlabs.android.dbflow.annotation.Column -import com.raizlabs.android.dbflow.annotation.PrimaryKey -import com.raizlabs.android.dbflow.annotation.Table - -@Table(name = "album", database = Database::class) -data class Album( - @JsonProperty("artist") - @Column - var artist: String? = null, - @JsonProperty("album") - @Column - var album: String? = null, - @JsonProperty("count") - @Column - var count: Int = 0, - @JsonIgnore - @Column - var cover: String? = null, - @JsonIgnore - @Column(name = "date_added") - var dateAdded: Long = 0, - @JsonIgnore - @Column - @PrimaryKey(autoincrement = true) - var id: Long = 0, -) : Data - -fun Album.key(): String = sha1("${artist}_$album") diff --git a/app/src/main/java/com/kelsos/mbrc/features/library/AlbumEntryAdapter.kt b/app/src/main/java/com/kelsos/mbrc/features/library/AlbumEntryAdapter.kt deleted file mode 100644 index 9aeb66b13..000000000 --- a/app/src/main/java/com/kelsos/mbrc/features/library/AlbumEntryAdapter.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.kelsos.mbrc.features.library - -import android.app.Activity -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import androidx.appcompat.widget.PopupMenu -import androidx.core.view.isGone -import androidx.recyclerview.widget.RecyclerView -import coil3.load -import coil3.request.crossfade -import coil3.request.error -import coil3.request.placeholder -import coil3.size.Scale -import com.kelsos.mbrc.R -import com.kelsos.mbrc.common.ui.SquareImageView -import com.raizlabs.android.dbflow.list.FlowCursorList -import java.io.File - -class AlbumEntryAdapter( - context: Activity, -) : RecyclerView.Adapter() { - private val inflater: LayoutInflater = LayoutInflater.from(context) - private var data: FlowCursorList? = null - private var listener: MenuItemSelectedListener? = null - private val cache = File(context.cacheDir, "covers") - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int, - ): ViewHolder { - val view = inflater.inflate(R.layout.ui_list_dual, parent, false) - val holder = ViewHolder(view) - holder.indicator.setOnClickListener { - val popupMenu = PopupMenu(it.context, it) - popupMenu.inflate(R.menu.popup_album) - popupMenu.setOnMenuItemClickListener { menuItem -> - val data = this.data ?: return@setOnMenuItemClickListener false - val position = holder.bindingAdapterPosition.toLong() - val album = data.getItem(position) ?: return@setOnMenuItemClickListener false - listener?.onMenuItemSelected(menuItem, album) - true - } - popupMenu.show() - } - - holder.itemView.setOnClickListener { - val data = this.data ?: return@setOnClickListener - val position = holder.bindingAdapterPosition.toLong() - val album = data.getItem(position) ?: return@setOnClickListener - listener?.onItemClicked(album) - } - return holder - } - - override fun onBindViewHolder( - holder: ViewHolder, - position: Int, - ) { - val data = this.data ?: return - val item = data.getItem(position.toLong()) ?: return - val (artist, album) = item - holder.album.text = if (album.isNullOrBlank()) holder.emptyAlbum else album - holder.artist.text = if (artist.isNullOrBlank()) holder.unknownArtist else artist - - holder.image.load(File(cache, item.key())) { - crossfade(false) - placeholder(R.drawable.ic_image_no_cover) - error(R.drawable.ic_image_no_cover) - size( - holder.itemView.context.resources - .getDimensionPixelSize(R.dimen.list_album_size), - ) - scale(Scale.FILL) - } - } - - fun refresh() { - data?.refresh() - notifyDataSetChanged() - } - - override fun getItemCount(): Int = data?.count?.toInt() ?: 0 - - fun setMenuItemSelectedListener(listener: MenuItemSelectedListener) { - this.listener = listener - } - - interface MenuItemSelectedListener { - fun onMenuItemSelected( - menuItem: MenuItem, - album: Album, - ) - - fun onItemClicked(album: Album) - } - - class ViewHolder( - itemView: View, - ) : RecyclerView.ViewHolder(itemView) { - val artist: TextView = itemView.findViewById(R.id.line_two) - val album: TextView = itemView.findViewById(R.id.line_one) - val image: SquareImageView = itemView.findViewById(R.id.cover) - val indicator: LinearLayout = itemView.findViewById(R.id.ui_item_context_indicator) - - val unknownArtist: String = itemView.context.getString(R.string.unknown_artist) - val emptyAlbum: String = itemView.context.getString(R.string.non_album_tracks) - - init { - image.isGone = false - } - } - - fun update(albums: FlowCursorList) { - data = albums - notifyDataSetChanged() - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/features/library/AlbumRepository.kt b/app/src/main/java/com/kelsos/mbrc/features/library/AlbumRepository.kt deleted file mode 100644 index c52fcda0b..000000000 --- a/app/src/main/java/com/kelsos/mbrc/features/library/AlbumRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.kelsos.mbrc.features.library - -import com.kelsos.mbrc.common.data.Repository -import com.raizlabs.android.dbflow.list.FlowCursorList - -interface AlbumRepository : Repository { - suspend fun getAlbumsByArtist(artist: String): FlowCursorList - - suspend fun updateCovers(updated: List) -} diff --git a/app/src/main/java/com/kelsos/mbrc/features/library/AlbumRepositoryImpl.kt b/app/src/main/java/com/kelsos/mbrc/features/library/AlbumRepositoryImpl.kt deleted file mode 100644 index d742bac60..000000000 --- a/app/src/main/java/com/kelsos/mbrc/features/library/AlbumRepositoryImpl.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.kelsos.mbrc.features.library - -import com.kelsos.mbrc.common.utilities.AppCoroutineDispatchers -import com.raizlabs.android.dbflow.list.FlowCursorList -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.withContext -import java.time.Instant - -class AlbumRepositoryImpl( - private val localDataSource: LocalAlbumDataSource, - private val remoteDataSource: RemoteAlbumDataSource, - private val dispatchers: AppCoroutineDispatchers, -) : AlbumRepository { - override suspend fun getAlbumsByArtist(artist: String): FlowCursorList = localDataSource.getAlbumsByArtist(artist) - - override suspend fun getAllCursor(): FlowCursorList = localDataSource.loadAllCursor() - - override suspend fun getAndSaveRemote(): FlowCursorList { - getRemote() - return localDataSource.loadAllCursor() - } - - override suspend fun getRemote() { - val epoch = Instant.now().epochSecond - val default = CachedAlbumInfo(0, null) - val cached = - localDataSource.loadAllCursor().associate { - it.album + it.artist to CachedAlbumInfo(it.id, it?.cover) - } - withContext(dispatchers.io) { - remoteDataSource - .fetch() - .onCompletion { - localDataSource.removePreviousEntries(epoch) - }.collect { albums -> - val list = - albums.map { - it.apply { - dateAdded = epoch - val key = it.album + it.artist - - if (cached.containsKey(key)) { - val cachedAlbum = cached.getOrDefault(key, default) - id = cachedAlbum.id - cover = cachedAlbum.cover - } - } - } - localDataSource.saveAll(list) - } - } - } - - override suspend fun search(term: String): FlowCursorList = localDataSource.search(term) - - override suspend fun cacheIsEmpty(): Boolean = localDataSource.isEmpty() - - override suspend fun count(): Long = localDataSource.count() - - override suspend fun updateCovers(updated: List) { - localDataSource.updateCovers(updated) - } -} diff --git a/app/src/main/java/com/kelsos/mbrc/features/library/AlbumTracksActivity.kt b/app/src/main/java/com/kelsos/mbrc/features/library/AlbumTracksActivity.kt deleted file mode 100644 index 694ba474f..000000000 --- a/app/src/main/java/com/kelsos/mbrc/features/library/AlbumTracksActivity.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.kelsos.mbrc.features.library - -import android.os.Bundle -import android.view.MenuItem -import android.widget.Button -import android.widget.ImageView -import android.widget.TextView -import androidx.activity.OnBackPressedCallback -import androidx.core.os.BundleCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.LinearLayoutManager -import coil3.load -import coil3.request.crossfade -import coil3.request.error -import coil3.request.placeholder -import coil3.size.Scale -import com.google.android.material.snackbar.Snackbar -import com.kelsos.mbrc.R -import com.kelsos.mbrc.common.ui.EmptyRecyclerView -import com.kelsos.mbrc.common.utilities.RemoteUtils.sha1 -import com.kelsos.mbrc.features.queue.PopupActionHandler -import com.raizlabs.android.dbflow.list.FlowCursorList -import org.koin.android.ext.android.inject -import org.koin.androidx.scope.ScopeActivity -import java.io.File - -class AlbumTracksActivity : - ScopeActivity(), - AlbumTracksView, - TrackEntryAdapter.MenuItemSelectedListener { - private val adapter: TrackEntryAdapter by inject() - private val actionHandler: PopupActionHandler by inject() - private val presenter: AlbumTracksPresenter by inject() - - private var album: AlbumInfo? = null - private lateinit var recyclerView: EmptyRecyclerView - - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_album_tracks) - onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finish() - } - }, - ) - - val extras = intent.extras - - if (extras != null) { - album = BundleCompat.getParcelable(extras, ALBUM, AlbumInfo::class.java) - } - - val selectedAlbum = album - if (selectedAlbum == null) { - finish() - return - } - - setSupportActionBar(findViewById(R.id.toolbar)) - val supportActionBar = supportActionBar ?: error("Actionbar should not be null") - supportActionBar.setDisplayHomeAsUpEnabled(true) - supportActionBar.setDisplayShowHomeEnabled(true) - - if (selectedAlbum.album.isEmpty()) { - supportActionBar.setTitle(R.string.non_album_tracks) - } else { - supportActionBar.title = selectedAlbum.album - } - - findViewById(R.id.album_tracks__album).text = selectedAlbum.album - findViewById(R.id.album_tracks__artist).text = selectedAlbum.artist - loadCover(selectedAlbum.artist, selectedAlbum.album) - - presenter.attach(this) - presenter.load(selectedAlbum) - adapter.setMenuItemSelectedListener(this) - recyclerView = findViewById(R.id.list_tracks) - recyclerView.layoutManager = LinearLayoutManager(baseContext) - recyclerView.adapter = adapter - recyclerView.emptyView = findViewById(R.id.empty_view) - val play = findViewById