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 4, 2024
1 parent 1fe824f commit e4dd394
Show file tree
Hide file tree
Showing 14 changed files with 263 additions and 16 deletions.
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.DownloadRequestManager
import io.newm.feature.musicplayer.service.DownloadRequestManagerImpl
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<DownloadRequestManager> { DownloadRequestManagerImpl(androidContext()) }
}

val androidModules = module {
Expand Down
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.DownloadRequestManager
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 downloadRequestManager: DownloadRequestManager,
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 -> {
downloadRequestManager.download(
event.tackId,
playList.tracks.first { it.id == event.tackId }.url
)
}

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 @@ -162,7 +162,8 @@ fun NFTLibraryScreenUi(
refreshing = state.refreshing,
eventLogger = eventLogger,
onApplyFilters = { filters -> eventSink(OnApplyFilters(filters)) },
currentTrackId = state.currentTrackId
currentTrackId = state.currentTrackId,
downloadsEnabled = state.downloadsEnabled,
)
}
}
Expand All @@ -185,7 +186,8 @@ private fun NFTTracks(
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 @@ -241,7 +243,8 @@ private fun NFTTracks(
track = track,
onPlaySong = onPlaySong,
onDownloadSong = { onDownloadSong(track.id) },
isSelected = track.id == currentTrackId
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 DownloadRequestManager {
fun download(id: String, url: String)
}

class DownloadRequestManagerImpl(
private val context: Context
) : DownloadRequestManager {
@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 e4dd394

Please sign in to comment.