Skip to content

Commit

Permalink
Add support for extensions working with flows
Browse files Browse the repository at this point in the history
  • Loading branch information
brahmkshatriya committed Feb 20, 2024
1 parent b5fc7bb commit 549eac7
Show file tree
Hide file tree
Showing 33 changed files with 439 additions and 175 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ android {

dependencies {

implementation("com.github.brahmkshatriya:echo-common:0.0.1")
implementation("com.github.JeelPatel231:plugger:1.0.1")
implementation("com.github.brahmkshatriya:echo-common:1.0.2")
implementation("com.github.brahmkshatriya:plugger:1.0.1")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.7.1")

Expand Down
46 changes: 40 additions & 6 deletions app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,46 @@ import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.media3.session.MediaBrowser
import androidx.media3.session.SessionToken
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.snackbar.Snackbar
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.AndroidEntryPoint
import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.databinding.ActivityMainBinding
import dev.brahmkshatriya.echo.player.PlaybackService
import dev.brahmkshatriya.echo.player.PlayerViewModel
import dev.brahmkshatriya.echo.player.initPlayer
import dev.brahmkshatriya.echo.ui.extension.ExtensionViewModel
import dev.brahmkshatriya.echo.ui.utils.checkPermissions
import dev.brahmkshatriya.echo.ui.utils.emit
import dev.brahmkshatriya.echo.ui.utils.updateBottomMarginWithSystemInsets
import kotlinx.coroutines.flow.MutableSharedFlow
import tel.jeelpa.plugger.PluginRepo
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {


@Inject
lateinit var pluginRepo: PluginRepo<ExtensionClient>

val binding by lazy(LazyThreadSafetyMode.NONE) {
ActivityMainBinding.inflate(layoutInflater)
}

var fromNotification: MutableSharedFlow<Boolean> = MutableSharedFlow()
private var controllerFuture: ListenableFuture<MediaBrowser>? = null

private val playerViewModel: PlayerViewModel by viewModels()
private val extensionViewModel: ExtensionViewModel by viewModels()

@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -46,16 +61,35 @@ class MainActivity : AppCompatActivity() {
navView.setupWithNavController(navHostFragment.navController)
updateBottomMarginWithSystemInsets(binding.navHostFragment)

if (extensionViewModel.extensionListFlow == null) {
extensionViewModel.extensionListFlow = pluginRepo.getAllPlugins { e ->
e.message?.let {
val snack = Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT)
if (binding.navView is BottomNavigationView)
snack.setAnchorView(binding.navView)
snack.show()
}
}
}

val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
val controllerFuture = MediaBrowser.Builder(this, sessionToken).buildAsync()
val listener = Runnable { initPlayer(this, controllerFuture.get()) }
controllerFuture.addListener(listener, ContextCompat.getMainExecutor(this))
MediaBrowser.Builder(this, sessionToken).buildAsync().also {
controllerFuture = it
val listener = Runnable { initPlayer(this, it.get()) }
it.addListener(listener, ContextCompat.getMainExecutor(this))
}

}

override fun onNewIntent(intent: Intent?) {
intent?.hasExtra("fromNotification")?.let {
emit(fromNotification) { it }
emit(playerViewModel.fromNotification) { it }
}
super.onNewIntent(intent)
}

override fun onDestroy() {
controllerFuture?.let { MediaBrowser.releaseFuture(it) }
super.onDestroy()
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package dev.brahmkshatriya.echo.data.extensions

import android.content.Context
import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.common.data.extensions.OfflineExtension
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import tel.jeelpa.plugger.PluginRepo

class LocalExtensionRepo : PluginRepo<ExtensionClient> {
override fun getAllPlugins(): Flow<List<ExtensionClient>> =
flowOf(listOf(OfflineExtension()))
class LocalExtensionRepo(val context: Context) : PluginRepo<ExtensionClient> {
override fun getAllPlugins(exceptionHandler: (Exception) -> Unit) =
flowOf(listOf(OfflineExtension(context)))
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.brahmkshatriya.echo.common.data.extensions
package dev.brahmkshatriya.echo.data.extensions

import android.content.Context
import android.net.Uri
Expand All @@ -11,6 +11,10 @@ import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.common.clients.HomeFeedClient
import dev.brahmkshatriya.echo.common.clients.SearchClient
import dev.brahmkshatriya.echo.common.clients.TrackClient
import dev.brahmkshatriya.echo.common.data.offline.LocalAlbum
import dev.brahmkshatriya.echo.common.data.offline.LocalArtist
import dev.brahmkshatriya.echo.common.data.offline.LocalStream
import dev.brahmkshatriya.echo.common.data.offline.LocalTrack
import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem
import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItemsContainer
import dev.brahmkshatriya.echo.common.models.ExtensionMetadata
Expand All @@ -19,15 +23,11 @@ import dev.brahmkshatriya.echo.common.models.QuickSearchItem
import dev.brahmkshatriya.echo.common.models.StreamableAudio
import dev.brahmkshatriya.echo.common.models.StreamableAudio.Companion.toAudio
import dev.brahmkshatriya.echo.common.models.Track
import dev.brahmkshatriya.echo.common.data.offline.LocalAlbum
import dev.brahmkshatriya.echo.common.data.offline.LocalArtist
import dev.brahmkshatriya.echo.common.data.offline.LocalStream
import dev.brahmkshatriya.echo.common.data.offline.LocalTrack
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.IOException

class OfflineExtension : ExtensionClient(), SearchClient, TrackClient, HomeFeedClient {
class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, TrackClient, HomeFeedClient {

override fun getMetadata() = ExtensionMetadata(
name = "Offline",
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/di/ApkManifestParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.brahmkshatriya.echo.di

import android.content.pm.ApplicationInfo
import tel.jeelpa.plugger.ManifestParser
import tel.jeelpa.plugger.models.PluginMetadata

class ApkManifestParser: ManifestParser<ApplicationInfo> {
override fun parseManifest(data: ApplicationInfo): PluginMetadata {

return PluginMetadata(
path = data.sourceDir,
className = data.metaData.getString("class")
?: error("Class Name not found in Metadata for ${data.packageName}"),
)
}
}

This file was deleted.

18 changes: 18 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/di/FlowModels.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.brahmkshatriya.echo.di

import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.common.clients.HomeFeedClient
import dev.brahmkshatriya.echo.common.clients.SearchClient
import dev.brahmkshatriya.echo.common.clients.TrackClient
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow


// Dagger cannot directly infer Foo<Bar>, if Bar is an interface
// That means the Flow<Clients?> cannot be directly injected,
// So, we need to wrap it in a data class and inject that instead
data class MutableExtensionFlow(val flow: MutableStateFlow<ExtensionClient?>)
data class ExtensionFlow(val flow: Flow<ExtensionClient?>)
data class SearchFlow(val flow: Flow<SearchClient?>)
data class HomeFeedFlow(val flow: Flow<HomeFeedClient?>)
data class TrackFlow(val flow: Flow<TrackClient?>)
37 changes: 18 additions & 19 deletions app/src/main/java/dev/brahmkshatriya/echo/di/PluginModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import dev.brahmkshatriya.echo.common.clients.HomeFeedClient
import dev.brahmkshatriya.echo.common.clients.SearchClient
import dev.brahmkshatriya.echo.common.clients.TrackClient
import dev.brahmkshatriya.echo.data.extensions.LocalExtensionRepo
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import tel.jeelpa.plugger.PluginRepo
import tel.jeelpa.plugger.RepoComposer
import tel.jeelpa.plugger.models.PluginConfiguration
Expand All @@ -36,40 +37,38 @@ class PluginModule {
val filePluginConfig = FilePluginConfig(application.filesDir.absolutePath, ".echo")
val apkPluginConfig = PluginConfiguration("dev.brahmkshatriya.echo")

val composer = RepoComposer(
return RepoComposer(
FileSystemPluginLoader(application, filePluginConfig, loader),
ApkPluginLoader(application, apkPluginConfig, loader),
LocalExtensionRepo()
ApkPluginLoader(application, apkPluginConfig, loader, ApkManifestParser()),
LocalExtensionRepo(application)
)

return ContextProviderForRepo(application, composer)
}

private val mutableExtensionFlow = MutableExtensionFlow(MutableStateFlow(null))
private val extensionFlow = mutableExtensionFlow.flow.asStateFlow()

@Provides
@Singleton
fun getExtensionClients(pluginLoader: PluginRepo<ExtensionClient>): List<ExtensionClient> {
val clients = runBlocking { pluginLoader.getAllPlugins().first() }
return clients
}
fun provideExtensionSharedFlow() = mutableExtensionFlow

@Provides
@Singleton
fun provideExtension(pluginLoader: PluginRepo<ExtensionClient>): ExtensionClient =
getExtensionClients(pluginLoader).first()
fun providesExtensionClient() =
ExtensionFlow(extensionFlow)

@Provides
@Singleton
fun provideSearchClient(pluginLoader: PluginRepo<ExtensionClient>): SearchClient =
provideExtension(pluginLoader) as SearchClient
fun providesSearchClient() =
SearchFlow(extensionFlow.map { it as? SearchClient })

@Provides
@Singleton
fun provideHomeClient(pluginLoader: PluginRepo<ExtensionClient>): HomeFeedClient =
provideExtension(pluginLoader) as HomeFeedClient
fun providesHomeClient() =
HomeFeedFlow(extensionFlow.map { it as? HomeFeedClient })

@Provides
@Singleton
fun provideTrackClient(pluginLoader: PluginRepo<ExtensionClient>): TrackClient =
provideExtension(pluginLoader) as TrackClient
fun providesTrackClient() =
TrackFlow(extensionFlow.map { it as? TrackClient })

}
17 changes: 12 additions & 5 deletions app/src/main/java/dev/brahmkshatriya/echo/player/InitPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ fun initPlayer(
activity: MainActivity,
player: MediaBrowser
) {
println("init player")

val playerBinding = activity.binding.bottomPlayer
val container = activity.binding.bottomPlayerContainer as View

Expand Down Expand Up @@ -89,9 +91,10 @@ fun initPlayer(

container.post {
bottomBehavior.state = PlayerBackButtonHelper.playerCollapsed.value
container.translationY = 0f
}
activity.observe(activity.fromNotification) {
bottomBehavior.state = STATE_EXPANDED
activity.observe(playerViewModel.fromNotification) {
if (it) bottomBehavior.state = STATE_EXPANDED
}

playerBinding.playerClose.setOnClickListener {
Expand Down Expand Up @@ -249,14 +252,18 @@ fun initPlayer(
}
observe(playerViewModel.seekToPrevious) {
player.seekToPrevious()
player.playWhenReady = true
}
observe(playerViewModel.seekToNext) {
player.seekToNext()
player.playWhenReady = true
}
observe(playerViewModel.audioIndexFlow) {
player.seekToDefaultPosition(it)
if (bottomBehavior.state == STATE_HIDDEN)
bottomBehavior.state = STATE_COLLAPSED
if (it >= 0) {
player.seekToDefaultPosition(it)
if (bottomBehavior.state == STATE_HIDDEN)
bottomBehavior.state = STATE_COLLAPSED
}
}
observe(playerViewModel.seekTo) {
player.seekTo(it)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.brahmkshatriya.echo.player

import android.app.Application
import android.app.PendingIntent
import android.content.Intent
import androidx.annotation.OptIn
Expand All @@ -14,17 +13,13 @@ import androidx.media3.session.MediaSession
import dagger.hilt.android.AndroidEntryPoint
import dev.brahmkshatriya.echo.MainActivity
import dev.brahmkshatriya.echo.R
import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.di.ExtensionFlow
import javax.inject.Inject

@AndroidEntryPoint
class PlaybackService : MediaLibraryService() {

@Inject
lateinit var app: Application

@Inject
lateinit var extension: ExtensionClient
lateinit var extension: ExtensionFlow

private var mediaLibrarySession: MediaLibrarySession? = null

Expand All @@ -49,9 +44,10 @@ class PlaybackService : MediaLibraryService() {
val pendingIntent = PendingIntent
.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)

mediaLibrarySession = MediaLibrarySession.Builder(this, player, PlayerSessionCallback(app, extension))
.setSessionActivity(pendingIntent)
.build()
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, PlayerSessionCallback(this, extension.flow))
.setSessionActivity(pendingIntent)
.build()

val notificationProvider = DefaultMediaNotificationProvider
.Builder(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.brahmkshatriya.echo.player
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Timeline
Expand Down Expand Up @@ -94,6 +95,7 @@ class PlayerListener(
}

fun update(mediaId: String){
println("Update ${player.duration == C.TIME_UNSET}")
viewModel.track.value = tracks[mediaId]
viewModel.totalDuration.value = player.duration.toInt()
viewModel.isPlaying.value = player.isPlaying
Expand Down
Loading

0 comments on commit 549eac7

Please sign in to comment.