diff --git a/app/build.gradle b/app/build.gradle index 3997a9ff3..5beefc70c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: "androidx.navigation.safeargs" android { compileSdkVersion 30 @@ -24,6 +26,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + + buildFeatures { + dataBinding true + } } configurations { @@ -41,10 +47,9 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1' - // Navigation - def navigationVersion = "2.3.0" - implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" - implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" + def navigation_version = "2.3.0" + implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" + implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" // Lifecycle implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 7cda53cc2..6bc1b27f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -11,7 +11,7 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) Log.d(this::class.simpleName, "Activity Created.") } diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index 0e0b5a4f1..d87fec3cd 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -13,15 +13,8 @@ import org.oxycblt.auxio.databinding.FragmentLibraryBinding class LibraryFragment : Fragment() { - // Lazily initiate the ViewModel when its first referenced. - // Not because this does anything, it just looks nicer. private val libraryModel: LibraryViewModel by lazy { - ViewModelProvider( - this, - LibraryViewModel.Factory( - requireActivity().application - ) - ).get(LibraryViewModel::class.java) + ViewModelProvider(this).get(LibraryViewModel::class.java) } override fun onCreateView( diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt index f76213535..8cb89b350 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt @@ -1,42 +1,11 @@ package org.oxycblt.auxio.library -import android.app.Application import android.util.Log import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import org.oxycblt.auxio.music.MusicRepository -class LibraryViewModel(private val app: Application) : ViewModel() { - - private val viewModelJob = Job() - private val ioScope = CoroutineScope( - Dispatchers.IO - ) +class LibraryViewModel() : ViewModel() { init { - startMusicRepo() - Log.d(this::class.simpleName, "ViewModel created.") } - - // TODO: Temp function, remove when LoadingFragment is added - private fun startMusicRepo() { - ioScope.launch { - MusicRepository.getInstance().init(app) - } - } - - class Factory(private val application: Application) : ViewModelProvider.Factory { - @Suppress("unchecked_cast") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(LibraryViewModel::class.java)) { - return LibraryViewModel(application) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt new file mode 100644 index 000000000..b9298af1d --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt @@ -0,0 +1,54 @@ +package org.oxycblt.auxio.loading + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.FragmentLoadingBinding +import org.oxycblt.auxio.music.MusicLoadResponse + +class LoadingFragment : Fragment() { + + private val loadingModel: LoadingViewModel by lazy { + ViewModelProvider(this, LoadingViewModel.Factory( + requireActivity().application) + ).get(LoadingViewModel::class.java) + } + + private lateinit var binding: FragmentLoadingBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate( + inflater, R.layout.fragment_loading, container, false + ) + + binding.lifecycleOwner = this + + loadingModel.musicRepoResponse.observe(viewLifecycleOwner, Observer { response -> + onMusicLoadResponse(response) + }) + + Log.d(this::class.simpleName, "Fragment created.") + + return binding.root + } + + private fun onMusicLoadResponse(response: MusicLoadResponse) { + if (response == MusicLoadResponse.DONE) { + this.findNavController().navigate( + LoadingFragmentDirections.actionToLibrary() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt new file mode 100644 index 000000000..daab79b58 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt @@ -0,0 +1,59 @@ +package org.oxycblt.auxio.loading + +import android.app.Application +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import kotlinx.coroutines.* +import org.oxycblt.auxio.music.MusicLoadResponse +import org.oxycblt.auxio.music.MusicRepository + +class LoadingViewModel(private val app: Application) : ViewModel() { + + private val loadingJob = Job() + private val ioScope = CoroutineScope( + Dispatchers.IO + ) + + private val mMusicRepoResponse = MutableLiveData() + val musicRepoResponse: LiveData get() = mMusicRepoResponse + + init { + startMusicRepo() + + Log.d(this::class.simpleName, "ViewModel created.") + } + + private fun startMusicRepo() { + val repo = MusicRepository.getInstance() + + // Allow MusicRepository to scan the file system on the IO thread + ioScope.launch { + val response = repo.init(app) + + // Then actually notify listeners of the response in the Main thread + withContext(Dispatchers.Main) { + mMusicRepoResponse.value = response + } + } + } + + override fun onCleared() { + super.onCleared() + + // Cancel the current loading job if the app has been stopped + loadingJob.cancel() + } + + class Factory(private val application: Application) : ViewModelProvider.Factory { + @Suppress("unchecked_cast") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(LoadingViewModel::class.java)) { + return LoadingViewModel(application) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index f41052fb1..e1210bcc6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -8,6 +8,13 @@ import android.media.MediaMetadataRetriever import android.provider.MediaStore import android.provider.MediaStore.Audio.AudioColumns import android.util.Log +import org.oxycblt.auxio.music.models.Album +import org.oxycblt.auxio.music.models.Artist +import org.oxycblt.auxio.music.models.Song + +enum class MusicLoadResponse { + DONE, FAILURE, NO_MUSIC +} // Storage for music data. Design largely adapted from Music Player GO: // https://github.com/enricocid/Music-Player-GO @@ -22,23 +29,27 @@ class MusicRepository { val albums: List get() = mAlbums val songs: List get() = mSongs - fun init(app: Application): Boolean { + fun init(app: Application): MusicLoadResponse { + findMusic(app)?.let { ss -> - if (ss.size > 0) { + return if (ss.size > 0) { processSongs(ss) - return true + MusicLoadResponse.DONE + } else { + MusicLoadResponse.NO_MUSIC } } - // Return false if the load as failed for any reason, either - // through there being no music or an Exception. - return false + // If the let function isn't run, then the loading has failed due to some Exception + // and FAILURE must be returned + return MusicLoadResponse.FAILURE } private fun findMusic(app: Application): MutableList? { val songList = mutableListOf() + val retriever = MediaMetadataRetriever() try { @@ -51,11 +62,15 @@ class MusicRepository { // Index music files from shared storage musicCursor?.use { cursor -> + // Don't run the more expensive file loading operations if there is no music + // to index. + if (cursor.count == 0) { + return songList + } + val idIndex = cursor.getColumnIndexOrThrow(AudioColumns._ID) val displayIndex = cursor.getColumnIndexOrThrow(AudioColumns.DISPLAY_NAME) - val retriever = MediaMetadataRetriever() - while (cursor.moveToNext()) { val id = cursor.getLong(idIndex) @@ -123,22 +138,26 @@ class MusicRepository { ) } - // Close the retriever when done so that it gets garbage collected [I hope] + // Close the retriever/cursor so that it gets garbage collected retriever.close() + cursor.close() } Log.d( this::class.simpleName, - "Music search finished with " + songList.size.toString() + " Songs found." + "Successfully found " + songList.size.toString() + " Songs." ) return songList + } catch (error: Exception) { // TODO: Add better error handling Log.e(this::class.simpleName, "Something went horribly wrong.") error.printStackTrace() + retriever.close() + return null } } @@ -165,14 +184,12 @@ class MusicRepository { it.name to it.artist to it.album to it.year to it.track to it.duration }.toMutableList() - // Sort the music by artists/albums + // Add an album abstraction for each group of songs val songsByAlbum = distinctSongs.groupBy { it.album } val albumList = mutableListOf() songsByAlbum.keys.iterator().forEach { album -> val albumSongs = songsByAlbum[album] - - // Add an album abstraction for each album item in the list of songs. albumSongs?.let { albumList.add( Album(albumSongs) @@ -181,7 +198,6 @@ class MusicRepository { } // Then abstract the remaining albums into artist objects - // TODO: If enabled val albumsByArtist = albumList.groupBy { it.artist } val artistList = mutableListOf() diff --git a/app/src/main/java/org/oxycblt/auxio/music/Album.kt b/app/src/main/java/org/oxycblt/auxio/music/models/Album.kt similarity index 93% rename from app/src/main/java/org/oxycblt/auxio/music/Album.kt rename to app/src/main/java/org/oxycblt/auxio/music/models/Album.kt index 3f2bf740b..b8ef517c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Album.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/models/Album.kt @@ -1,8 +1,8 @@ -package org.oxycblt.auxio.music +package org.oxycblt.auxio.music.models import android.graphics.Bitmap -// Basic Abstraction for Song +// Abstraction for Song data class Album( var songs: List ) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/Artist.kt b/app/src/main/java/org/oxycblt/auxio/music/models/Artist.kt similarity index 89% rename from app/src/main/java/org/oxycblt/auxio/music/Artist.kt rename to app/src/main/java/org/oxycblt/auxio/music/models/Artist.kt index d049c3d8f..237d5aa9d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Artist.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/models/Artist.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.music +package org.oxycblt.auxio.music.models // Abstraction for mAlbums data class Artist( @@ -7,6 +7,8 @@ data class Artist( var name: String? = null var genre: String? = null + // TODO: Artist photos + init { // Like Album, iterate through the child albums and pick out the first valid // tag for Album/Genre diff --git a/app/src/main/java/org/oxycblt/auxio/music/Song.kt b/app/src/main/java/org/oxycblt/auxio/music/models/Song.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/music/Song.kt rename to app/src/main/java/org/oxycblt/auxio/music/models/Song.kt index ae1fe5bb6..67335966b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Song.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/models/Song.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.music +package org.oxycblt.auxio.music.models import android.graphics.Bitmap import android.graphics.BitmapFactory diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 77475efbe..b8e7b16da 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -1,5 +1,6 @@ - @@ -13,9 +14,9 @@ android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:elevation="@dimen/elevation_normal" + android:textColor="?attr/titleTextColor" app:layout_constraintTop_toTopOf="parent" - app:title="@string/fragment_library_title" - tools:titleTextColor="@color/primaryTextColor" /> + app:title="@string/title_library_fragment" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_loading.xml b/app/src/main/res/layout/fragment_loading.xml new file mode 100644 index 000000000..6d6e6ecda --- /dev/null +++ b/app/src/main/res/layout/fragment_loading.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 7e40d75fd..196795a31 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -3,11 +3,20 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_main" - app:startDestination="@id/playerFragment"> + app:startDestination="@id/loadingFragment"> + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 548d426a4..a4a3f43ac 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,11 +1,12 @@ - #424242 - #6d6d6d - #1b1b1b - #212121 - #484848 - #000000 - #ffffff - #ffffff + + #424242 + #6d6d6d + #1b1b1b + + #484848 + #000000 + + #ffffff \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index aa535fa69..c71530ee3 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,4 +1,6 @@ - 4dp + 4dp + + 6dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9d0ac4a7..23b14bcd0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,5 +2,7 @@ Auxio - Library + Scanning your music library for the first time... + + Library \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b9d88b3c9..c485450f5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,8 +2,7 @@ \ No newline at end of file diff --git a/build.gradle b/build.gradle index b803cc3a1..7d653473b 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files