ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt

396 lines
15 KiB
Kotlin

package org.moire.ultrasonic.activity
import android.app.AlertDialog
import android.app.SearchManager
import android.content.Intent
import android.content.res.Resources
import android.media.AudioManager
import android.os.Bundle
import android.provider.MediaStore
import android.provider.SearchRecentSuggestions
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentContainerView
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.onNavDestinationSelected
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.google.android.material.navigation.NavigationView
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.fragment.OnBackPressedHandler
import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.NowPlayingEventListener
import org.moire.ultrasonic.util.PermissionUtil
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
import org.moire.ultrasonic.util.ThemeChangedEventListener
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* The main Activity of Ultrasonic which loads all other screens as Fragments
*/
class NavigationActivity : AppCompatActivity() {
private var chatMenuItem: MenuItem? = null
private var bookmarksMenuItem: MenuItem? = null
private var sharesMenuItem: MenuItem? = null
private var podcastsMenuItem: MenuItem? = null
private var nowPlayingView: FragmentContainerView? = null
private var nowPlayingHidden = false
private var navigationView: NavigationView? = null
private var drawerLayout: DrawerLayout? = null
private var host: NavHostFragment? = null
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var nowPlayingEventListener: NowPlayingEventListener
private lateinit var themeChangedEventListener: ThemeChangedEventListener
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
private val mediaPlayerController: MediaPlayerController by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject()
private val themeChangedEventDistributor: ThemeChangedEventDistributor by inject()
private val permissionUtil: PermissionUtil by inject()
private var infoDialogDisplayed = false
private var currentFragmentId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
setUncaughtExceptionHandler()
permissionUtil.onForegroundApplicationStarted(this)
Util.applyTheme(this)
super.onCreate(savedInstanceState)
volumeControlStream = AudioManager.STREAM_MUSIC
setContentView(R.layout.navigation_activity)
nowPlayingView = findViewById(R.id.now_playing_fragment)
navigationView = findViewById(R.id.nav_view)
drawerLayout = findViewById(R.id.drawer_layout)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
host = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ?: return
val navController = host!!.navController
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.mainFragment,
R.id.mediaLibraryFragment,
R.id.searchFragment,
R.id.playlistsFragment,
R.id.downloadsFragment,
R.id.sharesFragment,
R.id.bookmarksFragment,
R.id.chatFragment,
R.id.podcastFragment,
R.id.settingsFragment,
R.id.aboutFragment,
R.id.playerFragment
),
drawerLayout
)
setupActionBar(navController, appBarConfiguration)
setupNavigationMenu(navController)
navController.addOnDestinationChangedListener { _, destination, _ ->
val dest: String = try {
resources.getResourceName(destination.id)
} catch (ignored: Resources.NotFoundException) {
destination.id.toString()
}
Timber.d("Navigated to $dest")
currentFragmentId = destination.id
// Handle the hiding of the NowPlaying fragment when the Player is active
if (currentFragmentId == R.id.playerFragment) {
hideNowPlaying()
} else {
if (!nowPlayingHidden) showNowPlaying()
}
// Hides menu items for Offline mode
setMenuForServerSetting()
}
// Determine first run and migrate server settings to DB as early as possible
var showWelcomeScreen = Util.isFirstRun()
val areServersMigrated: Boolean = serverSettingsModel.migrateFromPreferences()
// If there are any servers in the DB, do not show the welcome screen
showWelcomeScreen = showWelcomeScreen and !areServersMigrated
loadSettings()
// This is a first run with only the demo entry inside the database
// We set the active server to the demo one and show the welcome dialog
if (showWelcomeScreen) {
showWelcomeDialog()
}
nowPlayingEventListener = object : NowPlayingEventListener {
override fun onDismissNowPlaying() {
nowPlayingHidden = true
hideNowPlaying()
}
override fun onHideNowPlaying() {
hideNowPlaying()
}
override fun onShowNowPlaying() {
showNowPlaying()
}
}
themeChangedEventListener = object : ThemeChangedEventListener {
override fun onThemeChanged() { recreate() }
}
nowPlayingEventDistributor.subscribe(nowPlayingEventListener)
themeChangedEventDistributor.subscribe(themeChangedEventListener)
}
override fun onResume() {
super.onResume()
setMenuForServerSetting()
// Lifecycle support's constructor registers some event receivers so it should be created early
lifecycleSupport.onCreate()
if (!nowPlayingHidden) showNowPlaying()
else hideNowPlaying()
}
override fun onDestroy() {
super.onDestroy()
nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
themeChangedEventDistributor.unsubscribe(themeChangedEventListener)
imageLoaderProvider.clearImageLoader()
permissionUtil.onForegroundApplicationStopped()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
val isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP
val isVolumeAdjust = isVolumeDown || isVolumeUp
val isJukebox = mediaPlayerController.isJukeboxEnabled
if (isVolumeAdjust && isJukebox) {
mediaPlayerController.adjustJukeboxVolume(isVolumeUp)
return true
}
return super.onKeyDown(keyCode, event)
}
private fun setupNavigationMenu(navController: NavController) {
navigationView?.setupWithNavController(navController)
// The exit menu is handled here manually
val exitItem: MenuItem? = navigationView?.menu?.findItem(R.id.menu_exit)
exitItem?.setOnMenuItemClickListener { item ->
if (item.itemId == R.id.menu_exit) {
setResult(Constants.RESULT_CLOSE_ALL)
mediaPlayerController.stopJukeboxService()
finish()
exit()
}
true
}
chatMenuItem = navigationView?.menu?.findItem(R.id.chatFragment)
bookmarksMenuItem = navigationView?.menu?.findItem(R.id.bookmarksFragment)
sharesMenuItem = navigationView?.menu?.findItem(R.id.sharesFragment)
podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment)
}
private fun setupActionBar(navController: NavController, appBarConfig: AppBarConfiguration) {
setupActionBarWithNavController(navController, appBarConfig)
}
override fun onBackPressed() {
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
this.drawerLayout?.closeDrawer(GravityCompat.START)
} else {
val currentFragment = host!!.childFragmentManager.fragments.last()
if (currentFragment is OnBackPressedHandler) currentFragment.onBackPressed()
else super.onBackPressed()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val retValue = super.onCreateOptionsMenu(menu)
if (navigationView == null) {
menuInflater.inflate(R.menu.navigation, menu)
return true
}
return retValue
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment)) ||
super.onOptionsItemSelected(item)
}
override fun onSupportNavigateUp(): Boolean {
val currentFragment = host!!.childFragmentManager.fragments.last()
return if (currentFragment is OnBackPressedHandler) {
currentFragment.onBackPressed()
true
} else {
findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
}
}
// TODO Test if this works with external Intents
// android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent == null) return
if (intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, false)) {
findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment)
return
}
val query = intent.getStringExtra(SearchManager.QUERY)
if (query != null) {
val autoPlay = intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
val suggestions = SearchRecentSuggestions(
this,
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
)
suggestions.saveRecentQuery(query, null)
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_QUERY, query)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoPlay)
findNavController(R.id.nav_host_fragment).navigate(R.id.searchFragment, bundle)
}
}
private fun loadSettings() {
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
val preferences = Settings.preferences
if (!preferences.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) {
Settings.cacheLocation = FileUtil.defaultMusicDirectory.path
}
}
private fun exit() {
lifecycleSupport.onDestroy()
finish()
}
private fun showWelcomeDialog() {
if (!infoDialogDisplayed) {
infoDialogDisplayed = true
AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_info)
.setTitle(R.string.main_welcome_title)
.setMessage(R.string.main_welcome_text_demo)
.setNegativeButton(R.string.main_welcome_cancel) { dialog, _ ->
// Go to the settings screen
dialog.dismiss()
findNavController(R.id.nav_host_fragment).navigate(R.id.settingsFragment)
}
.setPositiveButton(R.string.common_ok) { dialog, _ ->
// Add the demo server
val activeServerProvider: ActiveServerProvider by inject()
val demoIndex = serverSettingsModel.addDemoServer()
activeServerProvider.setActiveServerByIndex(demoIndex)
findNavController(R.id.nav_host_fragment).navigate(R.id.mainFragment)
dialog.dismiss()
}.show()
}
}
private fun setUncaughtExceptionHandler() {
val handler = Thread.getDefaultUncaughtExceptionHandler()
if (handler !is SubsonicUncaughtExceptionHandler) {
Thread.setDefaultUncaughtExceptionHandler(SubsonicUncaughtExceptionHandler(this))
}
}
private fun showNowPlaying() {
if (!Settings.showNowPlaying) {
hideNowPlaying()
return
}
// The logic for nowPlayingHidden is that the user can dismiss NowPlaying with a gesture,
// and when the MediaPlayerService requests that it should be shown, it returns
nowPlayingHidden = false
// Do not show for Player fragment
if (currentFragmentId == R.id.playerFragment) {
hideNowPlaying()
return
}
if (nowPlayingView != null) {
val playerState: PlayerState = mediaPlayerController.playerState
if (playerState == PlayerState.PAUSED || playerState == PlayerState.STARTED) {
val file: DownloadFile? = mediaPlayerController.currentPlaying
if (file != null) {
nowPlayingView?.visibility = View.VISIBLE
}
} else {
hideNowPlaying()
}
}
}
private fun hideNowPlaying() {
nowPlayingView?.visibility = View.GONE
}
private fun setMenuForServerSetting() {
if (isOffline()) {
chatMenuItem?.isVisible = false
bookmarksMenuItem?.isVisible = false
sharesMenuItem?.isVisible = false
podcastsMenuItem?.isVisible = false
return
}
val activeServerProvider: ActiveServerProvider by inject()
val activeServer = activeServerProvider.getActiveServer()
chatMenuItem?.isVisible = activeServer.chatSupport != false
bookmarksMenuItem?.isVisible = activeServer.bookmarkSupport != false
sharesMenuItem?.isVisible = activeServer.shareSupport != false
podcastsMenuItem?.isVisible = activeServer.podcastSupport != false
}
}