package org.moire.ultrasonic.activity import android.app.AlertDialog import android.app.SearchManager import android.content.Intent import android.content.res.ColorStateList 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 android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat 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.button.MaterialButton import com.google.android.material.navigation.NavigationView import io.reactivex.rxjava3.disposables.Disposable 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.ServerSettingDao 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.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.PermissionUtil import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.UncaughtExceptionHandler 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 var selectServerButton: MaterialButton? = null private var headerBackgroundImage: ImageView? = null private lateinit var appBarConfiguration: AppBarConfiguration private var themeChangedEventSubscription: Disposable? = null private var playerStateSubscription: Disposable? = null 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 permissionUtil: PermissionUtil by inject() private val activeServerProvider: ActiveServerProvider by inject() private val serverRepository: ServerSettingDao by inject() private var infoDialogDisplayed = false private var currentFragmentId: Int = 0 private var cachedServerCount: 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(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 setMenuForServerCapabilities() } // 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() } RxBus.dismissNowPlayingCommandObservable.subscribe { nowPlayingHidden = true hideNowPlaying() } playerStateSubscription = RxBus.playerStateObservable.subscribe { if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED) showNowPlaying() else hideNowPlaying() } themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe { recreate() } serverRepository.liveServerCount().observe( this, { count -> cachedServerCount = count ?: 0 updateNavigationHeaderForServer() } ) ActiveServerProvider.liveActiveServerId.observe(this, { updateNavigationHeaderForServer() }) } private fun updateNavigationHeaderForServer() { val activeServer = activeServerProvider.getActiveServer() if (cachedServerCount == 0) selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name) else selectServerButton?.text = activeServer.name val foregroundColor = ServerColor.getForegroundColor(this, activeServer.color) val backgroundColor = ServerColor.getBackgroundColor(this, activeServer.color) if (activeServer.index == 0) selectServerButton?.icon = ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off_dark) else selectServerButton?.icon = ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server_dark) selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor) selectServerButton?.setTextColor(foregroundColor) headerBackgroundImage?.setBackgroundColor(backgroundColor) } override fun onResume() { super.onResume() setMenuForServerCapabilities() // 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() themeChangedEventSubscription?.dispose() playerStateSubscription?.dispose() 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) selectServerButton = navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server) selectServerButton?.setOnClickListener { if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) this.drawerLayout?.closeDrawer(GravityCompat.START) navController.navigate(R.id.serverSelectorFragment) } headerBackgroundImage = navigationView?.getHeaderView(0)?.findViewById(R.id.img_header_bg) } 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.serverSelectorFragment) } .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 UncaughtExceptionHandler) { Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler(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 setMenuForServerCapabilities() { if (ActiveServerProvider.isOffline()) { chatMenuItem?.isVisible = false bookmarksMenuItem?.isVisible = false sharesMenuItem?.isVisible = false podcastsMenuItem?.isVisible = false return } val activeServer = activeServerProvider.getActiveServer() chatMenuItem?.isVisible = activeServer.chatSupport != false bookmarksMenuItem?.isVisible = activeServer.bookmarkSupport != false sharesMenuItem?.isVisible = activeServer.shareSupport != false podcastsMenuItem?.isVisible = activeServer.podcastSupport != false } }