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

396 lines
15 KiB
Kotlin
Raw Normal View History

package org.moire.ultrasonic.activity
import android.app.AlertDialog
2021-02-05 21:45:50 +01:00
import android.app.SearchManager
import android.content.Intent
import android.content.res.Resources
import android.media.AudioManager
import android.os.Bundle
2021-02-05 21:45:50 +01:00
import android.provider.MediaStore
import android.provider.SearchRecentSuggestions
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
2021-02-08 20:24:20 +01:00
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
2021-02-08 20:24:20 +01:00
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
2021-05-27 12:15:56 +02:00
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
2021-05-24 23:25:12 +02:00
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
2021-02-08 20:24:20 +01:00
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.fragment.OnBackPressedHandler
2021-02-08 21:46:31 +01:00
import org.moire.ultrasonic.fragment.ServerSettingsModel
2021-02-05 21:45:50 +01:00
import org.moire.ultrasonic.provider.SearchSuggestionProvider
2021-02-08 20:24:20 +01:00
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
2021-02-08 20:24:20 +01:00
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
/**
2021-02-09 19:49:26 +01:00
* 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
2021-02-08 20:24:20 +01:00
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
private val mediaPlayerController: MediaPlayerController by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
2021-02-08 20:24:20 +01:00
private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject()
private val themeChangedEventDistributor: ThemeChangedEventDistributor by inject()
private val permissionUtil: PermissionUtil by inject()
private var infoDialogDisplayed = false
2021-02-08 20:24:20 +01:00
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)
2021-02-08 20:24:20 +01:00
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")
2021-02-08 20:24:20 +01:00
currentFragmentId = destination.id
// Handle the hiding of the NowPlaying fragment when the Player is active
if (currentFragmentId == R.id.playerFragment) {
hideNowPlaying()
} else {
2021-02-08 21:46:31 +01:00
if (!nowPlayingHidden) showNowPlaying()
2021-02-08 20:24:20 +01:00
}
2021-02-09 19:49:26 +01:00
// 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()
}
2021-02-08 20:24:20 +01:00
nowPlayingEventListener = object : NowPlayingEventListener {
override fun onDismissNowPlaying() {
nowPlayingHidden = true
hideNowPlaying()
2021-02-08 20:24:20 +01:00
}
override fun onHideNowPlaying() {
hideNowPlaying()
}
override fun onShowNowPlaying() {
showNowPlaying()
}
}
themeChangedEventListener = object : ThemeChangedEventListener {
override fun onThemeChanged() { recreate() }
}
2021-02-08 20:24:20 +01:00
nowPlayingEventDistributor.subscribe(nowPlayingEventListener)
themeChangedEventDistributor.subscribe(themeChangedEventListener)
}
override fun onResume() {
super.onResume()
2021-02-09 19:49:26 +01:00
setMenuForServerSetting()
// Lifecycle support's constructor registers some event receivers so it should be created early
lifecycleSupport.onCreate()
2021-02-08 21:46:31 +01:00
if (!nowPlayingHidden) showNowPlaying()
else hideNowPlaying()
}
override fun onDestroy() {
2021-02-09 19:49:26 +01:00
super.onDestroy()
2021-02-08 20:24:20 +01:00
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
2021-02-05 21:45:50 +01:00
// 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
2021-02-05 21:45:50 +01:00
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)
2021-02-05 21:45:50 +01:00
if (query != null) {
val autoPlay = intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
val suggestions = SearchRecentSuggestions(
this,
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
)
2021-02-05 21:45:50 +01:00
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, _ ->
2021-06-14 15:42:24 +02:00
// 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))
}
}
2021-02-08 20:24:20 +01:00
private fun showNowPlaying() {
if (!Settings.showNowPlaying) {
2021-02-08 20:24:20 +01:00
hideNowPlaying()
return
}
2021-02-08 21:46:31 +01:00
// 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
2021-02-08 20:24:20 +01:00
// 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
}
2021-02-09 19:49:26 +01:00
private fun setMenuForServerSetting() {
2021-05-25 18:51:00 +02:00
if (isOffline()) {
2021-05-24 23:25:12 +02:00
chatMenuItem?.isVisible = false
bookmarksMenuItem?.isVisible = false
sharesMenuItem?.isVisible = false
podcastsMenuItem?.isVisible = false
return
}
2021-05-29 15:30:01 +02:00
val activeServerProvider: ActiveServerProvider by inject()
2021-05-29 15:46:09 +02:00
val activeServer = activeServerProvider.getActiveServer()
chatMenuItem?.isVisible = activeServer.chatSupport != false
bookmarksMenuItem?.isVisible = activeServer.bookmarkSupport != false
sharesMenuItem?.isVisible = activeServer.shareSupport != false
podcastsMenuItem?.isVisible = activeServer.podcastSupport != false
2021-02-09 19:49:26 +01:00
}
}