Skip to content

Commit

Permalink
Enables downloading tracks behind a feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
newmskywalker committed Dec 16, 2024
1 parent 1fe824f commit 83f3f3c
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 21 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Don't forget to give the project a star! ⭐️
<tr>
<td align="center"><a href="https://github.com/martyu"><img src="https://avatars.githubusercontent.com/u/595982?v=4" width="100px;" alt=""/><br /><sub><b>Marty Ulrich</b></sub></a><br /> </td>
<td align="center"><a href="https://github.com/cristhianescobar"><img src="https://avatars.githubusercontent.com/u/5350018?v=4" width="100px;" alt=""/><br /><sub><b>Cristhian Escobar</b></sub></a><br /> </td>
<td align="center"><a href="https://github.com/j-mateo"><img src="https://avatars.githubusercontent.com/u/3849278?v=4" width="100px;" alt=""/><br /><sub><b>Jose Mateo</b></sub></a><br /> </td>
<td align="center"><a href="https://github.com/j-mateo"><img src="https://avatars.githubusercontent.com/u/3849278?v=4" width="100px;" alt=""/><br /><sub><b>Skywalker</b></sub></a><br /> </td>
<td align="center"><a href="https://github.com/Jermainelr"><img src="https://avatars.githubusercontent.com/u/29381704?v=4" width="100px;" alt=""/><br /><sub><b>Jermaine Lara</b></sub></a><br /> </td>
<td align="center"><a href="https://github.com/wlara"><img src="https://avatars.githubusercontent.com/u/13438984?v=4" width="100px;" alt=""/><br /><sub><b>Walter Lara</b></sub></a><br /> </td>

Expand Down
2 changes: 2 additions & 0 deletions android/app-newm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ dependencies {
implementation(libs.firebase.analytics.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.material)
implementation(libs.androidx.media3.datasource)
implementation(libs.androidx.media3.database)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.launchdarkly.client)
implementation(libs.play.services.auth)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package io.newm.di.android

import android.annotation.SuppressLint
import androidx.activity.result.contract.ActivityResultContracts
import androidx.media3.database.DatabaseProvider
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.NoOpCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.Scopes
Expand All @@ -14,6 +20,8 @@ import io.newm.feature.login.screen.createaccount.CreateAccountScreenPresenter
import io.newm.feature.login.screen.login.LoginScreenPresenter
import io.newm.feature.login.screen.resetpassword.ResetPasswordScreenPresenter
import io.newm.feature.login.screen.welcome.WelcomeScreenPresenter
import io.newm.feature.musicplayer.service.DownloadManager
import io.newm.feature.musicplayer.service.DownloadManagerImpl
import io.newm.screens.forceupdate.ForceAppUpdatePresenter
import io.newm.screens.library.NFTLibraryPresenter
import io.newm.screens.profile.edit.ProfileEditPresenter
Expand All @@ -26,6 +34,7 @@ import io.newm.utils.ForceAppUpdateViewModel
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module

@SuppressLint("UnsafeOptInUsageError")
val viewModule = module {
single<FeatureFlagManager> { AndroidFeatureFlagManager(get(), get()) }
single { ForceAppUpdateViewModel(get(), get()) }
Expand Down Expand Up @@ -86,7 +95,9 @@ val viewModule = module {
get(),
get(),
get(),
get()
get(),
get(),
get(),
)
}
factory { params ->
Expand All @@ -111,6 +122,12 @@ val viewModule = module {
params.get(),
)
}
single<DatabaseProvider> { StandaloneDatabaseProvider(androidContext()) }
single<Cache> {
val downloadDirectory = androidContext().getExternalFilesDir(null)!!
SimpleCache(downloadDirectory, NoOpCacheEvictor(), get())
}
single<DownloadManager> { DownloadManagerImpl(androidContext()) }
}

val androidModules = module {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ sealed interface NFTLibraryEvent : CircuitUiEvent {

data class OnQueryChange(val newQuery: String) : NFTLibraryEvent

data class OnDownloadTrack(val tackId: String) : NFTLibraryEvent
data class OnDownloadTrack(val track: NFTTrack) : NFTLibraryEvent

data class OnApplyFilters(val filters: NFTLibraryFilters) : NFTLibraryEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import io.newm.feature.musicplayer.models.PlaybackState
import io.newm.feature.musicplayer.models.Playlist
import io.newm.feature.musicplayer.models.Track
import io.newm.feature.musicplayer.rememberMediaPlayer
import io.newm.feature.musicplayer.service.DownloadManager
import io.newm.feature.musicplayer.service.MusicPlayer
import io.newm.shared.public.analytics.NewmAppEventLogger
import io.newm.shared.public.analytics.events.AppScreens
import io.newm.shared.public.featureflags.FeatureFlagManager
import io.newm.shared.public.featureflags.FeatureFlags
import io.newm.shared.public.models.NFTTrack
import io.newm.shared.public.usecases.ConnectWalletUseCase
import io.newm.shared.public.usecases.HasWalletConnectionsUseCase
Expand All @@ -35,12 +38,17 @@ class NFTLibraryPresenter(
private val syncWalletConnectionsUseCase: SyncWalletConnectionsUseCase,
private val walletNFTTracksUseCase: WalletNFTTracksUseCase,
private val scope: CoroutineScope,
private val eventLogger: NewmAppEventLogger
private val eventLogger: NewmAppEventLogger,
private val downloadManager: DownloadManager,
private val featureFlagManager: FeatureFlagManager,
) : Presenter<NFTLibraryState> {
@Composable
override fun present(): NFTLibraryState {
val musicPlayer: MusicPlayer? = rememberMediaPlayer(eventLogger)

val downloadsEnabled =
remember { featureFlagManager.isEnabled(FeatureFlags.DownloadTracks) }

LaunchedEffect(Unit) {
syncWalletConnectionsUseCase.syncWalletConnectionsFromNetworkToDevice()
}
Expand Down Expand Up @@ -144,7 +152,13 @@ class NFTLibraryPresenter(
refreshing = refreshing,
eventSink = { event ->
when (event) {
is NFTLibraryEvent.OnDownloadTrack -> TODO("Not implemented yet")
is NFTLibraryEvent.OnDownloadTrack -> {
downloadManager.download(
id = event.track.id,
url = event.track.audioUrl
)
}

is NFTLibraryEvent.OnQueryChange -> {
eventLogger.logEvent(
AppScreens.NFTLibraryScreen.SEARCH_BUTTON,
Expand Down Expand Up @@ -176,7 +190,8 @@ class NFTLibraryPresenter(
}
}
},
currentTrackId = currentTrackId
currentTrackId = currentTrackId,
downloadsEnabled = downloadsEnabled,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,13 @@ fun NFTLibraryScreenUi(
filters = state.filters,
onQueryChange = { query -> eventSink(OnQueryChange(query)) },
onPlaySong = { track -> eventSink(PlaySong(track)) },
onDownloadSong = { trackId -> eventSink(OnDownloadTrack(trackId)) },
onDownloadSong = { track -> eventSink(OnDownloadTrack(track)) },
refresh = { eventSink(NFTLibraryEvent.OnRefresh) },
refreshing = state.refreshing,
eventLogger = eventLogger,
onApplyFilters = { filters -> eventSink(OnApplyFilters(filters)) },
currentTrackId = state.currentTrackId
currentTrackId = state.currentTrackId,
downloadsEnabled = state.downloadsEnabled,
)
}
}
Expand All @@ -180,12 +181,13 @@ private fun NFTTracks(
filters: NFTLibraryFilters,
onQueryChange: (String) -> Unit,
onPlaySong: (NFTTrack) -> Unit,
onDownloadSong: (String) -> Unit,
onDownloadSong: (NFTTrack) -> Unit,
refresh: () -> Unit,
refreshing: Boolean,
eventLogger: NewmAppEventLogger,
onApplyFilters: (NFTLibraryFilters) -> Unit,
currentTrackId: String?
currentTrackId: String?,
downloadsEnabled: Boolean,
) {
val scope = rememberCoroutineScope()
val filterSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
Expand Down Expand Up @@ -240,8 +242,9 @@ private fun NFTTracks(
TrackRowItemWrapper(
track = track,
onPlaySong = onPlaySong,
onDownloadSong = { onDownloadSong(track.id) },
isSelected = track.id == currentTrackId
onDownloadSong = { onDownloadSong(track) },
isSelected = track.id == currentTrackId,
downloadsEnabled = downloadsEnabled,
)
}
}
Expand All @@ -259,16 +262,14 @@ private fun NFTTracks(
}
}

// TODO: Remove this flag once the download functionality is implemented
private const val DOWNLOAD_UI_ENABLED = false

@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun TrackRowItemWrapper(
track: NFTTrack,
onPlaySong: (NFTTrack) -> Unit,
onDownloadSong: () -> Unit,
isSelected: Boolean
isSelected: Boolean,
downloadsEnabled: Boolean,
) {
val swipeableState = rememberSwipeableState(initialValue = false)
val deltaX = with(LocalDensity.current) { 82.dp.toPx() }
Expand All @@ -288,13 +289,13 @@ private fun TrackRowItemWrapper(
),
)
) {
if (!track.isDownloaded && DOWNLOAD_UI_ENABLED) {
if (!track.isDownloaded && downloadsEnabled) {
RevealedPanel(onDownloadSong)
}
TrackRowItem(
track = track,
onClick = onPlaySong,
modifier = if (DOWNLOAD_UI_ENABLED) Modifier.offset {
modifier = if (downloadsEnabled) Modifier.offset {
IntOffset(
x = -swipeableState.offset.value.roundToInt(),
y = 0
Expand Down Expand Up @@ -379,7 +380,8 @@ fun PreviewNftLibrary() {
),
refreshing = false,
eventSink = {},
currentTrackId = null
currentTrackId = null,
downloadsEnabled = true,
),
eventLogger = NewmAppEventLogger()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ sealed interface NFTLibraryState : CircuitUiState {
val filters: NFTLibraryFilters,
val refreshing: Boolean,
val eventSink: (NFTLibraryEvent) -> Unit,
val currentTrackId: String?
val currentTrackId: String?,
val downloadsEnabled: Boolean
) : NFTLibraryState

data class Error(val message: String) : NFTLibraryState
Expand Down
1 change: 1 addition & 0 deletions android/features/music-player/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.session)
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.workmanager)
implementation(libs.androidx.palette.ktx)
implementation(libs.koin.android)
implementation(libs.play.services.auth)
Expand Down
11 changes: 11 additions & 0 deletions android/features/music-player/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<application>

Expand All @@ -13,6 +15,15 @@
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
<service android:name=".service.NewmDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<!-- This is needed for Scheduler -->
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.newm.feature.musicplayer.service

import android.content.Context
import android.net.Uri
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadRequest
import androidx.media3.exoplayer.offline.DownloadService

interface DownloadManager {
fun download(id: String, url: String)
}

class DownloadManagerImpl(
private val context: Context
) : DownloadManager {
@UnstableApi
override fun download(id: String, url: String) {
val uri = Uri.parse(url)
val downloadRequest = DownloadRequest.Builder(id, uri).build()

DownloadService.sendAddDownload(
context,
NewmDownloadService::class.java,
downloadRequest,
true // isForeground
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@ import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import org.koin.android.ext.android.inject

@UnstableApi
class MediaService : MediaSessionService() {
private lateinit var mediaSession: MediaSession
private lateinit var audioManager: AudioManager
Expand All @@ -20,6 +29,8 @@ class MediaService : MediaSessionService() {
private var playBackAuthorized = false
private var resumeOnFocusGain = false

private val downloadCache: Cache by inject()

companion object {
private const val DUCKING_VOLUME = 0.2f
private const val NORMAL_VOLUME = 1.0f
Expand Down Expand Up @@ -75,10 +86,14 @@ class MediaService : MediaSessionService() {
.build()
}

@OptIn(UnstableApi::class)
override fun onCreate() {
super.onCreate()
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
player = ExoPlayer.Builder(this).build()

player = ExoPlayer.Builder(this)
.setMediaSourceFactory(buildMediaSourceFactory())
.build()

mediaSession = MediaSession.Builder(this, player)
.setSessionActivity()
Expand All @@ -87,6 +102,18 @@ class MediaService : MediaSessionService() {

}

private fun buildMediaSourceFactory(): DefaultMediaSourceFactory {
val httpDataSourceFactory = DefaultHttpDataSource.Factory()

val cacheDataSourceFactory: DataSource.Factory =
CacheDataSource.Factory()
.setCache(downloadCache)
.setUpstreamDataSourceFactory(httpDataSourceFactory)
.setCacheWriteDataSinkFactory(null) // Disable writing.

return DefaultMediaSourceFactory(this).setDataSourceFactory(cacheDataSourceFactory)
}

private fun MediaSession.Builder.setSessionActivity(): MediaSession.Builder {
val launchIntentForPackage = packageManager.getLaunchIntentForPackage(packageName)

Expand Down
Loading

0 comments on commit 83f3f3c

Please sign in to comment.