package se.ajpanton.notificationlog import android.os.Bundle import android.content.res.Configuration import android.view.Menu import android.view.MenuItem import android.view.View import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.core.graphics.Insets import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.commit import com.google.android.material.navigation.NavigationView import se.ajpanton.notificationlog.databinding.ActivityMainBinding import se.ajpanton.notificationlog.settings.LoggingType class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { private lateinit var binding: ActivityMainBinding private lateinit var drawerToggle: ActionBarDrawerToggle private var permanentSidebar = false private var currentItemId = R.id.nav_view_logs private var drawerNavigationBasePaddingLeft: Int? = null private var permanentNavigationBasePaddingLeft: Int? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Keep every app surface below the system bars rather than relying on edge-to-edge drawing. WindowCompat.setDecorFitsSystemWindows(window, true) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val isNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES WindowInsetsControllerCompat(window, binding.root).isAppearanceLightNavigationBars = !isNightMode setSupportActionBar(binding.toolbar) setupNavigationView(binding.navViewDrawer) setupNavigationView(binding.navViewPermanent) drawerToggle = ActionBarDrawerToggle( this, binding.drawerLayout, binding.toolbar, R.string.navigation_open, R.string.navigation_close, ) binding.drawerLayout.addDrawerListener(drawerToggle) binding.toolbar.setNavigationOnClickListener { if (!permanentSidebar) { binding.drawerLayout.openDrawer(GravityCompat.START) } } onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (!permanentSidebar && binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { binding.drawerLayout.closeDrawer(GravityCompat.START) return } isEnabled = false onBackPressedDispatcher.onBackPressed() isEnabled = true } }) currentItemId = savedInstanceState?.getInt(STATE_CURRENT_ITEM_ID) ?: R.id.nav_view_logs navigateTo(currentItemId) applyNavigationDrawerInsets() syncDrawerToggle() } override fun onSaveInstanceState(outState: Bundle) { outState.putInt(STATE_CURRENT_ITEM_ID, currentItemId) super.onSaveInstanceState(outState) } override fun onNavigationItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.nav_settings_header) { return false } navigateTo(item.itemId) if (!permanentSidebar) { binding.drawerLayout.closeDrawer(GravityCompat.START) } return true } private fun navigateTo(itemId: Int) { currentItemId = itemId syncDrawerSelection(itemId) val page = Page.fromMenuItem(itemId) supportFragmentManager.commit { replace( R.id.content_frame, when { page == Page.VIEW_LOGS -> ViewLogsFragment() page.loggingType != null -> EventSettingsFragment.newInstance(page.titleRes, page.loggingType) else -> PageFragment.newInstance(page.titleRes) }, ) runOnCommit(::applyPageTitleVisibility) } title = getString(page.titleRes) } private fun setupNavigationView(navigationView: NavigationView) { navigationView.setNavigationItemSelectedListener(this) navigationView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> updateNavigationHeaderHeight(navigationView) } } private fun syncDrawerSelection(itemId: Int) { syncNavigationSelection(binding.navViewDrawer, itemId) syncNavigationSelection(binding.navViewPermanent, itemId) } private fun syncNavigationSelection(navigationView: NavigationView, itemId: Int) { val menu = navigationView.menu ?: return for (index in 0 until menu.size()) { val item = menu.getItem(index) if (item.itemId == R.id.nav_settings_header) { item.isCheckable = false } else { item.isCheckable = true item.isChecked = item.itemId == itemId } } navigationView.invalidate() } private fun applyNavigationDrawerInsets() { binding.drawerLayout.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> applyNavigationDrawerMarginsFromRootInsets() } binding.drawerLayout.post(::applyNavigationDrawerMarginsFromRootInsets) } private fun applyNavigationDrawerMarginsFromRootInsets() { val rootInsets = ViewCompat.getRootWindowInsets(binding.drawerLayout) val safeInsets = rootInsets?.getInsetsIgnoringVisibility( WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(), ) ?: Insets.NONE applyNavigationDrawerSafeArea(safeInsets.left, safeInsets.right) } private fun applyNavigationDrawerSafeArea(leftInset: Int, rightInset: Int) { val safeLeft = maxOf(0, leftInset, binding.drawerLayout.paddingLeft) val safeRight = maxOf(0, rightInset, binding.drawerLayout.paddingRight) binding.drawerLayout.setDrawerSafeInsets(safeLeft, safeRight) drawerNavigationBasePaddingLeft = applyNavigationLeftPadding( binding.navViewDrawer, drawerNavigationBasePaddingLeft, safeLeft, ) permanentNavigationBasePaddingLeft = applyNavigationLeftPadding( binding.navViewPermanent, permanentNavigationBasePaddingLeft, 0, ) updatePermanentSidebarMode() } private fun applyNavigationLeftPadding( navigationView: NavigationView, basePaddingLeft: Int?, safeLeft: Int, ): Int { val base = basePaddingLeft ?: navigationView.paddingLeft val leftPadding = base + safeLeft if (navigationView.paddingLeft != leftPadding || navigationView.translationX != 0f) { navigationView.translationX = 0f navigationView.setPadding( leftPadding, navigationView.paddingTop, navigationView.paddingRight, navigationView.paddingBottom, ) navigationView.requestLayout() } return base } private fun updatePermanentSidebarMode() { val rootWidth = binding.drawerLayout.width if (rootWidth <= 0) { return } val drawerWidth = resources.getDimensionPixelSize(R.dimen.navigation_drawer_width) val foldedPortraitWidth = resources.getDimensionPixelSize(R.dimen.folded_portrait_width) val shouldUsePermanentSidebar = rootWidth >= drawerWidth + (foldedPortraitWidth * PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER).toInt() if (permanentSidebar != shouldUsePermanentSidebar) { permanentSidebar = shouldUsePermanentSidebar drawerToggle.isDrawerIndicatorEnabled = !permanentSidebar binding.toolbar.visibility = if (permanentSidebar) View.GONE else View.VISIBLE if (permanentSidebar) { binding.navViewPermanent.visibility = View.VISIBLE binding.drawerLayout.closeDrawer(GravityCompat.START, false) } else { binding.drawerLayout.closeDrawer(GravityCompat.START, false) binding.navViewPermanent.visibility = View.GONE } syncDrawerToggle() } if (permanentSidebar) { binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START) binding.navViewDrawer.visibility = View.GONE } else { binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) binding.navViewDrawer.visibility = View.VISIBLE } updateNavigationHeaderHeight(binding.navViewDrawer) updateNavigationHeaderHeight(binding.navViewPermanent) applyPageTitleVisibility() } private fun updateNavigationHeaderHeight(navigationView: NavigationView) { if (navigationView.headerCount == 0 || navigationView.height <= 0) { return } val menuRows = countVisibleMenuRows(navigationView.menu) val menuHeight = menuRows * resources.getDimensionPixelSize(R.dimen.navigation_menu_item_height) val desiredHeaderHeight = (navigationView.height - menuHeight).coerceIn( resources.getDimensionPixelSize(R.dimen.drawer_header_min_height), resources.getDimensionPixelSize(R.dimen.drawer_header_max_height), ) val header = navigationView.getHeaderView(0) ?: return if (header.layoutParams.height != desiredHeaderHeight) { header.layoutParams.height = desiredHeaderHeight header.requestLayout() } } private fun countVisibleMenuRows(menu: Menu): Int = (0 until menu.size()).count { menu.getItem(it).isVisible } private fun applyPageTitleVisibility() { (supportFragmentManager.findFragmentById(R.id.content_frame) as? PageFragment) ?.setPageTitleVisible(permanentSidebar) } private fun syncDrawerToggle() { drawerToggle.syncState() drawerToggle.drawerArrowDrawable.color = getColor(R.color.on_primary) } private enum class Page( val menuId: Int, val titleRes: Int, val loggingType: LoggingType? = null, ) { VIEW_LOGS(R.id.nav_view_logs, R.string.page_view_logs), SETTINGS(R.id.nav_settings, R.string.page_settings), APPEARING(R.id.nav_appearing, R.string.page_appearing, LoggingType.APPEARING), DISAPPEARING(R.id.nav_disappearing, R.string.page_disappearing, LoggingType.DISAPPEARING), TEXT_CONTENT(R.id.nav_text_content, R.string.page_text_content, LoggingType.TEXT_CONTENT), IMAGE_CONTENT(R.id.nav_image_content, R.string.page_image_content, LoggingType.IMAGE_CONTENT), EDITS(R.id.nav_edits, R.string.page_edits, LoggingType.EDITS), ; companion object { fun fromMenuItem(menuId: Int): Page = entries.firstOrNull { it.menuId == menuId } ?: VIEW_LOGS } } private companion object { const val STATE_CURRENT_ITEM_ID = "current_item_id" const val PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER = 1.1f } }