/*
* Copyright (c) 2021 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.oxycblt.auxio
import android.Manifest
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.logD
/**
* A wrapper around the home fragment that shows the playback fragment and controls the more
* high-level navigation features.
* @author OxygenCobalt
*
* TODO: Add a new view with a stack trace whenever the music loading process fails.
*/
class MainFragment : ViewBindingFragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels()
private var callback: DynamicBackPressedCallback? = null
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
// --- UI SETUP ---
// Build the permission launcher here as you can only do it in onCreateView/onCreate
val permLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
musicModel.reloadMusic(requireContext())
}
requireActivity()
.onBackPressedDispatcher
.addCallback(viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it })
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Auxio's layout completely breaks down when it's window is resized too small,
// but for some insane reason google decided to cripple the window APIs one could use
// to limit it's size. So, we just have our own special layout that is shown whenever
// the screen is too small because of course we have to.
if (requireActivity().isInMultiWindowMode) {
val config = resources.configuration
if (config.screenHeightDp < 250 || config.screenWidthDp < 250) {
binding.layoutTooSmall.visibility = View.VISIBLE
}
}
}
// --- VIEWMODEL SETUP ---
// Initialize music loading. Do it here so that it shows on every fragment that this
// one contains.
// TODO: Move this to a service [automatic rescanning]
musicModel.loadMusic(requireContext())
// Handle the music loader response.
musicModel.loaderResponse.observe(viewLifecycleOwner) { response ->
handleLoaderResponse(response, permLauncher)
}
navModel.mainNavigationAction.observe(viewLifecycleOwner, ::handleMainNavigation)
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleExploreNavigation)
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
}
override fun onResume() {
super.onResume()
callback?.isEnabled = true
}
override fun onPause() {
super.onPause()
callback?.isEnabled = false
}
private fun handleLoaderResponse(
response: MusicStore.Response?,
permLauncher: ActivityResultLauncher
) {
val binding = requireBinding()
// Handle the loader response.
when (response) {
// Ok, start restoring playback now
is MusicStore.Response.Ok -> playbackModel.setupPlayback(requireContext())
// Error, show the error to the user
is MusicStore.Response.Err -> {
logD("Received Response.Err")
Snackbar.make(binding.root, R.string.err_load_failed, Snackbar.LENGTH_INDEFINITE)
.apply {
setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) }
show()
}
}
is MusicStore.Response.NoMusic -> {
logD("Received Response.NoMusic")
Snackbar.make(binding.root, R.string.err_no_music, Snackbar.LENGTH_INDEFINITE)
.apply {
setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) }
show()
}
}
is MusicStore.Response.NoPerms -> {
logD("Received Response.NoPerms")
Snackbar.make(binding.root, R.string.err_no_perms, Snackbar.LENGTH_INDEFINITE)
.apply {
setAction(R.string.lbl_grant) {
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
show()
}
}
null -> {}
}
}
private fun handleMainNavigation(action: MainNavigationAction?) {
if (action == null) return
val binding = requireBinding()
when (action) {
MainNavigationAction.EXPAND -> binding.bottomSheetLayout.expand()
MainNavigationAction.COLLAPSE -> binding.bottomSheetLayout.collapse()
MainNavigationAction.SETTINGS ->
findNavController().navigate(MainFragmentDirections.actionShowSettings())
MainNavigationAction.ABOUT ->
findNavController().navigate(MainFragmentDirections.actionShowAbout())
MainNavigationAction.QUEUE ->
findNavController().navigate(MainFragmentDirections.actionShowQueue())
}
navModel.finishMainNavigation()
}
private fun handleExploreNavigation(item: Music?) {
if (item != null) {
requireBinding().bottomSheetLayout.collapse()
}
}
private fun updateSong(song: Song?) {
val binding = requireBinding()
if (song != null) {
binding.bottomSheetLayout.show()
} else {
binding.bottomSheetLayout.hide()
}
}
/**
* A back press callback that handles how to respond to backwards navigation in the detail
* fragments and the playback panel. TODO: Migrate to new predictive API
*/
inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
val binding = requireBinding()
if (!binding.bottomSheetLayout.collapse()) {
val navController = binding.exploreNavHost.findNavController()
if (navController.currentDestination?.id ==
navController.graph.startDestinationId) {
isEnabled = false
requireActivity().onBackPressed()
isEnabled = true
} else {
navController.navigateUp()
}
}
}
}
}