I am implementing a call answer screen. In this screen, I am using both proximity sensor and power manager wake lock to create the effect of screen turning off when the phone is close to your face. I've managed to implement the feature, however it is causing memory leak. Since I detected the leak when the fragment is still simple and contains few code, I've removed several classes and functions to trace and confirm the cause of the leak. I've managed to narrow down the cause of the leak to PowerManager.WakeLock.
this is the code that I use to implement the feature in the fragment. I've tried to release the wake lock on multiple point in fragment lifecycle, however it still causes memory leak.
override val sensorManager: SensorManager
get() = requireContext().getSystemService(Context.SENSOR_SERVICE) as SensorManager
override var proximitySensor: Sensor? = null
override val powerManager: PowerManager =
requireContext().getSystemService(Context.POWER_SERVICE) as PowerManager
override val lock: PowerManager.WakeLock = powerManager.newWakeLock(
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
PROXIMITY_WAKE_LOG_TAG
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
activity?.onBackPressedDispatcher?.addCallback {
activity?.finishAndRemoveTask()
}
}
override fun onResume() {
super.onResume()
proximitySensor?.let { proximity ->
sensorManager.apply {
registerListener(
this#AnswerFragment,
proximity,
SensorManager.SENSOR_DELAY_NORMAL
)
}
}
}
override fun onPause() {
super.onPause()
sensorManager.unregisterListener(this)
if (lock.isHeld) {
lock.release()
}
}
override fun onDestroyView() {
super.onDestroyView()
proximitySensor = null
if (lock.isHeld) {
lock.release()
}
_binding = null
}
override fun onDestroy() {
if (lock.isHeld) {
lock.release()
}
super.onDestroy()
}
override fun onSensorChanged(event: SensorEvent?) {
if (event?.values?.get(0) == 0.0f) {
// Object is near phone, turn off screen
lock.acquire()
} else {
// Object is not near phone, turn on screen
if (lock.isHeld) {
lock.release()
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
As a side note: I am also getting this error in my log
WakeLock finalized while still held
This is a known issue. See the LeakCanary issue and the underlying tools issue. For now, I recommend that you consider it to be a false positive, and ignore the leak.
Related
firstly, the code:
class MapFragment: BaseFragment<MapFragmentBinding>(R.layout.map_fragment) {
var mapView: MapView? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mapView = binding.mapView
MapsInitializer.initialize(requireContext(),
MapsInitializer.Renderer.LATEST
) {}
// seems a bit out of place, but due to the binding variable, from out baseFragment class, it has to be done here
mapView?.onCreate(savedInstanceState)
mapView?.getMapAsync { gMap ->
gMap.setOnMapLoadedCallback {
val randomLocation = LatLng(
Random.nextDouble(-170.0, 170.0),
Random.nextDouble(-170.0, 170.0)
)
gMap.addMarker(MarkerOptions().position(randomLocation).title(randomLocation.toString()))
gMap.animateCamera(CameraUpdateFactory.newLatLngZoom(randomLocation, 10.0f))
}
}
}
override fun onStart() {
super.onStart()
mapView?.onStart()
}
override fun onPause() {
super.onPause()
mapView?.onPause()
}
override fun onResume() {
super.onResume()
mapView?.onResume()
}
override fun onStop() {
super.onStop()
mapView?.onStop()
}
override fun onLowMemory() {
super.onLowMemory()
mapView?.onLowMemory()
}
override fun onDestroy() {
super.onDestroy()
mapView?.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView?.onSaveInstanceState(outState)
}
}
this is a mapFragment that is included into another fragment. It is reloaded fairly often (to be precise, the app has a recycler view filled with items, whenever one is pressed this fragment is visible).
I am not sure why but sometimes (whenever it is opened and closed 3-7 times) it animates the camera to random location, and does not display the marker at all. I noticed that it happens mostly whenever the "random location" is at Arctic Ocean or Antarctica (believe me I don't know whether that is related at all, I am as confused as you are).
Any hints/ideas why that might be happening?
Your range of a random latitude is invalid -170.0, 170.0. The map camera implementation will likely peg the out-of-range value to -90 or 90 and the map will refuse to place the marker.
A maximum range for random position could use :
val randomLocation = LatLng(
Random.nextDouble(-90.0, 90.0), // kotlin Random produces [-90,90)
Random.nextDouble(-180.0, 180.0)
The maps api for latitude permits:
[-90, 90] // inclusive at both ends
and for longitude permits:
[-180, 180) // inclusive on the negative and exclusive on the positive
I have a simple MainActivity and if the app is completely killed it looks like onCreate() is called once. If however I back out of the app so it still appears in the background, when I re-open it I get every log message twice. The weirdest part is if I generate a random number it is always the same in the 2 log messages.
I've tried adding android:LaunchMode="singleTop" (also singleInstance singleTask) in the activity and application tags of the Manifest.
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = binding.root
setContentView(view)
setupViews()
val data: Uri? = intent?.data
DataHolder.getInstance().setItem(data)
Timber.plant(Timber.DebugTree())
setupInjection()
Timber.d("review nanoTime = ${System.nanoTime()}")
Timber.d("review savedInstance = $savedInstanceState")
Timber.d("review random = ${Random.nextInt()}")
}
override fun onPause() {
Timber.d("review onPause()")
super.onPause()
}
override fun onStop() {
Timber.d("review onStop()")
super.onStop()
}
override fun onDestroy() {
Timber.d("review onDestroy()")
super.onDestroy()
finish()
}
override fun onStart() {
Timber.d("review onStart()")
super.onStart()
}
override fun onRestart() {
Timber.d("review onRestart()")
super.onRestart()
}
override fun onResume() {
Timber.d("review onResume()")
super.onResume()
}
private fun setupInjection() {
val appInjector = InjectorImpl(
firebaseAuth = FirebaseAuth.getInstance()
)
Injector.initialize(appInjector)
}
private fun setupViews() = binding.apply {
val navController = findNavController(R.id.nav_host_fragment)
navView.setupWithNavController(navController)
navView.setOnItemSelectedListener { item ->
when (item.itemId){
R.id.navigation_item_calculator -> {
navController.navigate(BuilderFragmentDirections.actionBuilderToCalculator())
}
R.id.navigation_item_builder -> {
navController.navigate(CalculatorFragmentDirections.actionCalculatorToBuilder())
}
}
true
}
navView.setOnItemReselectedListener { }
}
}
Here is a table of the log trace I get when I run the app on my phone from Android studio. Since the random numbers are the same I feel like this is actually a Logging bug in Android studio and the app isn't actually opened twice.
Realized my problem was with my logging library I used.
Timber was planting a new tree but wasn't uprooting old ones from being backed out so there were 2 instances of them. I fixed by putting a Timber.uprootAll() just before Timber.plant(Timber.DebugTree())
I have a Fragment with Mapbox and I want to display the device location on it.
class SampleMapFragment : Fragment(), PermissionsListener {
private lateinit var binding: FragmentExploreBinding
#Inject
lateinit var permissionsManager: PermissionsManager
private lateinit var mapboxMap: MapboxMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Mapbox.getInstance(requireContext().applicationContext, getString(R.string.mapbox_token))
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if (!::binding.isInitialized) {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_explore,
container,
false
)
binding.lifecycleOwner = this
binding.mapView.onCreate(savedInstanceState)
setUpMap()
}
return binding.root
}
private fun setUpMap() {
binding.mapView.getMapAsync { mapboxMap ->
this.mapboxMap = mapboxMap
mapboxMap.setStyle(Style.MAPBOX_STREETS) { loadedMapStyle ->
starLocationTracking(loadedMapStyle)
}
}
}
private fun starLocationTracking(loadedMapStyle: Style) {
if (!PermissionsManager.areLocationPermissionsGranted(requireContext())) {
permissionsManager.requestLocationPermissions(requireActivity())
return
}
initLocationComponent(loadedMapStyle)
}
private fun initLocationComponent(loadedMapStyle: Style) {
val customLocationComponentOptions = LocationComponentOptions.builder(requireActivity())
.elevation(5f)
.accuracyAlpha(.6f)
.accuracyColor(Color.RED)
.build()
val locationComponentActivationOptions = LocationComponentActivationOptions.builder(
requireActivity(),
loadedMapStyle
)
.locationComponentOptions(customLocationComponentOptions)
.build()
mapboxMap.locationComponent.apply {
activateLocationComponent(locationComponentActivationOptions)
isLocationComponentEnabled = true
renderMode = RenderMode.COMPASS
cameraMode = CameraMode.TRACKING
}
}
override fun onExplanationNeeded(permissionsToExplain: MutableList<String>?) {
// TODO some explanation can be shown here
}
override fun onPermissionResult(granted: Boolean) {
if (granted) mapboxMap.getStyle { loadedStyle -> starLocationTracking(loadedStyle) }
//else TODO some explanation can be shown here
}
fun onMapBoxRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) = permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
override fun onStart() {
super.onStart()
binding.mapView.onStart()
}
override fun onResume() {
super.onResume()
binding.mapView.onResume()
}
override fun onPause() {
super.onPause()
binding.mapView.onPause()
}
override fun onStop() {
super.onStop()
binding.mapView.onStop()
}
override fun onDestroy() {
super.onDestroy()
releaseMapResources()
releasePermissionsManagerListener()
}
private fun releaseMapResources() {
binding.mapView.onDestroy()
}
private fun releasePermissionsManagerListener() {
permissionsManager.listener = null
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
binding.mapView.onSaveInstanceState(outState)
}
override fun onLowMemory() {
super.onLowMemory()
binding.mapView.onLowMemory()
}
}
I tested the implementation above with 2 real devices and 1 emulator.
The solution works fine with the Android 10 device. User location is found, even after closing the location services and opening it again after the map, is visible.
But location is sometimes not found, sometimes it take very long, even the location services are ready before the map is visible while trying with Android 9 and Android 7 devices.
What could be the problem here? Any help would be appreciated.
"But location is sometimes not found, sometimes it take very long,
even the location services are ready before the map is visible while
trying with Android 9 and Android 7 devices."
This sounds as if the device tries to obtain the location from the GPS sensor, but takes long to retrieve it. With newer devices and Android 10 localization has been improved, so this could explain what you are seeing.
To understand what is going on, I would recommend to implement the Android FusedLocationProvider and compare the behaviour to the one wrapped by Mapbox SDKs (locationComponent).
Also, please check whether the "snappy" location you get with Android 10 is actually retrieved from the GPS sensor, or if it is "just" the last known location, that was cached by Android.
None of the other instances of this question are solving my problem. I have a Fragment that appears at the end of a transaction sequence. It is meant to close the app when a CountDownTimer contained within it counts down:
class TerminalFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onStart() {
super.onStart()
startOverButton.setOnClickListener {
returnToStart()
}
initUi()
startCountDown()
}
override fun onStop() {
super.onStop()
AppLog.i(TAG, "onStop()")
stopCountdown()
}
}
private fun startCountDown() {
terminalCountdown = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
AppLog.i(TAG, "Terminal countdown finished")
(context as MainActivity).finish()
}
override fun onTick(millisUntilFinished: Long) {
}
}
.start()
}
private fun stopCountdown() {
AppLog.i(TAG, "stopCountDown() - Terminal countdown stopped")
terminalCountdown?.cancel()
terminalCountdown = null
}
private fun returnToStart() {
AppLog.i(TAG, "returnToStart()")
stopCountdown()
(context as MainActivity).restartFlow()
}
stopCountDown() is being called whenever the fragment is navigated away from, but it somehow survives sometimes and closes the app from another Fragment. Using logs, I've also discovered that there appears to be 2 instances of this countdown sometimes. How do I insure that this countdown is never active outside of this fragment?
As Shrey Greg mentioned in a comment, I was creating a new instance of CountdownTimer every time I wanted to restart it. This lost the reference to the previous instance, making it impossible to cancel.
So I recently migrated to navigation components (2.2.0-alpha01). As I was working on a high end device I didn't really noticed any slowdowns, but as soon as I finished, testers started reporting slugish app navigation.
In my navigation code I use calls like findNavController().navigate(CustomFragmentDirections.actionEtc()) or findNavController().popBackStack(fragmentId, false)
I also use safeargs with navigation.
In my navigation xml I have actions that heavily rely on popUpTo and app:launchSingleTop="true"
To investigate I made very basic profiler in my BaseFragment class:
private var lastTimestamp = System.currentTimeMillis()
protected fun getEllapsedTime(): String {
val currTime = System.currentTimeMillis()
val elapsedTime = currTime - lastTimestamp
lastTimestamp = currTime
return "${elapsedTime}ms"
}
override fun onAttach(context: Context) {
Timber.d("onAttach(${javaClass.simpleName})(${getEllapsedTime()})")
super.onAttach(context)
}
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("onCreate(${javaClass.simpleName})(${getEllapsedTime()})")
super.onCreate(savedInstanceState)
savedInstanceState?.let { restoreState(it) }
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Timber.d("onCreateView(${javaClass.simpleName})(${getEllapsedTime()})")
return inflater.inflate(layoutRes, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Timber.d("onViewCreated(${javaClass.simpleName})(${getEllapsedTime()})")
super.onViewCreated(view, savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
Timber.d("onActivityCreated(${javaClass.simpleName})(${getEllapsedTime()})")
super.onActivityCreated(savedInstanceState)
}
override fun onStart() {
Timber.d("onStart(${javaClass.simpleName})(${getEllapsedTime()})")
super.onStart()
}
override fun onResume() {
Timber.d("onResume(${javaClass.simpleName})(${getEllapsedTime()})")
super.onResume()
requireActivity().window?.decorView?.post {
firstFrameRendered()
}
}
private fun firstFrameRendered() {
Timber.d("onFrameRendered(${javaClass.simpleName})(${getEllapsedTime()})")
}
override fun onPause() {
Timber.d("onPause(${javaClass.simpleName})(${getEllapsedTime()})")
super.onPause()
}
override fun onStop() {
Timber.d("onStop(${javaClass.simpleName})(${getEllapsedTime()})")
super.onStop()
}
override fun onDestroyView() {
Timber.d("onDestroyView(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDestroyView()
}
override fun onDestroy() {
Timber.d("onDestroy(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDestroy()
}
override fun onDetach() {
Timber.d("onDetach(${javaClass.simpleName})(${getEllapsedTime()})")
super.onDetach()
}
I tried profiling using android studio profiler, but didn't really noticed anything out of the ordinary. I also tried window.addOnFrameMetricsAvailableListener but it pretty much gave me same results as my profiler code. The main method of importance is onFrameRendered. Basic Idea is to let layout inflate and render and immediately after screen is rendered count how many milliseconds passed since onResume was called.
I tried different devices and timings were not very consistent, but after measuring same transitions many times I noticed some tendency, that all my layouts now take almost twice as long to load when compared to previous app navigation which was using simple supportFragmentManager transactions.
I tried isolating navigation from one fragment to the other and I would always get this poor performance.
At the moment I know it has something to do with the way navigation switches fragments, because if I mock NavController with my custom code that just directly uses FragmentManager I get the same good performance as the old code. Will update the question if I'll find the exact problem.
Meanwhile, does anyone have any ideas what might be wrong?