Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enables downloading tracks behind a feature flag #341

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/newmskywalker"><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 @@ -104,6 +104,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
Loading