ntfy
ntfy copied to clipboard
Android: notifications with large images attached crash the app
Hello
I've noticed this for a while but has it's only trigger by very large images, it's difficult to find a case.
A notification sent via curl with a very large image attached to a topic makes the app crash when opening that topic.
All other topics keep working, the app keeps receiving more notifications on that topic although unable to display them.
The last image to cause this was this: (WARNING: NSFW!)
https://i.imgur.com/pKiQk6l.jpg , 4.4mb 4480x6720
Android creates a log report and sends it somewhere, but i cannot intercept it.
The only error i found was:
java.lang.RuntimeException: Canvas: trying to draw too large(120422400bytes) bitmap.
I have experienced crashes with large photos as well, so I can confirm this happens. I think this should be one of the next Android tickets to be worked on. The app should never crash.
(Side note: You could have linked any other large photo ... Like, one from NASA or Wikipedia...)
This is a little trickier than expected, because I cannot catch a RuntimeException
, and whether the exception is raised depends on your system and your available RAM. Figuring out whether an image is too large is non-trivial, because a JPG of 4480x6720 (as you posted) expends to 120 MB in memory. I could limit previews to 1 MB JPG files, or to certain dimensions, but it's not a sure thing.
The only way to solve this would be to bring in a library that intelligently does the image loading in a memory efficient way. And I' not sure I want to bring in a library just for that.
(I am working on this though, and will hopefully have a solution soon)
Perhaps the server, upon receiving a image could create a thumbnail. The whole file and the thumbnail could be passed to the Android app, but only the thumb would be previewed.
In anycase, i find this issue peculiar. The tested smartphone is not weak on memory, 8GB.
This is the corresponding stack trace I got from the Google Play console for this issue; and from reproducing it locally:
Exception java.lang.RuntimeException: Canvas: trying to draw too large(120422400bytes) bitmap.
at android.graphics.RecordingCanvas.throwIfCannotDraw (RecordingCanvas.java:266)
at android.graphics.BaseRecordingCanvas.drawBitmap (BaseRecordingCanvas.java:94)
at android.graphics.drawable.BitmapDrawable.draw (BitmapDrawable.java:549)
at android.widget.ImageView.onDraw (ImageView.java:1446)
at com.google.android.material.imageview.ShapeableImageView.onDraw (ShapeableImageView.java:198)
at android.view.View.draw (View.java:23195)
at android.view.View.updateDisplayListIfDirty (View.java:22062)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at androidx.constraintlayout.widget.ConstraintLayout.dispatchDraw (ConstraintLayout.java:1994)
at android.view.View.draw (View.java:23198)
at android.view.View.updateDisplayListIfDirty (View.java:22062)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at android.view.View.draw (View.java:23198)
at android.view.View.updateDisplayListIfDirty (View.java:22062)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at androidx.recyclerview.widget.RecyclerView.drawChild (RecyclerView.java:5370)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at android.view.View.draw (View.java:23198)
at androidx.recyclerview.widget.RecyclerView.draw (RecyclerView.java:4769)
at android.view.View.updateDisplayListIfDirty (View.java:22062)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at android.view.View.draw (View.java:23198)
at android.view.View.updateDisplayListIfDirty (View.java:22062)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at androidx.constraintlayout.widget.ConstraintLayout.dispatchDraw (ConstraintLayout.java:1994)
at android.view.View.updateDisplayListIfDirty (View.java:22053)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at android.view.View.updateDisplayListIfDirty (View.java:22053)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at android.view.View.updateDisplayListIfDirty (View.java:22053)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at android.view.View.updateDisplayListIfDirty (View.java:22053)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at android.view.View.updateDisplayListIfDirty (View.java:22053)
at android.view.View.draw (View.java:22926)
at android.view.ViewGroup.drawChild (ViewGroup.java:4529)
at android.view.ViewGroup.dispatchDraw (ViewGroup.java:4290)
at android.view.View.draw (View.java:23198)
at com.android.internal.policy.DecorView.draw (DecorView.java:819)
at android.view.View.updateDisplayListIfDirty (View.java:22062)
at android.view.ThreadedRenderer.updateViewTreeDisplayList (ThreadedRenderer.java:682)
at android.view.ThreadedRenderer.updateRootDisplayList (ThreadedRenderer.java:688)
at android.view.ThreadedRenderer.draw (ThreadedRenderer.java:786)
at android.view.ViewRootImpl.draw (ViewRootImpl.java:4579)
at android.view.ViewRootImpl.performDraw (ViewRootImpl.java:4290)
at android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:3517)
at android.view.ViewRootImpl.doTraversal (ViewRootImpl.java:2286)
at android.view.ViewRootImpl$TraversalRunnable.run (ViewRootImpl.java:8948)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1231)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1239)
at android.view.Choreographer.doCallbacks (Choreographer.java:899)
at android.view.Choreographer.doFrame (Choreographer.java:832)
at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:1214)
at android.os.Handler.handleCallback (Handler.java:942)
at android.os.Handler.dispatchMessage (Handler.java:99)
at android.os.Looper.loopOnce (Looper.java:201)
at android.os.Looper.loop (Looper.java:288)
at android.app.ActivityThread.main (ActivityThread.java:7893)
at java.lang.reflect.Method.invoke (Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:936)
I managed to monkey patch this using this fix: https://github.com/binwiederhier/ntfy-android/commit/d09fdb2ff259f9e4d8cf83b515d37f4d3062d6b3 - The workaround is from here: https://stackoverflow.com/a/53334563/1440785
It works by checking if the loaded image is > 100 MB in memory and exits out gracefully if it does. The image is not rendered in that case. I could have gone for a "resize and display" option, but I think this is sufficient for now.
Will be in the next Android release.