diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7f32fce..72740cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -256,10 +256,10 @@ dependencies { implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}") // Conductor - val conductorVersion = "3.1.7" + val conductorVersion = "4.0.0-preview-2" implementation("com.bluelinelabs:conductor:$conductorVersion") implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion") - implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion") + implementation("com.github.tachiyomiorg:conductor-support-preference:3.1.7") // Crash reports/analytics implementation("ch.acra:acra-http:5.9.7") @@ -268,6 +268,9 @@ dependencies { // Markdown implementation("io.noties.markwon:core:4.6.2") + + // Memory leaks + implementation("com.squareup.leakcanary:leakcanary-android:2.7") } tasks { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2ac22ff..2c35405 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ (bundle: Bundle? = null) : lateinit var viewScope: CoroutineScope + init { + watchForLeaks() + } + init { addLifecycleListener( object : LifecycleListener() { @@ -59,7 +64,8 @@ abstract class BaseController(bundle: Bundle? = null) : return binding.root } - open fun onViewCreated(view: View) {} + @CallSuper + open fun onViewCreated(view: View) = Unit override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { view?.hideKeyboard() diff --git a/app/src/main/java/org/xtimms/ridebus/ui/base/controller/OneWayFadeChangeHandler.kt b/app/src/main/java/org/xtimms/ridebus/ui/base/controller/OneWayFadeChangeHandler.kt index e6f3ab1..b9c5992 100644 --- a/app/src/main/java/org/xtimms/ridebus/ui/base/controller/OneWayFadeChangeHandler.kt +++ b/app/src/main/java/org/xtimms/ridebus/ui/base/controller/OneWayFadeChangeHandler.kt @@ -5,20 +5,10 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.view.View import android.view.ViewGroup -import com.bluelinelabs.conductor.ControllerChangeHandler -import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler -/** - * A variation of [FadeChangeHandler] that only fades in. - */ -class OneWayFadeChangeHandler : FadeChangeHandler { - constructor() - constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush) - constructor(duration: Long) : super(duration) - constructor(duration: Long, removesFromViewOnPush: Boolean) : super( - duration, - removesFromViewOnPush - ) +class OneWayFadeChangeHandler : + AnimatorChangeHandler(DEFAULT_ANIMATION_DURATION, true) { override fun getAnimator( container: ViewGroup, @@ -33,14 +23,12 @@ class OneWayFadeChangeHandler : FadeChangeHandler { animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f)) } - if (from != null && (!isPush || removesFromViewOnPush())) { + if (from != null) { from.alpha = 0f } return animator } - override fun copy(): ControllerChangeHandler { - return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush()) - } + override fun resetFromView(from: View) = Unit } diff --git a/app/src/main/java/org/xtimms/ridebus/ui/base/controller/controllerLeakWatcher.kt b/app/src/main/java/org/xtimms/ridebus/ui/base/controller/controllerLeakWatcher.kt new file mode 100644 index 0000000..4939ced --- /dev/null +++ b/app/src/main/java/org/xtimms/ridebus/ui/base/controller/controllerLeakWatcher.kt @@ -0,0 +1,47 @@ +package org.xtimms.ridebus.ui.base.controller + +import android.view.View +import com.bluelinelabs.conductor.Controller +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import leakcanary.AppWatcher + +private class RefWatchingControllerLifecycleListener : Controller.LifecycleListener() { + + private var hasExited = false + + override fun postDestroy(controller: Controller) { + if (hasExited) { + controller.expectWeaklyReachable() + } + } + + override fun preDestroyView(controller: Controller, view: View) { + AppWatcher.objectWatcher.expectWeaklyReachable( + view, + "A destroyed controller view should have only weak references." + ) + } + + override fun onChangeEnd( + controller: Controller, + changeHandler: ControllerChangeHandler, + changeType: ControllerChangeType + ) { + hasExited = !changeType.isEnter + if (controller.isDestroyed) { + controller.expectWeaklyReachable() + } + } + + private fun Controller.expectWeaklyReachable() { + AppWatcher.objectWatcher.expectWeaklyReachable( + this, + "A destroyed controller should have only weak references." + ) + } +} + +fun Controller.watchForLeaks() { + addLifecycleListener(RefWatchingControllerLifecycleListener()) +} diff --git a/app/src/main/java/org/xtimms/ridebus/ui/main/MainActivity.kt b/app/src/main/java/org/xtimms/ridebus/ui/main/MainActivity.kt index 7de0825..7f885a3 100644 --- a/app/src/main/java/org/xtimms/ridebus/ui/main/MainActivity.kt +++ b/app/src/main/java/org/xtimms/ridebus/ui/main/MainActivity.kt @@ -9,7 +9,6 @@ import android.os.Build import android.os.Bundle import android.view.Gravity import android.view.ViewGroup -import android.widget.Toast import androidx.core.animation.doOnEnd import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -22,7 +21,7 @@ import androidx.core.view.isVisible import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.whenResumed +import androidx.lifecycle.withResumed import androidx.preference.PreferenceDialogController import com.bluelinelabs.conductor.Conductor import com.bluelinelabs.conductor.Controller @@ -30,8 +29,6 @@ import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.Router import com.google.android.material.navigation.NavigationBarView import dev.chrisbanes.insetter.applyInsetter -import kotlin.collections.set -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import logcat.LogPriority import org.xtimms.ridebus.BuildConfig @@ -64,6 +61,7 @@ import org.xtimms.ridebus.util.system.isTablet import org.xtimms.ridebus.util.system.logcat import org.xtimms.ridebus.util.system.toast import org.xtimms.ridebus.util.view.setNavigationBarTransparentCompat +import kotlin.collections.set class MainActivity : BaseActivity() { @@ -171,6 +169,8 @@ class MainActivity : BaseActivity() { val container: ViewGroup = binding.controllerContainer router = Conductor.attachRouter(this, container, savedInstanceState) + .setPopRootControllerMode(Router.PopRootControllerMode.NEVER) + .setOnBackPressedDispatcherEnabled(true) router.addChangeListener( object : ControllerChangeHandler.ControllerChangeListener { override fun onChangeStarted( @@ -209,7 +209,7 @@ class MainActivity : BaseActivity() { launchUI { requestNotificationsPermission() when (preferences.city().defaultValue) { - "-1" -> whenResumed { + "-1" -> withResumed { WelcomeDialogController().showDialog(router) } } @@ -373,35 +373,6 @@ class MainActivity : BaseActivity() { binding.toolbar.setNavigationOnClickListener(null) } - override fun onBackPressed() { - val backstackSize = router.backstackSize - if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) { - // Return to start screen - setSelectedNavItem(startScreenId) - } else if (shouldHandleExitConfirmation()) { - // Exit confirmation (resets after 2 seconds) - lifecycleScope.launchUI { resetExitConfirmation() } - } else if (backstackSize == 1 || !router.handleBack()) { - // Regular back - super.onBackPressed() - } - } - - private suspend fun resetExitConfirmation() { - isConfirmingExit = true - val toast = toast(R.string.confirm_exit, Toast.LENGTH_LONG) - delay(2000) - toast.cancel() - isConfirmingExit = false - } - - private fun shouldHandleExitConfirmation(): Boolean { - return router.backstackSize == 1 && - router.getControllerWithTag("$startScreenId") != null && - preferences.confirmExit() && - !isConfirmingExit - } - private fun setSelectedNavItem(itemId: Int) { if (!isFinishing) { nav.selectedItemId = itemId