This commit is contained in:
Dev Jadiya 2025-07-07 19:13:27 +05:30 committed by GitHub
commit 3231d74aa5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 141 additions and 63 deletions

View file

@ -258,6 +258,12 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();

View file

@ -143,6 +143,11 @@ class BookmarkLocationsFragment : DaggerFragment() {
} }
} }
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// Make sure to null out the binding to avoid memory leaks // Make sure to null out the binding to avoid memory leaks

View file

@ -67,6 +67,12 @@ public class BookmarkPicturesFragment extends DaggerFragment {
controller.stop(); controller.stop();
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();

View file

@ -687,14 +687,23 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
override fun onDestroy() { override fun onDestroy() {
try { try {
compositeDisposable.clear() compositeDisposable.clear()
// Remove child fragment safely
contributionsListFragment?.let {
childFragmentManager.beginTransaction()
.remove(it)
.commitAllowingStateLoss()
}
childFragmentManager.removeOnBackStackChangedListener(this) childFragmentManager.removeOnBackStackChangedListener(this)
locationManager!!.unregisterLocationManager() locationManager?.unregisterLocationManager()
locationManager!!.removeLocationListener(this) locationManager?.removeLocationListener(this)
super.onDestroy() // Nullify locationManager to prevent leaks
locationManager = null
} catch (exception: IllegalArgumentException) { } catch (exception: IllegalArgumentException) {
Timber.e(exception) Timber.e(exception)
} catch (exception: IllegalStateException) { } catch (exception: IllegalStateException) {
Timber.e(exception) Timber.e(exception)
} finally {
super.onDestroy()
} }
} }
@ -755,7 +764,9 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
presenter!!.onDetachView() presenter?.onDetachView()
binding = null
contributionsListFragment = null
} }
override fun notifyDataSetChanged() { override fun notifyDataSetChanged() {

View file

@ -527,6 +527,11 @@ after opening the app.
quizChecker!!.cleanup() quizChecker!!.cleanup()
locationManager!!.unregisterLocationManager() locationManager!!.unregisterLocationManager()
// Remove ourself from hashmap to prevent memory leaks // Remove ourself from hashmap to prevent memory leaks
try {
locationManager?.unregisterLocationManager()
} catch (e: Exception) {
Timber.e(e, "Error while cleaning up locationManager")
}
locationManager = null locationManager = null
super.onDestroy() super.onDestroy()
} }

View file

@ -196,8 +196,13 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
private var initialListTop: Int = 0 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 var _binding: FragmentMediaDetailBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val bindingOrNull get() = _binding
private var descriptionHtmlCode: String? = null private var descriptionHtmlCode: String? = null
@ -249,7 +254,9 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
private val scrollPosition: Unit private val scrollPosition: Unit
get() { get() {
initialListTop = binding.mediaDetailScrollView.scrollY bindingOrNull?.let {
initialListTop = it.mediaDetailScrollView.scrollY
}
} }
override fun onCreateView( override fun onCreateView(
@ -345,25 +352,27 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
handleBackEvent(view) handleBackEvent(view)
//set onCLick listeners /**
binding.mediaDetailLicense.setOnClickListener { onMediaDetailLicenceClicked() } * Safely sets click listeners on media detail UI elements using bindingOrNull.
binding.mediaDetailCoordinates.setOnClickListener { onMediaDetailCoordinatesClicked() } * Prevents potential crashes caused by view being destroyed during delayed callbacks.
binding.sendThanks.setOnClickListener { sendThanksToAuthor() } */
binding.dummyCaptionDescriptionContainer.setOnClickListener { showCaptionAndDescription() } bindingOrNull?.let { binding ->
binding.mediaDetailImageView.setOnClickListener { binding.mediaDetailLicense.setOnClickListener { onMediaDetailLicenceClicked() }
launchZoomActivity( binding.mediaDetailCoordinates.setOnClickListener { onMediaDetailCoordinatesClicked() }
binding.mediaDetailImageView 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 { binding.fileUsagesComposeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { setContent {
@ -409,14 +418,19 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
/** /**
* Gets the height of the frame layout as soon as the view is ready and updates aspect ratio * Gets the height of the frame layout as soon as the view is ready and updates aspect ratio
* of the picture. * of the picture.
* Updated to use bindingOrNull inside delayed callbacks to prevent crashes
* when view is destroyed before post/postDelayed is executed.
*/ */
view.post{ view.post {
val width = binding.mediaDetailScrollView.width val safeBinding = bindingOrNull ?: return@post
val width = safeBinding.mediaDetailScrollView.width
if (width > 0) { if (width > 0) {
frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight frameLayoutHeight = safeBinding.mediaDetailFrameLayout.measuredHeight
updateAspectRatio(width) updateAspectRatio(width)
} else { } else {
view.postDelayed({ updateAspectRatio(binding.root.width) }, 1) view.postDelayed({
bindingOrNull?.let { updateAspectRatio(it.root.width) }
}, 1)
} }
} }
@ -531,45 +545,56 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
binding.sendThanks.visibility = View.VISIBLE 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 { object : OnGlobalLayoutListener {
override fun onGlobalLayout() { override fun onGlobalLayout() {
val safeBinding = bindingOrNull ?: return
if (context == null) { if (context == null) {
return return
} }
binding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener( safeBinding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener(
this this
) )
oldWidthOfImageView = binding.mediaDetailScrollView.width oldWidthOfImageView = safeBinding.mediaDetailScrollView.width
if (media != null) { media?.filename?.let {
displayMediaDetails() displayMediaDetails()
fetchFileUsages(media?.filename!!) fetchFileUsages(it)
} }
} }
} }
) )
binding.progressBarEdit.visibility = View.GONE bindingOrNull?.progressBarEdit?.visibility = View.GONE
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) 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 { object : OnGlobalLayoutListener {
override fun onGlobalLayout() { override fun onGlobalLayout() {
val safeBinding = bindingOrNull ?: return
/** /**
* We update the height of the frame layout as the configuration changes. * We update the height of the frame layout as the configuration changes.
*/ */
binding.mediaDetailFrameLayout.post { safeBinding.mediaDetailFrameLayout.post {
frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight frameLayoutHeight = safeBinding.mediaDetailFrameLayout.measuredHeight
updateAspectRatio(binding.mediaDetailScrollView.width) updateAspectRatio(safeBinding.mediaDetailScrollView.width)
} }
if (binding.mediaDetailScrollView.width != oldWidthOfImageView) { if (safeBinding.mediaDetailScrollView.width != oldWidthOfImageView) {
if (newWidthOfImageView == 0) { if (newWidthOfImageView == 0) {
newWidthOfImageView = binding.mediaDetailScrollView.width newWidthOfImageView = safeBinding.mediaDetailScrollView.width
updateAspectRatio(newWidthOfImageView) updateAspectRatio(newWidthOfImageView)
} }
binding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener( safeBinding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener(
this this
) )
} }
} }
@ -618,7 +643,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
} }
private fun onMediaRefreshed(media: Media) { private fun onMediaRefreshed(media: Media) {
media.categories = this.media!!.categories media.categories = this.media?.categories
this.media = media this.media = media
setTextFields(media) setTextFields(media)
compositeDisposable.addAll( compositeDisposable.addAll(
@ -632,29 +657,30 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
} }
private fun onDiscussionLoaded(discussion: String) { private fun onDiscussionLoaded(discussion: String) {
binding.mediaDetailDisc.text = prettyDiscussion(discussion.trim { it <= ' ' }) bindingOrNull?.mediaDetailDisc?.text = prettyDiscussion(discussion.trim { it <= ' ' })
} }
private fun onDeletionPageExists(deletionPageExists: Boolean) { private fun onDeletionPageExists(deletionPageExists: Boolean) {
if (getUserName(requireContext()) == null && getUserName(requireContext()) != media!!.author) { val safeBinding = bindingOrNull ?: return
binding.nominateDeletion.visibility = View.GONE
binding.nominatedDeletionBanner.visibility = View.GONE if (getUserName(requireContext()) == null && getUserName(requireContext()) != media?.author) {
safeBinding.nominateDeletion.visibility = View.GONE
safeBinding.nominatedDeletionBanner.visibility = View.GONE
} else if (deletionPageExists) { } else if (deletionPageExists) {
if (applicationKvStore.getBoolean( if (applicationKvStore.getBoolean(
String.format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl), false String.format(NOMINATING_FOR_DELETION_MEDIA, media?.imageUrl), false
) )
) { ) {
applicationKvStore.remove( applicationKvStore.remove(
String.format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl) String.format(NOMINATING_FOR_DELETION_MEDIA, media?.imageUrl)
) )
binding.progressBarDeletion.visibility = View.GONE safeBinding.progressBarDeletion.visibility = View.GONE
} }
binding.nominateDeletion.visibility = View.GONE safeBinding.nominateDeletion.visibility = View.GONE
safeBinding.nominatedDeletionBanner.visibility = View.VISIBLE
binding.nominatedDeletionBanner.visibility = View.VISIBLE
} else if (!isCategoryImage) { } else if (!isCategoryImage) {
binding.nominateDeletion.visibility = View.VISIBLE safeBinding.nominateDeletion.visibility = View.VISIBLE
binding.nominatedDeletionBanner.visibility = View.GONE safeBinding.nominatedDeletionBanner.visibility = View.GONE
} }
} }
@ -717,7 +743,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
object : BaseControllerListener<ImageInfo?>() { object : BaseControllerListener<ImageInfo?>() {
override fun onIntermediateImageSet(id: String, imageInfo: ImageInfo?) { override fun onIntermediateImageSet(id: String, imageInfo: ImageInfo?) {
imageInfoCache = imageInfo imageInfoCache = imageInfo
updateAspectRatio(binding.mediaDetailScrollView.width) bindingOrNull?.let { updateAspectRatio(it.mediaDetailScrollView.width) }
} }
override fun onFinalImageSet( override fun onFinalImageSet(
@ -726,7 +752,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
animatable: Animatable? animatable: Animatable?
) { ) {
imageInfoCache = imageInfo imageInfoCache = imageInfo
updateAspectRatio(binding.mediaDetailScrollView.width) bindingOrNull?.let { updateAspectRatio(it.mediaDetailScrollView.width) }
} }
} }
@ -789,14 +815,18 @@ 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() { override fun onDestroyView() {
if (layoutListener != null && view != null) { layoutListener?.let {
requireView().viewTreeObserver.removeGlobalOnLayoutListener(layoutListener) // old Android was on crack. CRACK IS WHACK view?.viewTreeObserver?.removeOnGlobalLayoutListener(it)
layoutListener = null layoutListener = null
} }
compositeDisposable.clear() compositeDisposable.clear()
_binding = null
super.onDestroyView() super.onDestroyView()
} }
@ -2275,4 +2305,4 @@ fun FileUsagesContainer(
} }
} }
} }

View file

@ -160,10 +160,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
return binding.getRoot(); return binding.getRoot();
} }
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
imageProgressBar = null;
}
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putInt("current-page", binding.mediaDetailsPager.getCurrentItem()); if (binding != null && binding.mediaDetailsPager != null) {
outState.putInt("current-page", binding.mediaDetailsPager.getCurrentItem());
}
outState.putBoolean("editable", editable); outState.putBoolean("editable", editable);
outState.putBoolean("isFeaturedImage", isFeaturedImage); outState.putBoolean("isFeaturedImage", isFeaturedImage);
} }

View file

@ -107,6 +107,10 @@ class ProfileActivity : BaseActivity() {
public override fun onDestroy() { public override fun onDestroy() {
super.onDestroy() super.onDestroy()
compositeDisposable.clear() compositeDisposable.clear()
// Nullify fragment references to avoid memory leaks
if (::achievementsFragment.isInitialized) achievementsFragment.setHasOptionsMenu(false)
if (::leaderboardFragment.isInitialized) leaderboardFragment.setHasOptionsMenu(false)
contributionsFragment = null
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View file

@ -882,6 +882,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
uploadCategoriesFragment!!.callback = null uploadCategoriesFragment!!.callback = null
} }
onBackPressedCallback.remove() onBackPressedCallback.remove()
locationManager?.unregisterLocationManager()
UploadMediaPresenter.presenterCallback = null // Clearing reference
} }
/** /**

View file

@ -381,8 +381,8 @@
<string name="statistics_thanks">Thanks Received</string> <string name="statistics_thanks">Thanks Received</string>
<string name="statistics_featured">Featured Images</string> <string name="statistics_featured">Featured Images</string>
<string name="statistics_wikidata_edits">Images via \"Nearby Places\"</string> <string name="statistics_wikidata_edits">Images via \"Nearby Places\"</string>
<string name="level">Level %d</string> <string name="level" formatted="false">Level %d</string>
<string name="profileLevel">%s (Level %s)</string> <string name="profileLevel" formatted="false">%s (Level %s)</string>
<string name="images_uploaded">Images Uploaded</string> <string name="images_uploaded">Images Uploaded</string>
<string name="image_reverts">Images Not Reverted</string> <string name="image_reverts">Images Not Reverted</string>
<string name="images_used_by_wiki">Images Used</string> <string name="images_used_by_wiki">Images Used</string>