Skip to content
New issue

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

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

Already on GitHub? # to your account

Share track #6

Merged
merged 5 commits into from
Feb 10, 2023
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
10 changes: 5 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.compose.material:material:1.3.1'
implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03'
implementation 'com.google.android.material:material:1.7.0'
implementation 'com.google.android.material:material:1.8.0'

// accompanist
implementation 'com.google.accompanist:accompanist-navigation-animation:0.23.1'
implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0"
implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0"

// hilt
implementation "com.google.dagger:hilt-android:2.42"
Expand All @@ -83,9 +83,9 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'

// media3
implementation 'androidx.media3:media3-exoplayer:1.0.0-beta02'
implementation 'androidx.media3:media3-ui:1.0.0-beta02'
implementation 'androidx.media3:media3-session:1.0.0-beta02'
implementation 'androidx.media3:media3-exoplayer:1.0.0-beta03'
implementation 'androidx.media3:media3-ui:1.0.0-beta03'
implementation 'androidx.media3:media3-session:1.0.0-beta03'

// palette
implementation 'androidx.palette:palette:1.0.0'
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.rickinc.decibels.data

import android.content.Context
import android.provider.MediaStore
import com.rickinc.decibels.data.local.device.DeviceDataSource
import com.rickinc.decibels.domain.model.Track
import com.rickinc.decibels.domain.util.ContentProviderLiveData

class DeviceAudioFilesLiveData(
context: Context,
private val deviceDataSource: DeviceDataSource
) : ContentProviderLiveData<List<Track>>(context, uri) {

companion object {
private val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}

override fun getContentProviderValue(): List<Track> {
return emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.rickinc.decibels.data.local.device

import android.content.ContentUris
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Size
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import com.rickinc.decibels.domain.model.Track
import com.rickinc.decibels.domain.util.TrackConverter
import kotlinx.coroutines.*
import java.io.IOException

class DeviceDataSource(
private val context: Context,
) {

@RequiresApi(Build.VERSION_CODES.Q)
suspend fun getDeviceAudioFiles(): List<Track> {
return withContext(Dispatchers.IO) {
val list = mutableListOf<Track>()
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.ARTIST,
)
val selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0"
val sortOrder = "${MediaStore.Audio.Media.TITLE} ASC"
val query = context.contentResolver.query(
collection,
projection,
selection,
null,
sortOrder
)

query?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)

while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val duration = cursor.getInt(durationColumn)
val title = cursor.getString(titleColumn)
val artist = cursor.getString(artistColumn)
val albumId = cursor.getLong(albumIdColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id
)
val mimeType = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(context.contentResolver.getType(contentUri))

if (mimeType == TrackConverter.MP3) {
list.add(
Track(
trackId = id,
trackTitle = title,
trackLength = duration,
artist = artist,
albumId = albumId,
contentUri = contentUri,
mimeType = mimeType,
hasThumbnail = false
)
)
}
}
}
val tracksWithThumbnail = getTracksWithThumbnail(list)
tracksWithThumbnail
}
}

fun deleteAudioFileFromDevice(context: Context, track: Track) {
if (track.contentUri == null) return

context.contentResolver.delete(
track.contentUri,
"${MediaStore.Audio.Media._ID} = ?",
arrayOf(track.trackId.toString())
)
}

@RequiresApi(Build.VERSION_CODES.Q)
private suspend fun getTracksWithThumbnail(tracks: List<Track>): List<Track> {
val result: List<Track>
coroutineScope {
result = tracks.map {
async {
val thumbnailResult = getThumbnailAfterQ(it.contentUri!!)
val thumbnail = thumbnailResult.first
val hasOriginalBitmap = thumbnailResult.second
it.copy(thumbnail = thumbnail, hasThumbnail = hasOriginalBitmap)
}
}.awaitAll()
}

return result
}

private fun getThumbnailBeforeQ(path: String): Bitmap? {
return BitmapFactory.decodeFile(path)
}

@RequiresApi(Build.VERSION_CODES.Q)
private fun getThumbnailAfterQ(contentUri: Uri): Pair<Bitmap, Boolean> {
return try {
val bitmap = context.contentResolver.loadThumbnail(contentUri, Size(300, 300), null)
Pair(bitmap, true)
} catch (e: IOException) {
val drawable = ContextCompat.getDrawable(
context,
com.rickinc.decibels.R.drawable.ic_baseline_audio_file_24,
)

val bitmap = drawable!!.toBitmap(
width = 300,
height = 300,
Bitmap.Config.ARGB_8888
)
Pair(bitmap, false)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,100 +1,28 @@
package com.rickinc.decibels.data.repository

import android.content.ContentUris
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Size
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import com.rickinc.decibels.data.local.database.DecibelsDatabase
import com.rickinc.decibels.data.local.device.DeviceDataSource
import com.rickinc.decibels.domain.model.NowPlaying
import com.rickinc.decibels.domain.model.Result
import com.rickinc.decibels.domain.model.Track
import com.rickinc.decibels.domain.repository.AudioRepository
import com.rickinc.decibels.domain.util.TrackConverter.Companion.MP3
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import java.io.IOException


class AudioRepositoryImpl(
private val context: Context,
private val deviceDataSource: DeviceDataSource,
decibelsDatabase: DecibelsDatabase
) : AudioRepository {
private val dao = decibelsDatabase.dao

@RequiresApi(Build.VERSION_CODES.Q)
override suspend fun getAudioFiles(): Result<List<Track>> {
return withContext(Dispatchers.IO) {
val list = mutableListOf<Track>()
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.ARTIST,
)
val selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0"
val sortOrder = "${MediaStore.Audio.Media.TITLE} ASC"
val query = context.contentResolver.query(
collection,
projection,
selection,
null,
sortOrder
)

query?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)

while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val duration = cursor.getInt(durationColumn)
val title = cursor.getString(titleColumn)
val artist = cursor.getString(artistColumn)
val albumId = cursor.getLong(albumIdColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id
)
val mimeType = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(context.contentResolver.getType(contentUri))

if (mimeType == MP3) {
list.add(
Track(
trackId = id,
trackTitle = title,
trackLength = duration,
artist = artist,
albumId = albumId,
contentUri = contentUri,
mimeType = mimeType,
hasThumbnail = false
)
)
}
}
}
val tracksWithThumbnail = getTracksWithThumbnail(list)
Result.Success(tracksWithThumbnail)
Result.Success(deviceDataSource.getDeviceAudioFiles())
}
}

Expand All @@ -105,53 +33,6 @@ class AudioRepositoryImpl(
override fun getNowPlayingFlow(): Flow<NowPlaying?> = dao.getNowPlaying()

override fun deleteTrack(context: Context, track: Track) {
if (track.contentUri == null) return

context.contentResolver.delete(
track.contentUri,
"${MediaStore.Audio.Media._ID} = ?",
arrayOf(track.trackId.toString())
)
}

@RequiresApi(Build.VERSION_CODES.Q)
private suspend fun getTracksWithThumbnail(tracks: List<Track>): List<Track> {
val result: List<Track>
coroutineScope {
result = tracks.map {
async {
val thumbnailResult = getThumbnailAfterQ(it.contentUri!!)
val thumbnail = thumbnailResult.first
val hasOriginalBitmap = thumbnailResult.second
it.copy(thumbnail = thumbnail, hasThumbnail = hasOriginalBitmap)
}
}.awaitAll()
}

return result
}

private fun getThumbnailBeforeQ(path: String): Bitmap? {
return BitmapFactory.decodeFile(path)
}

@RequiresApi(Build.VERSION_CODES.Q)
private fun getThumbnailAfterQ(contentUri: Uri): Pair<Bitmap, Boolean> {
return try {
val bitmap = context.contentResolver.loadThumbnail(contentUri, Size(300, 300), null)
Pair(bitmap, true)
} catch (e: IOException) {
val drawable = ContextCompat.getDrawable(
context,
com.rickinc.decibels.R.drawable.ic_baseline_audio_file_24,
)

val bitmap = drawable!!.toBitmap(
width = 300,
height = 300,
Bitmap.Config.ARGB_8888
)
Pair(bitmap, false)
}
deviceDataSource.deleteAudioFileFromDevice(context, track)
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/com/rickinc/decibels/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.preference.PreferenceManager
import androidx.room.Room
import com.rickinc.decibels.data.local.database.DecibelsDatabase
import com.rickinc.decibels.data.local.device.DeviceDataSource
import com.rickinc.decibels.presentation.util.hasPermission
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -49,6 +50,12 @@ object AppModule {
.build()
}

@Singleton
@Provides
fun provideDeviceDataSource(@ApplicationContext appContext: Context): DeviceDataSource {
return DeviceDataSource(appContext)
}

@EntryPoint
@InstallIn(SingletonComponent::class)
interface SharedPreferencesEntryPoint {
Expand Down
Loading