Use Notifications Master app ID
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
package se.ajpanton.notificationsmaster
|
||||
|
||||
import android.os.Bundle
|
||||
import android.hardware.biometrics.BiometricPrompt
|
||||
import android.hardware.biometrics.BiometricManager.Authenticators
|
||||
import android.os.CancellationSignal
|
||||
import android.content.res.Configuration
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
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.core.view.forEach
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import se.ajpanton.notificationsmaster.databinding.ActivityMainBinding
|
||||
import se.ajpanton.notificationsmaster.settings.AppLockStore
|
||||
import se.ajpanton.notificationsmaster.settings.LogAppFilterStore
|
||||
|
||||
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var drawerToggle: ActionBarDrawerToggle
|
||||
private var permanentSidebar = false
|
||||
private var needsUnlock = true
|
||||
private lateinit var appLockStore: AppLockStore
|
||||
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)
|
||||
appLockStore = AppLockStore(this)
|
||||
updateSensitiveWindowPolicy(appLockStore.enabled)
|
||||
// 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)
|
||||
showLockedOverlay(appLockStore.enabled)
|
||||
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 onResume() {
|
||||
super.onResume()
|
||||
if (needsUnlock && appLockStore.enabled) {
|
||||
showLockedOverlay(true)
|
||||
requestUnlock()
|
||||
} else {
|
||||
showLockedOverlay(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (!isChangingConfigurations) {
|
||||
needsUnlock = true
|
||||
if (appLockStore.enabled) showLockedOverlay(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestUnlock() {
|
||||
BiometricPrompt.Builder(this)
|
||||
.setTitle("Unlock Notifications Master")
|
||||
.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL)
|
||||
.build()
|
||||
.authenticate(CancellationSignal(), mainExecutor, object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
|
||||
needsUnlock = false
|
||||
showLockedOverlay(false)
|
||||
}
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { finishAndRemoveTask() }
|
||||
})
|
||||
}
|
||||
|
||||
private fun showLockedOverlay(visible: Boolean) {
|
||||
binding.lockOverlay.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
/** Called immediately when the Settings switch changes, before a task snapshot can be captured. */
|
||||
fun setAppLockEnabled(enabled: Boolean) {
|
||||
appLockStore.enabled = enabled
|
||||
updateSensitiveWindowPolicy(enabled)
|
||||
}
|
||||
|
||||
private fun updateSensitiveWindowPolicy(locked: Boolean) {
|
||||
setRecentsScreenshotEnabled(!locked)
|
||||
if (locked) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
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 == Page.SETTINGS -> SettingsFragment()
|
||||
page == Page.LOG_DISPLAY -> LogDisplayFragment()
|
||||
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
|
||||
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
|
||||
else -> PageFragment.newInstance(page.titleRes)
|
||||
},
|
||||
)
|
||||
runOnCommit(::applyPageTitleVisibility)
|
||||
}
|
||||
title = getString(page.titleRes)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
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
|
||||
menu.forEach { item ->
|
||||
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 {
|
||||
var visible = 0
|
||||
menu.forEach { if (it.isVisible) visible++ }
|
||||
return visible
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun updateLogFilterAction() {
|
||||
if (currentItemId == R.id.nav_view_logs) invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
return updateToolbarActions(menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
menu.clear()
|
||||
return updateToolbarActions(menu)
|
||||
}
|
||||
|
||||
private fun updateToolbarActions(menu: Menu): Boolean {
|
||||
if (currentItemId != R.id.nav_view_logs) return false
|
||||
val active = LogAppFilterStore(this).selectedPackages().isNotEmpty()
|
||||
menu.add(Menu.NONE, MENU_LOG_FILTER, Menu.NONE, "Filter logs by app").apply {
|
||||
setIcon(if (active) R.drawable.ic_log_filter_active else R.drawable.ic_log_filter)
|
||||
icon?.setTint(getColor(R.color.on_primary))
|
||||
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == MENU_LOG_FILTER) {
|
||||
(supportFragmentManager.findFragmentById(R.id.content_frame) as? ViewLogsFragment)?.showAppFilter()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private enum class Page(
|
||||
val menuId: Int,
|
||||
val titleRes: Int,
|
||||
) {
|
||||
VIEW_LOGS(R.id.nav_view_logs, R.string.page_view_logs),
|
||||
SETTINGS(R.id.nav_settings, R.string.page_settings),
|
||||
LOG_DISPLAY(R.id.nav_log_display, R.string.page_log_display),
|
||||
FILTER_LOGGING(R.id.nav_filter_logging, R.string.page_filter_logging),
|
||||
FILTER_APPS(R.id.nav_filter_apps, R.string.page_filter_apps),
|
||||
;
|
||||
|
||||
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 MENU_LOG_FILTER = 1
|
||||
const val PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER = 1.1f
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user