Add Loading Screen

Add a basic loading screen that loads the music in the background before starting the app.
This commit is contained in:
OxygenCobalt 2020-08-19 09:28:15 -06:00
parent 3134e313a3
commit 1b21552576
18 changed files with 230 additions and 82 deletions

View file

@ -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"

View file

@ -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.")
}

View file

@ -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(

View file

@ -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 <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LibraryViewModel::class.java)) {
return LibraryViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}

View file

@ -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<FragmentLoadingBinding>(
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()
)
}
}
}

View file

@ -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<MusicLoadResponse>()
val musicRepoResponse: LiveData<MusicLoadResponse> 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 <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoadingViewModel::class.java)) {
return LoadingViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}

View file

@ -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<Album> get() = mAlbums
val songs: List<Song> 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<Song>? {
val songList = mutableListOf<Song>()
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<Album>()
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<Artist>()

View file

@ -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<Song>
) {

View file

@ -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

View file

@ -1,4 +1,4 @@
package org.oxycblt.auxio.music
package org.oxycblt.auxio.music.models
import android.graphics.Bitmap
import android.graphics.BitmapFactory

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
<layout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
@ -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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/loading_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateTint="?attr/colorAccent"
android:indeterminateTintMode="src_in"
android:paddingBottom="@dimen/padding_small"
app:layout_constraintBottom_toTopOf="@+id/text_indexing_library"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"/>
<TextView
android:id="@+id/text_indexing_library"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_loading_music"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -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">
<fragment
android:id="@+id/playerFragment"
android:id="@+id/loadingFragment"
android:name="org.oxycblt.auxio.loading.LoadingFragment"
android:label="LoadingFragment"
tools:layout="@layout/fragment_loading" >
<action
android:id="@+id/action_to_library"
app:destination="@id/libraryFragment" />
</fragment>
<fragment
android:id="@+id/libraryFragment"
android:name="org.oxycblt.auxio.library.LibraryFragment"
android:label="PlayerFragment"
android:label="LibraryFragment"
tools:layout="@layout/fragment_library" />
</navigation>

View file

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primaryColor">#424242</color>
<color name="primaryLightColor">#6d6d6d</color>
<color name="primaryDarkColor">#1b1b1b</color>
<color name="secondaryColor">#212121</color>
<color name="secondaryLightColor">#484848</color>
<color name="secondaryDarkColor">#000000</color>
<color name="primaryTextColor">#ffffff</color>
<color name="secondaryTextColor">#ffffff</color>
<!-- Dark Colors -->
<color name="gray">#424242</color>
<color name="lightGray">#6d6d6d</color>
<color name="darkGray">#1b1b1b</color>
<color name="secondaryGray">#484848</color>
<color name="black">#000000</color>
<color name="white">#ffffff</color>
</resources>

View file

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="elevation_normal">4dp</dimen>
<dimen name="padding_small">4dp</dimen>
<dimen name="elevation_normal">6dp</dimen>
</resources>

View file

@ -2,5 +2,7 @@
<resources>
<string name="app_name">Auxio</string>
<string name="fragment_library_title">Library</string>
<string name="label_loading_music">Scanning your music library for the first time...</string>
<string name="title_library_fragment">Library</string>
</resources>

View file

@ -2,8 +2,7 @@
<resources>
<!-- Base theme -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="colorPrimary">@color/primaryColor</item>
<item name="colorPrimaryDark">@color/primaryDarkColor</item>
<item name="colorAccent">@color/secondaryColor</item>
<!-- Make this accent settable -->
<item name="colorAccent">#295DE5</item>
</style>
</resources>

View file

@ -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