mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-30 22:34:02 +01:00 
			
		
		
		
	Merge 763c6093fe into 13048cc2fd
				
					
				
			This commit is contained in:
		
						commit
						3231d74aa5
					
				
					 10 changed files with 141 additions and 63 deletions
				
			
		|  | @ -258,6 +258,12 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         binding = null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|  |  | |||
|  | @ -143,6 +143,11 @@ class BookmarkLocationsFragment : DaggerFragment() { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroyView() { | ||||
|         super.onDestroyView() | ||||
|         binding = null | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         // Make sure to null out the binding to avoid memory leaks | ||||
|  |  | |||
|  | @ -67,6 +67,12 @@ public class BookmarkPicturesFragment extends DaggerFragment { | |||
|         controller.stop(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         binding = null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|  |  | |||
|  | @ -687,14 +687,23 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On | |||
|     override fun onDestroy() { | ||||
|         try { | ||||
|             compositeDisposable.clear() | ||||
|             // Remove child fragment safely | ||||
|             contributionsListFragment?.let { | ||||
|                 childFragmentManager.beginTransaction() | ||||
|                     .remove(it) | ||||
|                     .commitAllowingStateLoss() | ||||
|             } | ||||
|             childFragmentManager.removeOnBackStackChangedListener(this) | ||||
|             locationManager!!.unregisterLocationManager() | ||||
|             locationManager!!.removeLocationListener(this) | ||||
|             super.onDestroy() | ||||
|             locationManager?.unregisterLocationManager() | ||||
|             locationManager?.removeLocationListener(this) | ||||
|             // Nullify locationManager to prevent leaks | ||||
|             locationManager = null | ||||
|         } catch (exception: IllegalArgumentException) { | ||||
|             Timber.e(exception) | ||||
|         } catch (exception: IllegalStateException) { | ||||
|             Timber.e(exception) | ||||
|         } finally { | ||||
|             super.onDestroy() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -755,7 +764,9 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On | |||
| 
 | ||||
|     override fun onDestroyView() { | ||||
|         super.onDestroyView() | ||||
|         presenter!!.onDetachView() | ||||
|         presenter?.onDetachView() | ||||
|         binding = null | ||||
|         contributionsListFragment = null | ||||
|     } | ||||
| 
 | ||||
|     override fun notifyDataSetChanged() { | ||||
|  |  | |||
|  | @ -527,6 +527,11 @@ after opening the app. | |||
|         quizChecker!!.cleanup() | ||||
|         locationManager!!.unregisterLocationManager() | ||||
|         // Remove ourself from hashmap to prevent memory leaks | ||||
|         try { | ||||
|             locationManager?.unregisterLocationManager() | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e, "Error while cleaning up locationManager") | ||||
|         } | ||||
|         locationManager = null | ||||
|         super.onDestroy() | ||||
|     } | ||||
|  |  | |||
|  | @ -196,8 +196,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 | ||||
| 
 | ||||
|  | @ -249,7 +254,9 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C | |||
| 
 | ||||
|     private val scrollPosition: Unit | ||||
|         get() { | ||||
|             initialListTop = binding.mediaDetailScrollView.scrollY | ||||
|             bindingOrNull?.let { | ||||
|                 initialListTop = it.mediaDetailScrollView.scrollY | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     override fun onCreateView( | ||||
|  | @ -345,25 +352,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 { | ||||
|  | @ -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 | ||||
|          * 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) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -531,45 +545,56 @@ 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) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         ) | ||||
|         binding.progressBarEdit.visibility = View.GONE | ||||
|         bindingOrNull?.progressBarEdit?.visibility = View.GONE | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|  | @ -618,7 +643,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C | |||
|     } | ||||
| 
 | ||||
|     private fun onMediaRefreshed(media: Media) { | ||||
|         media.categories = this.media!!.categories | ||||
|         media.categories = this.media?.categories | ||||
|         this.media = media | ||||
|         setTextFields(media) | ||||
|         compositeDisposable.addAll( | ||||
|  | @ -632,29 +657,30 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C | |||
|     } | ||||
| 
 | ||||
|     private fun onDiscussionLoaded(discussion: String) { | ||||
|         binding.mediaDetailDisc.text = prettyDiscussion(discussion.trim { it <= ' ' }) | ||||
|         bindingOrNull?.mediaDetailDisc?.text = prettyDiscussion(discussion.trim { it <= ' ' }) | ||||
|     } | ||||
| 
 | ||||
|     private fun onDeletionPageExists(deletionPageExists: Boolean) { | ||||
|         if (getUserName(requireContext()) == null && getUserName(requireContext()) != media!!.author) { | ||||
|             binding.nominateDeletion.visibility = View.GONE | ||||
|             binding.nominatedDeletionBanner.visibility = View.GONE | ||||
|         val safeBinding = bindingOrNull ?: return | ||||
| 
 | ||||
|         if (getUserName(requireContext()) == null && getUserName(requireContext()) != media?.author) { | ||||
|             safeBinding.nominateDeletion.visibility = View.GONE | ||||
|             safeBinding.nominatedDeletionBanner.visibility = View.GONE | ||||
|         } else if (deletionPageExists) { | ||||
|             if (applicationKvStore.getBoolean( | ||||
|                     String.format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl), false | ||||
|                     String.format(NOMINATING_FOR_DELETION_MEDIA, media?.imageUrl), false | ||||
|                 ) | ||||
|             ) { | ||||
|                 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 | ||||
| 
 | ||||
|             binding.nominatedDeletionBanner.visibility = View.VISIBLE | ||||
|             safeBinding.nominateDeletion.visibility = View.GONE | ||||
|             safeBinding.nominatedDeletionBanner.visibility = View.VISIBLE | ||||
|         } else if (!isCategoryImage) { | ||||
|             binding.nominateDeletion.visibility = View.VISIBLE | ||||
|             binding.nominatedDeletionBanner.visibility = View.GONE | ||||
|             safeBinding.nominateDeletion.visibility = View.VISIBLE | ||||
|             safeBinding.nominatedDeletionBanner.visibility = View.GONE | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -717,7 +743,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C | |||
|         object : BaseControllerListener<ImageInfo?>() { | ||||
|             override fun onIntermediateImageSet(id: String, imageInfo: ImageInfo?) { | ||||
|                 imageInfoCache = imageInfo | ||||
|                 updateAspectRatio(binding.mediaDetailScrollView.width) | ||||
|                 bindingOrNull?.let { updateAspectRatio(it.mediaDetailScrollView.width) } | ||||
|             } | ||||
| 
 | ||||
|             override fun onFinalImageSet( | ||||
|  | @ -726,7 +752,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C | |||
|                 animatable: Animatable? | ||||
|             ) { | ||||
|                 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() { | ||||
|         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 | ||||
|         } | ||||
| 
 | ||||
|         compositeDisposable.clear() | ||||
| 
 | ||||
|         _binding = null | ||||
|         super.onDestroyView() | ||||
|     } | ||||
| 
 | ||||
|  | @ -2275,4 +2305,4 @@ fun FileUsagesContainer( | |||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | @ -160,10 +160,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple | |||
|         return binding.getRoot(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         binding = null; | ||||
|         imageProgressBar = null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onSaveInstanceState(Bundle 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("isFeaturedImage", isFeaturedImage); | ||||
|     } | ||||
|  |  | |||
|  | @ -107,6 +107,10 @@ class ProfileActivity : BaseActivity() { | |||
|     public override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         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 { | ||||
|  |  | |||
|  | @ -882,6 +882,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C | |||
|             uploadCategoriesFragment!!.callback = null | ||||
|         } | ||||
|         onBackPressedCallback.remove() | ||||
|         locationManager?.unregisterLocationManager() | ||||
|         UploadMediaPresenter.presenterCallback = null // Clearing reference | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -381,8 +381,8 @@ | |||
|   <string name="statistics_thanks">Thanks Received</string> | ||||
|   <string name="statistics_featured">Featured Images</string> | ||||
|   <string name="statistics_wikidata_edits">Images via \"Nearby Places\"</string> | ||||
|   <string name="level">Level %d</string> | ||||
|   <string name="profileLevel">%s (Level %s)</string> | ||||
|   <string name="level" formatted="false">Level %d</string> | ||||
|   <string name="profileLevel" formatted="false">%s (Level %s)</string> | ||||
|   <string name="images_uploaded">Images Uploaded</string> | ||||
|   <string name="image_reverts">Images Not Reverted</string> | ||||
|   <string name="images_used_by_wiki">Images Used</string> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dev Jadiya
						Dev Jadiya