diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt index e62b99ba7..c006ad209 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt @@ -190,8 +190,13 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C private var initialListTop: Int = 0 + /** + * Holds the view binding reference for this fragment. + * Use bindingOrNull for safe access after view destruction. + */ private var _binding: FragmentMediaDetailBinding? = null private val binding get() = _binding!! + private val bindingOrNull get() = _binding private var descriptionHtmlCode: String? = null @@ -340,25 +345,27 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C handleBackEvent(view) - //set onCLick listeners - binding.mediaDetailLicense.setOnClickListener { onMediaDetailLicenceClicked() } - binding.mediaDetailCoordinates.setOnClickListener { onMediaDetailCoordinatesClicked() } - binding.sendThanks.setOnClickListener { sendThanksToAuthor() } - binding.dummyCaptionDescriptionContainer.setOnClickListener { showCaptionAndDescription() } - binding.mediaDetailImageView.setOnClickListener { - launchZoomActivity( - binding.mediaDetailImageView - ) + /** + * Safely sets click listeners on media detail UI elements using bindingOrNull. + * Prevents potential crashes caused by view being destroyed during delayed callbacks. + */ + bindingOrNull?.let { binding -> + binding.mediaDetailLicense.setOnClickListener { onMediaDetailLicenceClicked() } + binding.mediaDetailCoordinates.setOnClickListener { onMediaDetailCoordinatesClicked() } + binding.sendThanks.setOnClickListener { sendThanksToAuthor() } + binding.dummyCaptionDescriptionContainer.setOnClickListener { showCaptionAndDescription() } + binding.mediaDetailImageView.setOnClickListener { + launchZoomActivity(binding.mediaDetailImageView) + } + binding.categoryEditButton.setOnClickListener { onCategoryEditButtonClicked() } + binding.depictionsEditButton.setOnClickListener { onDepictionsEditButtonClicked() } + binding.seeMore.setOnClickListener { onSeeMoreClicked() } + binding.mediaDetailAuthor.setOnClickListener { onAuthorViewClicked() } + binding.nominateDeletion.setOnClickListener { onDeleteButtonClicked() } + binding.descriptionEdit.setOnClickListener { onDescriptionEditClicked() } + binding.coordinateEdit.setOnClickListener { onUpdateCoordinatesClicked() } + binding.copyWikicode.setOnClickListener { onCopyWikicodeClicked() } } - binding.categoryEditButton.setOnClickListener { onCategoryEditButtonClicked() } - binding.depictionsEditButton.setOnClickListener { onDepictionsEditButtonClicked() } - binding.seeMore.setOnClickListener { onSeeMoreClicked() } - binding.mediaDetailAuthor.setOnClickListener { onAuthorViewClicked() } - binding.nominateDeletion.setOnClickListener { onDeleteButtonClicked() } - binding.descriptionEdit.setOnClickListener { onDescriptionEditClicked() } - binding.coordinateEdit.setOnClickListener { onUpdateCoordinatesClicked() } - binding.copyWikicode.setOnClickListener { onCopyWikicodeClicked() } - binding.fileUsagesComposeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -404,14 +411,19 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C /** * Gets the height of the frame layout as soon as the view is ready and updates aspect ratio * of the picture. + * Updated to use bindingOrNull inside delayed callbacks to prevent crashes + * when view is destroyed before post/postDelayed is executed. */ - view.post{ - val width = binding.mediaDetailScrollView.width + view.post { + val safeBinding = bindingOrNull ?: return@post + val width = safeBinding.mediaDetailScrollView.width if (width > 0) { - frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight + frameLayoutHeight = safeBinding.mediaDetailFrameLayout.measuredHeight updateAspectRatio(width) } else { - view.postDelayed({ updateAspectRatio(binding.root.width) }, 1) + view.postDelayed({ + bindingOrNull?.let { updateAspectRatio(it.root.width) } + }, 1) } } @@ -526,19 +538,25 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C binding.sendThanks.visibility = View.VISIBLE } - binding.mediaDetailScrollView.viewTreeObserver.addOnGlobalLayoutListener( + /** + * Sets a one-time global layout listener to safely access view dimensions. + * Uses bindingOrNull to avoid crash if the fragment view is destroyed + * before the callback is executed. + */ + bindingOrNull?.mediaDetailScrollView?.viewTreeObserver?.addOnGlobalLayoutListener( object : OnGlobalLayoutListener { override fun onGlobalLayout() { + val safeBinding = bindingOrNull ?: return if (context == null) { return } - binding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener( + safeBinding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener( this ) - oldWidthOfImageView = binding.mediaDetailScrollView.width - if (media != null) { + oldWidthOfImageView = safeBinding.mediaDetailScrollView.width + media?.filename?.let { displayMediaDetails() - fetchFileUsages(media?.filename!!) + fetchFileUsages(it) } } } @@ -548,23 +566,28 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - binding.mediaDetailScrollView.viewTreeObserver.addOnGlobalLayoutListener( + /** + * Sets a global layout listener to update the aspect ratio when device configuration changes. + * Uses bindingOrNull to avoid crashes in case the view is destroyed before callback. + */ + bindingOrNull?.mediaDetailScrollView?.viewTreeObserver?.addOnGlobalLayoutListener( object : OnGlobalLayoutListener { override fun onGlobalLayout() { + val safeBinding = bindingOrNull ?: return /** * We update the height of the frame layout as the configuration changes. */ - binding.mediaDetailFrameLayout.post { - frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight - updateAspectRatio(binding.mediaDetailScrollView.width) + safeBinding.mediaDetailFrameLayout.post { + frameLayoutHeight = safeBinding.mediaDetailFrameLayout.measuredHeight + updateAspectRatio(safeBinding.mediaDetailScrollView.width) } - if (binding.mediaDetailScrollView.width != oldWidthOfImageView) { + if (safeBinding.mediaDetailScrollView.width != oldWidthOfImageView) { if (newWidthOfImageView == 0) { - newWidthOfImageView = binding.mediaDetailScrollView.width + newWidthOfImageView = safeBinding.mediaDetailScrollView.width updateAspectRatio(newWidthOfImageView) } - binding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener( - this + safeBinding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener( + this ) } } @@ -784,9 +807,13 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C } } + /** + * Clears binding and removes layout listener to prevent memory leaks + * or crashes from delayed UI callbacks after the view is destroyed. + */ override fun onDestroyView() { - if (layoutListener != null && view != null) { - requireView().viewTreeObserver.removeGlobalOnLayoutListener(layoutListener) // old Android was on crack. CRACK IS WHACK + layoutListener?.let { + view?.viewTreeObserver?.removeOnGlobalLayoutListener(it) layoutListener = null } @@ -2263,4 +2290,4 @@ fun FileUsagesContainer( } } -} +} \ No newline at end of file