diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f9cf67e..b282a17 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,8 +34,11 @@ android:windowSoftInputMode="adjustResize"> + + + @@ -65,6 +68,36 @@ android:scheme="https" tools:ignore="IntentFilterUniqueDataAttributes" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/app/suhasdissa/vibeyou/MainActivity.kt b/app/src/main/java/app/suhasdissa/vibeyou/MainActivity.kt index a6f86d3..1ba8da7 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/MainActivity.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/MainActivity.kt @@ -104,8 +104,11 @@ class MainActivity : ComponentActivity() { private fun handleIntent(intent: Intent) { when (intent.action) { Intent.ACTION_VIEW -> { - val uri = intent.data - uri?.let { + val uri = intent.data ?: return + // Check if uri points to a device file + if (uri.scheme == "file" || uri.scheme == "content") { + playerViewModel.tryToPlayUri(uri) + } else { processLink(uri) } } diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/LocalMusicRepository.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/LocalMusicRepository.kt index 3da4073..83b79f3 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/LocalMusicRepository.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/LocalMusicRepository.kt @@ -5,6 +5,7 @@ import android.content.ContentResolver import android.content.ContentUris import android.net.Uri import android.os.Build +import android.os.Environment.getExternalStorageDirectory import android.provider.MediaStore import android.text.format.DateUtils import androidx.core.net.toUri @@ -16,6 +17,7 @@ import app.suhasdissa.vibeyou.backend.database.entities.SearchQuery import app.suhasdissa.vibeyou.utils.Pref import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.io.File class LocalMusicRepository( private val contentResolver: ContentResolver, @@ -26,6 +28,7 @@ class LocalMusicRepository( private var artistCache = listOf() suspend fun getAllSongs(): List = withContext(Dispatchers.IO) { + if (songsCache.isNotEmpty()) return@withContext songsCache val songs = mutableListOf() @@ -74,7 +77,8 @@ class LocalMusicRepository( MediaStore.Audio.Media.DATE_MODIFIED ) val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED) - val trackNumberColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.CD_TRACK_NUMBER) + val trackNumberColumn = + cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.CD_TRACK_NUMBER) while (cursor.moveToNext()) { val id = cursor.getLong(idColumn) @@ -122,6 +126,136 @@ class LocalMusicRepository( songs } + suspend fun getSongFromUri(uri: Uri): Song? = withContext(Dispatchers.IO) { + 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.TITLE, + MediaStore.Audio.Media.DISPLAY_NAME, + MediaStore.Audio.Media.DURATION, + MediaStore.Audio.Media.ARTIST, + MediaStore.Audio.Media.ALBUM_ID, + MediaStore.Audio.Media.ARTIST_ID, + MediaStore.Audio.Media.DATE_MODIFIED, + MediaStore.Audio.Media.DATE_ADDED, + MediaStore.Audio.Media.CD_TRACK_NUMBER + ) + + val selection = "${MediaStore.Audio.Media.DATA} = ?" + + val path = getPathFromContentUri(uri) + val selectionArgs = arrayOf(path) + + val query = contentResolver.query( + collection, + projection, + selection, + selectionArgs, + null + ) + + query?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) + val titleColumn = + cursor.getColumnIndex(MediaStore.Audio.Media.TITLE) + val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME) + val durationColumn = + cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION) + val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST) + val albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID) + val artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID) + val creationDateColumn = cursor.getColumnIndexOrThrow( + MediaStore.Audio.Media.DATE_MODIFIED + ) + val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED) + val trackNumberColumn = + cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.CD_TRACK_NUMBER) + + if (cursor.moveToFirst()) { + val id = cursor.getLong(idColumn) + val name = + if (cursor.isNull(titleColumn)) { + cursor.getString(nameColumn) + } else { + cursor.getString( + titleColumn + ) + } + val albumId = cursor.getLong(albumColumn) + + val contentUri: Uri = ContentUris.withAppendedId( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + id + ) + + val duration = cursor.getLong(durationColumn) / 1000 + if (duration == 0L) return@withContext null + + return@withContext Song( + id = contentUri.toString(), + title = name, + durationText = DateUtils.formatElapsedTime(duration), + thumbnailUri = getAlbumArt(albumId), + artistsText = cursor.getString(artistColumn), + albumId = albumId, + artistId = cursor.getLong(artistIdColumn), + isLocal = true, + creationDate = cursor.getLong(creationDateColumn), + dateAdded = cursor.getLong(dateAddedColumn), + trackNumber = cursor.getLong(trackNumberColumn) + ) + } + } + return@withContext null + } + + private fun getPathFromContentUri(uri: Uri): String? { + var songFile: File? = null + if (uri.authority == "com.android.externalstorage.documents") { + val path = uri.path?.split(":".toRegex(), 2)?.get(1) + if (path != null) { + songFile = File(getExternalStorageDirectory(), path) + } + } + if (songFile == null) { + val path = getFilePathFromUri(uri) + if (path != null) + songFile = File(path) + } + if (songFile == null && uri.path != null) { + songFile = File(uri.path!!) + } + if (songFile != null) { + return songFile.absolutePath + } + return null + } + + private fun getFilePathFromUri(uri: Uri): String? { + val column = MediaStore.Audio.Media.DATA + val projection = arrayOf(column) + val query = contentResolver.query(uri, projection, null, null, null) + + query?.use { cursor -> + try { + if (cursor.moveToFirst()) { + val columnIndex = cursor.getColumnIndexOrThrow(column) + return cursor.getString(columnIndex) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + return null + } + suspend fun getAllAlbums(): List = withContext(Dispatchers.IO) { if (albumCache.isNotEmpty()) return@withContext albumCache @@ -279,6 +413,7 @@ class LocalMusicRepository( searchDao.addSearchQuery(SearchQuery(id = 0, query)) } + fun deleteQuery(query: String) = searchDao.deleteQuery(query) fun getSearchHistory() = searchDao.getSearchHistory() diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlayerViewModel.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlayerViewModel.kt index 6d81b67..7432a12 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlayerViewModel.kt @@ -1,5 +1,6 @@ package app.suhasdissa.vibeyou.backend.viewmodel +import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log @@ -17,6 +18,7 @@ import androidx.media3.common.PlaybackParameters import androidx.media3.session.MediaController import app.suhasdissa.vibeyou.MellowMusicApplication import app.suhasdissa.vibeyou.backend.data.Song +import app.suhasdissa.vibeyou.backend.repository.LocalMusicRepository import app.suhasdissa.vibeyou.backend.repository.PipedMusicRepository import app.suhasdissa.vibeyou.backend.repository.SongDatabaseRepository import app.suhasdissa.vibeyou.utils.addNext @@ -33,6 +35,7 @@ import kotlinx.coroutines.launch class PlayerViewModel( private val songDatabaseRepository: SongDatabaseRepository, private val musicRepository: PipedMusicRepository, + private val localMusicRepository: LocalMusicRepository, private val controllerFuture: ListenableFuture ) : ViewModel() { @@ -148,6 +151,19 @@ class PlayerViewModel( } } + fun tryToPlayUri(uri: Uri) { + viewModelScope.launch { + val song: Song? = localMusicRepository.getSongFromUri(uri) + song?.let { + if (controller == null) { + toBePlayed = song.asMediaItem + } else { + controller?.forcePlay(song.asMediaItem) + } + } + } + } + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { @@ -155,6 +171,7 @@ class PlayerViewModel( PlayerViewModel( application.container.songDatabaseRepository, application.container.pipedMusicRepository, + application.container.localMusicRepository, application.container.controllerFuture ) }