Add permission dialog for file reading
Add a permission dialog for the apps READ_EXTERNAL_STORAGE permission.
This commit is contained in:
parent
5e6917f11c
commit
af26aed735
7 changed files with 123 additions and 15 deletions
|
@ -9,8 +9,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import org.oxycblt.auxio.theme.accent
|
import org.oxycblt.auxio.theme.accent
|
||||||
|
|
||||||
const val PERM_READ_EXTERNAL_STORAGE = 2488
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package org.oxycblt.auxio.loading
|
package org.oxycblt.auxio.loading
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
@ -25,6 +28,7 @@ class LoadingFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var binding: FragmentLoadingBinding
|
private lateinit var binding: FragmentLoadingBinding
|
||||||
|
private lateinit var permLauncher: ActivityResultLauncher<String>
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -52,6 +56,37 @@ class LoadingFragment : Fragment() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
loadingModel.doGrant.observe(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
{ grant ->
|
||||||
|
onGrant(grant)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set up the permission launcher, as its disallowed outside of onCreate.
|
||||||
|
permLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission())
|
||||||
|
{ granted: Boolean ->
|
||||||
|
|
||||||
|
// If its actually granted, restart the loading process again.
|
||||||
|
if (granted) {
|
||||||
|
binding.loadingBar.visibility = View.VISIBLE
|
||||||
|
binding.errorText.visibility = View.GONE
|
||||||
|
binding.statusIcon.visibility = View.GONE
|
||||||
|
binding.retryButton.visibility = View.GONE
|
||||||
|
binding.grantButton.visibility = View.GONE
|
||||||
|
|
||||||
|
loadingModel.retry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This never seems to return true but Im apparently supposed to use it so
|
||||||
|
if (shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||||
|
onMusicLoadResponse(MusicLoaderResponse.NO_PERMS)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
loadingModel.go()
|
||||||
|
}
|
||||||
|
|
||||||
Log.d(this::class.simpleName, "Fragment created.")
|
Log.d(this::class.simpleName, "Fragment created.")
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
@ -65,20 +100,30 @@ class LoadingFragment : Fragment() {
|
||||||
this.findNavController().navigate(
|
this.findNavController().navigate(
|
||||||
LoadingFragmentDirections.actionToMain()
|
LoadingFragmentDirections.actionToMain()
|
||||||
)
|
)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// If the response wasn't a success, then show the specific error message
|
// If the response wasn't a success, then show the specific error message
|
||||||
// depending on which error response was given, along with a retry button
|
// depending on which error response was given, along with a retry or grant button
|
||||||
|
|
||||||
binding.loadingBar.visibility = View.GONE
|
binding.loadingBar.visibility = View.GONE
|
||||||
binding.errorText.visibility = View.VISIBLE
|
binding.errorText.visibility = View.VISIBLE
|
||||||
binding.statusIcon.visibility = View.VISIBLE
|
binding.statusIcon.visibility = View.VISIBLE
|
||||||
binding.retryButton.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
binding.errorText.text =
|
when (response) {
|
||||||
if (response == MusicLoaderResponse.NO_MUSIC)
|
MusicLoaderResponse.NO_PERMS -> {
|
||||||
getString(R.string.error_no_music)
|
binding.grantButton.visibility = View.VISIBLE
|
||||||
else
|
binding.errorText.text = getString(R.string.error_no_perms)
|
||||||
getString(R.string.error_music_load_failed)
|
}
|
||||||
|
|
||||||
|
MusicLoaderResponse.NO_MUSIC -> {
|
||||||
|
binding.retryButton.visibility = View.VISIBLE
|
||||||
|
binding.errorText.text = getString(R.string.error_no_music)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
binding.retryButton.visibility = View.VISIBLE
|
||||||
|
binding.errorText.text = getString(R.string.error_music_load_failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingModel.doneWithResponse()
|
loadingModel.doneWithResponse()
|
||||||
|
@ -91,8 +136,17 @@ class LoadingFragment : Fragment() {
|
||||||
binding.errorText.visibility = View.GONE
|
binding.errorText.visibility = View.GONE
|
||||||
binding.statusIcon.visibility = View.GONE
|
binding.statusIcon.visibility = View.GONE
|
||||||
binding.retryButton.visibility = View.GONE
|
binding.retryButton.visibility = View.GONE
|
||||||
|
binding.grantButton.visibility = View.GONE
|
||||||
|
|
||||||
loadingModel.doneWithRetry()
|
loadingModel.doneWithRetry()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onGrant(grant: Boolean) {
|
||||||
|
if (grant) {
|
||||||
|
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
|
||||||
|
loadingModel.doneWithGrant()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,19 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
private val mDoRetry = MutableLiveData<Boolean>()
|
private val mDoRetry = MutableLiveData<Boolean>()
|
||||||
val doRetry: LiveData<Boolean> get() = mDoRetry
|
val doRetry: LiveData<Boolean> get() = mDoRetry
|
||||||
|
|
||||||
init {
|
private val mDoGrant = MutableLiveData<Boolean>()
|
||||||
startMusicRepo()
|
val doGrant: LiveData<Boolean> get() = mDoGrant
|
||||||
|
|
||||||
|
private var started = false
|
||||||
|
|
||||||
|
// Start the music loading. It has already been called, one needs to call retry() instead.
|
||||||
|
fun go() {
|
||||||
|
if (!started) {
|
||||||
|
startMusicRepo()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the music loading sequence.
|
||||||
private fun startMusicRepo() {
|
private fun startMusicRepo() {
|
||||||
val repo = MusicRepository.getInstance()
|
val repo = MusicRepository.getInstance()
|
||||||
|
|
||||||
|
@ -40,10 +49,14 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
// Then actually notify listeners of the response in the Main thread
|
// Then actually notify listeners of the response in the Main thread
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
mMusicRepoResponse.value = response
|
mMusicRepoResponse.value = response
|
||||||
|
|
||||||
|
started = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Functions for communicating between LoadingFragment & LoadingViewModel
|
||||||
|
|
||||||
fun doneWithResponse() {
|
fun doneWithResponse() {
|
||||||
mMusicRepoResponse.value = null
|
mMusicRepoResponse.value = null
|
||||||
}
|
}
|
||||||
|
@ -58,6 +71,14 @@ class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||||
mDoRetry.value = false
|
mDoRetry.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun grant() {
|
||||||
|
mDoGrant.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doneWithGrant() {
|
||||||
|
mDoGrant.value = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.models.Album
|
import org.oxycblt.auxio.music.models.Album
|
||||||
import org.oxycblt.auxio.music.models.Artist
|
import org.oxycblt.auxio.music.models.Artist
|
||||||
|
@ -20,6 +23,12 @@ class MusicRepository {
|
||||||
lateinit var songs: List<Song>
|
lateinit var songs: List<Song>
|
||||||
|
|
||||||
fun init(app: Application): MusicLoaderResponse {
|
fun init(app: Application): MusicLoaderResponse {
|
||||||
|
if (!checkPerms(app)) {
|
||||||
|
Log.i(this::class.simpleName, "No permissions, aborting...")
|
||||||
|
|
||||||
|
return MusicLoaderResponse.NO_PERMS
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(this::class.simpleName, "Starting initial music load...")
|
Log.i(this::class.simpleName, "Starting initial music load...")
|
||||||
|
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
@ -52,7 +61,13 @@ class MusicRepository {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return MusicLoaderResponse.NO_MUSIC
|
return loader.response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPerms(app: Application): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(
|
||||||
|
app.applicationContext, Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.oxycblt.auxio.music.toAlbumArtURI
|
||||||
import org.oxycblt.auxio.music.toNamedGenre
|
import org.oxycblt.auxio.music.toNamedGenre
|
||||||
|
|
||||||
enum class MusicLoaderResponse {
|
enum class MusicLoaderResponse {
|
||||||
DONE, FAILURE, NO_MUSIC
|
DONE, FAILURE, NO_MUSIC, NO_PERMS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Class that loads music from the FileSystem.
|
// Class that loads music from the FileSystem.
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
android:indeterminateTintMode="src_in"
|
android:indeterminateTintMode="src_in"
|
||||||
android:src="@drawable/ic_error"
|
android:src="@drawable/ic_error"
|
||||||
android:contentDescription="@string/description_error_icon"
|
android:contentDescription="@string/description_error_icon"
|
||||||
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/error_text"
|
app:layout_constraintBottom_toTopOf="@+id/error_text"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
@ -76,9 +77,27 @@
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/error_text"
|
app:layout_constraintTop_toBottomOf="@+id/error_text"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/grant_button"
|
||||||
app:layout_constraintVertical_bias="0.673"
|
app:layout_constraintVertical_bias="0.673"
|
||||||
tools:textColor="@color/blue"
|
tools:textColor="@color/blue"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/grant_button"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_semibold"
|
||||||
|
android:onClick="@{() -> loadingModel.grant()}"
|
||||||
|
android:text="@string/label_grant"
|
||||||
|
android:textColor="?attr/colorPrimary"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/retry_button"
|
||||||
|
tools:textColor="@color/blue"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</layout>
|
</layout>
|
|
@ -7,9 +7,10 @@
|
||||||
|
|
||||||
<string name="error_no_music">No music found.</string>
|
<string name="error_no_music">No music found.</string>
|
||||||
<string name="error_music_load_failed">Music loading failed.</string>
|
<string name="error_music_load_failed">Music loading failed.</string>
|
||||||
<string name="error_no_perms">Auxio needs to be allowed to read your music library.</string>
|
<string name="error_no_perms">Auxio needs access to your music library.</string>
|
||||||
|
|
||||||
<string name="label_retry">Retry</string>
|
<string name="label_retry">Retry</string>
|
||||||
|
<string name="label_grant">Grant</string>
|
||||||
<string name="label_single_song">1 Song</string>
|
<string name="label_single_song">1 Song</string>
|
||||||
<string name="label_unknown_genre">Unknown Genre</string>
|
<string name="label_unknown_genre">Unknown Genre</string>
|
||||||
<string name="label_unknown_artist">Unknown Artist</string>
|
<string name="label_unknown_artist">Unknown Artist</string>
|
||||||
|
|
Loading…
Reference in a new issue