mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-30 22:34:02 +01:00 
			
		
		
		
	Compare commits
	
		
			200 commits
		
	
	
		
			v5.2.0-alp
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a48e2116b5 | ||
|   | 1d216acf50 | ||
|   | 63f621cb56 | ||
|   | e81f916626 | ||
|   | 28fa7b1a20 | ||
|   | aae9d4a387 | ||
|   | 6873f63cf8 | ||
|   | 2d0255e5fb | ||
|   | 32ae406cca | ||
|   | 3e04a1f036 | ||
|   | 6487191394 | ||
|   | beaf211f39 | ||
|   | 3549789cdf | ||
|   | def33552f9 | ||
|   | 3a55583460 | ||
|   | 717a855149 | ||
|   | 29b6d0f8fe | ||
|   | b5b5d8a8e4 | ||
|   | 714e5f8a4b | ||
|   | 7d96e94689 | ||
|   | 7a865df909 | ||
|   | 864884e7b2 | ||
|   | 1ecaf09f21 | ||
|   | 1ff2a28326 | ||
|   | b48905a153 | ||
|   | 09c8d987e1 | ||
|   | 2e52adbef8 | ||
|   | 61c9de6fcc | ||
|   | 41d95814c9 | ||
|   | c4cb65fc3c | ||
|   | a1c5974e93 | ||
|   | 0c244f369c | ||
|   | b6014b017c | ||
|   | 91ea4a6e7b | ||
|   | 1e51c4c5d0 | ||
|   | fbd28a0564 | ||
|   | d0965206cd | ||
|   | bb330c1771 | ||
|   | 14d6c80241 | ||
|   | 4c621364c9 | ||
|   | 2a9d5db51e | ||
|   | b8d340fbe8 | ||
|   | dd1814c793 | ||
|   | adb6181e9f | ||
|   | 0a4b179db5 | ||
|   | e78db7fa08 | ||
|   | 7be615bacb | ||
|   | 95d58023c7 | ||
|   | 7b8fbc239b | ||
|   | 30d1107cef | ||
|   | fe16c44caa | ||
|   | 4ed9ad5085 | ||
|   | 755d8311dc | ||
|   | b6457cc6b9 | ||
|   | 2d51a7ce9a | ||
|   | 0ade0705e2 | ||
|   | 6bc25ccd9b | ||
|   | ed7007fc8c | ||
|   | 71ad6a2ce5 | ||
|   | e9a1af0f52 | ||
|   | 10c384ffa7 | ||
|   | 4e51977fb6 | ||
|   | d632c268ae | ||
|   | be371e5236 | ||
|   | 25d3068faf | ||
|   | 179c7c1855 | ||
|   | 8018000584 | ||
|   | 657af4fe04 | ||
|   | 219fcd3dd8 | ||
|   | 2e9726b84f | ||
|   | 64c6b0c8d0 | ||
|   | fcc63b9f09 | ||
|   | a283ffe2bc | ||
|   | 2811b181b7 | ||
|   | 730f314200 | ||
|   | 81da5c9a1a | ||
|   | a59bf64677 | ||
|   | e2c8f85a5b | ||
|   | dd96c64182 | ||
|   | 9ba702eaa9 | ||
|   | 296b4c1f52 | ||
|   | 48e7effd0a | ||
|   | b9f353bb5a | ||
|   | c22e8447b3 | ||
|   | f810a2d49b | ||
|   | 4f3f7b97fd | ||
|   | 718c466505 | ||
|   | b8a558303b | ||
|   | a892aa6dee | ||
|   | 5a6b3cbf09 | ||
|   | 5bdfbf5f6f | ||
|   | 1d7d2801e4 | ||
|   | 5201af70cd | ||
|   | d0e95bc3c2 | ||
|   | ffb9af1f1c | ||
|   | 6dcce45c59 | ||
|   | 6f36cae767 | ||
|   | 516039c91d | ||
|   | 8de57304bf | ||
|   | 869371b485 | ||
|   | 929711da98 | ||
|   | b2816e1459 | ||
|   | 532bd8baa6 | ||
|   | 90ab7a2766 | ||
|   | ee33a9350f | ||
|   | f1e6f1ad31 | ||
|   | 11e3e37263 | ||
|   | da694022ac | ||
|   | 29ade1e5b7 | ||
|   | 88565b70c5 | ||
|   | e5dbcfc2a1 | ||
|   | 0cda8e4d70 | ||
|   | 7500b6d374 | ||
|   | a4c7a9c4f7 | ||
|   | 8fc7e1039b | ||
|   | 79f52db929 | ||
|   | 13048cc2fd | ||
|   | 66395b9871 | ||
|   | 65f41beed8 | ||
|   | f98b49608e | ||
|   | 3bd0ec4466 | ||
|   | 4befff8f42 | ||
|   | 89436b0a75 | ||
|   | 6de5a07e0d | ||
|   | 27b9d70333 | ||
|   | 9a94dc2548 | ||
|   | b1a8308aaf | ||
|   | ad7dddaac4 | ||
|   | 5d7f42d127 | ||
|   | d9e8917418 | ||
|   | 09da7b8d68 | ||
|   | ca5c7ec966 | ||
|   | 9eff9e8e82 | ||
|   | 5665bc7f93 | ||
|   | 20e5df7d49 | ||
|   | d3ae925567 | ||
|   | af82cb2123 | ||
|   | 7df52e3f9c | ||
|   | 6b40560dfc | ||
|   | 54bb789461 | ||
|   | 7979be17c1 | ||
|   | 91564a1dff | ||
|   | 2b5f0e4ac9 | ||
|   | 9b04031c91 | ||
|   | 8ff52e6815 | ||
|   | c41b5cc9da | ||
|   | 767b625289 | ||
|   | f45f26e602 | ||
|   | 06a613e855 | ||
|   | 62c5231dc9 | ||
|   | 7a224a9120 | ||
|   | 593335aea3 | ||
|   | 6edc6a22e4 | ||
|   | 230604f5ef | ||
|   | 73f5200c2d | ||
|   | 95b8ac74b9 | ||
|   | cfc2cfcca1 | ||
|   | ed1485ca22 | ||
|   | c49c85e68b | ||
|   | 91ca2e6672 | ||
|   | 8849f8984b | ||
|   | bb21e4bdcd | ||
|   | eb617ae8ca | ||
|   | b3c1474b31 | ||
|   | 21ffcb56fd | ||
|   | f977e16774 | ||
|   | 012020735f | ||
|   | 3f2077a6db | ||
|   | f06ae4ebfe | ||
|   | 865824a8e3 | ||
|   | 4d2170257a | ||
|   | 0024e72a2e | ||
|   | 60aca9a5e3 | ||
|   | d0f6c16878 | ||
|   | 8fded5ef6e | ||
|   | 329a68216e | ||
|   | 30762971db | ||
|   | 7479d96675 | ||
|   | ed42d85f67 | ||
|   | 78d29bcf20 | ||
|   | 1a13cb3383 | ||
|   | 9289dcc42c | ||
|   | efdc9c5548 | ||
|   | 69b3544107 | ||
|   | 5b5aeead88 | ||
|   | 4bacac1f8b | ||
|   | 6aeb3c07cc | ||
|   | 2c41176a6e | ||
|   | e3dd00bcfa | ||
|   | 262efe4d8c | ||
|   | 2eed441462 | ||
|   | 56fa8ceb5a | ||
|   | 7bf9276d1a | ||
|   | 51da9e4dd6 | ||
|   | 731ff62faf | ||
|   | fdfd7781e9 | ||
|   | 6e090c8d7a | ||
|   | 44966645ca | ||
|   | 669f3043ae | ||
|   | 5a5e660a43 | 
					 452 changed files with 17677 additions and 12864 deletions
				
			
		
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,7 +1,7 @@ | ||||||
| name: "\U0001F41E Bug report" | name: "\U0001F41E Bug report" | ||||||
| description: Create a report to help us improve. | description: Create a report to help us improve. | ||||||
| title: "[Bug]: " | title: "[Bug]: " | ||||||
| labels: ["bug"] | type: Bug  # Retained to categorize the issue as per organization-level type | ||||||
| body: | body: | ||||||
|   - type: markdown |   - type: markdown | ||||||
|     attributes: |     attributes: | ||||||
|  | @ -70,7 +70,7 @@ body: | ||||||
|       required: false |       required: false | ||||||
|   - type: textarea |   - type: textarea | ||||||
|     attributes: |     attributes: | ||||||
|       label: Screen-shots |       label: Screenshots | ||||||
|       description: Add screenshots related to the issue (if available). Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher. |       description: Add screenshots related to the issue (if available). Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher. | ||||||
|     validations: |     validations: | ||||||
|       required: false |       required: false | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								.idea/codeStyles/Project.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								.idea/codeStyles/Project.xml
									
										
									
										generated
									
									
									
								
							|  | @ -16,6 +16,7 @@ | ||||||
|       <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" /> |       <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" /> | ||||||
|       <option name="IMPORT_LAYOUT_TABLE"> |       <option name="IMPORT_LAYOUT_TABLE"> | ||||||
|         <value> |         <value> | ||||||
|  |           <package name="" withSubpackages="true" static="false" module="true" /> | ||||||
|           <package name="" withSubpackages="true" static="true" /> |           <package name="" withSubpackages="true" static="true" /> | ||||||
|           <emptyLine /> |           <emptyLine /> | ||||||
|           <package name="" withSubpackages="true" static="false" /> |           <package name="" withSubpackages="true" static="false" /> | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										45
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
										
									
										generated
									
									
									
								
							|  | @ -2,11 +2,35 @@ | ||||||
|   <profile version="1.0"> |   <profile version="1.0"> | ||||||
|     <option name="myName" value="Project Default" /> |     <option name="myName" value="Project Default" /> | ||||||
|     <inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|  |     <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|     <inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|       <option name="reportWhenNoStatementFollow" value="true" /> |       <option name="reportWhenNoStatementFollow" value="true" /> | ||||||
|     </inspection_tool> |     </inspection_tool> | ||||||
|     <inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" /> |     <inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> |     <inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> | ||||||
|  |     <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|     <inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|       <option name="REPORT_VARIABLES" value="true" /> |       <option name="REPORT_VARIABLES" value="true" /> | ||||||
|       <option name="REPORT_PARAMETERS" value="true" /> |       <option name="REPORT_PARAMETERS" value="true" /> | ||||||
|  | @ -20,6 +44,27 @@ | ||||||
|     <inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|       <option name="ignoreInMatchingInstanceof" value="false" /> |       <option name="ignoreInMatchingInstanceof" value="false" /> | ||||||
|     </inspection_tool> |     </inspection_tool> | ||||||
|  |     <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> | ||||||
|  |       <option name="composableFile" value="true" /> | ||||||
|  |     </inspection_tool> | ||||||
|     <inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" /> |     <inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||||
|     <inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true"> |     <inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|  |  | ||||||
							
								
								
									
										85
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										85
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -1,5 +1,90 @@ | ||||||
| # Wikimedia Commons for Android | # Wikimedia Commons for Android | ||||||
| 
 | 
 | ||||||
|  | ## v6.0.2 | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | * Addressed a bug that prevented the keyboard from appearing in various text fields, such as on the upload wizard | ||||||
|  | * Links in the "File usages" list are now clickable and will take you to the correct page. | ||||||
|  | * Titles for file usages are now clearer and easier to understand | ||||||
|  | * Bug fixes and stability improvements | ||||||
|  | 
 | ||||||
|  | ## v6.0.1 | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | * The app now supports Android 15 with an improved user interface | ||||||
|  | * Enhanced Nearby with robust and more reliable labels | ||||||
|  | * Bug fixes and stability improvements | ||||||
|  | 
 | ||||||
|  | ## v5.6.1 | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | * The app no longer uploads images to Wikidata if one exists already for a given item | ||||||
|  | * File usage displays correctly now | ||||||
|  | * No more infinite circular progress bar on nominating an image for deletion | ||||||
|  | * Enhanced location updates while using GPS | ||||||
|  | * Author/uploader names are now available in Media Details for Commons licensing compliance | ||||||
|  | * Improved usage of popups in Nearby | ||||||
|  | * Bug fixes and stability improvements  | ||||||
|  | 
 | ||||||
|  | ## v5.5.0 | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | * Explore images will now be shown based on the map location and not at your current location | ||||||
|  | * Enhanced Wikidata feedback message | ||||||
|  | * Green labels in Explore map will no longer be hidden by other pins thumbnails | ||||||
|  | * Upload wizard's language drop-down now reflects the language used in the pin label | ||||||
|  | * Users can now pick only one image at a time while using the custom selector | ||||||
|  | * Bug fixes and stability improvements  | ||||||
|  | 
 | ||||||
|  | ## v5.4.1 | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | * Custom picker now detects images that are already available on Commons | ||||||
|  | * Improve credit line in image list | ||||||
|  | * Show place cards with loaded names only in the Nearby list | ||||||
|  | * Fix the error that occurs while loading images in Explore | ||||||
|  | 
 | ||||||
|  | ## v5.3.0 | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | * Enable EmailAuth support | ||||||
|  | * Explore map images no longer show "Unknown" | ||||||
|  | * Fix crash when removing last two images of multiupload | ||||||
|  | * Mark ❌ for closed locations (P3999) in Nearby | ||||||
|  | * Fix two pin labels staying visible at the same time in Explore map | ||||||
|  | * Refactoring and minor UI improvements | ||||||
|  | 
 | ||||||
|  | ## v5.2.0 | ||||||
|  | 
 | ||||||
|  | v5.2.0 boasts several new functionalities like: | ||||||
|  | 
 | ||||||
|  | * A new refresh button lets you quickly reload the Nearby map | ||||||
|  | * Bookmarks now support categories | ||||||
|  | * Improved feedback and consistency in the user interface | ||||||
|  | * Bug fixes and performance improvements | ||||||
|  | 
 | ||||||
|  | ### What's changed | ||||||
|  | * Implement "Refresh" button to clear the cache and reload the Nearby map. | ||||||
|  | * `CommonsApplication` migrate to kotlin & some lint fixes. | ||||||
|  | * Revert back to MainScope for database and UI updates and make database operations thread safe. | ||||||
|  | * Hide edit options for logged-out users in Explore screen. | ||||||
|  | * Introduced a button to delete the current folder in custom selector. | ||||||
|  | * Improve Unique File Name Search. | ||||||
|  | * Migration of several modules from Java to Kotlin. | ||||||
|  | * Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins. | ||||||
|  | * Bug fixes and enhancement of Achievements screen. | ||||||
|  | * Show where file is being used on Commons and other wikis. | ||||||
|  | * Migrate android.media.ExifInterface to androidx.exifinterface.media.ExifInterface as android.media.ExifInterface had security flaws on older devices. | ||||||
|  | * Make dialogs modal and always show the upload icon. | ||||||
|  | * Fix unintentional deletion of subfolders and non-images by custom selector. | ||||||
|  | * Bookmark categories. | ||||||
|  | * Add pull down to refresh in the Contributions screen. | ||||||
|  | * Fix race condition and lag when loading pin details, faster overlay management. | ||||||
|  | * Show cached pins in Nearby even when internet is unavailable | ||||||
|  | 
 | ||||||
|  |  Full changelog with the list of contributors: [`v5.1.2...v5.2.0`](https://github.com/commons-app/apps-android-commons/compare/v5.1.2...v5.2.0). | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| ## v5.1.2 | ## v5.1.2 | ||||||
| 
 | 
 | ||||||
| ### What's changed | ### What's changed | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
										
									
									
									
								
							|  | @ -29,11 +29,12 @@ Thank you all for your work! | ||||||
| 
 | 
 | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) | | | [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) | | ||||||
| | :---: | :---: | :---: | :---: | :---: | | | :---: | :---: | :---: | :---: | :---: | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) | | | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>bvibber</b></sub>](https://github.com/bvibber) | [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) | | | [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>bvibber</b></sub>](https://github.com/bvibber) | [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/71203077?v=4" width="100px;"/><br /><sub><b>Ayan-10</b></sub>](https://github.com/Ayan-10) | | | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) | [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/346271?v=4" width="100px;"/><br /><sub><b>amire80</b></sub>](https://github.com/amire80) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/126143257?v=4" width="100px;"/><br /><sub><b>shashankiitbhu</b></sub>](https://github.com/shashankiitbhu) | [<img src="https://avatars.githubusercontent.com/u/54663429?v=4" width="100px;"/><br /><sub><b>Pratham2305</b></sub>](https://github.com/Pratham2305) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) | [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | | | [<img src="https://avatars.githubusercontent.com/u/83745993?v=4" width="100px;"/><br /><sub><b>RitikaPahwa4444</b></sub>](https://github.com/RitikaPahwa4444) | [<img src="https://avatars.githubusercontent.com/u/71203077?v=4" width="100px;"/><br /><sub><b>Ayan-10</b></sub>](https://github.com/Ayan-10) | [<img src="https://avatars.githubusercontent.com/u/101377978?v=4" width="100px;"/><br /><sub><b>rohit9625</b></sub>](https://github.com/rohit9625) | [<img src="https://avatars.githubusercontent.com/u/126143257?v=4" width="100px;"/><br /><sub><b>shashankiitbhu</b></sub>](https://github.com/shashankiitbhu) | [<img src="https://avatars.githubusercontent.com/u/54663429?v=4" width="100px;"/><br /><sub><b>Pratham2305</b></sub>](https://github.com/Pratham2305) | | ||||||
| | [<img src="https://avatars.githubusercontent.com/u/65972015?v=4" width="100px;"/><br /><sub><b>Prince-kushwaha</b></sub>](https://github.com/Prince-kushwaha) | [<img src="https://avatars.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars.githubusercontent.com/u/54016427?v=4" width="100px;"/><br /><sub><b>4D17Y4</b></sub>](https://github.com/4D17Y4) | [<img src="https://avatars.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) | | | [<img src="https://avatars.githubusercontent.com/u/111801812?v=4" width="100px;"/><br /><sub><b>parneet-guraya</b></sub>](https://github.com/parneet-guraya) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) | [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | [<img src="https://avatars.githubusercontent.com/u/65972015?v=4" width="100px;"/><br /><sub><b>Prince-kushwaha</b></sub>](https://github.com/Prince-kushwaha) | | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| .. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors). | .. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors). | ||||||
|  |  | ||||||
							
								
								
									
										428
									
								
								app/build.gradle
									
										
									
									
									
								
							
							
						
						
									
										428
									
								
								app/build.gradle
									
										
									
									
									
								
							|  | @ -1,428 +0,0 @@ | ||||||
| plugins { |  | ||||||
|     id 'com.github.triplet.play' version '2.7.2' apply false |  | ||||||
| } |  | ||||||
| apply from: '../gitutils.gradle' |  | ||||||
| apply plugin: 'com.android.application' |  | ||||||
| apply plugin: 'kotlin-android' |  | ||||||
| apply plugin: 'kotlin-kapt' |  | ||||||
| apply plugin: 'kotlin-parcelize' |  | ||||||
| apply from: "$rootDir/jacoco.gradle" |  | ||||||
| 
 |  | ||||||
| def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists() |  | ||||||
| 
 |  | ||||||
| if (isRunningOnTravisAndIsNotPRBuild) { |  | ||||||
|     apply plugin: 'com.github.triplet.play' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| dependencies { |  | ||||||
| 
 |  | ||||||
|     // Utils |  | ||||||
|     implementation 'in.yuvi:http.fluent:1.3' |  | ||||||
|     implementation 'com.google.code.gson:gson:2.8.5' |  | ||||||
|     implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION!!"){ |  | ||||||
|         // Forcing dependency versions using force = true on a first-level dependency has been deprecated. |  | ||||||
|         //  Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies |  | ||||||
|         //force = true //API 19 support |  | ||||||
|     } |  | ||||||
|     implementation 'com.squareup.retrofit2:retrofit:2.8.1' |  | ||||||
|     implementation "com.squareup.retrofit2:converter-gson:2.8.1" |  | ||||||
|     implementation "com.squareup.retrofit2:adapter-rxjava2:2.8.1" |  | ||||||
|     implementation 'com.squareup.okio:okio:2.2.2' |  | ||||||
|     implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' |  | ||||||
|     implementation 'io.reactivex.rxjava2:rxjava:2.2.3' |  | ||||||
|     implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' |  | ||||||
|     implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0' |  | ||||||
|     implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.1.1' |  | ||||||
|     implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1' |  | ||||||
|     implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1' |  | ||||||
|     implementation 'com.facebook.fresco:fresco:1.13.0' |  | ||||||
|     implementation 'org.apache.commons:commons-lang3:3.8.1' |  | ||||||
| 
 |  | ||||||
|     // UI |  | ||||||
|     implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' |  | ||||||
|     implementation 'com.github.chrisbanes:PhotoView:2.0.0' |  | ||||||
|     implementation 'com.github.pedrovgs:renderers:3.3.3' |  | ||||||
|     implementation "org.maplibre.gl:android-sdk:$MAPLIBRE_VERSION" |  | ||||||
|     implementation 'org.maplibre.gl:android-plugin-scalebar-v9:1.0.0' |  | ||||||
| 
 |  | ||||||
|     implementation 'com.jakewharton.timber:timber:4.7.1' |  | ||||||
|     implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' |  | ||||||
|     implementation "com.google.android.material:material:1.12.0" |  | ||||||
|     implementation 'com.karumi:dexter:5.0.0' |  | ||||||
|     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' |  | ||||||
|     implementation 'androidx.compose.ui:ui-tooling-preview' |  | ||||||
|     androidTestImplementation 'androidx.compose.ui:ui-test-junit4' |  | ||||||
| 
 |  | ||||||
|     // Jetpack Compose |  | ||||||
|     def composeBom = platform('androidx.compose:compose-bom:2024.11.00') |  | ||||||
| 
 |  | ||||||
|     implementation "androidx.activity:activity-compose:1.9.3" |  | ||||||
|     implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4" |  | ||||||
|     implementation (composeBom) |  | ||||||
|     implementation "androidx.compose.runtime:runtime" |  | ||||||
|     implementation "androidx.compose.ui:ui" |  | ||||||
|     implementation "androidx.compose.ui:ui-viewbinding" |  | ||||||
|     implementation "androidx.compose.ui:ui-graphics" |  | ||||||
|     implementation "androidx.compose.ui:ui-tooling" |  | ||||||
|     implementation "androidx.compose.foundation:foundation" |  | ||||||
|     implementation "androidx.compose.foundation:foundation-layout" |  | ||||||
|     implementation "androidx.compose.material3:material3" |  | ||||||
|     androidTestImplementation(composeBom) |  | ||||||
| 
 |  | ||||||
|     implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION" |  | ||||||
|     implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION" |  | ||||||
|     implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION" |  | ||||||
|     testImplementation "androidx.paging:paging-common-ktx:$PAGING_VERSION" |  | ||||||
|     implementation "androidx.paging:paging-rxjava2-ktx:$PAGING_VERSION" |  | ||||||
|     implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02" |  | ||||||
|     implementation "com.squareup.okhttp3:okhttp-ws:$OKHTTP_VERSION" |  | ||||||
| 
 |  | ||||||
|     // Logging |  | ||||||
|     implementation 'ch.acra:acra-dialog:5.8.4' |  | ||||||
|     implementation 'ch.acra:acra-mail:5.8.4' |  | ||||||
|     implementation 'org.slf4j:slf4j-api:1.7.25' |  | ||||||
|     api('com.github.tony19:logback-android-classic:1.1.1-6') { |  | ||||||
|         exclude group: 'com.google.android', module: 'android' |  | ||||||
|     } |  | ||||||
|     implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION" |  | ||||||
| 
 |  | ||||||
|     // Dependency injector |  | ||||||
|     implementation "com.google.dagger:dagger-android:$DAGGER_VERSION" |  | ||||||
|     implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" |  | ||||||
|     debugImplementation 'androidx.compose.ui:ui-tooling' |  | ||||||
|     debugImplementation 'androidx.compose.ui:ui-test-manifest' |  | ||||||
|     kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" |  | ||||||
|     kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" |  | ||||||
|     annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" |  | ||||||
| 
 |  | ||||||
|     implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION" |  | ||||||
| 
 |  | ||||||
|     //Mocking |  | ||||||
|     testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' |  | ||||||
|     testImplementation 'org.mockito:mockito-inline:5.2.0' |  | ||||||
|     testImplementation 'org.mockito:mockito-core:5.6.0' |  | ||||||
|     testImplementation "org.powermock:powermock-module-junit4:2.0.9" |  | ||||||
|     testImplementation "org.powermock:powermock-api-mockito2:2.0.9" |  | ||||||
|     testImplementation("io.mockk:mockk:1.13.5") |  | ||||||
| 
 |  | ||||||
|     // Unit testing |  | ||||||
|     testImplementation 'junit:junit:4.13.2' |  | ||||||
|     testImplementation 'org.robolectric:robolectric:4.11.1' |  | ||||||
|     testImplementation 'androidx.test:core:1.5.0' |  | ||||||
|     testImplementation "androidx.test:runner:1.5.2" |  | ||||||
|     testImplementation 'androidx.test.ext:junit:1.1.5' |  | ||||||
|     testImplementation "androidx.test:rules:1.5.0" |  | ||||||
|     testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION" |  | ||||||
|     testImplementation "com.jraska.livedata:testing-ktx:1.2.0" |  | ||||||
|     testImplementation "androidx.arch.core:core-testing:2.2.0" |  | ||||||
|     testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0" |  | ||||||
|     testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0" |  | ||||||
|     testImplementation 'com.facebook.soloader:soloader:0.10.5' |  | ||||||
|     testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" |  | ||||||
|     debugImplementation("androidx.fragment:fragment-testing:1.6.2") |  | ||||||
|     testImplementation "commons-io:commons-io:2.6" |  | ||||||
| 
 |  | ||||||
|     // Android testing |  | ||||||
|     androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04' |  | ||||||
|     androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' |  | ||||||
|     androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04' |  | ||||||
|     androidTestImplementation 'androidx.test:runner:1.4.0' |  | ||||||
|     androidTestImplementation 'androidx.test:rules:1.4.1-alpha04' |  | ||||||
|     androidTestImplementation 'androidx.test:core:1.4.0' |  | ||||||
|     androidTestImplementation 'androidx.test.ext:junit:1.1.3' |  | ||||||
|     androidTestImplementation 'androidx.annotation:annotation:1.3.0' |  | ||||||
|     androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.0' |  | ||||||
|     androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0" |  | ||||||
|     androidTestUtil 'androidx.test:orchestrator:1.4.1' |  | ||||||
| 
 |  | ||||||
|     // Debugging |  | ||||||
|     debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY_VERSION" |  | ||||||
| 
 |  | ||||||
|     // Support libraries |  | ||||||
|     implementation "com.google.android.material:material:1.1.0-alpha04" |  | ||||||
|     implementation "androidx.browser:browser:1.3.0" |  | ||||||
|     implementation "androidx.cardview:cardview:1.0.0" |  | ||||||
|     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' |  | ||||||
|     implementation 'androidx.exifinterface:exifinterface:1.3.7' |  | ||||||
|     implementation "androidx.core:core-ktx:$CORE_KTX_VERSION" |  | ||||||
|     implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1' |  | ||||||
| 
 |  | ||||||
|     //swipe_layout |  | ||||||
|     implementation 'com.daimajia.swipelayout:library:1.2.0@aar' |  | ||||||
| 
 |  | ||||||
|     //Room |  | ||||||
|     implementation "androidx.room:room-runtime:$ROOM_VERSION" |  | ||||||
|     implementation "androidx.room:room-ktx:$ROOM_VERSION" |  | ||||||
|     implementation "androidx.room:room-rxjava2:$ROOM_VERSION" |  | ||||||
|     kapt "androidx.room:room-compiler:$ROOM_VERSION" |  | ||||||
|     // For Kotlin use kapt instead of annotationProcessor |  | ||||||
|     testImplementation "androidx.arch.core:core-testing:2.1.0" |  | ||||||
| 
 |  | ||||||
|     // Pref |  | ||||||
|     // Java language implementation |  | ||||||
|     implementation "androidx.preference:preference:$PREFERENCE_VERSION" |  | ||||||
|     // Kotlin |  | ||||||
|     implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION" |  | ||||||
|     //Android Media |  | ||||||
|     implementation 'com.github.juanitobananas:AndroidMediaUtil:v1.0-1' |  | ||||||
| 
 |  | ||||||
|     implementation "androidx.multidex:multidex:$MULTIDEX_VERSION" |  | ||||||
| 
 |  | ||||||
|     def work_version = "2.8.1" |  | ||||||
|     // Kotlin + coroutines |  | ||||||
|     implementation "androidx.work:work-runtime-ktx:$work_version" |  | ||||||
|     implementation("androidx.work:work-runtime:$work_version") |  | ||||||
|     testImplementation "androidx.work:work-testing:$work_version" |  | ||||||
| 
 |  | ||||||
|     //Glide |  | ||||||
|     implementation 'com.github.bumptech.glide:glide:4.16.0' |  | ||||||
|     annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' |  | ||||||
|     kaptTest "androidx.databinding:databinding-compiler:8.0.2" |  | ||||||
|     kaptAndroidTest "androidx.databinding:databinding-compiler:8.0.2" |  | ||||||
| 
 |  | ||||||
|     implementation("io.github.coordinates2country:coordinates2country-android:1.8") {  exclude group: 'com.google.android', module: 'android' } |  | ||||||
| 
 |  | ||||||
|     //OSMDroid |  | ||||||
|     implementation ("org.osmdroid:osmdroid-android:$OSMDROID_VERSION") |  | ||||||
|     constraints { |  | ||||||
|         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") { |  | ||||||
|             because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") |  | ||||||
|         } |  | ||||||
|         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") { |  | ||||||
|             because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| task disableAnimations(type: Exec) { |  | ||||||
|     def adb = "$System.env.ANDROID_HOME/platform-tools/adb" |  | ||||||
|     commandLine "$adb", 'shell', 'settings', 'put', 'global', 'window_animation_scale', '0' |  | ||||||
|     commandLine "$adb", 'shell', 'settings', 'put', 'global', 'transition_animation_scale', '0' |  | ||||||
|     commandLine "$adb", 'shell', 'settings', 'put', 'global', 'animator_duration_scale', '0' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| project.gradle.taskGraph.whenReady { |  | ||||||
|     connectedBetaDebugAndroidTest.dependsOn disableAnimations |  | ||||||
|     connectedProdDebugAndroidTest.dependsOn disableAnimations |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| android { |  | ||||||
|     compileSdkVersion 34 |  | ||||||
| 
 |  | ||||||
|     defaultConfig { |  | ||||||
|         //applicationId 'fr.free.nrw.commons' |  | ||||||
| 
 |  | ||||||
|         versionCode 1049 |  | ||||||
|         versionName '5.2.0' |  | ||||||
|         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) |  | ||||||
| 
 |  | ||||||
|         minSdkVersion 21 |  | ||||||
|         targetSdkVersion 34 |  | ||||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |  | ||||||
|         testInstrumentationRunnerArguments clearPackageData: 'true' |  | ||||||
| 
 |  | ||||||
|         multiDexEnabled true |  | ||||||
| 
 |  | ||||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |  | ||||||
| 
 |  | ||||||
|         vectorDrawables.useSupportLibrary = true |  | ||||||
|     } |  | ||||||
|     packagingOptions { |  | ||||||
|         jniLibs { |  | ||||||
|             excludes += ['META-INF/androidx.*'] |  | ||||||
|         } |  | ||||||
|         resources { |  | ||||||
|             excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro', '/META-INF/LICENSE.md', '/META-INF/LICENSE-notice.md'] |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     testOptions { |  | ||||||
|         animationsDisabled true |  | ||||||
| 
 |  | ||||||
|         unitTests { |  | ||||||
|             returnDefaultValues = true |  | ||||||
|             includeAndroidResources = true |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         unitTests.all { |  | ||||||
|             jvmArgs '-noverify' |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     sourceSets { |  | ||||||
|         // use kotlin only in tests (for now) |  | ||||||
|         test.java.srcDirs += 'src/test/kotlin' |  | ||||||
| 
 |  | ||||||
|         // use main assets and resources in test |  | ||||||
|         test.assets.srcDirs += 'src/main/assets' |  | ||||||
|         test.resources.srcDirs += 'src/main/resoures' |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     signingConfigs { |  | ||||||
|         release |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     buildTypes { |  | ||||||
|         release { |  | ||||||
|             minifyEnabled true |  | ||||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' |  | ||||||
|             testProguardFile 'test-proguard-rules.txt' |  | ||||||
|             signingConfig signingConfigs.debug |  | ||||||
|             if (isRunningOnTravisAndIsNotPRBuild) { |  | ||||||
|                 signingConfig signingConfigs.release |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         debug { |  | ||||||
|             minifyEnabled false |  | ||||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' |  | ||||||
|             testProguardFile 'test-proguard-rules.txt' |  | ||||||
|             versionNameSuffix "-debug-" + getBranchName() |  | ||||||
|             enableUnitTestCoverage true |  | ||||||
|             enableAndroidTestCoverage true |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (isRunningOnTravisAndIsNotPRBuild) { |  | ||||||
|         // configure keystore based on env vars in Travis for automated alpha builds |  | ||||||
|         signingConfigs.release.storeFile = file("../nr-commons.keystore") |  | ||||||
|         signingConfigs.release.storePassword = System.getenv("keystore_password") |  | ||||||
|         signingConfigs.release.keyAlias = System.getenv("key_alias") |  | ||||||
|         signingConfigs.release.keyPassword = System.getenv("key_password") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     configurations.all { |  | ||||||
|         resolutionStrategy.force 'androidx.annotation:annotation:1.1.0' |  | ||||||
|         resolutionStrategy.force 'com.jakewharton.timber:timber:4.7.1' |  | ||||||
|         resolutionStrategy.force 'androidx.fragment:fragment:1.3.6' |  | ||||||
|         exclude module: 'okhttp-ws' |  | ||||||
|     } |  | ||||||
|     flavorDimensions 'tier' |  | ||||||
|     productFlavors { |  | ||||||
|         prod { |  | ||||||
| 
 |  | ||||||
|             applicationId 'fr.free.nrw.commons' |  | ||||||
| 
 |  | ||||||
|             buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" |  | ||||||
|             buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"" |  | ||||||
|             buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" |  | ||||||
|             buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" |  | ||||||
|             buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" |  | ||||||
|             buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\"" |  | ||||||
|             buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"" |  | ||||||
|             buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" |  | ||||||
|             buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\"" |  | ||||||
|             buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" |  | ||||||
|             buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"" |  | ||||||
|             buildConfigField "String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\"" |  | ||||||
|             buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" |  | ||||||
|             buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"" |  | ||||||
|             buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"" |  | ||||||
|             buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://commons-app.github.io/privacy-policy\"" |  | ||||||
|             buildConfigField "String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"" |  | ||||||
|             buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\"" |  | ||||||
|             buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\"" |  | ||||||
|             buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"" |  | ||||||
|             buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"" |  | ||||||
|             buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"" |  | ||||||
|             buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\"" |  | ||||||
|             buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\"" |  | ||||||
|             buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\"" |  | ||||||
|             buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\"" |  | ||||||
|             buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"" |  | ||||||
|             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" |  | ||||||
|             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" |  | ||||||
|             buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\"" |  | ||||||
|             dimension 'tier' |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         beta { |  | ||||||
|             applicationId 'fr.free.nrw.commons.beta' |  | ||||||
| 
 |  | ||||||
|             // What values do we need to hit the BETA versions of the site / api ? |  | ||||||
|             buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" |  | ||||||
|             buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"" |  | ||||||
|             buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" |  | ||||||
|             buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" |  | ||||||
|             buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" |  | ||||||
|             buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\"" |  | ||||||
|             buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"" |  | ||||||
|             buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" |  | ||||||
|             buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"" |  | ||||||
|             buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" |  | ||||||
|             buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"" |  | ||||||
|             buildConfigField "String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\"" |  | ||||||
|             buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" |  | ||||||
|             buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"" |  | ||||||
|             buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"" |  | ||||||
|             buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://commons-app.github.io/privacy-policy\"" |  | ||||||
|             buildConfigField "String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"" |  | ||||||
|             buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\"" |  | ||||||
|             buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\"" |  | ||||||
|             buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"" |  | ||||||
|             buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\"" |  | ||||||
|             buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\"" |  | ||||||
|             buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\"" |  | ||||||
|             buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"" |  | ||||||
|             buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"" |  | ||||||
|             buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\"" |  | ||||||
|             buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"" |  | ||||||
|             buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" |  | ||||||
|             buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" |  | ||||||
|             buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\"" |  | ||||||
|             dimension 'tier' |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     compileOptions { |  | ||||||
|         sourceCompatibility JavaVersion.VERSION_17 |  | ||||||
|         targetCompatibility JavaVersion.VERSION_17 |  | ||||||
|     } |  | ||||||
|     kotlinOptions { |  | ||||||
|         jvmTarget = "17" |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     buildToolsVersion buildToolsVersion |  | ||||||
| 
 |  | ||||||
|     buildFeatures { |  | ||||||
|         viewBinding true |  | ||||||
|         compose true |  | ||||||
|     } |  | ||||||
|     composeOptions { |  | ||||||
|         kotlinCompilerExtensionVersion '1.5.8' |  | ||||||
|     } |  | ||||||
|     namespace 'fr.free.nrw.commons' |  | ||||||
|     lint { |  | ||||||
|         abortOnError false |  | ||||||
|         disable 'MissingTranslation', 'ExtraTranslation' |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| String getTestUserName() { |  | ||||||
|     def propFile = rootProject.file("./local.properties") |  | ||||||
|     def properties = new Properties() |  | ||||||
|     properties.load(new FileInputStream(propFile)) |  | ||||||
|     return properties['TEST_USER_NAME'] |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| String getTestPassword() { |  | ||||||
|     def propFile = rootProject.file("./local.properties") |  | ||||||
|     def properties = new Properties() |  | ||||||
|     properties.load(new FileInputStream(propFile)) |  | ||||||
|     return properties['TEST_USER_PASSWORD'] |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| if (isRunningOnTravisAndIsNotPRBuild) { |  | ||||||
|     play { |  | ||||||
|         track = "alpha" |  | ||||||
|         userFraction = 1 |  | ||||||
|         serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME") |  | ||||||
|         serviceAccountCredentials = file("../play.p12") |  | ||||||
| 
 |  | ||||||
|         resolutionStrategy = "auto" |  | ||||||
|         outputProcessor { // this: ApkVariantOutput |  | ||||||
|             versionNameOverride = "$versionNameOverride.$versionCode" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										447
									
								
								app/build.gradle.kts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										447
									
								
								app/build.gradle.kts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,447 @@ | ||||||
|  | import java.util.Properties | ||||||
|  | import java.io.ByteArrayOutputStream | ||||||
|  | 
 | ||||||
|  | plugins { | ||||||
|  |     alias(libs.plugins.android.application) | ||||||
|  |     alias(libs.plugins.jetbrains.kotlin.android) | ||||||
|  |     alias(libs.plugins.kotlin.kapt) | ||||||
|  |     alias(libs.plugins.kotlin.parcelize) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | apply(from = "$rootDir/jacoco.gradle") | ||||||
|  | 
 | ||||||
|  | val isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file("../play.p12").exists() | ||||||
|  | 
 | ||||||
|  | if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|  |     apply(plugin = "com.github.triplet.play") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | android { | ||||||
|  |     namespace = "fr.free.nrw.commons" | ||||||
|  |     compileSdk = 35 | ||||||
|  | 
 | ||||||
|  |     defaultConfig { | ||||||
|  |         applicationId = "fr.free.nrw.commons" | ||||||
|  |         minSdk = 21 | ||||||
|  |         targetSdk = 35 | ||||||
|  |         versionCode = 1059 | ||||||
|  |         versionName = "6.1.0" | ||||||
|  | 
 | ||||||
|  |         setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) | ||||||
|  |         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||||||
|  |         testInstrumentationRunnerArguments["clearPackageData"] = "true" | ||||||
|  | 
 | ||||||
|  |         multiDexEnabled = true | ||||||
|  | 
 | ||||||
|  |         vectorDrawables { | ||||||
|  |             useSupportLibrary = true | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     sourceSets { | ||||||
|  |         getByName("test") { | ||||||
|  |             // Use kotlin only in tests (for now) | ||||||
|  |             java.srcDirs("src/test/kotlin") | ||||||
|  | 
 | ||||||
|  |             // Use main assets and resources in test | ||||||
|  |             assets.srcDirs("src/main/assets") | ||||||
|  |             resources.srcDirs("src/main/resources") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     signingConfigs { | ||||||
|  |         create("release") { | ||||||
|  |             // Configure keystore based on env vars in Travis for automated alpha builds | ||||||
|  |             if(isRunningOnTravisAndIsNotPRBuild) { | ||||||
|  |                 storeFile = file("../nr-commons.keystore") | ||||||
|  |                 storePassword = System.getenv("keystore_password") | ||||||
|  |                 keyAlias = System.getenv("key_alias") | ||||||
|  |                 keyPassword = System.getenv("key_password") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     buildTypes { | ||||||
|  |         release { | ||||||
|  |             isMinifyEnabled = true | ||||||
|  |             proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt") | ||||||
|  |             testProguardFile("test-proguard-rules.txt") | ||||||
|  | 
 | ||||||
|  |             signingConfig = signingConfigs.getByName("debug") | ||||||
|  |             if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|  |                 signingConfig = signingConfigs.getByName("release") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         debug { | ||||||
|  |             isMinifyEnabled = false | ||||||
|  |             proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt") | ||||||
|  |             testProguardFile("test-proguard-rules.txt") | ||||||
|  | 
 | ||||||
|  |             versionNameSuffix = "-debug-" + getBranchName() | ||||||
|  |             enableUnitTestCoverage = true | ||||||
|  |             enableAndroidTestCoverage = true | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     configurations.all { | ||||||
|  |         resolutionStrategy { | ||||||
|  |             force("androidx.annotation:annotation:1.1.0") | ||||||
|  |             force("com.jakewharton.timber:timber:4.7.1") | ||||||
|  |             force("androidx.fragment:fragment:1.3.6") | ||||||
|  |         } | ||||||
|  |         exclude(module = "okhttp-ws") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     flavorDimensions += "tier" | ||||||
|  |     productFlavors { | ||||||
|  |         create("prod") { | ||||||
|  |             dimension = "tier" | ||||||
|  |             applicationId = "fr.free.nrw.commons" | ||||||
|  | 
 | ||||||
|  |             buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"") | ||||||
|  |             buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"") | ||||||
|  |             buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"") | ||||||
|  |             buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"") | ||||||
|  |             buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"") | ||||||
|  |             buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\"") | ||||||
|  |             buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"") | ||||||
|  |             buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"") | ||||||
|  |             buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.org\"") | ||||||
|  |             buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"") | ||||||
|  |             buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"") | ||||||
|  |             buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\"") | ||||||
|  |             buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"") | ||||||
|  |             buildConfigField("String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"") | ||||||
|  |             buildConfigField("String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"") | ||||||
|  |             buildConfigField("String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"") | ||||||
|  |             buildConfigField("String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"") | ||||||
|  |             buildConfigField("String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\"") | ||||||
|  |             buildConfigField("String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\"") | ||||||
|  |             buildConfigField("String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"") | ||||||
|  |             buildConfigField("String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"") | ||||||
|  |             buildConfigField("String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"") | ||||||
|  |             buildConfigField("String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\"") | ||||||
|  |             buildConfigField("String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\"") | ||||||
|  |             buildConfigField("String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\"") | ||||||
|  |             buildConfigField("String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\"") | ||||||
|  |             buildConfigField("String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"") | ||||||
|  |             buildConfigField("String", "TEST_USERNAME", "\"" + getTestUserName() + "\"") | ||||||
|  |             buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"") | ||||||
|  |             buildConfigField("String", "DEPICTS_PROPERTY", "\"P180\"") | ||||||
|  |             buildConfigField("String", "CREATOR_PROPERTY", "\"P170\"") | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         create("beta") { | ||||||
|  |             dimension = "tier" | ||||||
|  |             applicationId = "fr.free.nrw.commons.beta" | ||||||
|  | 
 | ||||||
|  |             // What values do we need to hit the BETA versions of the site / api ? | ||||||
|  |             buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"") | ||||||
|  |             buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"") | ||||||
|  |             buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"") | ||||||
|  |             buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"") | ||||||
|  |             buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"") | ||||||
|  |             buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\"") | ||||||
|  |             buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"") | ||||||
|  |             buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"") | ||||||
|  |             buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"") | ||||||
|  |             buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"") | ||||||
|  |             buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"") | ||||||
|  |             buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\"") | ||||||
|  |             buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"") | ||||||
|  |             buildConfigField("String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"") | ||||||
|  |             buildConfigField("String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"") | ||||||
|  |             buildConfigField("String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"") | ||||||
|  |             buildConfigField("String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"") | ||||||
|  |             buildConfigField("String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\"") | ||||||
|  |             buildConfigField("String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\"") | ||||||
|  |             buildConfigField("String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"") | ||||||
|  |             buildConfigField("String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\"") | ||||||
|  |             buildConfigField("String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\"") | ||||||
|  |             buildConfigField("String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\"") | ||||||
|  |             buildConfigField("String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"") | ||||||
|  |             buildConfigField("String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"") | ||||||
|  |             buildConfigField("String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\"") | ||||||
|  |             buildConfigField("String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"") | ||||||
|  |             buildConfigField("String", "TEST_USERNAME", "\"" + getTestUserName() + "\"") | ||||||
|  |             buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"") | ||||||
|  |             buildConfigField("String", "DEPICTS_PROPERTY", "\"P245962\"") | ||||||
|  |             buildConfigField("String", "CREATOR_PROPERTY", "\"P253075\"") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     compileOptions { | ||||||
|  |         sourceCompatibility = JavaVersion.VERSION_17 | ||||||
|  |         targetCompatibility = JavaVersion.VERSION_17 | ||||||
|  |     } | ||||||
|  |     kotlinOptions { | ||||||
|  |         jvmTarget = "17" | ||||||
|  |     } | ||||||
|  |     buildFeatures { | ||||||
|  |         buildConfig = true | ||||||
|  |         viewBinding = true | ||||||
|  |         compose = true | ||||||
|  |     } | ||||||
|  |     buildToolsVersion = buildToolsVersion | ||||||
|  |     composeOptions { | ||||||
|  |         kotlinCompilerExtensionVersion = "1.5.8" | ||||||
|  |     } | ||||||
|  |     packaging { | ||||||
|  |         jniLibs { | ||||||
|  |             excludes += listOf("META-INF/androidx.*") | ||||||
|  |         } | ||||||
|  |         resources { | ||||||
|  |             excludes += listOf( | ||||||
|  |                 "META-INF/androidx.*", | ||||||
|  |                 "META-INF/proguard/androidx-annotations.pro", | ||||||
|  |                 "/META-INF/LICENSE.md", | ||||||
|  |                 "/META-INF/LICENSE-notice.md" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     testOptions { | ||||||
|  |         animationsDisabled = true | ||||||
|  |         unitTests { | ||||||
|  |             isReturnDefaultValues = true | ||||||
|  |             isIncludeAndroidResources = true | ||||||
|  |         } | ||||||
|  |         unitTests.all { | ||||||
|  |             it.jvmArgs("-noverify") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     lint { | ||||||
|  |         abortOnError = false | ||||||
|  |         disable += listOf("MissingTranslation", "ExtraTranslation") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | dependencies { | ||||||
|  |     // Utils | ||||||
|  |     implementation(libs.gson) | ||||||
|  |     implementation(libs.okhttp) | ||||||
|  |     implementation(libs.retrofit) | ||||||
|  |     implementation(libs.retrofit.converter.gson) | ||||||
|  |     implementation(libs.retrofit.adapter.rxjava) | ||||||
|  |     implementation(libs.rxandroid) | ||||||
|  |     implementation(libs.rxjava) | ||||||
|  |     implementation(libs.rxbinding) | ||||||
|  |     implementation(libs.rxbinding.appcompat) | ||||||
|  |     implementation(libs.facebook.fresco) | ||||||
|  |     implementation(libs.facebook.fresco.middleware) | ||||||
|  |     implementation(libs.apache.commons.lang3) | ||||||
|  | 
 | ||||||
|  |     // UI | ||||||
|  |     implementation("${libs.viewpagerindicator.library.get()}@aar") | ||||||
|  |     implementation(libs.photoview) | ||||||
|  |     implementation(libs.android.sdk) | ||||||
|  |     implementation(libs.android.plugin.scalebar) | ||||||
|  | 
 | ||||||
|  |     implementation(libs.timber) | ||||||
|  |     implementation(libs.android.material) | ||||||
|  |     implementation(libs.dexter) | ||||||
|  | 
 | ||||||
|  |     // Jetpack Compose | ||||||
|  |     implementation(libs.androidx.core.ktx) | ||||||
|  |     implementation(libs.androidx.lifecycle.runtime.ktx) | ||||||
|  |     implementation(libs.androidx.activity.compose) | ||||||
|  |     implementation(platform(libs.androidx.compose.bom)) | ||||||
|  |     implementation(libs.androidx.compose.runtime) | ||||||
|  |     implementation(libs.androidx.ui) | ||||||
|  |     implementation(libs.androidx.ui.graphics) | ||||||
|  |     implementation(libs.androidx.ui.tooling.preview) | ||||||
|  |     implementation(libs.androidx.ui.viewbinding) | ||||||
|  |     implementation(libs.androidx.material3) | ||||||
|  |     implementation(libs.androidx.foundation) | ||||||
|  |     implementation(libs.androidx.foundation.layout) | ||||||
|  |     androidTestImplementation(platform(libs.androidx.compose.bom)) | ||||||
|  |     androidTestImplementation(libs.androidx.ui.test.junit4) | ||||||
|  |     debugImplementation(libs.androidx.ui.tooling) | ||||||
|  |     debugImplementation(libs.androidx.ui.test.manifest) | ||||||
|  | 
 | ||||||
|  |     implementation(libs.adapterdelegates4.kotlin.dsl.viewbinding) | ||||||
|  |     implementation(libs.adapterdelegates4.pagination) | ||||||
|  |     implementation(libs.androidx.paging.runtime.ktx) | ||||||
|  |     testImplementation(libs.androidx.paging.common.ktx) | ||||||
|  |     implementation(libs.androidx.paging.rxjava2.ktx) | ||||||
|  |     implementation(libs.androidx.recyclerview) | ||||||
|  | 
 | ||||||
|  |     // Logging | ||||||
|  |     implementation(libs.acra.dialog) | ||||||
|  |     implementation(libs.acra.mail) | ||||||
|  |     implementation(libs.slf4j.api) | ||||||
|  |     implementation(libs.logback.android.classic) { | ||||||
|  |         exclude(group = "com.google.android", module = "android") | ||||||
|  |     } | ||||||
|  |     implementation(libs.logging.interceptor) | ||||||
|  | 
 | ||||||
|  |     // Dependency injector | ||||||
|  |     implementation(libs.dagger.android) | ||||||
|  |     implementation(libs.dagger.android.support) | ||||||
|  |     kapt(libs.dagger.android.processor) | ||||||
|  |     kapt(libs.dagger.compiler) | ||||||
|  |     annotationProcessor(libs.dagger.android.processor) | ||||||
|  | 
 | ||||||
|  |     implementation(libs.kotlin.reflect) | ||||||
|  | 
 | ||||||
|  |     //Mocking | ||||||
|  |     testImplementation(libs.mockito.kotlin) | ||||||
|  |     testImplementation(libs.mockito.core) | ||||||
|  |     testImplementation(libs.powermock.module.junit) | ||||||
|  |     testImplementation(libs.powermock.api.mockito) | ||||||
|  |     testImplementation(libs.mockk) | ||||||
|  | 
 | ||||||
|  |     // Unit testing | ||||||
|  |     testImplementation(libs.junit) | ||||||
|  |     testImplementation(libs.robolectric) | ||||||
|  |     testImplementation(libs.androidx.test.core) | ||||||
|  |     testImplementation(libs.androidx.runner) | ||||||
|  |     testImplementation(libs.androidx.test.ext.junit) | ||||||
|  |     testImplementation(libs.androidx.test.rules) | ||||||
|  |     testImplementation(libs.mockwebserver) | ||||||
|  |     testImplementation(libs.livedata.testing.ktx) | ||||||
|  |     testImplementation(libs.androidx.core.testing) | ||||||
|  |     testImplementation(libs.junit.jupiter.api) | ||||||
|  |     testRuntimeOnly(libs.junit.jupiter.engine) | ||||||
|  |     testImplementation(libs.soloader) | ||||||
|  |     testImplementation(libs.kotlinx.coroutines.test) | ||||||
|  |     debugImplementation(libs.androidx.fragment.testing) | ||||||
|  |     testImplementation(libs.commons.io) | ||||||
|  | 
 | ||||||
|  |     // Android testing | ||||||
|  |     androidTestImplementation(libs.androidx.espresso.core) | ||||||
|  |     androidTestImplementation(libs.androidx.espresso.intents) | ||||||
|  |     androidTestImplementation(libs.androidx.espresso.contrib) | ||||||
|  |     androidTestImplementation(libs.androidx.runner) | ||||||
|  |     androidTestImplementation(libs.androidx.test.rules) | ||||||
|  |     androidTestImplementation(libs.androidx.test.core) | ||||||
|  |     androidTestImplementation(libs.androidx.test.ext.junit) | ||||||
|  |     androidTestImplementation(libs.androidx.annotation) | ||||||
|  |     androidTestImplementation(libs.mockwebserver) | ||||||
|  |     androidTestImplementation(libs.androidx.uiautomator) | ||||||
|  | 
 | ||||||
|  |     // Debugging | ||||||
|  |     debugImplementation(libs.leakcanary.android) | ||||||
|  | 
 | ||||||
|  |     // Support libraries | ||||||
|  |     implementation(libs.androidx.browser) | ||||||
|  |     implementation(libs.androidx.cardview) | ||||||
|  |     implementation(libs.androidx.constraintlayout) | ||||||
|  |     implementation(libs.androidx.exifinterface) | ||||||
|  |     implementation(libs.recyclerview.fastscroll) | ||||||
|  | 
 | ||||||
|  |     //swipe_layout | ||||||
|  |     implementation(libs.swipelayout.library) | ||||||
|  | 
 | ||||||
|  |     //Room | ||||||
|  |     implementation(libs.androidx.room.runtime) | ||||||
|  |     implementation(libs.androidx.room.ktx) | ||||||
|  |     implementation(libs.androidx.room.rxjava) | ||||||
|  |     kapt(libs.androidx.room.compiler) | ||||||
|  | 
 | ||||||
|  |     // Preferences | ||||||
|  |     implementation(libs.androidx.preference) | ||||||
|  |     implementation(libs.androidx.preference.ktx) | ||||||
|  | 
 | ||||||
|  |     //Android Media | ||||||
|  |     implementation(libs.juanitobananas.androidDmediaUtil) | ||||||
|  |     implementation(libs.androidx.multidex) | ||||||
|  | 
 | ||||||
|  |     // Kotlin + coroutines | ||||||
|  |     implementation(libs.androidx.work.runtime.ktx) | ||||||
|  |     implementation(libs.androidx.work.runtime) | ||||||
|  |     implementation(libs.kotlinx.coroutines.rx2) | ||||||
|  |     testImplementation(libs.androidx.work.testing) | ||||||
|  | 
 | ||||||
|  |     //Glide | ||||||
|  |     implementation(libs.glide) | ||||||
|  |     annotationProcessor(libs.glide.compiler) | ||||||
|  |     kaptTest(libs.androidx.databinding.compiler) | ||||||
|  |     kaptAndroidTest(libs.androidx.databinding.compiler) | ||||||
|  | 
 | ||||||
|  |     implementation(libs.coordinates2country.android) { | ||||||
|  |         exclude(group = "com.google.android", module = "android") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     //OSMDroid | ||||||
|  |     implementation(libs.osmdroid.android) | ||||||
|  |     constraints { | ||||||
|  |         implementation(libs.kotlin.stdlib.jdk7) { | ||||||
|  |             because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") | ||||||
|  |         } | ||||||
|  |         implementation(libs.kotlin.stdlib.jdk8) { | ||||||
|  |             because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | tasks.register<Exec>("disableAnimations") { | ||||||
|  |     val adb = "${System.getenv("ANDROID_HOME")}/platform-tools/adb" | ||||||
|  |     commandLine(adb, "shell", "settings", "put", "global", "window_animation_scale", "0") | ||||||
|  |     commandLine(adb, "shell", "settings", "put", "global", "transition_animation_scale", "0") | ||||||
|  |     commandLine(adb, "shell", "settings", "put", "global", "animator_duration_scale", "0") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | project.gradle.taskGraph.whenReady { | ||||||
|  |     val connectedBetaDebugAndroidTest = tasks.named("connectedBetaDebugAndroidTest") | ||||||
|  |     val connectedProdDebugAndroidTest = tasks.named("connectedProdDebugAndroidTest") | ||||||
|  | 
 | ||||||
|  |     connectedBetaDebugAndroidTest.configure { | ||||||
|  |         dependsOn("disableAnimations") | ||||||
|  |     } | ||||||
|  |     connectedProdDebugAndroidTest.configure { | ||||||
|  |         dependsOn("disableAnimations") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun getTestUserName(): String? { | ||||||
|  |     val propFile = rootProject.file("./local.properties") | ||||||
|  |     val properties = Properties() | ||||||
|  |     propFile.inputStream().use { properties.load(it) } | ||||||
|  |     return properties.getProperty("TEST_USER_NAME") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun getTestPassword(): String? { | ||||||
|  |     val propFile = rootProject.file("./local.properties") | ||||||
|  |     val properties = Properties() | ||||||
|  |     propFile.inputStream().use { properties.load(it) } | ||||||
|  |     return properties.getProperty("TEST_USER_PASSWORD") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | if (isRunningOnTravisAndIsNotPRBuild) { | ||||||
|  |     configure<com.github.triplet.gradle.play.PlayPublisherExtension> { | ||||||
|  |         track = "alpha" | ||||||
|  |         userFraction = 1.0 | ||||||
|  |         serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME") | ||||||
|  |         serviceAccountCredentials = file("../play.p12") | ||||||
|  | 
 | ||||||
|  |         resolutionStrategy = "auto" | ||||||
|  |         outputProcessor { // this: ApkVariantOutput | ||||||
|  |             versionNameOverride = "$versionNameOverride.$versionCode" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun getBuildVersion(): String? { | ||||||
|  |     return try { | ||||||
|  |         val stdout = ByteArrayOutputStream() | ||||||
|  |         exec { | ||||||
|  |             commandLine("git", "rev-parse", "--short", "HEAD") | ||||||
|  |             standardOutput = stdout | ||||||
|  |         } | ||||||
|  |         stdout.toString().trim() | ||||||
|  |     } catch (e: Exception) { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun getBranchName(): String? { | ||||||
|  |     return try { | ||||||
|  |         val stdout = ByteArrayOutputStream() | ||||||
|  |         exec { | ||||||
|  |             commandLine("git", "rev-parse", "--abbrev-ref", "HEAD") | ||||||
|  |             standardOutput = stdout | ||||||
|  |         } | ||||||
|  |         stdout.toString().trim() | ||||||
|  |     } catch (e: Exception) { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -66,6 +66,9 @@ | ||||||
| # Application classes that will be serialized/deserialized over Gson | # Application classes that will be serialized/deserialized over Gson | ||||||
| -keep class com.google.gson.examples.android.model.** { *; } | -keep class com.google.gson.examples.android.model.** { *; } | ||||||
| 
 | 
 | ||||||
|  | # Prevent R8 from obfuscating project classes used by Gson for parsing | ||||||
|  | -keep class fr.free.nrw.commons.fileusages.** { *; } | ||||||
|  | 
 | ||||||
| # Prevent proguard from stripping interface information from TypeAdapterFactory, | # Prevent proguard from stripping interface information from TypeAdapterFactory, | ||||||
| # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) | ||||||
| -keep class * implements com.google.gson.TypeAdapterFactory | -keep class * implements com.google.gson.TypeAdapterFactory | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ class UploadCancelledTest { | ||||||
|     fun setup() { |     fun setup() { | ||||||
|         try { |         try { | ||||||
|             Intents.init() |             Intents.init() | ||||||
|         } catch (ex: IllegalStateException) { |         } catch (_: IllegalStateException) { | ||||||
|         } |         } | ||||||
|         device.unfreezeRotation() |         device.unfreezeRotation() | ||||||
|         device.setOrientationNatural() |         device.setOrientationNatural() | ||||||
|  | @ -65,7 +65,7 @@ class UploadCancelledTest { | ||||||
|     fun teardown() { |     fun teardown() { | ||||||
|         try { |         try { | ||||||
|             Intents.release() |             Intents.release() | ||||||
|         } catch (ex: IllegalStateException) { |         } catch (_: IllegalStateException) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -71,7 +71,7 @@ class UploadTest { | ||||||
|     fun setup() { |     fun setup() { | ||||||
|         try { |         try { | ||||||
|             Intents.init() |             Intents.init() | ||||||
|         } catch (ex: IllegalStateException) { |         } catch (_: IllegalStateException) { | ||||||
|         } |         } | ||||||
|         UITestHelper.loginUser() |         UITestHelper.loginUser() | ||||||
|         UITestHelper.skipWelcome() |         UITestHelper.skipWelcome() | ||||||
|  |  | ||||||
|  | @ -57,8 +57,7 @@ | ||||||
|     tools:replace="android:appComponentFactory"> |     tools:replace="android:appComponentFactory"> | ||||||
|     <activity |     <activity | ||||||
|       android:name=".activity.SingleWebViewActivity" |       android:name=".activity.SingleWebViewActivity" | ||||||
|       android:exported="false" |       android:exported="false" /> | ||||||
|       android:label="@string/title_activity_single_web_view" /> |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".nearby.WikidataFeedback" |       android:name=".nearby.WikidataFeedback" | ||||||
|       android:exported="false" /> |       android:exported="false" /> | ||||||
|  | @ -85,6 +84,7 @@ | ||||||
|       android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> |       android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> | ||||||
|     <activity |     <activity | ||||||
|       android:name=".auth.LoginActivity" |       android:name=".auth.LoginActivity" | ||||||
|  |       android:windowSoftInputMode="adjustPan" | ||||||
|       android:exported="true"> |       android:exported="true"> | ||||||
|       <intent-filter> |       <intent-filter> | ||||||
|         <category android:name="android.intent.category.LAUNCHER" /> |         <category android:name="android.intent.category.LAUNCHER" /> | ||||||
|  | @ -103,7 +103,7 @@ | ||||||
|       android:exported="true" |       android:exported="true" | ||||||
|       android:hardwareAccelerated="false" |       android:hardwareAccelerated="false" | ||||||
|       android:icon="@mipmap/ic_launcher" |       android:icon="@mipmap/ic_launcher" | ||||||
|       android:windowSoftInputMode="adjustResize"> |       android:windowSoftInputMode="adjustPan"> | ||||||
|       <intent-filter android:label="@string/intent_share_upload_label"> |       <intent-filter android:label="@string/intent_share_upload_label"> | ||||||
|         <action android:name="android.intent.action.SEND" /> |         <action android:name="android.intent.action.SEND" /> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| package fr.free.nrw.commons | package fr.free.nrw.commons | ||||||
| 
 | 
 | ||||||
| import android.annotation.SuppressLint | import android.annotation.SuppressLint | ||||||
|  | import android.content.ActivityNotFoundException | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
|  | import android.content.Intent.ACTION_VIEW | ||||||
| import android.net.Uri | import android.net.Uri | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.view.Menu | import android.view.Menu | ||||||
|  | @ -16,6 +18,10 @@ import fr.free.nrw.commons.theme.BaseActivity | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha | import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha | ||||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||||
| import java.util.Collections | import java.util.Collections | ||||||
|  | import androidx.core.net.toUri | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets | ||||||
|  | import fr.free.nrw.commons.utils.handleWebUrl | ||||||
|  | import fr.free.nrw.commons.utils.setUnderlinedText | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Represents about screen of this app |  * Represents about screen of this app | ||||||
|  | @ -42,6 +48,7 @@ class AboutActivity : BaseActivity() { | ||||||
|          */ |          */ | ||||||
|         binding = ActivityAboutBinding.inflate(layoutInflater) |         binding = ActivityAboutBinding.inflate(layoutInflater) | ||||||
|         val view: View = binding!!.root |         val view: View = binding!!.root | ||||||
|  |         applyEdgeToEdgeTopInsets(binding!!.toolbarLayout) | ||||||
|         setContentView(view) |         setContentView(view) | ||||||
| 
 | 
 | ||||||
|         setSupportActionBar(binding!!.toolbarBinding.toolbar) |         setSupportActionBar(binding!!.toolbarBinding.toolbar) | ||||||
|  | @ -59,30 +66,12 @@ class AboutActivity : BaseActivity() { | ||||||
|         binding!!.aboutImprove.setHtmlText(improveText) |         binding!!.aboutImprove.setHtmlText(improveText) | ||||||
|         binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha() |         binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha() | ||||||
| 
 | 
 | ||||||
|         Utils.setUnderlinedText( |         binding!!.aboutFaq.setUnderlinedText(R.string.about_faq) | ||||||
|             binding!!.aboutFaq, R.string.about_faq, |         binding!!.aboutRateUs.setUnderlinedText(R.string.about_rate_us) | ||||||
|             applicationContext |         binding!!.aboutUserGuide.setUnderlinedText(R.string.user_guide) | ||||||
|         ) |         binding!!.aboutPrivacyPolicy.setUnderlinedText(R.string.about_privacy_policy) | ||||||
|         Utils.setUnderlinedText( |         binding!!.aboutTranslate.setUnderlinedText(R.string.about_translate) | ||||||
|             binding!!.aboutRateUs, R.string.about_rate_us, |         binding!!.aboutCredits.setUnderlinedText(R.string.about_credits) | ||||||
|             applicationContext |  | ||||||
|         ) |  | ||||||
|         Utils.setUnderlinedText( |  | ||||||
|             binding!!.aboutUserGuide, R.string.user_guide, |  | ||||||
|             applicationContext |  | ||||||
|         ) |  | ||||||
|         Utils.setUnderlinedText( |  | ||||||
|             binding!!.aboutPrivacyPolicy, R.string.about_privacy_policy, |  | ||||||
|             applicationContext |  | ||||||
|         ) |  | ||||||
|         Utils.setUnderlinedText( |  | ||||||
|             binding!!.aboutTranslate, R.string.about_translate, |  | ||||||
|             applicationContext |  | ||||||
|         ) |  | ||||||
|         Utils.setUnderlinedText( |  | ||||||
|             binding!!.aboutCredits, R.string.about_credits, |  | ||||||
|             applicationContext |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|         /* |         /* | ||||||
|           To set listeners, we can create a separate method and use lambda syntax. |           To set listeners, we can create a separate method and use lambda syntax. | ||||||
|  | @ -106,47 +95,56 @@ class AboutActivity : BaseActivity() { | ||||||
|     fun launchFacebook(view: View?) { |     fun launchFacebook(view: View?) { | ||||||
|         val intent: Intent |         val intent: Intent | ||||||
|         try { |         try { | ||||||
|             intent = Intent(Intent.ACTION_VIEW, Uri.parse(Urls.FACEBOOK_APP_URL)) |             intent = Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri()) | ||||||
|             intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME) |             intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME) | ||||||
|             startActivity(intent) |             startActivity(intent) | ||||||
|         } catch (e: Exception) { |         } catch (e: Exception) { | ||||||
|             Utils.handleWebUrl(this, Uri.parse(Urls.FACEBOOK_WEB_URL)) |             handleWebUrl(this, Urls.FACEBOOK_WEB_URL.toUri()) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun launchGithub(view: View?) { |     fun launchGithub(view: View?) { | ||||||
|         val intent: Intent |         val intent: Intent | ||||||
|         try { |         try { | ||||||
|             intent = Intent(Intent.ACTION_VIEW, Uri.parse(Urls.GITHUB_REPO_URL)) |             intent = Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri()) | ||||||
|             intent.setPackage(Urls.GITHUB_PACKAGE_NAME) |             intent.setPackage(Urls.GITHUB_PACKAGE_NAME) | ||||||
|             startActivity(intent) |             startActivity(intent) | ||||||
|         } catch (e: Exception) { |         } catch (e: Exception) { | ||||||
|             Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL)) |             handleWebUrl(this, Urls.GITHUB_REPO_URL.toUri()) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun launchWebsite(view: View?) { |     fun launchWebsite(view: View?) { | ||||||
|         Utils.handleWebUrl(this, Uri.parse(Urls.WEBSITE_URL)) |         handleWebUrl(this, Urls.WEBSITE_URL.toUri()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun launchRatings(view: View?) { |     fun launchRatings(view: View?) { | ||||||
|         Utils.rateApp(this) |         try { | ||||||
|  |             startActivity( | ||||||
|  |                 Intent( | ||||||
|  |                     ACTION_VIEW, | ||||||
|  |                     (Urls.PLAY_STORE_PREFIX + packageName).toUri() | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         } catch (_: ActivityNotFoundException) { | ||||||
|  |             handleWebUrl(this, (Urls.PLAY_STORE_URL_PREFIX + packageName).toUri()) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun launchCredits(view: View?) { |     fun launchCredits(view: View?) { | ||||||
|         Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL)) |         handleWebUrl(this, Urls.CREDITS_URL.toUri()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun launchUserGuide(view: View?) { |     fun launchUserGuide(view: View?) { | ||||||
|         Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL)) |         handleWebUrl(this, Urls.USER_GUIDE_URL.toUri()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun launchPrivacyPolicy(view: View?) { |     fun launchPrivacyPolicy(view: View?) { | ||||||
|         Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)) |         handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun launchFrequentlyAskedQuesions(view: View?) { |     fun launchFrequentlyAskedQuesions(view: View?) { | ||||||
|         Utils.handleWebUrl(this, Uri.parse(Urls.FAQ_URL)) |         handleWebUrl(this, Urls.FAQ_URL.toUri()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onCreateOptionsMenu(menu: Menu): Boolean { |     override fun onCreateOptionsMenu(menu: Menu): Boolean { | ||||||
|  | @ -193,7 +191,7 @@ class AboutActivity : BaseActivity() { | ||||||
| 
 | 
 | ||||||
|         val positiveButtonRunnable = Runnable { |         val positiveButtonRunnable = Runnable { | ||||||
|             val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition] |             val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition] | ||||||
|             Utils.handleWebUrl(this@AboutActivity, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode)) |             handleWebUrl(this@AboutActivity, (Urls.TRANSLATE_WIKI_URL + langCode).toUri()) | ||||||
|         } |         } | ||||||
|         showAlertDialog( |         showAlertDialog( | ||||||
|             this, |             this, | ||||||
|  |  | ||||||
|  | @ -1,18 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Base presenter, enforcing contracts to atach and detach view |  | ||||||
|  */ |  | ||||||
| public interface BasePresenter<T> { |  | ||||||
|     /** |  | ||||||
|      * Until a view is attached, it is open to listen events from the presenter |  | ||||||
|      */ |  | ||||||
|     void onAttachView(@NonNull T view); |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Detaching a view makes sure that the view no more receives events from the presenter |  | ||||||
|      */ |  | ||||||
|     void onDetachView(); |  | ||||||
| } |  | ||||||
							
								
								
									
										10
									
								
								app/src/main/java/fr/free/nrw/commons/BasePresenter.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/java/fr/free/nrw/commons/BasePresenter.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | package fr.free.nrw.commons | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Base presenter, enforcing contracts to attach and detach view | ||||||
|  |  */ | ||||||
|  | interface BasePresenter<T> { | ||||||
|  |     fun onAttachView(view: T) | ||||||
|  | 
 | ||||||
|  |     fun onDetachView() | ||||||
|  | } | ||||||
|  | @ -15,9 +15,8 @@ import com.facebook.drawee.backends.pipeline.Fresco | ||||||
| import com.facebook.imagepipeline.core.ImagePipelineConfig | import com.facebook.imagepipeline.core.ImagePipelineConfig | ||||||
| import fr.free.nrw.commons.auth.LoginActivity | import fr.free.nrw.commons.auth.LoginActivity | ||||||
| import fr.free.nrw.commons.auth.SessionManager | import fr.free.nrw.commons.auth.SessionManager | ||||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable | ||||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao | import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable | ||||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao |  | ||||||
| import fr.free.nrw.commons.category.CategoryDao | import fr.free.nrw.commons.category.CategoryDao | ||||||
| import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler | import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler | ||||||
| import fr.free.nrw.commons.concurrency.ThreadPoolService | import fr.free.nrw.commons.concurrency.ThreadPoolService | ||||||
|  | @ -257,8 +256,8 @@ class CommonsApplication : MultiDexApplication() { | ||||||
|         } catch (e: SQLiteException) { |         } catch (e: SQLiteException) { | ||||||
|             Timber.e(e) |             Timber.e(e) | ||||||
|         } |         } | ||||||
|         BookmarkPicturesDao.Table.onDelete(db) |         BookmarksTable.onDelete(db) | ||||||
|         BookmarkItemsDao.Table.onDelete(db) |         BookmarkItemsTable.onDelete(db) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,79 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * represents Licence object |  | ||||||
|  */ |  | ||||||
| public class License { |  | ||||||
|     private String key; |  | ||||||
|     private String template; |  | ||||||
|     private String url; |  | ||||||
|     private String name; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Constructs a new instance of License. |  | ||||||
|      * |  | ||||||
|      * @param key       license key |  | ||||||
|      * @param template  license template |  | ||||||
|      * @param url       license URL |  | ||||||
|      * @param name      licence name |  | ||||||
|      * |  | ||||||
|      * @throws RuntimeException if License.key or Licence.template is null |  | ||||||
|      */ |  | ||||||
|     public License(String key, String template, String url, String name) { |  | ||||||
|         if (key == null) { |  | ||||||
|             throw new RuntimeException("License.key must not be null"); |  | ||||||
|         } |  | ||||||
|         if (template == null) { |  | ||||||
|             throw new RuntimeException("License.template must not be null"); |  | ||||||
|         } |  | ||||||
|         this.key = key; |  | ||||||
|         this.template = template; |  | ||||||
|         this.url = url; |  | ||||||
|         this.name = name; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Gets the license key. |  | ||||||
|      * @return license key as a String. |  | ||||||
|      */ |  | ||||||
|     public String getKey() { |  | ||||||
|         return key; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Gets the license template. |  | ||||||
|      * @return license template as a String. |  | ||||||
|      */ |  | ||||||
|     public String getTemplate() { |  | ||||||
|         return template; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Gets the license name. If name is null, return license key. |  | ||||||
|      * @return license name as string. if name null, license key as String |  | ||||||
|      */ |  | ||||||
|     public String getName() { |  | ||||||
|         if (name == null) { |  | ||||||
|             // hack |  | ||||||
|             return getKey(); |  | ||||||
|         } else { |  | ||||||
|             return name; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Gets the license URL |  | ||||||
|      * |  | ||||||
|      * @param language license language |  | ||||||
|      * @return URL |  | ||||||
|      */ |  | ||||||
|     public @Nullable String getUrl(String language) { |  | ||||||
|         if (url == null) { |  | ||||||
|             return null; |  | ||||||
|         } else { |  | ||||||
|             return url.replace("$lang", language); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,30 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.location.LatLng; |  | ||||||
| import fr.free.nrw.commons.nearby.Place; |  | ||||||
| import java.util.List; |  | ||||||
| 
 |  | ||||||
| public abstract class MapController { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * We pass this variable as a group of placeList and boundaryCoordinates |  | ||||||
|      */ |  | ||||||
|     public class NearbyPlacesInfo { |  | ||||||
|         public List<Place> placeList; // List of nearby places |  | ||||||
|         public LatLng[] boundaryCoordinates; // Corners of nearby area |  | ||||||
|         public LatLng currentLatLng; // Current location when this places are populated |  | ||||||
|         public LatLng searchLatLng; // Search location for finding this places |  | ||||||
|         public List<Media> mediaList; // Search location for finding this places |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * We pass this variable as a group of placeList and boundaryCoordinates |  | ||||||
|      */ |  | ||||||
|     public class ExplorePlacesInfo { |  | ||||||
|         public List<Place> explorePlaceList; // List of nearby places |  | ||||||
|         public LatLng[] boundaryCoordinates; // Corners of nearby area |  | ||||||
|         public LatLng currentLatLng; // Current location when this places are populated |  | ||||||
|         public LatLng searchLatLng; // Search location for finding this places |  | ||||||
|         public List<Media> mediaList; // Search location for finding this places |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										46
									
								
								app/src/main/java/fr/free/nrw/commons/MapController.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/src/main/java/fr/free/nrw/commons/MapController.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | ||||||
|  | package fr.free.nrw.commons | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.location.LatLng | ||||||
|  | import fr.free.nrw.commons.nearby.Place | ||||||
|  | 
 | ||||||
|  | abstract class MapController { | ||||||
|  |     /** | ||||||
|  |      * We pass this variable as a group of placeList and boundaryCoordinates | ||||||
|  |      */ | ||||||
|  |     inner class NearbyPlacesInfo { | ||||||
|  |         @JvmField | ||||||
|  |         var placeList: List<Place> = emptyList() // List of nearby places | ||||||
|  | 
 | ||||||
|  |         @JvmField | ||||||
|  |         var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area | ||||||
|  | 
 | ||||||
|  |         @JvmField | ||||||
|  |         var currentLatLng: LatLng? = null // Current location when this places are populated | ||||||
|  | 
 | ||||||
|  |         @JvmField | ||||||
|  |         var searchLatLng: LatLng? = null // Search location for finding this places | ||||||
|  | 
 | ||||||
|  |         @JvmField | ||||||
|  |         var mediaList: List<Media>? = null // Search location for finding this places | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * We pass this variable as a group of placeList and boundaryCoordinates | ||||||
|  |      */ | ||||||
|  |     inner class ExplorePlacesInfo { | ||||||
|  |         @JvmField | ||||||
|  |         var explorePlaceList: List<Place> = emptyList() // List of nearby places | ||||||
|  | 
 | ||||||
|  |         @JvmField | ||||||
|  |         var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area | ||||||
|  | 
 | ||||||
|  |         @JvmField | ||||||
|  |         var currentLatLng: LatLng? = null // Current location when this places are populated | ||||||
|  | 
 | ||||||
|  |         @JvmField | ||||||
|  |         var searchLatLng: LatLng? = null // Search location for finding this places | ||||||
|  | 
 | ||||||
|  |         @JvmField | ||||||
|  |         var mediaList: List<Media> = emptyList() // Search location for finding this places | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| package fr.free.nrw.commons | package fr.free.nrw.commons | ||||||
| 
 | 
 | ||||||
| import android.os.Parcelable | import android.os.Parcelable | ||||||
|  | import fr.free.nrw.commons.BuildConfig.COMMONS_URL | ||||||
| import fr.free.nrw.commons.location.LatLng | import fr.free.nrw.commons.location.LatLng | ||||||
|  | import fr.free.nrw.commons.wikidata.model.WikiSite | ||||||
| import fr.free.nrw.commons.wikidata.model.page.PageTitle | import fr.free.nrw.commons.wikidata.model.page.PageTitle | ||||||
| import kotlinx.parcelize.IgnoredOnParcel | import kotlinx.parcelize.IgnoredOnParcel | ||||||
| import kotlinx.parcelize.Parcelize | import kotlinx.parcelize.Parcelize | ||||||
|  | @ -28,9 +30,7 @@ class Media constructor( | ||||||
|      */ |      */ | ||||||
|     var filename: String? = null, |     var filename: String? = null, | ||||||
|     /** |     /** | ||||||
|      * Gets or sets the file description. |      * The fallback description of the file, used if no other description is provided. | ||||||
|      * @return file description as a string |  | ||||||
|      * @param fallbackDescription the new description of the file |  | ||||||
|      */ |      */ | ||||||
|     var fallbackDescription: String? = null, |     var fallbackDescription: String? = null, | ||||||
|     /** |     /** | ||||||
|  | @ -40,19 +40,25 @@ class Media constructor( | ||||||
|      */ |      */ | ||||||
|     var dateUploaded: Date? = null, |     var dateUploaded: Date? = null, | ||||||
|     /** |     /** | ||||||
|      * Gets or sets the license name of the file. |      * The license name of the file. | ||||||
|      * @return license as a String |  | ||||||
|      * @param license license name as a String |  | ||||||
|      */ |      */ | ||||||
|     var license: String? = null, |     var license: String? = null, | ||||||
|  |     /** | ||||||
|  |      * The URL corresponding to the license. | ||||||
|  |      */ | ||||||
|     var licenseUrl: String? = null, |     var licenseUrl: String? = null, | ||||||
|     /** |     /** | ||||||
|      * Gets or sets the name of the creator of the file. |      * The name of the creator of the file. | ||||||
|      * @return author name as a String |  | ||||||
|      * @param author creator name as a string |  | ||||||
|      */ |      */ | ||||||
|     var author: String? = null, |     var author: String? = null, | ||||||
|  |     /** | ||||||
|  |      * The username of the uploader. | ||||||
|  |      */ | ||||||
|     var user: String? = null, |     var user: String? = null, | ||||||
|  |     /** | ||||||
|  |      * The full name of the file's creator, if different from username. | ||||||
|  |      */ | ||||||
|  |     var creatorName: String? = null, | ||||||
|     /** |     /** | ||||||
|      * Gets the categories the file falls under. |      * Gets the categories the file falls under. | ||||||
|      * @return file categories as an ArrayList of Strings |      * @return file categories as an ArrayList of Strings | ||||||
|  | @ -66,6 +72,7 @@ class Media constructor( | ||||||
|     var captions: Map<String, String> = emptyMap(), |     var captions: Map<String, String> = emptyMap(), | ||||||
|     var descriptions: Map<String, String> = emptyMap(), |     var descriptions: Map<String, String> = emptyMap(), | ||||||
|     var depictionIds: List<String> = emptyList(), |     var depictionIds: List<String> = emptyList(), | ||||||
|  |     var creatorIds: List<String> = emptyList(), | ||||||
|     /** |     /** | ||||||
|      * This field was added to find non-hidden categories |      * This field was added to find non-hidden categories | ||||||
|      * Stores the mapping of category title to hidden attribute |      * Stores the mapping of category title to hidden attribute | ||||||
|  | @ -130,6 +137,7 @@ class Media constructor( | ||||||
|      * returns user |      * returns user | ||||||
|      * @return Author or User |      * @return Author or User | ||||||
|      */ |      */ | ||||||
|  |     @Deprecated("Use user for uploader username. Use attributedAuthor() for attribution. Note that the uploader may not be the creator/author.") | ||||||
|     fun getAuthorOrUser(): String? { |     fun getAuthorOrUser(): String? { | ||||||
|         return if (!author.isNullOrEmpty()) { |         return if (!author.isNullOrEmpty()) { | ||||||
|             author |             author | ||||||
|  | @ -138,6 +146,19 @@ class Media constructor( | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns author if it's not null or empty, otherwise | ||||||
|  |      * returns creator name | ||||||
|  |      * @return name of author or creator | ||||||
|  |      */ | ||||||
|  |     fun getAttributedAuthor(): String? { | ||||||
|  |         return if (!author.isNullOrEmpty()) { | ||||||
|  |             author | ||||||
|  |         } else{ | ||||||
|  |             creatorName | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Gets media display title |      * Gets media display title | ||||||
|      * @return Media title |      * @return Media title | ||||||
|  | @ -154,7 +175,8 @@ class Media constructor( | ||||||
|      * Gets file page title |      * Gets file page title | ||||||
|      * @return New media page title |      * @return New media page title | ||||||
|      */ |      */ | ||||||
|     val pageTitle: PageTitle get() = Utils.getPageTitle(filename!!) |     val pageTitle: PageTitle | ||||||
|  |         get() = PageTitle(filename!!, WikiSite(COMMONS_URL)) | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns wikicode to use the media file on a MediaWiki site |      * Returns wikicode to use the media file on a MediaWiki site | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| package fr.free.nrw.commons | package fr.free.nrw.commons | ||||||
| 
 | 
 | ||||||
| import androidx.core.text.HtmlCompat | import androidx.core.text.HtmlCompat | ||||||
| import fr.free.nrw.commons.media.IdAndCaptions | import fr.free.nrw.commons.media.IdAndLabels | ||||||
| import fr.free.nrw.commons.media.MediaClient | import fr.free.nrw.commons.media.MediaClient | ||||||
| import fr.free.nrw.commons.media.PAGE_ID_PREFIX | import fr.free.nrw.commons.media.PAGE_ID_PREFIX | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
|  | @ -29,7 +29,17 @@ class MediaDataExtractor | ||||||
|                     it |                     it | ||||||
|                         .entities() |                         .entities() | ||||||
|                         .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } |                         .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } | ||||||
|                 }.map { it.map { (key, value) -> IdAndCaptions(key, value) } } |                 }.map { it.map { (key, value) -> IdAndLabels(key, value) } } | ||||||
|  |                 .onErrorReturn { emptyList() } | ||||||
|  | 
 | ||||||
|  |         fun fetchCreatorIdsAndLabels(media: Media) = | ||||||
|  |             mediaClient | ||||||
|  |                 .getEntities(media.creatorIds) | ||||||
|  |                 .map { | ||||||
|  |                     it | ||||||
|  |                         .entities() | ||||||
|  |                         .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } | ||||||
|  |                 }.map { it.map { (key, value) -> IdAndLabels(key, value) } } | ||||||
|                 .onErrorReturn { emptyList() } |                 .onErrorReturn { emptyList() } | ||||||
| 
 | 
 | ||||||
|         fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename) |         fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename) | ||||||
|  |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Base interface for all the views |  | ||||||
|  */ |  | ||||||
| public interface MvpView { |  | ||||||
|     void showMessage(String message); |  | ||||||
| } |  | ||||||
|  | @ -1,154 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; |  | ||||||
| import java.io.File; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.concurrent.TimeUnit; |  | ||||||
| import okhttp3.Cache; |  | ||||||
| import okhttp3.Interceptor; |  | ||||||
| import okhttp3.OkHttpClient; |  | ||||||
| import okhttp3.Request; |  | ||||||
| import okhttp3.Response; |  | ||||||
| import okhttp3.ResponseBody; |  | ||||||
| import okhttp3.logging.HttpLoggingInterceptor; |  | ||||||
| import okhttp3.logging.HttpLoggingInterceptor.Level; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| public final class OkHttpConnectionFactory { |  | ||||||
|     private static final String CACHE_DIR_NAME = "okhttp-cache"; |  | ||||||
|     private static final long NET_CACHE_SIZE = 64 * 1024 * 1024; |  | ||||||
| 
 |  | ||||||
|     public static OkHttpClient CLIENT; |  | ||||||
| 
 |  | ||||||
|     @NonNull public static OkHttpClient getClient(final CommonsCookieJar cookieJar) { |  | ||||||
|         if (CLIENT == null) { |  | ||||||
|             CLIENT = createClient(cookieJar); |  | ||||||
|         } |  | ||||||
|         return CLIENT; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @NonNull |  | ||||||
|     private static OkHttpClient createClient(final CommonsCookieJar cookieJar) { |  | ||||||
|         return new OkHttpClient.Builder() |  | ||||||
|                 .cookieJar(cookieJar) |  | ||||||
|                 .cache((CommonsApplication.getInstance()!=null) ? new Cache(new File(CommonsApplication.getInstance().getCacheDir(), CACHE_DIR_NAME), NET_CACHE_SIZE) : null) |  | ||||||
|                 .connectTimeout(120, TimeUnit.SECONDS) |  | ||||||
|                 .writeTimeout(120, TimeUnit.SECONDS) |  | ||||||
|                 .readTimeout(120, TimeUnit.SECONDS) |  | ||||||
|                 .addInterceptor(getLoggingInterceptor()) |  | ||||||
|                 .addInterceptor(new UnsuccessfulResponseInterceptor()) |  | ||||||
|                 .addInterceptor(new CommonHeaderRequestInterceptor()) |  | ||||||
|                 .build(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static HttpLoggingInterceptor getLoggingInterceptor() { |  | ||||||
|         final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor() |  | ||||||
|             .setLevel(Level.BASIC); |  | ||||||
| 
 |  | ||||||
|         httpLoggingInterceptor.redactHeader("Authorization"); |  | ||||||
|         httpLoggingInterceptor.redactHeader("Cookie"); |  | ||||||
| 
 |  | ||||||
|         return httpLoggingInterceptor; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static class CommonHeaderRequestInterceptor implements Interceptor { |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         @NonNull |  | ||||||
|         public Response intercept(@NonNull final Chain chain) throws IOException { |  | ||||||
|             final Request request = chain.request().newBuilder() |  | ||||||
|                     .header("User-Agent", CommonsApplication.getInstance().getUserAgent()) |  | ||||||
|                     .build(); |  | ||||||
|             return chain.proceed(request); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static class UnsuccessfulResponseInterceptor implements Interceptor { |  | ||||||
|         private static final String SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log"; |  | ||||||
|         public static final String SUPPRESS_ERROR_LOG_HEADER = SUPPRESS_ERROR_LOG+": true"; |  | ||||||
|         private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList( |  | ||||||
|             "api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1"); |  | ||||||
| 
 |  | ||||||
|         private static final String ERRORS_PREFIX = "{\"error"; |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         @NonNull |  | ||||||
|         public Response intercept(@NonNull final Chain chain) throws IOException { |  | ||||||
|             final Request rq = chain.request(); |  | ||||||
| 
 |  | ||||||
|             // If the request contains our special "suppress errors" header, make note of it |  | ||||||
|             // but don't pass that on to the server. |  | ||||||
|             final boolean suppressErrors = rq.headers().names().contains(SUPPRESS_ERROR_LOG); |  | ||||||
|             final Request request = rq.newBuilder() |  | ||||||
|                 .removeHeader(SUPPRESS_ERROR_LOG) |  | ||||||
|                 .build(); |  | ||||||
| 
 |  | ||||||
|             final Response rsp = chain.proceed(request); |  | ||||||
| 
 |  | ||||||
|             // Do not intercept certain requests and let the caller handle the errors |  | ||||||
|             if(isExcludedUrl(chain.request())) { |  | ||||||
|                 return rsp; |  | ||||||
|             } |  | ||||||
|             if (rsp.isSuccessful()) { |  | ||||||
|                 try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) { |  | ||||||
|                     if (ERRORS_PREFIX.equals(responseBody.string())) { |  | ||||||
|                         try (final ResponseBody body = rsp.body()) { |  | ||||||
|                             throw new IOException(body.string()); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } catch (final IOException e) { |  | ||||||
|                     // Log the error as debug (and therefore, "expected") or at error level |  | ||||||
|                     if (suppressErrors) { |  | ||||||
|                         Timber.d(e, "Suppressed (known / expected) error"); |  | ||||||
|                     } else { |  | ||||||
|                         Timber.e(e); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 return rsp; |  | ||||||
|             } |  | ||||||
|             throw new HttpStatusException(rsp); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private boolean isExcludedUrl(final Request request) { |  | ||||||
|             final String requestUrl = request.url().toString(); |  | ||||||
|             for(final String url: DO_NOT_INTERCEPT) { |  | ||||||
|                 if(requestUrl.contains(url)) { |  | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private OkHttpConnectionFactory() { |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static class HttpStatusException extends IOException { |  | ||||||
|         private final int code; |  | ||||||
|         private final String url; |  | ||||||
|         public HttpStatusException(@NonNull Response rsp) { |  | ||||||
|             this.code = rsp.code(); |  | ||||||
|             this.url = rsp.request().url().uri().toString(); |  | ||||||
|             try { |  | ||||||
|                 if (rsp.body() != null && rsp.body().contentType() != null |  | ||||||
|                         && rsp.body().contentType().toString().contains("json")) { |  | ||||||
|                 } |  | ||||||
|             } catch (Exception e) { |  | ||||||
|                 // Log? |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public int code() { |  | ||||||
|             return code; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         @Override |  | ||||||
|         public String getMessage() { |  | ||||||
|             String str = "Code: " + code + ", URL: " + url; |  | ||||||
|             return str; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										135
									
								
								app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,135 @@ | ||||||
|  | package fr.free.nrw.commons | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.VisibleForTesting | ||||||
|  | import fr.free.nrw.commons.wikidata.GsonUtil | ||||||
|  | import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar | ||||||
|  | import fr.free.nrw.commons.wikidata.mwapi.MwErrorResponse | ||||||
|  | import fr.free.nrw.commons.wikidata.mwapi.MwIOException | ||||||
|  | import fr.free.nrw.commons.wikidata.mwapi.MwLegacyServiceError | ||||||
|  | import okhttp3.Cache | ||||||
|  | import okhttp3.Interceptor | ||||||
|  | import okhttp3.OkHttpClient | ||||||
|  | import okhttp3.Request | ||||||
|  | import okhttp3.Response | ||||||
|  | import okhttp3.logging.HttpLoggingInterceptor | ||||||
|  | import timber.log.Timber | ||||||
|  | import java.io.File | ||||||
|  | import java.io.IOException | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
|  | 
 | ||||||
|  | object OkHttpConnectionFactory { | ||||||
|  |     private const val CACHE_DIR_NAME = "okhttp-cache" | ||||||
|  |     private const val NET_CACHE_SIZE = (64 * 1024 * 1024).toLong() | ||||||
|  | 
 | ||||||
|  |     @VisibleForTesting | ||||||
|  |     var CLIENT: OkHttpClient? = null | ||||||
|  | 
 | ||||||
|  |     fun getClient(cookieJar: CommonsCookieJar): OkHttpClient { | ||||||
|  |         if (CLIENT == null) { | ||||||
|  |             CLIENT = createClient(cookieJar) | ||||||
|  |         } | ||||||
|  |         return CLIENT!! | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun createClient(cookieJar: CommonsCookieJar): OkHttpClient { | ||||||
|  |         return OkHttpClient.Builder() | ||||||
|  |             .cookieJar(cookieJar) | ||||||
|  |             .cache( | ||||||
|  |                 if (CommonsApplication.instance != null) Cache( | ||||||
|  |                     File(CommonsApplication.instance.cacheDir, CACHE_DIR_NAME), | ||||||
|  |                     NET_CACHE_SIZE | ||||||
|  |                 ) else null | ||||||
|  |             ) | ||||||
|  |             .connectTimeout(120, TimeUnit.SECONDS) | ||||||
|  |             .writeTimeout(120, TimeUnit.SECONDS) | ||||||
|  |             .readTimeout(120, TimeUnit.SECONDS) | ||||||
|  |             .addInterceptor(HttpLoggingInterceptor().apply { | ||||||
|  |                 setLevel(HttpLoggingInterceptor.Level.BASIC) | ||||||
|  |                 redactHeader("Authorization") | ||||||
|  |                 redactHeader("Cookie") | ||||||
|  |             }) | ||||||
|  |             .addInterceptor(UnsuccessfulResponseInterceptor()) | ||||||
|  |             .addInterceptor(CommonHeaderRequestInterceptor()) | ||||||
|  |             .build() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class CommonHeaderRequestInterceptor : Interceptor { | ||||||
|  |     @Throws(IOException::class) | ||||||
|  |     override fun intercept(chain: Interceptor.Chain): Response { | ||||||
|  |         val request = chain.request().newBuilder() | ||||||
|  |             .header("User-Agent", CommonsApplication.instance.userAgent) | ||||||
|  |             .build() | ||||||
|  |         return chain.proceed(request) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | private const val SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log" | ||||||
|  | const val SUPPRESS_ERROR_LOG_HEADER: String = "$SUPPRESS_ERROR_LOG: true" | ||||||
|  | 
 | ||||||
|  | private class UnsuccessfulResponseInterceptor : Interceptor { | ||||||
|  |     @Throws(IOException::class) | ||||||
|  |     override fun intercept(chain: Interceptor.Chain): Response { | ||||||
|  |         val rq = chain.request() | ||||||
|  | 
 | ||||||
|  |         // If the request contains our special "suppress errors" header, make note of it | ||||||
|  |         // but don't pass that on to the server. | ||||||
|  |         val suppressErrors = rq.headers.names().contains(SUPPRESS_ERROR_LOG) | ||||||
|  |         val request = rq.newBuilder() | ||||||
|  |             .removeHeader(SUPPRESS_ERROR_LOG) | ||||||
|  |             .build() | ||||||
|  | 
 | ||||||
|  |         val rsp = chain.proceed(request) | ||||||
|  | 
 | ||||||
|  |         // Do not intercept certain requests and let the caller handle the errors | ||||||
|  |         if (isExcludedUrl(chain.request())) { | ||||||
|  |             return rsp | ||||||
|  |         } | ||||||
|  |         if (rsp.isSuccessful) { | ||||||
|  |             try { | ||||||
|  |                 rsp.peekBody(ERRORS_PREFIX.length.toLong()).use { responseBody -> | ||||||
|  |                     if (ERRORS_PREFIX == responseBody.string()) { | ||||||
|  |                         rsp.body.use { body -> | ||||||
|  |                             val bodyString = body!!.string() | ||||||
|  | 
 | ||||||
|  |                             throw MwIOException( | ||||||
|  |                                 "MediaWiki API returned error: $bodyString", | ||||||
|  |                                 GsonUtil.defaultGson.fromJson( | ||||||
|  |                                     bodyString, | ||||||
|  |                                     MwErrorResponse::class.java | ||||||
|  |                                 ).error!!, | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } catch (e: MwIOException) { | ||||||
|  |                 // Log the error as debug (and therefore, "expected") or at error level | ||||||
|  |                 if (suppressErrors) { | ||||||
|  |                     Timber.d(e, "Suppressed (known / expected) error") | ||||||
|  |                 } else { | ||||||
|  |                     Timber.e(e) | ||||||
|  |                     throw e | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return rsp | ||||||
|  |         } | ||||||
|  |         throw IOException("Unsuccessful response") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun isExcludedUrl(request: Request): Boolean { | ||||||
|  |         val requestUrl = request.url.toString() | ||||||
|  |         for (url in DO_NOT_INTERCEPT) { | ||||||
|  |             if (requestUrl.contains(url)) { | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         val DO_NOT_INTERCEPT = listOf( | ||||||
|  |             "api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1" | ||||||
|  |         ) | ||||||
|  |         const val ERRORS_PREFIX = "{\"error" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,264 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import android.content.ClipData; |  | ||||||
| import android.content.ClipboardManager; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.graphics.Bitmap; |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.text.SpannableString; |  | ||||||
| import android.text.style.UnderlineSpan; |  | ||||||
| import android.view.View; |  | ||||||
| import android.widget.TextView; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.browser.customtabs.CustomTabColorSchemeParams; |  | ||||||
| import androidx.browser.customtabs.CustomTabsIntent; |  | ||||||
| import androidx.core.content.ContextCompat; |  | ||||||
| 
 |  | ||||||
| import java.util.Calendar; |  | ||||||
| import java.util.Date; |  | ||||||
| import fr.free.nrw.commons.wikidata.model.WikiSite; |  | ||||||
| import fr.free.nrw.commons.wikidata.model.page.PageTitle; |  | ||||||
| 
 |  | ||||||
| import java.util.Locale; |  | ||||||
| import java.util.regex.Pattern; |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.location.LatLng; |  | ||||||
| import fr.free.nrw.commons.settings.Prefs; |  | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| public class Utils { |  | ||||||
| 
 |  | ||||||
|     public static PageTitle getPageTitle(@NonNull String title) { |  | ||||||
|         return new PageTitle(title, new WikiSite(BuildConfig.COMMONS_URL)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Generates licence name with given ID |  | ||||||
|      * @param license License ID |  | ||||||
|      * @return Name of license |  | ||||||
|      */ |  | ||||||
|     public static int licenseNameFor(String license) { |  | ||||||
|         switch (license) { |  | ||||||
|             case Prefs.Licenses.CC_BY_3: |  | ||||||
|                 return R.string.license_name_cc_by; |  | ||||||
|             case Prefs.Licenses.CC_BY_4: |  | ||||||
|                 return R.string.license_name_cc_by_four; |  | ||||||
|             case Prefs.Licenses.CC_BY_SA_3: |  | ||||||
|                 return R.string.license_name_cc_by_sa; |  | ||||||
|             case Prefs.Licenses.CC_BY_SA_4: |  | ||||||
|                 return R.string.license_name_cc_by_sa_four; |  | ||||||
|             case Prefs.Licenses.CC0: |  | ||||||
|                 return R.string.license_name_cc0; |  | ||||||
|         } |  | ||||||
|         throw new IllegalStateException("Unrecognized license value: " + license); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Generates license url with given ID |  | ||||||
|      * @param license License ID |  | ||||||
|      * @return Url of license |  | ||||||
|      */ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     @NonNull |  | ||||||
|     public static String licenseUrlFor(String license) { |  | ||||||
|         switch (license) { |  | ||||||
|             case Prefs.Licenses.CC_BY_3: |  | ||||||
|                 return "https://creativecommons.org/licenses/by/3.0/"; |  | ||||||
|             case Prefs.Licenses.CC_BY_4: |  | ||||||
|                 return "https://creativecommons.org/licenses/by/4.0/"; |  | ||||||
|             case Prefs.Licenses.CC_BY_SA_3: |  | ||||||
|                 return "https://creativecommons.org/licenses/by-sa/3.0/"; |  | ||||||
|             case Prefs.Licenses.CC_BY_SA_4: |  | ||||||
|                 return "https://creativecommons.org/licenses/by-sa/4.0/"; |  | ||||||
|             case Prefs.Licenses.CC0: |  | ||||||
|                 return "https://creativecommons.org/publicdomain/zero/1.0/"; |  | ||||||
|             default: |  | ||||||
|                 throw new IllegalStateException("Unrecognized license value: " + license); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected |  | ||||||
|      * @param title File name |  | ||||||
|      * @param extension Correct extension |  | ||||||
|      * @return File with correct extension |  | ||||||
|      */ |  | ||||||
|     public static String fixExtension(String title, String extension) { |  | ||||||
|         Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE); |  | ||||||
| 
 |  | ||||||
|         // People are used to ".jpg" more than ".jpeg" which the system gives us. |  | ||||||
|         if (extension != null && extension.toLowerCase(Locale.ENGLISH).equals("jpeg")) { |  | ||||||
|             extension = "jpg"; |  | ||||||
|         } |  | ||||||
|         title = jpegPattern.matcher(title).replaceFirst(".jpg"); |  | ||||||
|         if (extension != null && !title.toLowerCase(Locale.getDefault()) |  | ||||||
|                 .endsWith("." + extension.toLowerCase(Locale.ENGLISH))) { |  | ||||||
|             title += "." + extension; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // If extension is still null, make it jpg. (Hotfix for https://github.com/commons-app/apps-android-commons/issues/228) |  | ||||||
|         // If title has an extension in it, if won't be true |  | ||||||
|         if (extension == null && title.lastIndexOf(".")<=0) { |  | ||||||
|            extension = "jpg"; |  | ||||||
|            title += "." + extension; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return title; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Launches intent to rate app |  | ||||||
|      * @param context |  | ||||||
|      */ |  | ||||||
|     public static void rateApp(Context context) { |  | ||||||
|         final String appPackageName = context.getPackageName(); |  | ||||||
|         try { |  | ||||||
|             context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.PLAY_STORE_PREFIX + appPackageName))); |  | ||||||
|         } |  | ||||||
|         catch (android.content.ActivityNotFoundException anfe) { |  | ||||||
|             handleWebUrl(context, Uri.parse(Urls.PLAY_STORE_URL_PREFIX + appPackageName)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Opens Custom Tab Activity with in-app browser for the specified URL. |  | ||||||
|      * Launches intent for web URL |  | ||||||
|      * @param context |  | ||||||
|      * @param url |  | ||||||
|      */ |  | ||||||
|     public static void handleWebUrl(Context context, Uri url) { |  | ||||||
|         Timber.d("Launching web url %s", url.toString()); |  | ||||||
| 
 |  | ||||||
|         final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder() |  | ||||||
|             .setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor)) |  | ||||||
|             .setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor)) |  | ||||||
|             .build(); |  | ||||||
| 
 |  | ||||||
|         CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); |  | ||||||
|         builder.setDefaultColorSchemeParams(color); |  | ||||||
|         builder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right); |  | ||||||
|         CustomTabsIntent customTabsIntent = builder.build(); |  | ||||||
|         // Clear previous browser tasks, so that back/exit buttons work as intended. |  | ||||||
|         customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); |  | ||||||
|         customTabsIntent.launchUrl(context, url); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Util function to handle geo coordinates. It no longer depends on google maps and any app |  | ||||||
|      * capable of handling the map intent can handle it |  | ||||||
|      * |  | ||||||
|      * @param context The context for launching intent |  | ||||||
|      * @param latLng  The latitude and longitude of the location |  | ||||||
|      */ |  | ||||||
|     public static void handleGeoCoordinates(final Context context, final LatLng latLng) { |  | ||||||
|         handleGeoCoordinates(context, latLng, 16); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Util function to handle geo coordinates with specified zoom level. It no longer depends on |  | ||||||
|      * google maps and any app capable of handling the map intent can handle it |  | ||||||
|      * |  | ||||||
|      * @param context   The context for launching intent |  | ||||||
|      * @param latLng    The latitude and longitude of the location |  | ||||||
|      * @param zoomLevel The zoom level |  | ||||||
|      */ |  | ||||||
|     public static void handleGeoCoordinates(final Context context, final LatLng latLng, |  | ||||||
|         final double zoomLevel) { |  | ||||||
|         final Intent mapIntent = new Intent(Intent.ACTION_VIEW, latLng.getGmmIntentUri(zoomLevel)); |  | ||||||
|         if (mapIntent.resolveActivity(context.getPackageManager()) != null) { |  | ||||||
|             context.startActivity(mapIntent); |  | ||||||
|         } else { |  | ||||||
|             ViewUtil.showShortToast(context, context.getString(R.string.map_application_missing)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * To take screenshot of the screen and return it in Bitmap format |  | ||||||
|      * |  | ||||||
|      * @param view |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     public static Bitmap getScreenShot(View view) { |  | ||||||
|         View screenView = view.getRootView(); |  | ||||||
|         screenView.setDrawingCacheEnabled(true); |  | ||||||
|         Bitmap drawingCache = screenView.getDrawingCache(); |  | ||||||
|         if (drawingCache != null) { |  | ||||||
|             Bitmap bitmap = Bitmap.createBitmap(drawingCache); |  | ||||||
|             screenView.setDrawingCacheEnabled(false); |  | ||||||
|             return bitmap; |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /* |  | ||||||
|     *Copies the content to the clipboard |  | ||||||
|     * |  | ||||||
|     */ |  | ||||||
|     public static void copy(String label,String text, Context context){ |  | ||||||
|         ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); |  | ||||||
|         ClipData clip = ClipData.newPlainText(label, text); |  | ||||||
|         clipboard.setPrimaryClip(clip); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method sets underlined string text to a TextView |  | ||||||
|      * |  | ||||||
|      * @param textView TextView associated with string resource |  | ||||||
|      * @param stringResourceName string resource name |  | ||||||
|      * @param context |  | ||||||
|      */ |  | ||||||
|     public static void setUnderlinedText(TextView textView, int stringResourceName, Context context) { |  | ||||||
|         SpannableString content = new SpannableString(context.getString(stringResourceName)); |  | ||||||
|         content.setSpan(new UnderlineSpan(), 0, content.length(), 0); |  | ||||||
|         textView.setText(content); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * For now we are enabling the monuments only when the date lies between 1 Sept & 31 OCt |  | ||||||
|      * @param date |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     public static boolean isMonumentsEnabled(final Date date) { |  | ||||||
|         if (date.getMonth() == 8) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Util function to get the start date of wlm monument |  | ||||||
|      * For this release we are hardcoding it to be 1st September |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     public static String getWLMStartDate() { |  | ||||||
|         return "1 Sep"; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /*** |  | ||||||
|      * Util function to get the end date of wlm monument |  | ||||||
|      * For this release we are hardcoding it to be 31st October |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     public static String getWLMEndDate() { |  | ||||||
|         return "30 Sep"; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /*** |  | ||||||
|      * Function to get the current WLM year |  | ||||||
|      * It increments at the start of September in line with the other WLM functions |  | ||||||
|      * (No consideration of locales for now) |  | ||||||
|      * @param calendar |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     public static int getWikiLovesMonumentsYear(Calendar calendar) { |  | ||||||
|         int year = calendar.get(Calendar.YEAR); |  | ||||||
|         if (calendar.get(Calendar.MONTH) < Calendar.SEPTEMBER) { |  | ||||||
|             year -= 1; |  | ||||||
|         } |  | ||||||
|         return year; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,7 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| 
 |  | ||||||
| public interface ViewHolder<T> { |  | ||||||
|     void bindModel(Context context, T model); |  | ||||||
| } |  | ||||||
|  | @ -1,57 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import androidx.fragment.app.Fragment; |  | ||||||
| import androidx.fragment.app.FragmentManager; |  | ||||||
| import androidx.fragment.app.FragmentPagerAdapter; |  | ||||||
| 
 |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * This adapter will be used to display fragments in a ViewPager |  | ||||||
|  */ |  | ||||||
| public class ViewPagerAdapter extends FragmentPagerAdapter { |  | ||||||
|     private List<Fragment> fragmentList = new ArrayList<>(); |  | ||||||
|     private List<String> fragmentTitleList = new ArrayList<>(); |  | ||||||
| 
 |  | ||||||
|     public ViewPagerAdapter(FragmentManager manager) { |  | ||||||
|         super(manager); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method returns the fragment of the viewpager at a particular position |  | ||||||
|      * @param position |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public Fragment getItem(int position) { |  | ||||||
|         return fragmentList.get(position); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method returns the total number of fragments in the viewpager. |  | ||||||
|      * @return size |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public int getCount() { |  | ||||||
|         return fragmentList.size(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method sets the fragment and title list in the viewpager |  | ||||||
|      * @param fragmentList List of all fragments to be displayed in the viewpager |  | ||||||
|      * @param fragmentTitleList List of all titles of the fragments |  | ||||||
|      */ |  | ||||||
|     public void setTabData(List<Fragment> fragmentList, List<String> fragmentTitleList) { |  | ||||||
|         this.fragmentList = fragmentList; |  | ||||||
|         this.fragmentTitleList = fragmentTitleList; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method returns the title of the page at a particular position |  | ||||||
|      * @param position |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public CharSequence getPageTitle(int position) { |  | ||||||
|         return fragmentTitleList.get(position); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										44
									
								
								app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | ||||||
|  | package fr.free.nrw.commons | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.FragmentManager | ||||||
|  | import androidx.fragment.app.FragmentPagerAdapter | ||||||
|  | import java.util.Locale | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This adapter will be used to display fragments in a ViewPager | ||||||
|  |  */ | ||||||
|  | class ViewPagerAdapter : FragmentPagerAdapter { | ||||||
|  |     private val context: Context | ||||||
|  |     private var fragmentList: List<Fragment> = emptyList() | ||||||
|  |     private var fragmentTitleList: List<String> = emptyList() | ||||||
|  | 
 | ||||||
|  |     constructor(context: Context, manager: FragmentManager) : super(manager) { | ||||||
|  |         this.context = context | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     constructor(context: Context, manager: FragmentManager, behavior: Int) : super(manager, behavior) { | ||||||
|  |         this.context = context | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getItem(position: Int): Fragment = fragmentList[position] | ||||||
|  | 
 | ||||||
|  |     override fun getPageTitle(position: Int): CharSequence = fragmentTitleList[position] | ||||||
|  | 
 | ||||||
|  |     override fun getCount(): Int = fragmentList.size | ||||||
|  | 
 | ||||||
|  |     fun setTabs(vararg titlesToFragments: Pair<Int, Fragment>) { | ||||||
|  |         // Enforce that every title must come from strings.xml and all will consistently be uppercase | ||||||
|  |         fragmentTitleList = titlesToFragments.map { | ||||||
|  |             context.getString(it.first).uppercase(Locale.ROOT) | ||||||
|  |         } | ||||||
|  |         fragmentList = titlesToFragments.map { it.second } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         // Convenience method for Java callers, can be removed when everything is migrated | ||||||
|  |         @JvmStatic | ||||||
|  |         fun pairOf(first: Int, second: Fragment) = first to second | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,109 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import android.app.AlertDialog; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.view.View; |  | ||||||
| import fr.free.nrw.commons.databinding.ActivityWelcomeBinding; |  | ||||||
| import fr.free.nrw.commons.databinding.PopupForCopyrightBinding; |  | ||||||
| import fr.free.nrw.commons.quiz.QuizActivity; |  | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; |  | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils; |  | ||||||
| 
 |  | ||||||
| public class WelcomeActivity extends BaseActivity { |  | ||||||
| 
 |  | ||||||
|     private ActivityWelcomeBinding binding; |  | ||||||
|     private PopupForCopyrightBinding copyrightBinding; |  | ||||||
| 
 |  | ||||||
|     private final WelcomePagerAdapter adapter = new WelcomePagerAdapter(); |  | ||||||
|     private boolean isQuiz; |  | ||||||
|     private AlertDialog.Builder dialogBuilder; |  | ||||||
|     private AlertDialog dialog; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Initialises exiting fields and dependencies |  | ||||||
|      * |  | ||||||
|      * @param savedInstanceState WelcomeActivity bundled data |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(final Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         binding = ActivityWelcomeBinding.inflate(getLayoutInflater()); |  | ||||||
|         final View view = binding.getRoot(); |  | ||||||
|         setContentView(view); |  | ||||||
| 
 |  | ||||||
|         if (getIntent() != null) { |  | ||||||
|             final Bundle bundle = getIntent().getExtras(); |  | ||||||
|             if (bundle != null) { |  | ||||||
|                 isQuiz = bundle.getBoolean("isQuiz"); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             isQuiz = false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Enable skip button if beta flavor |  | ||||||
|         if (ConfigUtils.isBetaFlavour()) { |  | ||||||
|             binding.finishTutorialButton.setVisibility(View.VISIBLE); |  | ||||||
| 
 |  | ||||||
|             dialogBuilder = new AlertDialog.Builder(this); |  | ||||||
|             copyrightBinding = PopupForCopyrightBinding.inflate(getLayoutInflater()); |  | ||||||
|             final View contactPopupView = copyrightBinding.getRoot(); |  | ||||||
|             dialogBuilder.setView(contactPopupView); |  | ||||||
|             dialogBuilder.setCancelable(false); |  | ||||||
|             dialog = dialogBuilder.create(); |  | ||||||
|             dialog.show(); |  | ||||||
| 
 |  | ||||||
|             copyrightBinding.buttonOk.setOnClickListener(v -> dialog.dismiss()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         binding.welcomePager.setAdapter(adapter); |  | ||||||
|         binding.welcomePagerIndicator.setViewPager(binding.welcomePager); |  | ||||||
| 
 |  | ||||||
|         binding.finishTutorialButton.setOnClickListener(v -> finishTutorial()); |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * References WelcomePageAdapter to null before the activity is destroyed |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void onDestroy() { |  | ||||||
|         if (isQuiz) { |  | ||||||
|             final Intent i = new Intent(this, QuizActivity.class); |  | ||||||
|             startActivity(i); |  | ||||||
|         } |  | ||||||
|         super.onDestroy(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Creates a way to change current activity to WelcomeActivity |  | ||||||
|      * |  | ||||||
|      * @param context Activity context |  | ||||||
|      */ |  | ||||||
|     public static void startYourself(final Context context) { |  | ||||||
|         final Intent welcomeIntent = new Intent(context, WelcomeActivity.class); |  | ||||||
|         context.startActivity(welcomeIntent); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Override onBackPressed() to go to previous tutorial 'pages' if not on first page |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void onBackPressed() { |  | ||||||
|         if (binding.welcomePager.getCurrentItem() != 0) { |  | ||||||
|             binding.welcomePager.setCurrentItem(binding.welcomePager.getCurrentItem() - 1, true); |  | ||||||
|         } else { |  | ||||||
|             if (defaultKvStore.getBoolean("firstrun", true)) { |  | ||||||
|                 finishAffinity(); |  | ||||||
|             } else { |  | ||||||
|                 super.onBackPressed(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void finishTutorial() { |  | ||||||
|         defaultKvStore.putBoolean("firstrun", false); |  | ||||||
|         finish(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										80
									
								
								app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,80 @@ | ||||||
|  | package fr.free.nrw.commons | ||||||
|  | 
 | ||||||
|  | import android.app.AlertDialog | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.View | ||||||
|  | import fr.free.nrw.commons.databinding.ActivityWelcomeBinding | ||||||
|  | import fr.free.nrw.commons.databinding.PopupForCopyrightBinding | ||||||
|  | import fr.free.nrw.commons.quiz.QuizActivity | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets | ||||||
|  | import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour | ||||||
|  | 
 | ||||||
|  | class WelcomeActivity : BaseActivity() { | ||||||
|  |     private var binding: ActivityWelcomeBinding? = null | ||||||
|  |     private var isQuiz = false | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialises exiting fields and dependencies | ||||||
|  |      * | ||||||
|  |      * @param savedInstanceState WelcomeActivity bundled data | ||||||
|  |      */ | ||||||
|  |     public override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         binding = ActivityWelcomeBinding.inflate(layoutInflater) | ||||||
|  |         applyEdgeToEdgeAllInsets(binding!!.welcomePager.rootView) | ||||||
|  |         setContentView(binding!!.root) | ||||||
|  | 
 | ||||||
|  |         isQuiz = intent?.extras?.getBoolean("isQuiz", false) ?: false | ||||||
|  | 
 | ||||||
|  |         // Enable skip button if beta flavor | ||||||
|  |         if (isBetaFlavour) { | ||||||
|  |             binding!!.finishTutorialButton.visibility = View.VISIBLE | ||||||
|  | 
 | ||||||
|  |             val copyrightBinding = PopupForCopyrightBinding.inflate(layoutInflater) | ||||||
|  | 
 | ||||||
|  |             val dialog = AlertDialog.Builder(this) | ||||||
|  |                 .setView(copyrightBinding.root) | ||||||
|  |                 .setCancelable(false) | ||||||
|  |                 .create() | ||||||
|  |             dialog.show() | ||||||
|  | 
 | ||||||
|  |             copyrightBinding.buttonOk.setOnClickListener { v: View? -> dialog.dismiss() } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val adapter = WelcomePagerAdapter() | ||||||
|  |         binding!!.welcomePager.adapter = adapter | ||||||
|  |         binding!!.welcomePagerIndicator.setViewPager(binding!!.welcomePager) | ||||||
|  |         binding!!.finishTutorialButton.setOnClickListener { v: View? -> finishTutorial() } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public override fun onDestroy() { | ||||||
|  |         if (isQuiz) { | ||||||
|  |             startActivity(Intent(this, QuizActivity::class.java)) | ||||||
|  |         } | ||||||
|  |         super.onDestroy() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onBackPressed() { | ||||||
|  |         if (binding!!.welcomePager.currentItem != 0) { | ||||||
|  |             binding!!.welcomePager.setCurrentItem(binding!!.welcomePager.currentItem - 1, true) | ||||||
|  |         } else { | ||||||
|  |             if (defaultKvStore.getBoolean("firstrun", true)) { | ||||||
|  |                 finishAffinity() | ||||||
|  |             } else { | ||||||
|  |                 super.onBackPressed() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun finishTutorial() { | ||||||
|  |         defaultKvStore.putBoolean("firstrun", false) | ||||||
|  |         finish() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun Context.startWelcome() { | ||||||
|  |     startActivity(Intent(this, WelcomeActivity::class.java)) | ||||||
|  | } | ||||||
|  | @ -1,74 +0,0 @@ | ||||||
| package fr.free.nrw.commons; |  | ||||||
| 
 |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.TextView; |  | ||||||
| 
 |  | ||||||
| import androidx.viewpager.widget.PagerAdapter; |  | ||||||
| 
 |  | ||||||
| public class WelcomePagerAdapter extends PagerAdapter { |  | ||||||
|     private static final int[] PAGE_LAYOUTS = new int[]{ |  | ||||||
|             R.layout.welcome_wikipedia, |  | ||||||
|             R.layout.welcome_do_upload, |  | ||||||
|             R.layout.welcome_dont_upload, |  | ||||||
|             R.layout.welcome_image_example, |  | ||||||
|             R.layout.welcome_final |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Gets total number of layouts |  | ||||||
|      * @return Number of layouts |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public int getCount() { |  | ||||||
|         return PAGE_LAYOUTS.length; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Compares given view with provided object |  | ||||||
|      * @param view Adapter view |  | ||||||
|      * @param object Adapter object |  | ||||||
|      * @return Equality between view and object |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public boolean isViewFromObject(View view, Object object) { |  | ||||||
|         return (view == object); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public Object instantiateItem(ViewGroup container, int position) { |  | ||||||
|         LayoutInflater inflater = LayoutInflater.from(container.getContext()); |  | ||||||
|         ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false); |  | ||||||
| 
 |  | ||||||
|         // If final page |  | ||||||
|         if (position == PAGE_LAYOUTS.length - 1) { |  | ||||||
|             // Add link to more information |  | ||||||
|             TextView moreInfo = layout.findViewById(R.id.welcomeInfo); |  | ||||||
|             Utils.setUnderlinedText(moreInfo, R.string.welcome_help_button_text, container.getContext()); |  | ||||||
|             moreInfo.setOnClickListener(view -> Utils.handleWebUrl( |  | ||||||
|                     container.getContext(), |  | ||||||
|                     Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents") |  | ||||||
|             )); |  | ||||||
| 
 |  | ||||||
|             // Handle click of finishTutorialButton ("YES!" button) inside layout |  | ||||||
|             layout.findViewById(R.id.finishTutorialButton) |  | ||||||
|                     .setOnClickListener(view -> ((WelcomeActivity) container.getContext()).finishTutorial()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         container.addView(layout); |  | ||||||
|         return layout; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Provides a way to remove an item from container |  | ||||||
|      * @param container Adapter view group container |  | ||||||
|      * @param position Index of item |  | ||||||
|      * @param obj Adapter object |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void destroyItem(ViewGroup container, int position, Object obj) { |  | ||||||
|         container.removeView((View) obj); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										70
									
								
								app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | package fr.free.nrw.commons | ||||||
|  | 
 | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.TextView | ||||||
|  | import androidx.core.net.toUri | ||||||
|  | import androidx.viewpager.widget.PagerAdapter | ||||||
|  | import fr.free.nrw.commons.utils.UnderlineUtils.setUnderlinedText | ||||||
|  | import fr.free.nrw.commons.utils.handleWebUrl | ||||||
|  | 
 | ||||||
|  | class WelcomePagerAdapter : PagerAdapter() { | ||||||
|  |     /** | ||||||
|  |      * Gets total number of layouts | ||||||
|  |      * @return Number of layouts | ||||||
|  |      */ | ||||||
|  |     override fun getCount(): Int = PAGE_LAYOUTS.size | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Compares given view with provided object | ||||||
|  |      * @param view Adapter view | ||||||
|  |      * @param obj Adapter object | ||||||
|  |      * @return Equality between view and object | ||||||
|  |      */ | ||||||
|  |     override fun isViewFromObject(view: View, obj: Any): Boolean = (view === obj) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Provides a way to remove an item from container | ||||||
|  |      * @param container Adapter view group container | ||||||
|  |      * @param position Index of item | ||||||
|  |      * @param obj Adapter object | ||||||
|  |      */ | ||||||
|  |     override fun destroyItem(container: ViewGroup, position: Int, obj: Any) = | ||||||
|  |         container.removeView(obj as View) | ||||||
|  | 
 | ||||||
|  |     override fun instantiateItem(container: ViewGroup, position: Int): Any { | ||||||
|  |         val inflater = LayoutInflater.from(container.context) | ||||||
|  |         val layout = inflater.inflate(PAGE_LAYOUTS[position], container, false) as ViewGroup | ||||||
|  | 
 | ||||||
|  |         // If final page | ||||||
|  |         if (position == PAGE_LAYOUTS.size - 1) { | ||||||
|  |             // Add link to more information | ||||||
|  |             val moreInfo = layout.findViewById<TextView>(R.id.welcomeInfo) | ||||||
|  |             setUnderlinedText(moreInfo, R.string.welcome_help_button_text) | ||||||
|  |             moreInfo.setOnClickListener { | ||||||
|  |                 handleWebUrl( | ||||||
|  |                     container.context, | ||||||
|  |                     "https://commons.wikimedia.org/wiki/Help:Contents".toUri() | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Handle click of finishTutorialButton ("YES!" button) inside layout | ||||||
|  |             layout.findViewById<View>(R.id.finishTutorialButton) | ||||||
|  |                 .setOnClickListener { view: View? -> (container.context as WelcomeActivity).finishTutorial() } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         container.addView(layout) | ||||||
|  |         return layout | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         private val PAGE_LAYOUTS = intArrayOf( | ||||||
|  |             R.layout.welcome_wikipedia, | ||||||
|  |             R.layout.welcome_do_upload, | ||||||
|  |             R.layout.welcome_dont_upload, | ||||||
|  |             R.layout.welcome_image_example, | ||||||
|  |             R.layout.welcome_final | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -129,9 +129,10 @@ interface PageEditInterface { | ||||||
|     ): Observable<Entities> |     ): Observable<Entities> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get wiki text for provided file names |      * Gets the wiki text for the provided file name. | ||||||
|      * @param titles : Name of the file |      * | ||||||
|      * @return Single<MwQueryResult> |      * @param title The title (name) of the file to fetch wiki text for. | ||||||
|  |      * @return A Single emitting the wiki query response. | ||||||
|      */ |      */ | ||||||
|     @GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=") |     @GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=") | ||||||
|     fun getWikiText( |     fun getWikiText( | ||||||
|  |  | ||||||
|  | @ -158,7 +158,9 @@ class SingleWebViewActivity : ComponentActivity() { | ||||||
| 
 | 
 | ||||||
|                     webChromeClient = object : WebChromeClient() { |                     webChromeClient = object : WebChromeClient() { | ||||||
|                         override fun onConsoleMessage(message: ConsoleMessage): Boolean { |                         override fun onConsoleMessage(message: ConsoleMessage): Boolean { | ||||||
|                             Timber.d("Console: ${message.message()} -- From line ${message.lineNumber()} of ${message.sourceId()}") |                             Timber.d("%s%s", | ||||||
|  |                                 "Console: ${message.message()} -- From line ", | ||||||
|  |                                 "${message.lineNumber()} of ${message.sourceId()}") | ||||||
|                             return true |                             return true | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  | @ -22,10 +22,10 @@ import androidx.appcompat.app.AlertDialog | ||||||
| import androidx.appcompat.app.AppCompatDelegate | import androidx.appcompat.app.AppCompatDelegate | ||||||
| import androidx.core.app.NavUtils | import androidx.core.app.NavUtils | ||||||
| import androidx.core.content.ContextCompat | import androidx.core.content.ContextCompat | ||||||
|  | import androidx.core.view.WindowCompat | ||||||
| import fr.free.nrw.commons.BuildConfig | import fr.free.nrw.commons.BuildConfig | ||||||
| import fr.free.nrw.commons.CommonsApplication | import fr.free.nrw.commons.CommonsApplication | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.Utils |  | ||||||
| import fr.free.nrw.commons.auth.login.LoginCallback | import fr.free.nrw.commons.auth.login.LoginCallback | ||||||
| import fr.free.nrw.commons.auth.login.LoginClient | import fr.free.nrw.commons.auth.login.LoginClient | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult | import fr.free.nrw.commons.auth.login.LoginResult | ||||||
|  | @ -33,11 +33,14 @@ import fr.free.nrw.commons.contributions.MainActivity | ||||||
| import fr.free.nrw.commons.databinding.ActivityLoginBinding | import fr.free.nrw.commons.databinding.ActivityLoginBinding | ||||||
| import fr.free.nrw.commons.di.ApplicationlessInjection | import fr.free.nrw.commons.di.ApplicationlessInjection | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets | ||||||
| import fr.free.nrw.commons.utils.AbstractTextWatcher | import fr.free.nrw.commons.utils.AbstractTextWatcher | ||||||
| import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags | import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags | ||||||
| import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour | import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour | ||||||
| import fr.free.nrw.commons.utils.SystemThemeUtils | import fr.free.nrw.commons.utils.SystemThemeUtils | ||||||
| import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard | import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard | ||||||
|  | import fr.free.nrw.commons.utils.handleKeyboardInsets | ||||||
|  | import fr.free.nrw.commons.utils.handleWebUrl | ||||||
| import io.reactivex.disposables.CompositeDisposable | import io.reactivex.disposables.CompositeDisposable | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
| import java.util.Locale | import java.util.Locale | ||||||
|  | @ -65,6 +68,7 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
|     private val delegate: AppCompatDelegate by lazy { |     private val delegate: AppCompatDelegate by lazy { | ||||||
|         AppCompatDelegate.create(this, null) |         AppCompatDelegate.create(this, null) | ||||||
|     } |     } | ||||||
|  |     private var lastLoginResult: LoginResult? = null | ||||||
| 
 | 
 | ||||||
|     public override fun onCreate(savedInstanceState: Bundle?) { |     public override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
|  | @ -78,7 +82,14 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
|         delegate.installViewFactory() |         delegate.installViewFactory() | ||||||
|         delegate.onCreate(savedInstanceState) |         delegate.onCreate(savedInstanceState) | ||||||
| 
 | 
 | ||||||
|  |         WindowCompat.getInsetsController(window, window.decorView) | ||||||
|  |             .isAppearanceLightStatusBars = !isDarkTheme | ||||||
|  | 
 | ||||||
|  |         WindowCompat.setDecorFitsSystemWindows(window, false) | ||||||
|  | 
 | ||||||
|         binding = ActivityLoginBinding.inflate(layoutInflater) |         binding = ActivityLoginBinding.inflate(layoutInflater) | ||||||
|  |         applyEdgeToEdgeAllInsets(binding!!.root) | ||||||
|  |         binding!!.root.handleKeyboardInsets() | ||||||
|         with(binding!!) { |         with(binding!!) { | ||||||
|             setContentView(root) |             setContentView(root) | ||||||
| 
 | 
 | ||||||
|  | @ -91,7 +102,19 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
|             aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() } |             aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() } | ||||||
|             signUpButton.setOnClickListener { signUp() } |             signUpButton.setOnClickListener { signUp() } | ||||||
|             loginButton.setOnClickListener { performLogin() } |             loginButton.setOnClickListener { performLogin() } | ||||||
|             loginPassword.setOnEditorActionListener(::onEditorAction) |             loginPassword.setOnEditorActionListener { textView, actionId, keyEvent -> | ||||||
|  |                 if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) { | ||||||
|  |                     if (actionId == EditorInfo.IME_ACTION_NEXT && lastLoginResult != null) { | ||||||
|  |                         askUserForTwoFactorAuthWithKeyboard() | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         performLogin() | ||||||
|  |                         true | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     false | ||||||
|  |                 } | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             loginPassword.onFocusChangeListener = |             loginPassword.onFocusChangeListener = | ||||||
|                 View.OnFocusChangeListener(::onPasswordFocusChanged) |                 View.OnFocusChangeListener(::onPasswordFocusChanged) | ||||||
|  | @ -112,6 +135,39 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @VisibleForTesting | ||||||
|  |     fun askUserForTwoFactorAuthWithKeyboard() { | ||||||
|  |         if (binding == null) { | ||||||
|  |             Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuthWithKeyboard") | ||||||
|  |             binding = ActivityLoginBinding.inflate(layoutInflater) | ||||||
|  |             setContentView(binding!!.root) | ||||||
|  |         } | ||||||
|  |         progressDialog!!.dismiss() | ||||||
|  |         if (binding != null) { | ||||||
|  |             with(binding!!) { | ||||||
|  |                 twoFactorContainer.visibility = View.VISIBLE | ||||||
|  |                 twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) | ||||||
|  |                 loginTwoFactor.visibility = View.VISIBLE | ||||||
|  |                 loginTwoFactor.requestFocus() | ||||||
|  | 
 | ||||||
|  |                 val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager | ||||||
|  |                 imm.showSoftInput(loginTwoFactor, InputMethodManager.SHOW_IMPLICIT) | ||||||
|  | 
 | ||||||
|  |                 loginTwoFactor.setOnEditorActionListener { _, actionId, event -> | ||||||
|  |                     if (actionId == EditorInfo.IME_ACTION_DONE || | ||||||
|  |                         (event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) { | ||||||
|  |                         performLogin() | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Timber.e("Binding is null in askUserForTwoFactorAuthWithKeyboard after reinitialization attempt") | ||||||
|  |         } | ||||||
|  |         showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed)) | ||||||
|  |     } | ||||||
|     override fun onPostCreate(savedInstanceState: Bundle?) { |     override fun onPostCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onPostCreate(savedInstanceState) |         super.onPostCreate(savedInstanceState) | ||||||
|         delegate.onPostCreate(savedInstanceState) |         delegate.onPostCreate(savedInstanceState) | ||||||
|  | @ -235,7 +291,7 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
|         } else false |         } else false | ||||||
| 
 | 
 | ||||||
|     private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) = |     private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) = | ||||||
|         actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER |         actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER | ||||||
| 
 | 
 | ||||||
|     private fun skipLogin() { |     private fun skipLogin() { | ||||||
|         AlertDialog.Builder(this) |         AlertDialog.Builder(this) | ||||||
|  | @ -253,10 +309,10 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun forgotPassword() = |     private fun forgotPassword() = | ||||||
|         Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)) |         handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)) | ||||||
| 
 | 
 | ||||||
|     private fun onPrivacyPolicyClicked() = |     private fun onPrivacyPolicyClicked() = | ||||||
|         Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)) |         handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)) | ||||||
| 
 | 
 | ||||||
|     private fun signUp() = |     private fun signUp() = | ||||||
|         startActivity(Intent(this, SignupActivity::class.java)) |         startActivity(Intent(this, SignupActivity::class.java)) | ||||||
|  | @ -271,6 +327,7 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
|         showLoggingProgressBar() |         showLoggingProgressBar() | ||||||
|         loginClient.doLogin(username, |         loginClient.doLogin(username, | ||||||
|             password, |             password, | ||||||
|  |             lastLoginResult, | ||||||
|             twoFactorCode, |             twoFactorCode, | ||||||
|             Locale.getDefault().language, |             Locale.getDefault().language, | ||||||
|             object : LoginCallback { |             object : LoginCallback { | ||||||
|  | @ -280,10 +337,18 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
|                     onLoginSuccess(loginResult) |                     onLoginSuccess(loginResult) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread { |                 override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread { | ||||||
|                     Timber.d("Requesting 2FA prompt") |                     Timber.d("Requesting 2FA prompt") | ||||||
|                     progressDialog!!.dismiss() |                     progressDialog!!.dismiss() | ||||||
|                     askUserForTwoFactorAuth() |                     lastLoginResult = loginResult | ||||||
|  |                     askUserForTwoFactorAuthWithKeyboard() | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread { | ||||||
|  |                     Timber.d("Requesting email auth prompt") | ||||||
|  |                     progressDialog!!.dismiss() | ||||||
|  |                     lastLoginResult = loginResult | ||||||
|  |                     askUserForTwoFactorAuthWithKeyboard() | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 override fun passwordResetPrompt(token: String?) = runOnUiThread { |                 override fun passwordResetPrompt(token: String?) = runOnUiThread { | ||||||
|  | @ -338,15 +403,35 @@ class LoginActivity : AccountAuthenticatorActivity() { | ||||||
| 
 | 
 | ||||||
|     @VisibleForTesting |     @VisibleForTesting | ||||||
|     fun askUserForTwoFactorAuth() { |     fun askUserForTwoFactorAuth() { | ||||||
|  |         if (binding == null) { | ||||||
|  |             Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuth") | ||||||
|  |             binding = ActivityLoginBinding.inflate(layoutInflater) | ||||||
|  |             setContentView(binding!!.root) | ||||||
|  |         } | ||||||
|         progressDialog!!.dismiss() |         progressDialog!!.dismiss() | ||||||
|  |         if (binding != null) { | ||||||
|             with(binding!!) { |             with(binding!!) { | ||||||
|                 twoFactorContainer.visibility = View.VISIBLE |                 twoFactorContainer.visibility = View.VISIBLE | ||||||
|  |                 twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) | ||||||
|                 loginTwoFactor.visibility = View.VISIBLE |                 loginTwoFactor.visibility = View.VISIBLE | ||||||
|                 loginTwoFactor.requestFocus() |                 loginTwoFactor.requestFocus() | ||||||
|  | 
 | ||||||
|  |                 loginTwoFactor.setOnEditorActionListener { _, actionId, event -> | ||||||
|  |                     if (actionId == EditorInfo.IME_ACTION_DONE || | ||||||
|  |                         (event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) { | ||||||
|  |                         performLogin() | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Timber.e("Binding is null in askUserForTwoFactorAuth after reinitialization attempt") | ||||||
|         } |         } | ||||||
|         val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager |         val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager | ||||||
|         imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) |         imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) | ||||||
|         showMessageAndCancelDialog(R.string.login_failed_2fa_needed) |         showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @VisibleForTesting |     @VisibleForTesting | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import android.widget.Toast | ||||||
| import fr.free.nrw.commons.BuildConfig | import fr.free.nrw.commons.BuildConfig | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.theme.BaseActivity | import fr.free.nrw.commons.theme.BaseActivity | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
| 
 | 
 | ||||||
| class SignupActivity : BaseActivity() { | class SignupActivity : BaseActivity() { | ||||||
|  | @ -21,6 +22,7 @@ class SignupActivity : BaseActivity() { | ||||||
|         Timber.d("Signup Activity started") |         Timber.d("Signup Activity started") | ||||||
| 
 | 
 | ||||||
|         webView = WebView(this) |         webView = WebView(this) | ||||||
|  |         applyEdgeToEdgeAllInsets(webView!!) | ||||||
|         with(webView!!) { |         with(webView!!) { | ||||||
|             setContentView(this) |             setContentView(this) | ||||||
|             webViewClient = MyWebViewClient() |             webViewClient = MyWebViewClient() | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ class CsrfTokenClient( | ||||||
|             try { |             try { | ||||||
|                 if (retry > 0) { |                 if (retry > 0) { | ||||||
|                     // Log in explicitly |                     // Log in explicitly | ||||||
|                     loginClient.loginBlocking(userName, password, "") |                     loginClient.loginBlocking(userName, password) | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Get CSRFToken response off the main thread. |                 // Get CSRFToken response off the main thread. | ||||||
|  | @ -92,6 +92,8 @@ class CsrfTokenClient( | ||||||
|                 override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught } |                 override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught } | ||||||
| 
 | 
 | ||||||
|                 override fun twoFactorPrompt() = cb.twoFactorPrompt() |                 override fun twoFactorPrompt() = cb.twoFactorPrompt() | ||||||
|  | 
 | ||||||
|  |                 override fun emailAuthPrompt() = cb.emailAuthPrompt() | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -165,10 +167,17 @@ class CsrfTokenClient( | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             override fun twoFactorPrompt( |             override fun twoFactorPrompt( | ||||||
|  |                 loginResult: LoginResult, | ||||||
|                 caught: Throwable, |                 caught: Throwable, | ||||||
|                 token: String?, |                 token: String?, | ||||||
|             ) = callback.twoFactorPrompt() |             ) = callback.twoFactorPrompt() | ||||||
| 
 | 
 | ||||||
|  |             override fun emailAuthPrompt( | ||||||
|  |                 loginResult: LoginResult, | ||||||
|  |                 caught: Throwable, | ||||||
|  |                 token: String?, | ||||||
|  |             ) = callback.emailAuthPrompt() | ||||||
|  | 
 | ||||||
|             // Should not happen here, but call the callback just in case. |             // Should not happen here, but call the callback just in case. | ||||||
|             override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password.")) |             override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password.")) | ||||||
| 
 | 
 | ||||||
|  | @ -190,6 +199,8 @@ class CsrfTokenClient( | ||||||
|         fun failure(caught: Throwable?) |         fun failure(caught: Throwable?) | ||||||
| 
 | 
 | ||||||
|         fun twoFactorPrompt() |         fun twoFactorPrompt() | ||||||
|  | 
 | ||||||
|  |         fun emailAuthPrompt() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     companion object { |     companion object { | ||||||
|  |  | ||||||
|  | @ -4,6 +4,13 @@ interface LoginCallback { | ||||||
|     fun success(loginResult: LoginResult) |     fun success(loginResult: LoginResult) | ||||||
| 
 | 
 | ||||||
|     fun twoFactorPrompt( |     fun twoFactorPrompt( | ||||||
|  |         loginResult: LoginResult, | ||||||
|  |         caught: Throwable, | ||||||
|  |         token: String?, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     fun emailAuthPrompt( | ||||||
|  |         loginResult: LoginResult, | ||||||
|         caught: Throwable, |         caught: Throwable, | ||||||
|         token: String?, |         token: String?, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package fr.free.nrw.commons.auth.login | package fr.free.nrw.commons.auth.login | ||||||
| 
 | 
 | ||||||
| import android.text.TextUtils | import android.text.TextUtils | ||||||
|  | import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult | import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult | import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult | ||||||
| import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL | import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL | ||||||
|  | @ -51,6 +52,7 @@ class LoginClient( | ||||||
|                         password, |                         password, | ||||||
|                         null, |                         null, | ||||||
|                         null, |                         null, | ||||||
|  |                         null, | ||||||
|                         response.body()!!.query()!!.loginToken(), |                         response.body()!!.query()!!.loginToken(), | ||||||
|                         userLanguage, |                         userLanguage, | ||||||
|                         cb, |                         cb, | ||||||
|  | @ -75,6 +77,7 @@ class LoginClient( | ||||||
|         password: String, |         password: String, | ||||||
|         retypedPassword: String?, |         retypedPassword: String?, | ||||||
|         twoFactorCode: String?, |         twoFactorCode: String?, | ||||||
|  |         emailAuthCode: String?, | ||||||
|         loginToken: String?, |         loginToken: String?, | ||||||
|         userLanguage: String, |         userLanguage: String, | ||||||
|         cb: LoginCallback, |         cb: LoginCallback, | ||||||
|  | @ -82,7 +85,7 @@ class LoginClient( | ||||||
|         this.userLanguage = userLanguage |         this.userLanguage = userLanguage | ||||||
| 
 | 
 | ||||||
|         loginCall = |         loginCall = | ||||||
|             if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { |             if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { | ||||||
|                 loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) |                 loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) | ||||||
|             } else { |             } else { | ||||||
|                 loginInterface.postLogIn( |                 loginInterface.postLogIn( | ||||||
|  | @ -90,6 +93,7 @@ class LoginClient( | ||||||
|                     password, |                     password, | ||||||
|                     retypedPassword, |                     retypedPassword, | ||||||
|                     twoFactorCode, |                     twoFactorCode, | ||||||
|  |                     emailAuthCode, | ||||||
|                     loginToken, |                     loginToken, | ||||||
|                     userLanguage, |                     userLanguage, | ||||||
|                     true, |                     true, | ||||||
|  | @ -112,10 +116,18 @@ class LoginClient( | ||||||
|                             when (loginResult) { |                             when (loginResult) { | ||||||
|                                 is OAuthResult -> |                                 is OAuthResult -> | ||||||
|                                     cb.twoFactorPrompt( |                                     cb.twoFactorPrompt( | ||||||
|  |                                         loginResult, | ||||||
|                                         LoginFailedException(loginResult.message), |                                         LoginFailedException(loginResult.message), | ||||||
|                                         loginToken, |                                         loginToken, | ||||||
|                                     ) |                                     ) | ||||||
| 
 | 
 | ||||||
|  |                                 is EmailAuthResult -> | ||||||
|  |                                     cb.emailAuthPrompt( | ||||||
|  |                                         loginResult, | ||||||
|  |                                         LoginFailedException(loginResult.message), | ||||||
|  |                                         loginToken | ||||||
|  |                                     ) | ||||||
|  | 
 | ||||||
|                                 is ResetPasswordResult -> cb.passwordResetPrompt(loginToken) |                                 is ResetPasswordResult -> cb.passwordResetPrompt(loginToken) | ||||||
| 
 | 
 | ||||||
|                                 is LoginResult.Result -> |                                 is LoginResult.Result -> | ||||||
|  | @ -147,6 +159,7 @@ class LoginClient( | ||||||
|     fun doLogin( |     fun doLogin( | ||||||
|         username: String, |         username: String, | ||||||
|         password: String, |         password: String, | ||||||
|  |         lastLoginResult: LoginResult?, | ||||||
|         twoFactorCode: String, |         twoFactorCode: String, | ||||||
|         userLanguage: String, |         userLanguage: String, | ||||||
|         loginCallback: LoginCallback, |         loginCallback: LoginCallback, | ||||||
|  | @ -159,7 +172,10 @@ class LoginClient( | ||||||
|                 ) = if (response.isSuccessful) { |                 ) = if (response.isSuccessful) { | ||||||
|                     val loginToken = response.body()?.query()?.loginToken() |                     val loginToken = response.body()?.query()?.loginToken() | ||||||
|                     loginToken?.let { |                     loginToken?.let { | ||||||
|                         login(username, password, null, twoFactorCode, it, userLanguage, loginCallback) |                         login(username, password, null, | ||||||
|  |                             if (lastLoginResult is OAuthResult) twoFactorCode else null, | ||||||
|  |                             if (lastLoginResult is EmailAuthResult) twoFactorCode else null, | ||||||
|  |                             it, userLanguage, loginCallback) | ||||||
|                     } ?: run { |                     } ?: run { | ||||||
|                         loginCallback.error(IOException("Failed to retrieve login token")) |                         loginCallback.error(IOException("Failed to retrieve login token")) | ||||||
|                     } |                     } | ||||||
|  | @ -181,7 +197,8 @@ class LoginClient( | ||||||
|     fun loginBlocking( |     fun loginBlocking( | ||||||
|         userName: String, |         userName: String, | ||||||
|         password: String, |         password: String, | ||||||
|         twoFactorCode: String?, |         twoFactorCode: String? = null, | ||||||
|  |         emailAuthCode: String? = null | ||||||
|     ) { |     ) { | ||||||
|         val tokenResponse = getLoginToken().execute() |         val tokenResponse = getLoginToken().execute() | ||||||
|         if (tokenResponse |         if (tokenResponse | ||||||
|  | @ -195,7 +212,7 @@ class LoginClient( | ||||||
| 
 | 
 | ||||||
|         val loginToken = tokenResponse.body()?.query()?.loginToken() |         val loginToken = tokenResponse.body()?.query()?.loginToken() | ||||||
|         val tempLoginCall = |         val tempLoginCall = | ||||||
|             if (twoFactorCode.isNullOrEmpty()) { |             if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) { | ||||||
|                 loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) |                 loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) | ||||||
|             } else { |             } else { | ||||||
|                 loginInterface.postLogIn( |                 loginInterface.postLogIn( | ||||||
|  | @ -203,6 +220,7 @@ class LoginClient( | ||||||
|                     password, |                     password, | ||||||
|                     null, |                     null, | ||||||
|                     twoFactorCode, |                     twoFactorCode, | ||||||
|  |                     emailAuthCode, | ||||||
|                     loginToken, |                     loginToken, | ||||||
|                     userLanguage, |                     userLanguage, | ||||||
|                     true, |                     true, | ||||||
|  | @ -214,7 +232,7 @@ class LoginClient( | ||||||
|         val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.") |         val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.") | ||||||
| 
 | 
 | ||||||
|         if ("UI" == loginResult.status) { |         if ("UI" == loginResult.status) { | ||||||
|             if (loginResult is OAuthResult) { |             if (loginResult is OAuthResult || loginResult is EmailAuthResult) { | ||||||
|                 // TODO: Find a better way to boil up the warning about 2FA |                 // TODO: Find a better way to boil up the warning about 2FA | ||||||
|                 throw LoginFailedException(loginResult.message) |                 throw LoginFailedException(loginResult.message) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -35,7 +35,8 @@ interface LoginInterface { | ||||||
|         @Field("password") pass: String?, |         @Field("password") pass: String?, | ||||||
|         @Field("retype") retypedPass: String?, |         @Field("retype") retypedPass: String?, | ||||||
|         @Field("OATHToken") twoFactorCode: String?, |         @Field("OATHToken") twoFactorCode: String?, | ||||||
|         @Field("logintoken") token: String?, |         @Field("token") emailAuthToken: String?, | ||||||
|  |         @Field("logintoken") loginToken: String?, | ||||||
|         @Field("uselang") userLanguage: String?, |         @Field("uselang") userLanguage: String?, | ||||||
|         @Field("logincontinue") loginContinue: Boolean, |         @Field("logincontinue") loginContinue: Boolean, | ||||||
|     ): Call<LoginResponse?> |     ): Call<LoginResponse?> | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ package fr.free.nrw.commons.auth.login | ||||||
| 
 | 
 | ||||||
| import com.google.gson.annotations.SerializedName | import com.google.gson.annotations.SerializedName | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult | import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult | ||||||
|  | import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult | import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult | ||||||
| import fr.free.nrw.commons.auth.login.LoginResult.Result | import fr.free.nrw.commons.auth.login.LoginResult.Result | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwServiceError | import fr.free.nrw.commons.wikidata.mwapi.MwServiceError | ||||||
|  | @ -27,11 +28,13 @@ internal class ClientLogin { | ||||||
|     fun toLoginResult(password: String): LoginResult { |     fun toLoginResult(password: String): LoginResult { | ||||||
|         var userMessage = message |         var userMessage = message | ||||||
|         if ("UI" == status) { |         if ("UI" == status) { | ||||||
|             if (requests != null) { |             requests?.forEach { request -> | ||||||
|                 for (req in requests) { |                 request.id()?.let { | ||||||
|                     if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) { |                     if (it.endsWith("TOTPAuthenticationRequest")) { | ||||||
|                         return OAuthResult(status, userName, password, message) |                         return OAuthResult(status, userName, password, message) | ||||||
|                     } else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) { |                     } else if (it.endsWith("EmailAuthAuthenticationRequest")) { | ||||||
|  |                         return EmailAuthResult(status, userName, password, message) | ||||||
|  |                     } else if (it.endsWith("PasswordAuthenticationRequest")) { | ||||||
|                         return ResetPasswordResult(status, userName, password, message) |                         return ResetPasswordResult(status, userName, password, message) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  | @ -49,7 +52,7 @@ internal class Request { | ||||||
|     private val required: String? = null |     private val required: String? = null | ||||||
|     private val provider: String? = null |     private val provider: String? = null | ||||||
|     private val account: String? = null |     private val account: String? = null | ||||||
|     private val fields: Map<String, RequestField>? = null |     internal val fields: Map<String, RequestField>? = null | ||||||
| 
 | 
 | ||||||
|     fun id(): String? = id |     fun id(): String? = id | ||||||
| } | } | ||||||
|  | @ -57,5 +60,5 @@ internal class Request { | ||||||
| internal class RequestField { | internal class RequestField { | ||||||
|     private val type: String? = null |     private val type: String? = null | ||||||
|     private val label: String? = null |     private val label: String? = null | ||||||
|     private val help: String? = null |     internal val help: String? = null | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -24,6 +24,13 @@ sealed class LoginResult( | ||||||
|         message: String?, |         message: String?, | ||||||
|     ) : LoginResult(status, userName, password, message) |     ) : LoginResult(status, userName, password, message) | ||||||
| 
 | 
 | ||||||
|  |     class EmailAuthResult( | ||||||
|  |         status: String, | ||||||
|  |         userName: String?, | ||||||
|  |         password: String?, | ||||||
|  |         message: String?, | ||||||
|  |     ) : LoginResult(status, userName, password, message) | ||||||
|  | 
 | ||||||
|     class ResetPasswordResult( |     class ResetPasswordResult( | ||||||
|         status: String, |         status: String, | ||||||
|         userName: String?, |         userName: String?, | ||||||
|  |  | ||||||
|  | @ -1,105 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks; |  | ||||||
| 
 |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.fragment.app.FragmentManager; |  | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; |  | ||||||
| import fr.free.nrw.commons.databinding.FragmentBookmarksBinding; |  | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; |  | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; |  | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import fr.free.nrw.commons.contributions.ContributionController; |  | ||||||
| import javax.inject.Named; |  | ||||||
| 
 |  | ||||||
| public class BookmarkFragment extends CommonsDaggerSupportFragment { |  | ||||||
| 
 |  | ||||||
|     private FragmentManager supportFragmentManager; |  | ||||||
|     private BookmarksPagerAdapter adapter; |  | ||||||
|     FragmentBookmarksBinding binding; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     ContributionController controller; |  | ||||||
|     /** |  | ||||||
|      * To check if the user is loggedIn or not. |  | ||||||
|      */ |  | ||||||
|     @Inject |  | ||||||
|     @Named("default_preferences") |  | ||||||
|     public |  | ||||||
|     JsonKvStore applicationKvStore; |  | ||||||
| 
 |  | ||||||
|     @NonNull |  | ||||||
|     public static BookmarkFragment newInstance() { |  | ||||||
|         BookmarkFragment fragment = new BookmarkFragment(); |  | ||||||
|         fragment.setRetainInstance(true); |  | ||||||
|         return fragment; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setScroll(boolean canScroll) { |  | ||||||
|         if (binding!=null) { |  | ||||||
|             binding.viewPagerBookmarks.setCanScroll(canScroll); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(@NonNull final LayoutInflater inflater, |  | ||||||
|         @Nullable final ViewGroup container, |  | ||||||
|         @Nullable final Bundle savedInstanceState) { |  | ||||||
|         super.onCreateView(inflater, container, savedInstanceState); |  | ||||||
|         binding = FragmentBookmarksBinding.inflate(inflater, container, false); |  | ||||||
| 
 |  | ||||||
|         // Activity can call methods in the fragment by acquiring a |  | ||||||
|         // reference to the Fragment from FragmentManager, using findFragmentById() |  | ||||||
|         supportFragmentManager = getChildFragmentManager(); |  | ||||||
| 
 |  | ||||||
|         adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(), |  | ||||||
|             applicationKvStore.getBoolean("login_skipped")); |  | ||||||
|         binding.viewPagerBookmarks.setAdapter(adapter); |  | ||||||
|         binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks); |  | ||||||
| 
 |  | ||||||
|         ((MainActivity) getActivity()).showTabs(); |  | ||||||
|         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); |  | ||||||
| 
 |  | ||||||
|         setupTabLayout(); |  | ||||||
|         return binding.getRoot(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method sets up the tab layout. If the adapter has only one element it sets the |  | ||||||
|      * visibility of tabLayout to gone. |  | ||||||
|      */ |  | ||||||
|     public void setupTabLayout() { |  | ||||||
|         binding.tabLayout.setVisibility(View.VISIBLE); |  | ||||||
|         if (adapter.getCount() == 1) { |  | ||||||
|             binding.tabLayout.setVisibility(View.GONE); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public void onBackPressed() { |  | ||||||
|         if (((BookmarkListRootFragment) (adapter.getItem(binding.tabLayout.getSelectedTabPosition()))) |  | ||||||
|             .backPressed()) { |  | ||||||
|             // The event is handled internally by the adapter , no further action required. |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         // Event is not handled by the adapter ( performed back action ) change action bar. |  | ||||||
|         ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDestroy() { |  | ||||||
|         super.onDestroy(); |  | ||||||
|         binding = null; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,98 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks | ||||||
|  | 
 | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import fr.free.nrw.commons.contributions.ContributionController | ||||||
|  | import fr.free.nrw.commons.contributions.MainActivity | ||||||
|  | import fr.free.nrw.commons.databinding.FragmentBookmarksBinding | ||||||
|  | import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
|  | import fr.free.nrw.commons.theme.BaseActivity | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Named | ||||||
|  | 
 | ||||||
|  | class BookmarkFragment : CommonsDaggerSupportFragment() { | ||||||
|  |     private var adapter: BookmarksPagerAdapter? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     var binding: FragmentBookmarksBinding? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var controller: ContributionController? = null | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * To check if the user is loggedIn or not. | ||||||
|  |      */ | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     @Named("default_preferences") | ||||||
|  |     var applicationKvStore: JsonKvStore? = null | ||||||
|  | 
 | ||||||
|  |     fun setScroll(canScroll: Boolean) { | ||||||
|  |         binding?.let { | ||||||
|  |             it.viewPagerBookmarks.canScroll = canScroll | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         super.onCreateView(inflater, container, savedInstanceState) | ||||||
|  |         binding = FragmentBookmarksBinding.inflate(inflater, container, false) | ||||||
|  | 
 | ||||||
|  |         // Activity can call methods in the fragment by acquiring a | ||||||
|  |         // reference to the Fragment from FragmentManager, using findFragmentById() | ||||||
|  |         val supportFragmentManager = childFragmentManager | ||||||
|  | 
 | ||||||
|  |         adapter = BookmarksPagerAdapter( | ||||||
|  |             supportFragmentManager, requireContext(), | ||||||
|  |             applicationKvStore!!.getBoolean("login_skipped") | ||||||
|  |         ) | ||||||
|  |         binding!!.viewPagerBookmarks.adapter = adapter | ||||||
|  |         binding!!.tabLayout.setupWithViewPager(binding!!.viewPagerBookmarks) | ||||||
|  | 
 | ||||||
|  |         (requireActivity() as MainActivity).showTabs() | ||||||
|  |         (requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) | ||||||
|  | 
 | ||||||
|  |         setupTabLayout() | ||||||
|  |         return binding!!.root | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method sets up the tab layout. If the adapter has only one element it sets the | ||||||
|  |      * visibility of tabLayout to gone. | ||||||
|  |      */ | ||||||
|  |     fun setupTabLayout() { | ||||||
|  |         binding!!.tabLayout.visibility = View.VISIBLE | ||||||
|  |         if (adapter!!.count == 1) { | ||||||
|  |             binding!!.tabLayout.visibility = View.GONE | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     fun onBackPressed() { | ||||||
|  |         if (((adapter!!.getItem(binding!!.tabLayout.selectedTabPosition)) as BookmarkListRootFragment).backPressed()) { | ||||||
|  |             // The event is handled internally by the adapter , no further action required. | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Event is not handled by the adapter ( performed back action ) change action bar. | ||||||
|  |         (requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         binding = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         fun newInstance(): BookmarkFragment = BookmarkFragment().apply { | ||||||
|  |             retainInstance = true | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,266 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.AdapterView; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.fragment.app.Fragment; |  | ||||||
| import androidx.fragment.app.FragmentManager; |  | ||||||
| import fr.free.nrw.commons.Media; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment; |  | ||||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment; |  | ||||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; |  | ||||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; |  | ||||||
| import fr.free.nrw.commons.category.CategoryImagesCallback; |  | ||||||
| import fr.free.nrw.commons.category.GridViewAdapter; |  | ||||||
| import fr.free.nrw.commons.contributions.MainActivity; |  | ||||||
| import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding; |  | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; |  | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment; |  | ||||||
| import fr.free.nrw.commons.navtab.NavTab; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Iterator; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements |  | ||||||
|     FragmentManager.OnBackStackChangedListener, |  | ||||||
|     MediaDetailPagerFragment.MediaDetailProvider, |  | ||||||
|     AdapterView.OnItemClickListener, CategoryImagesCallback { |  | ||||||
| 
 |  | ||||||
|     private MediaDetailPagerFragment mediaDetails; |  | ||||||
|     //private BookmarkPicturesFragment bookmarkPicturesFragment; |  | ||||||
|     private BookmarkLocationsFragment bookmarkLocationsFragment; |  | ||||||
|     public Fragment listFragment; |  | ||||||
|     private BookmarksPagerAdapter bookmarksPagerAdapter; |  | ||||||
| 
 |  | ||||||
|     FragmentFeaturedRootBinding binding; |  | ||||||
| 
 |  | ||||||
|     public BookmarkListRootFragment() { |  | ||||||
|         //empty constructor necessary otherwise crashes on recreate |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPagerAdapter) { |  | ||||||
|         String title = bundle.getString("categoryName"); |  | ||||||
|         int order = bundle.getInt("order"); |  | ||||||
|         final int orderItem = bundle.getInt("orderItem"); |  | ||||||
| 
 |  | ||||||
|         switch (order){ |  | ||||||
|             case 0: listFragment = new BookmarkPicturesFragment(); |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|             case 1: listFragment = new BookmarkLocationsFragment(); |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|             case 3: listFragment = new BookmarkCategoriesFragment(); |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|             if(orderItem == 2) { |  | ||||||
|                 listFragment = new BookmarkItemsFragment(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         Bundle featuredArguments = new Bundle(); |  | ||||||
|         featuredArguments.putString("categoryName", title); |  | ||||||
|         listFragment.setArguments(featuredArguments); |  | ||||||
|         this.bookmarksPagerAdapter = bookmarksPagerAdapter; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(@NonNull final LayoutInflater inflater, |  | ||||||
|         @Nullable final ViewGroup container, |  | ||||||
|         @Nullable final Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false); |  | ||||||
|         return binding.getRoot(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { |  | ||||||
|         super.onViewCreated(view, savedInstanceState); |  | ||||||
|         if (savedInstanceState == null) { |  | ||||||
|             setFragment(listFragment, mediaDetails); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void setFragment(Fragment fragment, Fragment otherFragment) { |  | ||||||
|         if (fragment.isAdded() && otherFragment != null) { |  | ||||||
|             getChildFragmentManager() |  | ||||||
|                 .beginTransaction() |  | ||||||
|                 .hide(otherFragment) |  | ||||||
|                 .show(fragment) |  | ||||||
|                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") |  | ||||||
|                 .commit(); |  | ||||||
|             getChildFragmentManager().executePendingTransactions(); |  | ||||||
|         } else if (fragment.isAdded() && otherFragment == null) { |  | ||||||
|             getChildFragmentManager() |  | ||||||
|                 .beginTransaction() |  | ||||||
|                 .show(fragment) |  | ||||||
|                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") |  | ||||||
|                 .commit(); |  | ||||||
|             getChildFragmentManager().executePendingTransactions(); |  | ||||||
|         } else if (!fragment.isAdded() && otherFragment != null) { |  | ||||||
|             getChildFragmentManager() |  | ||||||
|                 .beginTransaction() |  | ||||||
|                 .hide(otherFragment) |  | ||||||
|                 .add(R.id.explore_container, fragment) |  | ||||||
|                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") |  | ||||||
|                 .commit(); |  | ||||||
|             getChildFragmentManager().executePendingTransactions(); |  | ||||||
|         } else if (!fragment.isAdded()) { |  | ||||||
|             getChildFragmentManager() |  | ||||||
|                 .beginTransaction() |  | ||||||
|                 .replace(R.id.explore_container, fragment) |  | ||||||
|                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") |  | ||||||
|                 .commit(); |  | ||||||
|             getChildFragmentManager().executePendingTransactions(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void removeFragment(Fragment fragment) { |  | ||||||
|         getChildFragmentManager() |  | ||||||
|             .beginTransaction() |  | ||||||
|             .remove(fragment) |  | ||||||
|             .commit(); |  | ||||||
|         getChildFragmentManager().executePendingTransactions(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onAttach(final Context context) { |  | ||||||
|         super.onAttach(context); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onMediaClicked(int position) { |  | ||||||
|         Timber.d("on media clicked"); |  | ||||||
|     /*container.setVisibility(View.VISIBLE); |  | ||||||
|     ((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); |  | ||||||
|     mediaDetails = new MediaDetailPagerFragment(false, true, position); |  | ||||||
|     setFragment(mediaDetails, bookmarkPicturesFragment);*/ |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index |  | ||||||
|      * |  | ||||||
|      * @param i It is the index of which media object is to be returned which is same as current |  | ||||||
|      *          index of viewPager. |  | ||||||
|      * @return Media Object |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public Media getMediaAtPosition(int i) { |  | ||||||
|         if (bookmarksPagerAdapter.getMediaAdapter() == null) { |  | ||||||
|             // not yet ready to return data |  | ||||||
|             return null; |  | ||||||
|         } else { |  | ||||||
|             return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(i); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain |  | ||||||
|      * same number of media items as that of media elements in adapter. |  | ||||||
|      * |  | ||||||
|      * @return Total Media count in the adapter |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public int getTotalMediaCount() { |  | ||||||
|         if (bookmarksPagerAdapter.getMediaAdapter() == null) { |  | ||||||
|             return 0; |  | ||||||
|         } |  | ||||||
|         return bookmarksPagerAdapter.getMediaAdapter().getCount(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public Integer getContributionStateAt(int position) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Reload media detail fragment once media is nominated |  | ||||||
|      * |  | ||||||
|      * @param index item position that has been nominated |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void refreshNominatedMedia(int index) { |  | ||||||
|         if (mediaDetails != null && !listFragment.isVisible()) { |  | ||||||
|             removeFragment(mediaDetails); |  | ||||||
|             mediaDetails = MediaDetailPagerFragment.newInstance(false, true); |  | ||||||
|             ((BookmarkFragment) getParentFragment()).setScroll(false); |  | ||||||
|             setFragment(mediaDetails, listFragment); |  | ||||||
|             mediaDetails.showImage(index); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This method is called on success of API call for featured images or mobile uploads. The |  | ||||||
|      * viewpager will notified that number of items have changed. |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void viewPagerNotifyDataSetChanged() { |  | ||||||
|         if (mediaDetails != null) { |  | ||||||
|             mediaDetails.notifyDataSetChanged(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public boolean backPressed() { |  | ||||||
|         //check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException |  | ||||||
|         if (mediaDetails != null) { |  | ||||||
|             if (mediaDetails.isVisible()) { |  | ||||||
|                 // todo add get list fragment |  | ||||||
|                 ((BookmarkFragment) getParentFragment()).setupTabLayout(); |  | ||||||
|                 ArrayList<Integer> removed = mediaDetails.getRemovedItems(); |  | ||||||
|                 removeFragment(mediaDetails); |  | ||||||
|                 ((BookmarkFragment) getParentFragment()).setScroll(true); |  | ||||||
|                 setFragment(listFragment, mediaDetails); |  | ||||||
|                 ((MainActivity) getActivity()).showTabs(); |  | ||||||
|                 if (listFragment instanceof BookmarkPicturesFragment) { |  | ||||||
|                     GridViewAdapter adapter = ((GridViewAdapter) ((BookmarkPicturesFragment) listFragment) |  | ||||||
|                         .getAdapter()); |  | ||||||
|                     Iterator i = removed.iterator(); |  | ||||||
|                     while (i.hasNext()) { |  | ||||||
|                         adapter.remove(adapter.getItem((int) i.next())); |  | ||||||
|                     } |  | ||||||
|                     mediaDetails.clearRemoved(); |  | ||||||
| 
 |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 moveToContributionsFragment(); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             moveToContributionsFragment(); |  | ||||||
|         } |  | ||||||
|         // notify mediaDetails did not handled the backPressed further actions required. |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     void moveToContributionsFragment() { |  | ||||||
|         ((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code()); |  | ||||||
|         ((MainActivity) getActivity()).showTabs(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |  | ||||||
|         Timber.d("on media clicked"); |  | ||||||
|         binding.exploreContainer.setVisibility(View.VISIBLE); |  | ||||||
|         ((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE); |  | ||||||
|         mediaDetails = MediaDetailPagerFragment.newInstance(false, true); |  | ||||||
|         ((BookmarkFragment) getParentFragment()).setScroll(false); |  | ||||||
|         setFragment(mediaDetails, listFragment); |  | ||||||
|         mediaDetails.showImage(position); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onBackStackChanged() { |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDestroy() { |  | ||||||
|         super.onDestroy(); |  | ||||||
|         binding = null; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,226 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks | ||||||
|  | 
 | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.AdapterView | ||||||
|  | import android.widget.AdapterView.OnItemClickListener | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.FragmentManager | ||||||
|  | import fr.free.nrw.commons.Media | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment | ||||||
|  | import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment | ||||||
|  | import fr.free.nrw.commons.category.CategoryImagesCallback | ||||||
|  | import fr.free.nrw.commons.category.GridViewAdapter | ||||||
|  | import fr.free.nrw.commons.contributions.MainActivity | ||||||
|  | import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding | ||||||
|  | import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||||
|  | import fr.free.nrw.commons.media.MediaDetailPagerFragment | ||||||
|  | import fr.free.nrw.commons.media.MediaDetailPagerFragment.Companion.newInstance | ||||||
|  | import fr.free.nrw.commons.media.MediaDetailProvider | ||||||
|  | import fr.free.nrw.commons.navtab.NavTab | ||||||
|  | import timber.log.Timber | ||||||
|  | 
 | ||||||
|  | class BookmarkListRootFragment : CommonsDaggerSupportFragment, | ||||||
|  |     FragmentManager.OnBackStackChangedListener, MediaDetailProvider, OnItemClickListener, | ||||||
|  |     CategoryImagesCallback { | ||||||
|  |     private var mediaDetails: MediaDetailPagerFragment? = null | ||||||
|  |     private val bookmarkLocationsFragment: BookmarkLocationsFragment? = null | ||||||
|  |     var listFragment: Fragment? = null | ||||||
|  |     private var bookmarksPagerAdapter: BookmarksPagerAdapter? = null | ||||||
|  | 
 | ||||||
|  |     var binding: FragmentFeaturedRootBinding? = null | ||||||
|  | 
 | ||||||
|  |     constructor() | ||||||
|  | 
 | ||||||
|  |     constructor(bundle: Bundle, bookmarksPagerAdapter: BookmarksPagerAdapter) { | ||||||
|  |         val title = bundle.getString("categoryName") | ||||||
|  |         val order = bundle.getInt("order") | ||||||
|  |         val orderItem = bundle.getInt("orderItem") | ||||||
|  | 
 | ||||||
|  |         when (order) { | ||||||
|  |             0 -> listFragment = BookmarkPicturesFragment() | ||||||
|  |             1 -> listFragment = BookmarkLocationsFragment() | ||||||
|  |             3 -> listFragment = BookmarkCategoriesFragment() | ||||||
|  |         } | ||||||
|  |         if (orderItem == 2) { | ||||||
|  |             listFragment = BookmarkItemsFragment() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val featuredArguments = Bundle() | ||||||
|  |         featuredArguments.putString("categoryName", title) | ||||||
|  |         listFragment!!.setArguments(featuredArguments) | ||||||
|  |         this.bookmarksPagerAdapter = bookmarksPagerAdapter | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View? { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         binding = FragmentFeaturedRootBinding.inflate(inflater, container, false) | ||||||
|  |         return binding!!.getRoot() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         if (savedInstanceState == null) { | ||||||
|  |             setFragment(listFragment!!, mediaDetails) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun setFragment(fragment: Fragment, otherFragment: Fragment?) { | ||||||
|  |         if (fragment.isAdded() && otherFragment != null) { | ||||||
|  |             getChildFragmentManager() | ||||||
|  |                 .beginTransaction() | ||||||
|  |                 .hide(otherFragment) | ||||||
|  |                 .show(fragment) | ||||||
|  |                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") | ||||||
|  |                 .commit() | ||||||
|  |             getChildFragmentManager().executePendingTransactions() | ||||||
|  |         } else if (fragment.isAdded() && otherFragment == null) { | ||||||
|  |             getChildFragmentManager() | ||||||
|  |                 .beginTransaction() | ||||||
|  |                 .show(fragment) | ||||||
|  |                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") | ||||||
|  |                 .commit() | ||||||
|  |             getChildFragmentManager().executePendingTransactions() | ||||||
|  |         } else if (!fragment.isAdded() && otherFragment != null) { | ||||||
|  |             getChildFragmentManager() | ||||||
|  |                 .beginTransaction() | ||||||
|  |                 .hide(otherFragment) | ||||||
|  |                 .add(R.id.explore_container, fragment) | ||||||
|  |                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") | ||||||
|  |                 .commit() | ||||||
|  |             getChildFragmentManager().executePendingTransactions() | ||||||
|  |         } else if (!fragment.isAdded()) { | ||||||
|  |             getChildFragmentManager() | ||||||
|  |                 .beginTransaction() | ||||||
|  |                 .replace(R.id.explore_container, fragment) | ||||||
|  |                 .addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG") | ||||||
|  |                 .commit() | ||||||
|  |             getChildFragmentManager().executePendingTransactions() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun removeFragment(fragment: Fragment) { | ||||||
|  |         getChildFragmentManager() | ||||||
|  |             .beginTransaction() | ||||||
|  |             .remove(fragment) | ||||||
|  |             .commit() | ||||||
|  |         getChildFragmentManager().executePendingTransactions() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onMediaClicked(position: Int) { | ||||||
|  |         Timber.d("on media clicked") | ||||||
|  |         /*container.setVisibility(View.VISIBLE); | ||||||
|  |     ((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE); | ||||||
|  |     mediaDetails = new MediaDetailPagerFragment(false, true, position); | ||||||
|  |     setFragment(mediaDetails, bookmarkPicturesFragment);*/ | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index | ||||||
|  |      * | ||||||
|  |      * @param i It is the index of which media object is to be returned which is same as current | ||||||
|  |      * index of viewPager. | ||||||
|  |      * @return Media Object | ||||||
|  |      */ | ||||||
|  |     override fun getMediaAtPosition(i: Int): Media? = | ||||||
|  |         bookmarksPagerAdapter!!.mediaAdapter?.getItem(i) as Media? | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain | ||||||
|  |      * same number of media items as that of media elements in adapter. | ||||||
|  |      * | ||||||
|  |      * @return Total Media count in the adapter | ||||||
|  |      */ | ||||||
|  |     override fun getTotalMediaCount(): Int = | ||||||
|  |         bookmarksPagerAdapter!!.mediaAdapter?.count ?: 0 | ||||||
|  | 
 | ||||||
|  |     override fun getContributionStateAt(position: Int): Int? { | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reload media detail fragment once media is nominated | ||||||
|  |      * | ||||||
|  |      * @param index item position that has been nominated | ||||||
|  |      */ | ||||||
|  |     override fun refreshNominatedMedia(index: Int) { | ||||||
|  |         if (mediaDetails != null && !listFragment!!.isVisible()) { | ||||||
|  |             removeFragment(mediaDetails!!) | ||||||
|  |             mediaDetails = newInstance(false, true) | ||||||
|  |             (parentFragment as BookmarkFragment).setScroll(false) | ||||||
|  |             setFragment(mediaDetails!!, listFragment) | ||||||
|  |             mediaDetails!!.showImage(index) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * This method is called on success of API call for featured images or mobile uploads. The | ||||||
|  |      * viewpager will notified that number of items have changed. | ||||||
|  |      */ | ||||||
|  |     override fun viewPagerNotifyDataSetChanged() { | ||||||
|  |         if (mediaDetails != null) { | ||||||
|  |             mediaDetails!!.notifyDataSetChanged() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun backPressed(): Boolean { | ||||||
|  |         //check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException | ||||||
|  |         if (mediaDetails != null) { | ||||||
|  |             if (mediaDetails!!.isVisible()) { | ||||||
|  |                 // todo add get list fragment | ||||||
|  |                 (parentFragment as BookmarkFragment).setupTabLayout() | ||||||
|  |                 val removed: ArrayList<Int> = mediaDetails!!.removedItems | ||||||
|  |                 removeFragment(mediaDetails!!) | ||||||
|  |                 (parentFragment as BookmarkFragment).setScroll(true) | ||||||
|  |                 setFragment(listFragment!!, mediaDetails) | ||||||
|  |                 (requireActivity() as MainActivity).showTabs() | ||||||
|  |                 if (listFragment is BookmarkPicturesFragment) { | ||||||
|  |                     val adapter = ((listFragment as BookmarkPicturesFragment) | ||||||
|  |                         .getAdapter() as GridViewAdapter?) | ||||||
|  |                     val i: MutableIterator<*> = removed.iterator() | ||||||
|  |                     while (i.hasNext()) { | ||||||
|  |                         adapter!!.remove(adapter.getItem(i.next() as Int)) | ||||||
|  |                     } | ||||||
|  |                     mediaDetails!!.clearRemoved() | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 moveToContributionsFragment() | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             moveToContributionsFragment() | ||||||
|  |         } | ||||||
|  |         // notify mediaDetails did not handled the backPressed further actions required. | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun moveToContributionsFragment() { | ||||||
|  |         (requireActivity() as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code()) | ||||||
|  |         (requireActivity() as MainActivity).showTabs() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { | ||||||
|  |         Timber.d("on media clicked") | ||||||
|  |         binding!!.exploreContainer.visibility = View.VISIBLE | ||||||
|  |         (parentFragment as BookmarkFragment).binding!!.tabLayout.setVisibility(View.GONE) | ||||||
|  |         mediaDetails = newInstance(false, true) | ||||||
|  |         (parentFragment as BookmarkFragment).setScroll(false) | ||||||
|  |         setFragment(mediaDetails!!, listFragment) | ||||||
|  |         mediaDetails!!.showImage(position) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onBackStackChanged() = Unit | ||||||
|  | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         binding = null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,94 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.widget.ListAdapter; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.fragment.app.Fragment; |  | ||||||
| import androidx.fragment.app.FragmentManager; |  | ||||||
| import androidx.fragment.app.FragmentPagerAdapter; |  | ||||||
| 
 |  | ||||||
| import java.util.ArrayList; |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; |  | ||||||
| 
 |  | ||||||
| public class BookmarksPagerAdapter extends FragmentPagerAdapter { |  | ||||||
| 
 |  | ||||||
|     private ArrayList<BookmarkPages> pages; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Default Constructor |  | ||||||
|      * @param fm |  | ||||||
|      * @param context |  | ||||||
|      * @param onlyPictures is true if the fragment requires only BookmarkPictureFragment |  | ||||||
|      *                     (i.e. when no user is logged in). |  | ||||||
|      */ |  | ||||||
|     BookmarksPagerAdapter(FragmentManager fm, Context context,boolean onlyPictures) { |  | ||||||
|         super(fm); |  | ||||||
|         pages = new ArrayList<>(); |  | ||||||
|         Bundle picturesBundle = new Bundle(); |  | ||||||
|         picturesBundle.putString("categoryName", context.getString(R.string.title_page_bookmarks_pictures)); |  | ||||||
|         picturesBundle.putInt("order", 0); |  | ||||||
|         pages.add(new BookmarkPages( |  | ||||||
|                 new BookmarkListRootFragment(picturesBundle, this), |  | ||||||
|                 context.getString(R.string.title_page_bookmarks_pictures))); |  | ||||||
|         if (!onlyPictures) { |  | ||||||
|             // if onlyPictures is false we also add the location fragment. |  | ||||||
|             Bundle locationBundle = new Bundle(); |  | ||||||
|             locationBundle.putString("categoryName", |  | ||||||
|                 context.getString(R.string.title_page_bookmarks_locations)); |  | ||||||
|             locationBundle.putInt("order", 1); |  | ||||||
|             pages.add(new BookmarkPages( |  | ||||||
|                 new BookmarkListRootFragment(locationBundle, this), |  | ||||||
|                 context.getString(R.string.title_page_bookmarks_locations))); |  | ||||||
| 
 |  | ||||||
|             locationBundle.putInt("orderItem", 2); |  | ||||||
|             pages.add(new BookmarkPages( |  | ||||||
|                 new BookmarkListRootFragment(locationBundle, this), |  | ||||||
|                 context.getString(R.string.title_page_bookmarks_items))); |  | ||||||
|         } |  | ||||||
|         final Bundle categoriesBundle = new Bundle(); |  | ||||||
|         categoriesBundle.putString("categoryName", |  | ||||||
|             context.getString(R.string.title_page_bookmarks_categories)); |  | ||||||
|         categoriesBundle.putInt("order", 3); |  | ||||||
|         pages.add(new BookmarkPages( |  | ||||||
|             new BookmarkListRootFragment(categoriesBundle, this), |  | ||||||
|             context.getString(R.string.title_page_bookmarks_categories))); |  | ||||||
|         notifyDataSetChanged(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public Fragment getItem(int position) { |  | ||||||
|         return pages.get(position).getPage(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public int getCount() { |  | ||||||
|         return pages.size(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public CharSequence getPageTitle(int position) { |  | ||||||
|         return pages.get(position).getTitle(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Return the Adapter used to display the picture gridview |  | ||||||
|      * @return adapter |  | ||||||
|      */ |  | ||||||
|     public ListAdapter getMediaAdapter() { |  | ||||||
|         BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment); |  | ||||||
|         return fragment.getAdapter(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Update the pictures list for the bookmark fragment |  | ||||||
|      */ |  | ||||||
|     public void requestPictureListUpdate() { |  | ||||||
|         BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment); |  | ||||||
|         fragment.onResume(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,82 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import android.widget.ListAdapter | ||||||
|  | import androidx.core.os.bundleOf | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.FragmentManager | ||||||
|  | import androidx.fragment.app.FragmentPagerAdapter | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment | ||||||
|  | 
 | ||||||
|  | class BookmarksPagerAdapter internal constructor( | ||||||
|  |     fm: FragmentManager, context: Context, onlyPictures: Boolean | ||||||
|  | ) : FragmentPagerAdapter(fm) { | ||||||
|  |     private val pages = mutableListOf<BookmarkPages>() | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Default Constructor | ||||||
|  |      * @param fm | ||||||
|  |      * @param context | ||||||
|  |      * @param onlyPictures is true if the fragment requires only BookmarkPictureFragment | ||||||
|  |      * (i.e. when no user is logged in). | ||||||
|  |      */ | ||||||
|  |     init { | ||||||
|  |         pages.add( | ||||||
|  |             BookmarkPages( | ||||||
|  |                 BookmarkListRootFragment( | ||||||
|  |                     bundleOf( | ||||||
|  |                         "categoryName" to context.getString(R.string.title_page_bookmarks_pictures), | ||||||
|  |                         "order" to 0 | ||||||
|  |                     ), this | ||||||
|  |                 ), context.getString(R.string.title_page_bookmarks_pictures) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         if (!onlyPictures) { | ||||||
|  |             // if onlyPictures is false we also add the location fragment. | ||||||
|  |             val locationBundle = bundleOf( | ||||||
|  |                 "categoryName" to context.getString(R.string.title_page_bookmarks_locations), | ||||||
|  |                 "order" to 1 | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             pages.add( | ||||||
|  |                 BookmarkPages( | ||||||
|  |                     BookmarkListRootFragment(locationBundle, this), | ||||||
|  |                     context.getString(R.string.title_page_bookmarks_locations) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             locationBundle.putInt("orderItem", 2) | ||||||
|  |             pages.add( | ||||||
|  |                 BookmarkPages( | ||||||
|  |                     BookmarkListRootFragment(locationBundle, this), | ||||||
|  |                     context.getString(R.string.title_page_bookmarks_items) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         pages.add( | ||||||
|  |             BookmarkPages( | ||||||
|  |                 BookmarkListRootFragment( | ||||||
|  |                     bundleOf( | ||||||
|  |                         "categoryName" to context.getString(R.string.title_page_bookmarks_categories), | ||||||
|  |                         "order" to 3 | ||||||
|  |                     ), this), | ||||||
|  |                 context.getString(R.string.title_page_bookmarks_categories) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         notifyDataSetChanged() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getItem(position: Int): Fragment = pages[position].page!! | ||||||
|  | 
 | ||||||
|  |     override fun getCount(): Int = pages.size | ||||||
|  | 
 | ||||||
|  |     override fun getPageTitle(position: Int): CharSequence? = pages[position].title | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the Adapter used to display the picture gridview | ||||||
|  |      * @return adapter | ||||||
|  |      */ | ||||||
|  |     val mediaAdapter: ListAdapter? | ||||||
|  |         get() = (((pages[0].page as BookmarkListRootFragment).listFragment) as BookmarkPicturesFragment).getAdapter() | ||||||
|  | } | ||||||
|  | @ -1,129 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.items; |  | ||||||
| 
 |  | ||||||
| import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_ID; |  | ||||||
| import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.TABLE_NAME; |  | ||||||
| 
 |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
| import android.database.sqlite.SQLiteQueryBuilder; |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.text.TextUtils; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import fr.free.nrw.commons.BuildConfig; |  | ||||||
| import fr.free.nrw.commons.data.DBOpenHelper; |  | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerContentProvider; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Handles private storage for bookmarked items |  | ||||||
|  */ |  | ||||||
| public class BookmarkItemsContentProvider extends CommonsDaggerContentProvider { |  | ||||||
| 
 |  | ||||||
|     private static final String BASE_PATH = "bookmarksItems"; |  | ||||||
|     public static final Uri BASE_URI = |  | ||||||
|         Uri.parse("content://" + BuildConfig.BOOKMARK_ITEMS_AUTHORITY + "/" + BASE_PATH); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Append bookmark items ID to the base uri |  | ||||||
|      */ |  | ||||||
|     public static Uri uriForName(final String id) { |  | ||||||
|         return Uri.parse(BASE_URI + "/" + id); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     DBOpenHelper dbOpenHelper; |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public String getType(@NonNull final Uri uri) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Queries the SQLite database for the bookmark items |  | ||||||
|      * @param uri : contains the uri for bookmark items |  | ||||||
|      * @param projection : contains the all fields of the table |  | ||||||
|      * @param selection : handles Where |  | ||||||
|      * @param selectionArgs : the condition of Where clause |  | ||||||
|      * @param sortOrder : ascending or descending |  | ||||||
|      */ |  | ||||||
|     @SuppressWarnings("ConstantConditions") |  | ||||||
|     @Override |  | ||||||
|     public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection, |  | ||||||
|         final String[] selectionArgs, final String sortOrder) { |  | ||||||
|         final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); |  | ||||||
|         queryBuilder.setTables(TABLE_NAME); |  | ||||||
|         final SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); |  | ||||||
|         final Cursor cursor = queryBuilder.query(db, projection, selection, |  | ||||||
|             selectionArgs, null, null, sortOrder); |  | ||||||
|         cursor.setNotificationUri(getContext().getContentResolver(), uri); |  | ||||||
|         return cursor; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the update query of local SQLite Database |  | ||||||
|      * @param uri : contains the uri for bookmark items |  | ||||||
|      * @param contentValues : new values to be entered to db |  | ||||||
|      * @param selection : handles Where |  | ||||||
|      * @param selectionArgs : the condition of Where clause |  | ||||||
|      */ |  | ||||||
|     @SuppressWarnings("ConstantConditions") |  | ||||||
|     @Override |  | ||||||
|     public int update(@NonNull final Uri uri, final ContentValues contentValues, |  | ||||||
|         final String selection, final String[] selectionArgs) { |  | ||||||
|         final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); |  | ||||||
|         final int rowsUpdated; |  | ||||||
|         if (TextUtils.isEmpty(selection)) { |  | ||||||
|             final int id = Integer.parseInt(uri.getLastPathSegment()); |  | ||||||
|             rowsUpdated = sqlDB.update(TABLE_NAME, |  | ||||||
|                 contentValues, |  | ||||||
|                 COLUMN_ID + " = ?", |  | ||||||
|                 new String[]{String.valueOf(id)}); |  | ||||||
|         } else { |  | ||||||
|             throw new IllegalArgumentException( |  | ||||||
|                 "Parameter `selection` should be empty when updating an ID"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         getContext().getContentResolver().notifyChange(uri, null); |  | ||||||
|         return rowsUpdated; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the insertion of new bookmark items record to local SQLite Database |  | ||||||
|      * @param uri |  | ||||||
|      * @param contentValues |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     @SuppressWarnings("ConstantConditions") |  | ||||||
|     @Override |  | ||||||
|     public Uri insert(@NonNull final Uri uri, final ContentValues contentValues) { |  | ||||||
|         final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); |  | ||||||
|         final long id = sqlDB.insert(TABLE_NAME, null, contentValues); |  | ||||||
|         getContext().getContentResolver().notifyChange(uri, null); |  | ||||||
|         return Uri.parse(BASE_URI + "/" + id); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the deletion of new bookmark items record to local SQLite Database |  | ||||||
|      * @param uri |  | ||||||
|      * @param s |  | ||||||
|      * @param strings |  | ||||||
|      * @return |  | ||||||
|      */ |  | ||||||
|     @SuppressWarnings("ConstantConditions") |  | ||||||
|     @Override |  | ||||||
|     public int delete(@NonNull final Uri uri, final String s, final String[] strings) { |  | ||||||
|         final int rows; |  | ||||||
|         final SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); |  | ||||||
|         Timber.d("Deleting bookmark name %s", uri.getLastPathSegment()); |  | ||||||
|         rows = db.delete( |  | ||||||
|             TABLE_NAME, |  | ||||||
|             "item_id = ?", |  | ||||||
|             new String[]{uri.getLastPathSegment()} |  | ||||||
|         ); |  | ||||||
|         getContext().getContentResolver().notifyChange(uri, null); |  | ||||||
|         return rows; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,101 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.items | ||||||
|  | 
 | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.Cursor | ||||||
|  | import android.database.sqlite.SQLiteQueryBuilder | ||||||
|  | import android.net.Uri | ||||||
|  | import fr.free.nrw.commons.BuildConfig | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.TABLE_NAME | ||||||
|  | import fr.free.nrw.commons.di.CommonsDaggerContentProvider | ||||||
|  | import androidx.core.net.toUri | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handles private storage for bookmarked items | ||||||
|  |  */ | ||||||
|  | class BookmarkItemsContentProvider : CommonsDaggerContentProvider() { | ||||||
|  |     override fun getType(uri: Uri): String? = null | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Queries the SQLite database for the bookmark items | ||||||
|  |      * @param uri : contains the uri for bookmark items | ||||||
|  |      * @param projection : contains the all fields of the table | ||||||
|  |      * @param selection : handles Where | ||||||
|  |      * @param selectionArgs : the condition of Where clause | ||||||
|  |      * @param sortOrder : ascending or descending | ||||||
|  |      */ | ||||||
|  |     override fun query( | ||||||
|  |         uri: Uri, projection: Array<String>?, selection: String?, | ||||||
|  |         selectionArgs: Array<String>?, sortOrder: String? | ||||||
|  |     ): Cursor { | ||||||
|  |         val queryBuilder = SQLiteQueryBuilder().apply { | ||||||
|  |             tables = TABLE_NAME | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return queryBuilder.query( | ||||||
|  |             requireDb(), projection, selection, | ||||||
|  |             selectionArgs, null, null, sortOrder | ||||||
|  |         ).apply { | ||||||
|  |             setNotificationUri(context?.contentResolver, uri) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the update query of local SQLite Database | ||||||
|  |      * @param uri : contains the uri for bookmark items | ||||||
|  |      * @param contentValues : new values to be entered to db | ||||||
|  |      * @param selection : handles Where | ||||||
|  |      * @param selectionArgs : the condition of Where clause | ||||||
|  |      */ | ||||||
|  |     override fun update( | ||||||
|  |         uri: Uri, contentValues: ContentValues?, | ||||||
|  |         selection: String?, selectionArgs: Array<String>? | ||||||
|  |     ): Int { | ||||||
|  |         val rowsUpdated: Int | ||||||
|  |         if (selection.isNullOrEmpty()) { | ||||||
|  |             val id = uri.lastPathSegment!!.toInt() | ||||||
|  |             rowsUpdated = requireDb().update( | ||||||
|  |                 TABLE_NAME, | ||||||
|  |                 contentValues, | ||||||
|  |                 "$COLUMN_ID = ?", | ||||||
|  |                 arrayOf(id.toString()) | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             throw IllegalArgumentException( | ||||||
|  |                 "Parameter `selection` should be empty when updating an ID" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         context?.contentResolver?.notifyChange(uri, null) | ||||||
|  |         return rowsUpdated | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the insertion of new bookmark items record to local SQLite Database | ||||||
|  |      */ | ||||||
|  |     override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { | ||||||
|  |         val id = requireDb().insert(TABLE_NAME, null, contentValues) | ||||||
|  |         context?.contentResolver?.notifyChange(uri, null) | ||||||
|  |         return "$BASE_URI/$id".toUri() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the deletion of new bookmark items record to local SQLite Database | ||||||
|  |      */ | ||||||
|  |     override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int { | ||||||
|  |         val rows: Int = requireDb().delete( | ||||||
|  |             TABLE_NAME, | ||||||
|  |             "$COLUMN_ID = ?", | ||||||
|  |             arrayOf(uri.lastPathSegment) | ||||||
|  |         ) | ||||||
|  |        context?.contentResolver?.notifyChange(uri, null) | ||||||
|  |         return rows | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         private const val BASE_PATH = "bookmarksItems" | ||||||
|  |         val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_ITEMS_AUTHORITY}/$BASE_PATH".toUri() | ||||||
|  |         fun uriForName(id: String) = "$BASE_URI/$id".toUri() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,27 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.items; |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; |  | ||||||
| import java.util.List; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import javax.inject.Singleton; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Handles loading bookmarked items from Database |  | ||||||
|  */ |  | ||||||
| @Singleton |  | ||||||
| public class BookmarkItemsController { |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     BookmarkItemsDao bookmarkItemsDao; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     public BookmarkItemsController() {} |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Load from DB the bookmarked items |  | ||||||
|      * @return a list of DepictedItem objects. |  | ||||||
|      */ |  | ||||||
|     public List<DepictedItem> loadFavoritesItems() { |  | ||||||
|         return bookmarkItemsDao.getAllBookmarksItems(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.items | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Singleton | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handles loading bookmarked items from Database | ||||||
|  |  */ | ||||||
|  | @Singleton | ||||||
|  | class BookmarkItemsController @Inject constructor() { | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var bookmarkItemsDao: BookmarkItemsDao? = null | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load from DB the bookmarked items | ||||||
|  |      * @return a list of DepictedItem objects. | ||||||
|  |      */ | ||||||
|  |     fun loadFavoritesItems(): List<DepictedItem> { | ||||||
|  |         return bookmarkItemsDao?.getAllBookmarksItems() ?: emptyList() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,329 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.items; |  | ||||||
| 
 |  | ||||||
| import android.annotation.SuppressLint; |  | ||||||
| import android.content.ContentProviderClient; |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
| import android.os.RemoteException; |  | ||||||
| import fr.free.nrw.commons.category.CategoryItem; |  | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.List; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import javax.inject.Named; |  | ||||||
| import javax.inject.Provider; |  | ||||||
| import javax.inject.Singleton; |  | ||||||
| import org.apache.commons.lang3.StringUtils; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Handles database operations for bookmarked items |  | ||||||
|  */ |  | ||||||
| @Singleton |  | ||||||
| public class BookmarkItemsDao { |  | ||||||
| 
 |  | ||||||
|     private final Provider<ContentProviderClient> clientProvider; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     public BookmarkItemsDao( |  | ||||||
|         @Named("bookmarksItem") final Provider<ContentProviderClient> clientProvider) { |  | ||||||
|         this.clientProvider = clientProvider; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Find all persisted items bookmarks on database |  | ||||||
|      * @return list of bookmarks |  | ||||||
|      */ |  | ||||||
|     public List<DepictedItem> getAllBookmarksItems() { |  | ||||||
|         final List<DepictedItem> items = new ArrayList<>(); |  | ||||||
|         final ContentProviderClient db = clientProvider.get(); |  | ||||||
|         try (final Cursor cursor = db.query( |  | ||||||
|             BookmarkItemsContentProvider.BASE_URI, |  | ||||||
|             Table.ALL_FIELDS, |  | ||||||
|             null, |  | ||||||
|             new String[]{}, |  | ||||||
|             null)) { |  | ||||||
|             while (cursor != null && cursor.moveToNext()) { |  | ||||||
|                 items.add(fromCursor(cursor)); |  | ||||||
|             } |  | ||||||
|         } catch (final RemoteException e) { |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } finally { |  | ||||||
|             db.release(); |  | ||||||
|         } |  | ||||||
|         return items; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Look for a bookmark in database and in order to insert or delete it |  | ||||||
|      * @param depictedItem : Bookmark object |  | ||||||
|      * @return boolean : is bookmark now favorite ? |  | ||||||
|      */ |  | ||||||
|     public boolean updateBookmarkItem(final DepictedItem depictedItem) { |  | ||||||
|         final boolean bookmarkExists = findBookmarkItem(depictedItem.getId()); |  | ||||||
|         if (bookmarkExists) { |  | ||||||
|             deleteBookmarkItem(depictedItem); |  | ||||||
|         } else { |  | ||||||
|             addBookmarkItem(depictedItem); |  | ||||||
|         } |  | ||||||
|         return !bookmarkExists; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Add a Bookmark to database |  | ||||||
|      * @param depictedItem : Bookmark to add |  | ||||||
|      */ |  | ||||||
|     private void addBookmarkItem(final DepictedItem depictedItem) { |  | ||||||
|         final ContentProviderClient db = clientProvider.get(); |  | ||||||
|         try { |  | ||||||
|             db.insert(BookmarkItemsContentProvider.BASE_URI, toContentValues(depictedItem)); |  | ||||||
|         } catch (final RemoteException e) { |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } finally { |  | ||||||
|             db.release(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Delete a bookmark from database |  | ||||||
|      * @param depictedItem : Bookmark to delete |  | ||||||
|      */ |  | ||||||
|     private void deleteBookmarkItem(final DepictedItem depictedItem) { |  | ||||||
|         final ContentProviderClient db = clientProvider.get(); |  | ||||||
|         try { |  | ||||||
|             db.delete(BookmarkItemsContentProvider.uriForName(depictedItem.getId()), null, null); |  | ||||||
|         } catch (final RemoteException e) { |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } finally { |  | ||||||
|             db.release(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Find a bookmark from database based on its name |  | ||||||
|      * @param depictedItemID : Bookmark to find |  | ||||||
|      * @return boolean : is bookmark in database ? |  | ||||||
|      */ |  | ||||||
|     public boolean findBookmarkItem(final String depictedItemID) { |  | ||||||
|         if (depictedItemID == null) { //Avoiding NPE's |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|         final ContentProviderClient db = clientProvider.get(); |  | ||||||
|         try (final Cursor cursor = db.query( |  | ||||||
|             BookmarkItemsContentProvider.BASE_URI, |  | ||||||
|             Table.ALL_FIELDS, |  | ||||||
|             Table.COLUMN_ID + "=?", |  | ||||||
|             new String[]{depictedItemID}, |  | ||||||
|             null |  | ||||||
|         )) { |  | ||||||
|             if (cursor != null && cursor.moveToFirst()) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } catch (final RemoteException e) { |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } finally { |  | ||||||
|             db.release(); |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Recives real data from cursor |  | ||||||
|      * @param cursor : Object for storing database data |  | ||||||
|      * @return DepictedItem |  | ||||||
|      */ |  | ||||||
|     @SuppressLint("Range") |  | ||||||
|     DepictedItem fromCursor(final Cursor cursor) { |  | ||||||
|         final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); |  | ||||||
|         final String description |  | ||||||
|             = cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)); |  | ||||||
|         final String imageUrl = cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE)); |  | ||||||
|         final String instanceListString |  | ||||||
|             = cursor.getString(cursor.getColumnIndex(Table.COLUMN_INSTANCE_LIST)); |  | ||||||
|         final List<String> instanceList = StringToArray(instanceListString); |  | ||||||
|         final String categoryNameListString = cursor.getString(cursor |  | ||||||
|             .getColumnIndex(Table.COLUMN_CATEGORIES_NAME_LIST)); |  | ||||||
|         final List<String> categoryNameList = StringToArray(categoryNameListString); |  | ||||||
|         final String categoryDescriptionListString = cursor.getString(cursor |  | ||||||
|             .getColumnIndex(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST)); |  | ||||||
|         final List<String> categoryDescriptionList = StringToArray(categoryDescriptionListString); |  | ||||||
|         final String categoryThumbnailListString = cursor.getString(cursor |  | ||||||
|             .getColumnIndex(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST)); |  | ||||||
|         final List<String> categoryThumbnailList = StringToArray(categoryThumbnailListString); |  | ||||||
|         final List<CategoryItem> categoryList = convertToCategoryItems(categoryNameList, |  | ||||||
|             categoryDescriptionList, categoryThumbnailList); |  | ||||||
|         final boolean isSelected |  | ||||||
|             = Boolean.parseBoolean(cursor.getString(cursor |  | ||||||
|             .getColumnIndex(Table.COLUMN_IS_SELECTED))); |  | ||||||
|         final String id = cursor.getString(cursor.getColumnIndex(Table.COLUMN_ID)); |  | ||||||
| 
 |  | ||||||
|         return new DepictedItem( |  | ||||||
|             fileName, |  | ||||||
|             description, |  | ||||||
|             imageUrl, |  | ||||||
|             instanceList, |  | ||||||
|             categoryList, |  | ||||||
|             isSelected, |  | ||||||
|             id |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private List<CategoryItem> convertToCategoryItems(List<String> categoryNameList, |  | ||||||
|         List<String> categoryDescriptionList, List<String> categoryThumbnailList) { |  | ||||||
|         List<CategoryItem> categoryItems = new ArrayList<>(); |  | ||||||
|         for(int i=0; i<categoryNameList.size(); i++){ |  | ||||||
|             categoryItems.add(new CategoryItem(categoryNameList.get(i), |  | ||||||
|                 categoryDescriptionList.get(i), |  | ||||||
|                 categoryThumbnailList.get(i), false)); |  | ||||||
|         } |  | ||||||
|         return categoryItems; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Converts string to List |  | ||||||
|      * @param listString comma separated single string from of list items |  | ||||||
|      * @return List of string |  | ||||||
|      */ |  | ||||||
|     private List<String> StringToArray(final String listString) { |  | ||||||
|         final String[] elements = listString.split(","); |  | ||||||
|         return Arrays.asList(elements); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Converts string to List |  | ||||||
|      * @param list list of items |  | ||||||
|      * @return string comma separated single string of items |  | ||||||
|      */ |  | ||||||
|     private String ArrayToString(final List<String> list) { |  | ||||||
|         if (list != null) { |  | ||||||
|             return StringUtils.join(list, ','); |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Takes data from DepictedItem and create a content value object |  | ||||||
|      * @param depictedItem depicted item |  | ||||||
|      * @return ContentValues |  | ||||||
|      */ |  | ||||||
|     private ContentValues toContentValues(final DepictedItem depictedItem) { |  | ||||||
| 
 |  | ||||||
|         final List<String> namesOfCommonsCategories = new ArrayList<>(); |  | ||||||
|         for (final CategoryItem category : |  | ||||||
|             depictedItem.getCommonsCategories()) { |  | ||||||
|             namesOfCommonsCategories.add(category.getName()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final List<String> descriptionsOfCommonsCategories = new ArrayList<>(); |  | ||||||
|         for (final CategoryItem category : |  | ||||||
|             depictedItem.getCommonsCategories()) { |  | ||||||
|             descriptionsOfCommonsCategories.add(category.getDescription()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final List<String> thumbnailsOfCommonsCategories = new ArrayList<>(); |  | ||||||
|         for (final CategoryItem category : |  | ||||||
|             depictedItem.getCommonsCategories()) { |  | ||||||
|             thumbnailsOfCommonsCategories.add(category.getThumbnail()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final ContentValues cv = new ContentValues(); |  | ||||||
|         cv.put(Table.COLUMN_NAME, depictedItem.getName()); |  | ||||||
|         cv.put(Table.COLUMN_DESCRIPTION, depictedItem.getDescription()); |  | ||||||
|         cv.put(Table.COLUMN_IMAGE, depictedItem.getImageUrl()); |  | ||||||
|         cv.put(Table.COLUMN_INSTANCE_LIST, ArrayToString(depictedItem.getInstanceOfs())); |  | ||||||
|         cv.put(Table.COLUMN_CATEGORIES_NAME_LIST, ArrayToString(namesOfCommonsCategories)); |  | ||||||
|         cv.put(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST, |  | ||||||
|             ArrayToString(descriptionsOfCommonsCategories)); |  | ||||||
|         cv.put(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST, |  | ||||||
|             ArrayToString(thumbnailsOfCommonsCategories)); |  | ||||||
|         cv.put(Table.COLUMN_IS_SELECTED, depictedItem.isSelected()); |  | ||||||
|         cv.put(Table.COLUMN_ID, depictedItem.getId()); |  | ||||||
|         return cv; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Table of bookmarksItems data |  | ||||||
|      */ |  | ||||||
|     public static final class Table { |  | ||||||
|         public static final String TABLE_NAME = "bookmarksItems"; |  | ||||||
|         public static final String COLUMN_NAME = "item_name"; |  | ||||||
|         public static final String COLUMN_DESCRIPTION = "item_description"; |  | ||||||
|         public static final String COLUMN_IMAGE = "item_image_url"; |  | ||||||
|         public static final String COLUMN_INSTANCE_LIST = "item_instance_of"; |  | ||||||
|         public static final String COLUMN_CATEGORIES_NAME_LIST = "item_name_categories"; |  | ||||||
|         public static final String COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories"; |  | ||||||
|         public static final String COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories"; |  | ||||||
|         public static final String COLUMN_IS_SELECTED = "item_is_selected"; |  | ||||||
|         public static final String COLUMN_ID = "item_id"; |  | ||||||
| 
 |  | ||||||
|         public static final String[] ALL_FIELDS = { |  | ||||||
|             COLUMN_NAME, |  | ||||||
|             COLUMN_DESCRIPTION, |  | ||||||
|             COLUMN_IMAGE, |  | ||||||
|             COLUMN_INSTANCE_LIST, |  | ||||||
|             COLUMN_CATEGORIES_NAME_LIST, |  | ||||||
|             COLUMN_CATEGORIES_DESCRIPTION_LIST, |  | ||||||
|             COLUMN_CATEGORIES_THUMBNAIL_LIST, |  | ||||||
|             COLUMN_IS_SELECTED, |  | ||||||
|             COLUMN_ID |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; |  | ||||||
|         static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" |  | ||||||
|             + COLUMN_NAME + " STRING," |  | ||||||
|             + COLUMN_DESCRIPTION + " STRING," |  | ||||||
|             + COLUMN_IMAGE + " STRING," |  | ||||||
|             + COLUMN_INSTANCE_LIST + " STRING," |  | ||||||
|             + COLUMN_CATEGORIES_NAME_LIST + " STRING," |  | ||||||
|             + COLUMN_CATEGORIES_DESCRIPTION_LIST + " STRING," |  | ||||||
|             + COLUMN_CATEGORIES_THUMBNAIL_LIST + " STRING," |  | ||||||
|             + COLUMN_IS_SELECTED + " STRING," |  | ||||||
|             + COLUMN_ID + " STRING PRIMARY KEY" |  | ||||||
|             + ");"; |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Creates table |  | ||||||
|          * @param db SQLiteDatabase |  | ||||||
|          */ |  | ||||||
|         public static void onCreate(final SQLiteDatabase db) { |  | ||||||
|             db.execSQL(CREATE_TABLE_STATEMENT); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Deletes database |  | ||||||
|          * @param db SQLiteDatabase |  | ||||||
|          */ |  | ||||||
|         public static void onDelete(final SQLiteDatabase db) { |  | ||||||
|             db.execSQL(DROP_TABLE_STATEMENT); |  | ||||||
|             onCreate(db); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /** |  | ||||||
|          * Updates database |  | ||||||
|          * @param db SQLiteDatabase |  | ||||||
|          * @param from starting |  | ||||||
|          * @param to end |  | ||||||
|          */ |  | ||||||
|         public static void onUpdate(final SQLiteDatabase db, int from, final int to) { |  | ||||||
|             if (from == to) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             if (from < 18) { |  | ||||||
|                 // doesn't exist yet |  | ||||||
|                 from++; |  | ||||||
|                 onUpdate(db, from, to); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (from == 18) { |  | ||||||
|                 // table added in version 19 |  | ||||||
|                 onCreate(db); |  | ||||||
|                 from++; |  | ||||||
|                 onUpdate(db, from, to); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,203 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.items | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.content.ContentProviderClient | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.Cursor | ||||||
|  | import android.os.RemoteException | ||||||
|  | import androidx.core.content.contentValuesOf | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.BASE_URI | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.uriForName | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_DESCRIPTION_LIST | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_NAME_LIST | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_THUMBNAIL_LIST | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_DESCRIPTION | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IMAGE | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_INSTANCE_LIST | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IS_SELECTED | ||||||
|  | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_NAME | ||||||
|  | import fr.free.nrw.commons.category.CategoryItem | ||||||
|  | import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||||
|  | import fr.free.nrw.commons.utils.arrayToString | ||||||
|  | import fr.free.nrw.commons.utils.getString | ||||||
|  | import fr.free.nrw.commons.utils.getStringArray | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Named | ||||||
|  | import javax.inject.Provider | ||||||
|  | import javax.inject.Singleton | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handles database operations for bookmarked items | ||||||
|  |  */ | ||||||
|  | @Singleton | ||||||
|  | class BookmarkItemsDao @Inject constructor( | ||||||
|  |     @param:Named("bookmarksItem") private val clientProvider: Provider<ContentProviderClient> | ||||||
|  | ) { | ||||||
|  |     /** | ||||||
|  |      * Find all persisted items bookmarks on database | ||||||
|  |      * @return list of bookmarks | ||||||
|  |      */ | ||||||
|  |     fun getAllBookmarksItems(): List<DepictedItem> { | ||||||
|  |         val items: MutableList<DepictedItem> = mutableListOf() | ||||||
|  |         val db = clientProvider.get() | ||||||
|  |         try { | ||||||
|  |             db.query( | ||||||
|  |                 BASE_URI, | ||||||
|  |                 BookmarkItemsTable.ALL_FIELDS, | ||||||
|  |                 null, | ||||||
|  |                 arrayOf(), | ||||||
|  |                 null | ||||||
|  |             ).use { cursor -> | ||||||
|  |                 while (cursor != null && cursor.moveToNext()) { | ||||||
|  |                     items.add(fromCursor(cursor)) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (e: RemoteException) { | ||||||
|  |             throw RuntimeException(e) | ||||||
|  |         } finally { | ||||||
|  |             db.release() | ||||||
|  |         } | ||||||
|  |         return items | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Look for a bookmark in database and in order to insert or delete it | ||||||
|  |      * @param depictedItem : Bookmark object | ||||||
|  |      * @return boolean : is bookmark now favorite ? | ||||||
|  |      */ | ||||||
|  |     fun updateBookmarkItem(depictedItem: DepictedItem): Boolean { | ||||||
|  |         val bookmarkExists = findBookmarkItem(depictedItem.id) | ||||||
|  |         if (bookmarkExists) { | ||||||
|  |             deleteBookmarkItem(depictedItem) | ||||||
|  |         } else { | ||||||
|  |             addBookmarkItem(depictedItem) | ||||||
|  |         } | ||||||
|  |         return !bookmarkExists | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add a Bookmark to database | ||||||
|  |      * @param depictedItem : Bookmark to add | ||||||
|  |      */ | ||||||
|  |     private fun addBookmarkItem(depictedItem: DepictedItem) { | ||||||
|  |         val db = clientProvider.get() | ||||||
|  |         try { | ||||||
|  |             db.insert(BASE_URI, toContentValues(depictedItem)) | ||||||
|  |         } catch (e: RemoteException) { | ||||||
|  |             throw RuntimeException(e) | ||||||
|  |         } finally { | ||||||
|  |             db.release() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Delete a bookmark from database | ||||||
|  |      * @param depictedItem : Bookmark to delete | ||||||
|  |      */ | ||||||
|  |     private fun deleteBookmarkItem(depictedItem: DepictedItem) { | ||||||
|  |         val db = clientProvider.get() | ||||||
|  |         try { | ||||||
|  |             db.delete(uriForName(depictedItem.id), null, null) | ||||||
|  |         } catch (e: RemoteException) { | ||||||
|  |             throw RuntimeException(e) | ||||||
|  |         } finally { | ||||||
|  |             db.release() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Find a bookmark from database based on its name | ||||||
|  |      * @param depictedItemID : Bookmark to find | ||||||
|  |      * @return boolean : is bookmark in database ? | ||||||
|  |      */ | ||||||
|  |     fun findBookmarkItem(depictedItemID: String?): Boolean { | ||||||
|  |         if (depictedItemID == null) { //Avoiding NPE's | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |         val db = clientProvider.get() | ||||||
|  |         try { | ||||||
|  |             db.query( | ||||||
|  |                 BASE_URI, | ||||||
|  |                 BookmarkItemsTable.ALL_FIELDS, | ||||||
|  |                 COLUMN_ID + "=?", | ||||||
|  |                 arrayOf(depictedItemID), | ||||||
|  |                 null | ||||||
|  |             ).use { cursor -> | ||||||
|  |                 if (cursor != null && cursor.moveToFirst()) { | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (e: RemoteException) { | ||||||
|  |             throw RuntimeException(e) | ||||||
|  |         } finally { | ||||||
|  |             db.release() | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Recives real data from cursor | ||||||
|  |      * @param cursor : Object for storing database data | ||||||
|  |      * @return DepictedItem | ||||||
|  |      */ | ||||||
|  |     @SuppressLint("Range") | ||||||
|  |     fun fromCursor(cursor: Cursor) = with(cursor) { | ||||||
|  |         var name = getString(COLUMN_NAME) | ||||||
|  |         if (name == null) { | ||||||
|  |             name = "" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         var id = getString(COLUMN_ID) | ||||||
|  |         if (id == null) { | ||||||
|  |             id = "" | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         DepictedItem( | ||||||
|  |             name, | ||||||
|  |             getString(COLUMN_DESCRIPTION), | ||||||
|  |             getString(COLUMN_IMAGE), | ||||||
|  |             getStringArray(COLUMN_INSTANCE_LIST), | ||||||
|  |             convertToCategoryItems( | ||||||
|  |                 getStringArray(COLUMN_CATEGORIES_NAME_LIST), | ||||||
|  |                 getStringArray(COLUMN_CATEGORIES_DESCRIPTION_LIST), | ||||||
|  |                 getStringArray(COLUMN_CATEGORIES_THUMBNAIL_LIST) | ||||||
|  |             ), | ||||||
|  |             getString(COLUMN_IS_SELECTED).toBoolean(), | ||||||
|  |             id | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun convertToCategoryItems( | ||||||
|  |         categoryNameList: List<String>, | ||||||
|  |         categoryDescriptionList: List<String>, | ||||||
|  |         categoryThumbnailList: List<String> | ||||||
|  |     ): List<CategoryItem> = categoryNameList.mapIndexed { index, name -> | ||||||
|  |         CategoryItem( | ||||||
|  |             name = name, | ||||||
|  |             description = categoryDescriptionList.getOrNull(index), | ||||||
|  |             thumbnail = categoryThumbnailList.getOrNull(index), | ||||||
|  |             isSelected = false | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Takes data from DepictedItem and create a content value object | ||||||
|  |      * @param depictedItem depicted item | ||||||
|  |      * @return ContentValues | ||||||
|  |      */ | ||||||
|  |     private fun toContentValues(depictedItem: DepictedItem): ContentValues { | ||||||
|  |         return contentValuesOf( | ||||||
|  |             COLUMN_NAME to depictedItem.name, | ||||||
|  |             COLUMN_DESCRIPTION to depictedItem.description, | ||||||
|  |             COLUMN_IMAGE to depictedItem.imageUrl, | ||||||
|  |             COLUMN_INSTANCE_LIST to arrayToString(depictedItem.instanceOfs), | ||||||
|  |             COLUMN_CATEGORIES_NAME_LIST to arrayToString(depictedItem.commonsCategories.map { it.name }), | ||||||
|  |             COLUMN_CATEGORIES_DESCRIPTION_LIST to arrayToString(depictedItem.commonsCategories.map { it.description }), | ||||||
|  |             COLUMN_CATEGORIES_THUMBNAIL_LIST to arrayToString(depictedItem.commonsCategories.map { it.thumbnail }), | ||||||
|  |             COLUMN_IS_SELECTED to depictedItem.isSelected, | ||||||
|  |             COLUMN_ID to depictedItem.id, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,81 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.items; |  | ||||||
| 
 |  | ||||||
| import android.content.Context; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.ProgressBar; |  | ||||||
| import android.widget.RelativeLayout; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; |  | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| import dagger.android.support.DaggerFragment; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding; |  | ||||||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; |  | ||||||
| import java.util.List; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import org.jetbrains.annotations.NotNull; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Tab fragment to show list of bookmarked Wikidata Items |  | ||||||
|  */ |  | ||||||
| public class BookmarkItemsFragment extends DaggerFragment { |  | ||||||
| 
 |  | ||||||
|     private FragmentBookmarksItemsBinding binding; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     BookmarkItemsController controller; |  | ||||||
| 
 |  | ||||||
|     public static BookmarkItemsFragment newInstance() { |  | ||||||
|         return new BookmarkItemsFragment(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView( |  | ||||||
|         @NonNull final LayoutInflater inflater, |  | ||||||
|         final ViewGroup container, |  | ||||||
|         final Bundle savedInstanceState |  | ||||||
|     ) { |  | ||||||
|         binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false); |  | ||||||
|         return binding.getRoot(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) { |  | ||||||
|         super.onViewCreated(view, savedInstanceState); |  | ||||||
|         initList(requireContext()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onResume() { |  | ||||||
|         super.onResume(); |  | ||||||
|         initList(requireContext()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get list of DepictedItem and sets to the adapter |  | ||||||
|      * @param context context |  | ||||||
|      */ |  | ||||||
|     private void initList(final Context context) { |  | ||||||
|         final List<DepictedItem> depictItems = controller.loadFavoritesItems(); |  | ||||||
|         final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context); |  | ||||||
|         binding.listView.setAdapter(adapter); |  | ||||||
|         binding.loadingImagesProgressBar.setVisibility(View.GONE); |  | ||||||
|         if (depictItems.isEmpty()) { |  | ||||||
|             binding.statusMessage.setText(R.string.bookmark_empty); |  | ||||||
|             binding.statusMessage.setVisibility(View.VISIBLE); |  | ||||||
|         } else { |  | ||||||
|             binding.statusMessage.setVisibility(View.GONE); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDestroy() { |  | ||||||
|         super.onDestroy(); |  | ||||||
|         binding = null; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,62 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.items | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import dagger.android.support.DaggerFragment | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding | ||||||
|  | import javax.inject.Inject | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Tab fragment to show list of bookmarked Wikidata Items | ||||||
|  |  */ | ||||||
|  | class BookmarkItemsFragment : DaggerFragment() { | ||||||
|  |     private var binding: FragmentBookmarksItemsBinding? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var controller: BookmarkItemsController? = null | ||||||
|  | 
 | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false) | ||||||
|  |         return binding!!.root | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         initList(requireContext()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onResume() { | ||||||
|  |         super.onResume() | ||||||
|  |         initList(requireContext()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get list of DepictedItem and sets to the adapter | ||||||
|  |      * @param context context | ||||||
|  |      */ | ||||||
|  |     private fun initList(context: Context) { | ||||||
|  |         val depictItems = controller!!.loadFavoritesItems() | ||||||
|  |         binding!!.listView.adapter = BookmarkItemsAdapter(depictItems, context) | ||||||
|  |         binding!!.loadingImagesProgressBar.visibility = View.GONE | ||||||
|  |         if (depictItems.isEmpty()) { | ||||||
|  |             binding!!.statusMessage.setText(R.string.bookmark_empty) | ||||||
|  |             binding!!.statusMessage.visibility = View.VISIBLE | ||||||
|  |         } else { | ||||||
|  |             binding!!.statusMessage.visibility = View.GONE | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         binding = null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,90 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.items | ||||||
|  | 
 | ||||||
|  | import android.database.sqlite.SQLiteDatabase | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Table of bookmarksItems data | ||||||
|  |  */ | ||||||
|  | object BookmarkItemsTable { | ||||||
|  |     const val TABLE_NAME = "bookmarksItems" | ||||||
|  |     const val COLUMN_NAME = "item_name" | ||||||
|  |     const val COLUMN_DESCRIPTION = "item_description" | ||||||
|  |     const val COLUMN_IMAGE = "item_image_url" | ||||||
|  |     const val COLUMN_INSTANCE_LIST = "item_instance_of" | ||||||
|  |     const val COLUMN_CATEGORIES_NAME_LIST = "item_name_categories" | ||||||
|  |     const val COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories" | ||||||
|  |     const val COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories" | ||||||
|  |     const val COLUMN_IS_SELECTED = "item_is_selected" | ||||||
|  |     const val COLUMN_ID = "item_id" | ||||||
|  | 
 | ||||||
|  |     val ALL_FIELDS = arrayOf( | ||||||
|  |         COLUMN_NAME, | ||||||
|  |         COLUMN_DESCRIPTION, | ||||||
|  |         COLUMN_IMAGE, | ||||||
|  |         COLUMN_INSTANCE_LIST, | ||||||
|  |         COLUMN_CATEGORIES_NAME_LIST, | ||||||
|  |         COLUMN_CATEGORIES_DESCRIPTION_LIST, | ||||||
|  |         COLUMN_CATEGORIES_THUMBNAIL_LIST, | ||||||
|  |         COLUMN_IS_SELECTED, | ||||||
|  |         COLUMN_ID | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" | ||||||
|  | 
 | ||||||
|  |     val CREATE_TABLE_STATEMENT = | ||||||
|  |         """CREATE TABLE $TABLE_NAME ( | ||||||
|  |              $COLUMN_NAME STRING, | ||||||
|  |              $COLUMN_DESCRIPTION STRING, | ||||||
|  |              $COLUMN_IMAGE STRING, | ||||||
|  |              $COLUMN_INSTANCE_LIST STRING, | ||||||
|  |              $COLUMN_CATEGORIES_NAME_LIST STRING, | ||||||
|  |              $COLUMN_CATEGORIES_DESCRIPTION_LIST STRING, | ||||||
|  |              $COLUMN_CATEGORIES_THUMBNAIL_LIST STRING, | ||||||
|  |              $COLUMN_IS_SELECTED STRING, | ||||||
|  |              $COLUMN_ID STRING PRIMARY KEY | ||||||
|  |            );""".trimIndent() | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Creates table | ||||||
|  |      * | ||||||
|  |      * @param db SQLiteDatabase | ||||||
|  |      */ | ||||||
|  |     fun onCreate(db: SQLiteDatabase) { | ||||||
|  |         db.execSQL(CREATE_TABLE_STATEMENT) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Deletes database | ||||||
|  |      * | ||||||
|  |      * @param db SQLiteDatabase | ||||||
|  |      */ | ||||||
|  |     fun onDelete(db: SQLiteDatabase) { | ||||||
|  |         db.execSQL(DROP_TABLE_STATEMENT) | ||||||
|  |         onCreate(db) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Updates database | ||||||
|  |      * | ||||||
|  |      * @param db   SQLiteDatabase | ||||||
|  |      * @param from starting | ||||||
|  |      * @param to   end | ||||||
|  |      */ | ||||||
|  |     fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { | ||||||
|  |         if (from == to) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (from < 18) { | ||||||
|  |             // doesn't exist yet | ||||||
|  |             onUpdate(db, from + 1, to) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (from == 18) { | ||||||
|  |             // table added in version 19 | ||||||
|  |             onCreate(db) | ||||||
|  |             onUpdate(db, from + 1, to) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.locations | package fr.free.nrw.commons.bookmarks.locations | ||||||
| 
 | 
 | ||||||
| import android.Manifest.permission | import android.Manifest.permission | ||||||
| import android.annotation.SuppressLint |  | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.view.LayoutInflater | import android.view.LayoutInflater | ||||||
| import android.view.View | import android.view.View | ||||||
|  | @ -9,15 +8,12 @@ import android.view.ViewGroup | ||||||
| import androidx.activity.result.ActivityResultLauncher | import androidx.activity.result.ActivityResultLauncher | ||||||
| import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions | import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions | ||||||
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult | import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult | ||||||
| import androidx.lifecycle.Lifecycle |  | ||||||
| import androidx.lifecycle.lifecycleScope | import androidx.lifecycle.lifecycleScope | ||||||
| import androidx.lifecycle.repeatOnLifecycle |  | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
| import dagger.android.support.DaggerFragment | import dagger.android.support.DaggerFragment | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.contributions.ContributionController | import fr.free.nrw.commons.contributions.ContributionController | ||||||
| import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding | import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding | ||||||
| import fr.free.nrw.commons.filepicker.FilePicker |  | ||||||
| import fr.free.nrw.commons.nearby.Place | import fr.free.nrw.commons.nearby.Place | ||||||
| import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions | import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions | ||||||
| import fr.free.nrw.commons.nearby.fragments.PlaceAdapter | import fr.free.nrw.commons.nearby.fragments.PlaceAdapter | ||||||
|  | @ -41,9 +37,8 @@ class BookmarkLocationsFragment : DaggerFragment() { | ||||||
|     private val cameraPickLauncherForResult = |     private val cameraPickLauncherForResult = | ||||||
|         registerForActivityResult(StartActivityForResult()) { result -> |         registerForActivityResult(StartActivityForResult()) { result -> | ||||||
|             contributionController.handleActivityResultWithCallback( |             contributionController.handleActivityResultWithCallback( | ||||||
|                 requireActivity(), |                 requireActivity() | ||||||
|                 object: FilePicker.HandleActivityResult { |             ) { callbacks -> | ||||||
|                     override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) { |  | ||||||
|                 contributionController.onPictureReturnedFromCamera( |                 contributionController.onPictureReturnedFromCamera( | ||||||
|                     result, |                     result, | ||||||
|                     requireActivity(), |                     requireActivity(), | ||||||
|  | @ -51,15 +46,12 @@ class BookmarkLocationsFragment : DaggerFragment() { | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|     private val galleryPickLauncherForResult = |     private val galleryPickLauncherForResult = | ||||||
|         registerForActivityResult(StartActivityForResult()) { result -> |         registerForActivityResult(StartActivityForResult()) { result -> | ||||||
|             contributionController.handleActivityResultWithCallback( |             contributionController.handleActivityResultWithCallback( | ||||||
|                 requireActivity(), |                 requireActivity() | ||||||
|                 object: FilePicker.HandleActivityResult { |             ) { callbacks -> | ||||||
|                     override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) { |  | ||||||
|                 contributionController.onPictureReturnedFromGallery( |                 contributionController.onPictureReturnedFromGallery( | ||||||
|                     result, |                     result, | ||||||
|                     requireActivity(), |                     requireActivity(), | ||||||
|  | @ -67,8 +59,6 @@ class BookmarkLocationsFragment : DaggerFragment() { | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|     companion object { |     companion object { | ||||||
|         fun newInstance(): BookmarkLocationsFragment { |         fun newInstance(): BookmarkLocationsFragment { | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ class Bookmark( | ||||||
|     /** |     /** | ||||||
|      * Gets or Sets the content URI - marking this bookmark as already saved in the database |      * Gets or Sets the content URI - marking this bookmark as already saved in the database | ||||||
|      * @return content URI |      * @return content URI | ||||||
|      * @param contentUri the content URI |      * contentUri the content URI | ||||||
|      */ |      */ | ||||||
|     var contentUri: Uri?, |     var contentUri: Uri?, | ||||||
| ) { | ) { | ||||||
|  |  | ||||||
|  | @ -1,120 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.pictures; |  | ||||||
| 
 |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
| import android.database.sqlite.SQLiteQueryBuilder; |  | ||||||
| // We can get uri using java.Net.Uri, but andoid implimentation is faster (but it's forgiving with handling exceptions though) |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.text.TextUtils; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| 
 |  | ||||||
| import javax.inject.Inject; |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.BuildConfig; |  | ||||||
| import fr.free.nrw.commons.data.DBOpenHelper; |  | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerContentProvider; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME; |  | ||||||
| import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.TABLE_NAME; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Handles private storage for Bookmark pictures |  | ||||||
|  */ |  | ||||||
| public class BookmarkPicturesContentProvider extends CommonsDaggerContentProvider { |  | ||||||
| 
 |  | ||||||
|     private static final String BASE_PATH = "bookmarks"; |  | ||||||
|     public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_AUTHORITY + "/" + BASE_PATH); |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Append bookmark pictures name to the base uri  |  | ||||||
|      */ |  | ||||||
|     public static Uri uriForName(String name) { |  | ||||||
|         return Uri.parse(BASE_URI.toString() + "/" + name); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     DBOpenHelper dbOpenHelper; |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public String getType(@NonNull Uri uri) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Queries the SQLite database for the bookmark pictures |  | ||||||
|      * @param uri : contains the uri for bookmark pictures |  | ||||||
|      * @param projection |  | ||||||
|      * @param selection : handles Where |  | ||||||
|      * @param selectionArgs : the condition of Where clause |  | ||||||
|      * @param sortOrder : ascending or descending |  | ||||||
|      */ |  | ||||||
|     @SuppressWarnings("ConstantConditions") |  | ||||||
|     @Override |  | ||||||
|     public Cursor query(@NonNull Uri uri, String[] projection, String selection, |  | ||||||
|                         String[] selectionArgs, String sortOrder) { |  | ||||||
|         SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); |  | ||||||
|         queryBuilder.setTables(TABLE_NAME); |  | ||||||
| 
 |  | ||||||
|         SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); |  | ||||||
|         Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); |  | ||||||
|         cursor.setNotificationUri(getContext().getContentResolver(), uri); |  | ||||||
| 
 |  | ||||||
|         return cursor; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the update query of local SQLite Database  |  | ||||||
|      * @param uri : contains the uri for bookmark pictures |  | ||||||
|      * @param contentValues : new values to be entered to db |  | ||||||
|      * @param selection : handles Where |  | ||||||
|      * @param selectionArgs : the condition of Where clause |  | ||||||
|      */ |  | ||||||
|     @SuppressWarnings("ConstantConditions") |  | ||||||
|     @Override |  | ||||||
|     public int update(@NonNull Uri uri, ContentValues contentValues, String selection, |  | ||||||
|                       String[] selectionArgs) { |  | ||||||
|         SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); |  | ||||||
|         int rowsUpdated; |  | ||||||
|         if (TextUtils.isEmpty(selection)) { |  | ||||||
|             int id = Integer.valueOf(uri.getLastPathSegment()); |  | ||||||
|             rowsUpdated = sqlDB.update(TABLE_NAME, |  | ||||||
|                     contentValues, |  | ||||||
|                     COLUMN_MEDIA_NAME + " = ?", |  | ||||||
|                     new String[]{String.valueOf(id)}); |  | ||||||
|         } else { |  | ||||||
|             throw new IllegalArgumentException( |  | ||||||
|                     "Parameter `selection` should be empty when updating an ID"); |  | ||||||
|         } |  | ||||||
|         getContext().getContentResolver().notifyChange(uri, null); |  | ||||||
|         return rowsUpdated; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the insertion of new bookmark pictures record to local SQLite Database |  | ||||||
|      */ |  | ||||||
|     @SuppressWarnings("ConstantConditions") |  | ||||||
|     @Override |  | ||||||
|     public Uri insert(@NonNull Uri uri, ContentValues contentValues) { |  | ||||||
|         SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); |  | ||||||
|         long id = sqlDB.insert(BookmarkPicturesDao.Table.TABLE_NAME, null, contentValues); |  | ||||||
|         getContext().getContentResolver().notifyChange(uri, null); |  | ||||||
|         return Uri.parse(BASE_URI + "/" + id); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @SuppressWarnings("ConstantConditions") |  | ||||||
|     @Override |  | ||||||
|     public int delete(@NonNull Uri uri, String s, String[] strings) { |  | ||||||
|         int rows; |  | ||||||
|         SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); |  | ||||||
|         Timber.d("Deleting bookmark name %s", uri.getLastPathSegment()); |  | ||||||
|         rows = db.delete(TABLE_NAME, |  | ||||||
|                 "media_name = ?", |  | ||||||
|                 new String[]{uri.getLastPathSegment()} |  | ||||||
|         ); |  | ||||||
|         getContext().getContentResolver().notifyChange(uri, null); |  | ||||||
|         return rows; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,100 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.pictures | ||||||
|  | 
 | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.Cursor | ||||||
|  | import android.database.sqlite.SQLiteQueryBuilder | ||||||
|  | import android.net.Uri | ||||||
|  | import fr.free.nrw.commons.BuildConfig | ||||||
|  | import fr.free.nrw.commons.di.CommonsDaggerContentProvider | ||||||
|  | import androidx.core.net.toUri | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.TABLE_NAME | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handles private storage for Bookmark pictures | ||||||
|  |  */ | ||||||
|  | class BookmarkPicturesContentProvider : CommonsDaggerContentProvider() { | ||||||
|  |     override fun getType(uri: Uri): String? = null | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Queries the SQLite database for the bookmark pictures | ||||||
|  |      * @param uri : contains the uri for bookmark pictures | ||||||
|  |      * @param projection | ||||||
|  |      * @param selection : handles Where | ||||||
|  |      * @param selectionArgs : the condition of Where clause | ||||||
|  |      * @param sortOrder : ascending or descending | ||||||
|  |      */ | ||||||
|  |     override fun query( | ||||||
|  |         uri: Uri, projection: Array<String>?, selection: String?, | ||||||
|  |         selectionArgs: Array<String>?, sortOrder: String? | ||||||
|  |     ): Cursor { | ||||||
|  |         val queryBuilder = SQLiteQueryBuilder().apply { | ||||||
|  |             tables = TABLE_NAME | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val cursor = queryBuilder.query( | ||||||
|  |             requireDb(), projection, selection, | ||||||
|  |             selectionArgs, null, null, sortOrder | ||||||
|  |         ) | ||||||
|  |         cursor.setNotificationUri(context?.contentResolver, uri) | ||||||
|  | 
 | ||||||
|  |         return cursor | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the update query of local SQLite Database | ||||||
|  |      * @param uri : contains the uri for bookmark pictures | ||||||
|  |      * @param contentValues : new values to be entered to db | ||||||
|  |      * @param selection : handles Where | ||||||
|  |      * @param selectionArgs : the condition of Where clause | ||||||
|  |      */ | ||||||
|  |     override fun update( | ||||||
|  |         uri: Uri, contentValues: ContentValues?, selection: String?, | ||||||
|  |         selectionArgs: Array<String>? | ||||||
|  |     ): Int { | ||||||
|  |         val rowsUpdated: Int | ||||||
|  |         if (selection.isNullOrEmpty()) { | ||||||
|  |             val id = uri.lastPathSegment!!.toInt() | ||||||
|  |             rowsUpdated = requireDb().update( | ||||||
|  |                 TABLE_NAME, | ||||||
|  |                 contentValues, | ||||||
|  |                 "$COLUMN_MEDIA_NAME = ?", | ||||||
|  |                 arrayOf(id.toString()) | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             throw IllegalArgumentException( | ||||||
|  |                 "Parameter `selection` should be empty when updating an ID" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         context?.contentResolver?.notifyChange(uri, null) | ||||||
|  |         return rowsUpdated | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the insertion of new bookmark pictures record to local SQLite Database | ||||||
|  |      */ | ||||||
|  |     override fun insert(uri: Uri, contentValues: ContentValues?): Uri { | ||||||
|  |         val id = requireDb().insert(TABLE_NAME, null, contentValues) | ||||||
|  |         context?.contentResolver?.notifyChange(uri, null) | ||||||
|  |         return "$BASE_URI/$id".toUri() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int { | ||||||
|  |         val rows: Int = requireDb().delete( | ||||||
|  |             TABLE_NAME, | ||||||
|  |             "media_name = ?", | ||||||
|  |             arrayOf(uri.lastPathSegment) | ||||||
|  |         ) | ||||||
|  |         context?.contentResolver?.notifyChange(uri, null) | ||||||
|  |         return rows | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         private const val BASE_PATH = "bookmarks" | ||||||
|  |         @JvmField | ||||||
|  |         val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_AUTHORITY}/$BASE_PATH".toUri() | ||||||
|  | 
 | ||||||
|  |         @JvmStatic | ||||||
|  |         fun uriForName(name: String): Uri = "$BASE_URI/$name".toUri() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,63 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.pictures; |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.Media; |  | ||||||
| import fr.free.nrw.commons.bookmarks.models.Bookmark; |  | ||||||
| import fr.free.nrw.commons.media.MediaClient; |  | ||||||
| import io.reactivex.Observable; |  | ||||||
| import io.reactivex.ObservableSource; |  | ||||||
| import io.reactivex.Single; |  | ||||||
| import io.reactivex.functions.Function; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import javax.inject.Singleton; |  | ||||||
| 
 |  | ||||||
| @Singleton |  | ||||||
| public class BookmarkPicturesController { |  | ||||||
| 
 |  | ||||||
|     private final MediaClient mediaClient; |  | ||||||
|     private final BookmarkPicturesDao bookmarkDao; |  | ||||||
| 
 |  | ||||||
|     private List<Bookmark> currentBookmarks; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     public BookmarkPicturesController(MediaClient mediaClient, BookmarkPicturesDao bookmarkDao) { |  | ||||||
|         this.mediaClient = mediaClient; |  | ||||||
|         this.bookmarkDao = bookmarkDao; |  | ||||||
|         currentBookmarks = new ArrayList<>(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Loads the Media objects from the raw data stored in DB and the API. |  | ||||||
|      * @return a list of bookmarked Media object |  | ||||||
|      */ |  | ||||||
|     Single<List<Media>> loadBookmarkedPictures() { |  | ||||||
|         List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks(); |  | ||||||
|         currentBookmarks = bookmarks; |  | ||||||
|         return Observable.fromIterable(bookmarks) |  | ||||||
|                 .flatMap((Function<Bookmark, ObservableSource<Media>>) this::getMediaFromBookmark) |  | ||||||
|                 .toList(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private Observable<Media> getMediaFromBookmark(Bookmark bookmark) { |  | ||||||
|         return mediaClient.getMedia(bookmark.getMediaName()) |  | ||||||
|                 .toObservable() |  | ||||||
|             .onErrorResumeNext(Observable.empty()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Loads the Media objects from the raw data stored in DB and the API. |  | ||||||
|      * @return a list of bookmarked Media object |  | ||||||
|      */ |  | ||||||
|     boolean needRefreshBookmarkedPictures() { |  | ||||||
|         List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks(); |  | ||||||
|         return bookmarks.size() != currentBookmarks.size(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Cancels the requests to the API and the DB |  | ||||||
|      */ |  | ||||||
|     void stop() { |  | ||||||
|         //noop |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.pictures | ||||||
|  | 
 | ||||||
|  | import fr.free.nrw.commons.Media | ||||||
|  | import fr.free.nrw.commons.bookmarks.models.Bookmark | ||||||
|  | import fr.free.nrw.commons.media.MediaClient | ||||||
|  | import io.reactivex.Observable | ||||||
|  | import io.reactivex.Single | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Singleton | ||||||
|  | 
 | ||||||
|  | @Singleton | ||||||
|  | class BookmarkPicturesController @Inject constructor( | ||||||
|  |     private val mediaClient: MediaClient, | ||||||
|  |     private val bookmarkDao: BookmarkPicturesDao | ||||||
|  | ) { | ||||||
|  |     private var currentBookmarks: List<Bookmark> = listOf() | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Loads the Media objects from the raw data stored in DB and the API. | ||||||
|  |      * @return a list of bookmarked Media object | ||||||
|  |      */ | ||||||
|  |     fun loadBookmarkedPictures(): Single<List<Media>> { | ||||||
|  |         val bookmarks = bookmarkDao.getAllBookmarks() | ||||||
|  |         currentBookmarks = bookmarks | ||||||
|  |         return Observable.fromIterable(bookmarks).flatMap { | ||||||
|  |             mediaClient.getMedia(it.mediaName) | ||||||
|  |                 .toObservable() | ||||||
|  |                 .onErrorResumeNext(Observable.empty()) | ||||||
|  |         }.toList() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun needRefreshBookmarkedPictures(): Boolean { | ||||||
|  |         val bookmarks = bookmarkDao.getAllBookmarks() | ||||||
|  |         return bookmarks.size != currentBookmarks.size | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun stop() = Unit | ||||||
|  | } | ||||||
|  | @ -1,227 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.pictures; |  | ||||||
| 
 |  | ||||||
| import android.annotation.SuppressLint; |  | ||||||
| import android.content.ContentProviderClient; |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
| import android.os.RemoteException; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| 
 |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
| 
 |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import javax.inject.Named; |  | ||||||
| import javax.inject.Provider; |  | ||||||
| import javax.inject.Singleton; |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.bookmarks.models.Bookmark; |  | ||||||
| 
 |  | ||||||
| import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI; |  | ||||||
| 
 |  | ||||||
| @Singleton |  | ||||||
| public class BookmarkPicturesDao { |  | ||||||
| 
 |  | ||||||
|     private final Provider<ContentProviderClient> clientProvider; |  | ||||||
| 
 |  | ||||||
|     @Inject |  | ||||||
|     public BookmarkPicturesDao(@Named("bookmarks") Provider<ContentProviderClient> clientProvider) { |  | ||||||
|         this.clientProvider = clientProvider; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Find all persisted pictures bookmarks on database |  | ||||||
|      * |  | ||||||
|      * @return list of bookmarks |  | ||||||
|      */ |  | ||||||
|     @NonNull |  | ||||||
|     public List<Bookmark> getAllBookmarks() { |  | ||||||
|         List<Bookmark> items = new ArrayList<>(); |  | ||||||
|         Cursor cursor = null; |  | ||||||
|         ContentProviderClient db = clientProvider.get(); |  | ||||||
|         try { |  | ||||||
|             cursor = db.query( |  | ||||||
|                     BookmarkPicturesContentProvider.BASE_URI, |  | ||||||
|                     Table.ALL_FIELDS, |  | ||||||
|                     null, |  | ||||||
|                     new String[]{}, |  | ||||||
|                     null); |  | ||||||
|             while (cursor != null && cursor.moveToNext()) { |  | ||||||
|                 items.add(fromCursor(cursor)); |  | ||||||
|             } |  | ||||||
|         } catch (RemoteException e) { |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } finally { |  | ||||||
|             if (cursor != null) { |  | ||||||
|                 cursor.close(); |  | ||||||
|             } |  | ||||||
|             db.release(); |  | ||||||
|         } |  | ||||||
|         return items; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Look for a bookmark in database and in order to insert or delete it |  | ||||||
|      * |  | ||||||
|      * @param bookmark : Bookmark object |  | ||||||
|      * @return boolean : is bookmark now fav ? |  | ||||||
|      */ |  | ||||||
|     public boolean updateBookmark(Bookmark bookmark) { |  | ||||||
|         boolean bookmarkExists = findBookmark(bookmark); |  | ||||||
|         if (bookmarkExists) { |  | ||||||
|             deleteBookmark(bookmark); |  | ||||||
|         } else { |  | ||||||
|             addBookmark(bookmark); |  | ||||||
|         } |  | ||||||
|         return !bookmarkExists; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Add a Bookmark to database |  | ||||||
|      * |  | ||||||
|      * @param bookmark : Bookmark to add |  | ||||||
|      */ |  | ||||||
|     private void addBookmark(Bookmark bookmark) { |  | ||||||
|         ContentProviderClient db = clientProvider.get(); |  | ||||||
|         try { |  | ||||||
|             db.insert(BASE_URI, toContentValues(bookmark)); |  | ||||||
|         } catch (RemoteException e) { |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } finally { |  | ||||||
|             db.release(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Delete a bookmark from database |  | ||||||
|      * |  | ||||||
|      * @param bookmark : Bookmark to delete |  | ||||||
|      */ |  | ||||||
|     private void deleteBookmark(Bookmark bookmark) { |  | ||||||
|         ContentProviderClient db = clientProvider.get(); |  | ||||||
|         try { |  | ||||||
|             if (bookmark.getContentUri() == null) { |  | ||||||
|                 throw new RuntimeException("tried to delete item with no content URI"); |  | ||||||
|             } else { |  | ||||||
|                 db.delete(bookmark.getContentUri(), null, null); |  | ||||||
|             } |  | ||||||
|         } catch (RemoteException e) { |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } finally { |  | ||||||
|             db.release(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Find a bookmark from database based on its name |  | ||||||
|      * |  | ||||||
|      * @param bookmark : Bookmark to find |  | ||||||
|      * @return boolean : is bookmark in database ? |  | ||||||
|      */ |  | ||||||
|     public boolean findBookmark(Bookmark bookmark) { |  | ||||||
|         if (bookmark == null) {//Avoiding NPE's |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Cursor cursor = null; |  | ||||||
|         ContentProviderClient db = clientProvider.get(); |  | ||||||
|         try { |  | ||||||
|             cursor = db.query( |  | ||||||
|                     BookmarkPicturesContentProvider.BASE_URI, |  | ||||||
|                     Table.ALL_FIELDS, |  | ||||||
|                     Table.COLUMN_MEDIA_NAME + "=?", |  | ||||||
|                     new String[]{bookmark.getMediaName()}, |  | ||||||
|                     null); |  | ||||||
|             if (cursor != null && cursor.moveToFirst()) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } catch (RemoteException e) { |  | ||||||
|             // This feels lazy, but to hell with checked exceptions. :) |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } finally { |  | ||||||
|             if (cursor != null) { |  | ||||||
|                 cursor.close(); |  | ||||||
|             } |  | ||||||
|             db.release(); |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @SuppressLint("Range") |  | ||||||
|     @NonNull |  | ||||||
|     Bookmark fromCursor(Cursor cursor) { |  | ||||||
|         String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME)); |  | ||||||
|         return new Bookmark( |  | ||||||
|                 fileName, |  | ||||||
|                 cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)), |  | ||||||
|                 BookmarkPicturesContentProvider.uriForName(fileName) |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private ContentValues toContentValues(Bookmark bookmark) { |  | ||||||
|         ContentValues cv = new ContentValues(); |  | ||||||
|         cv.put(BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME, bookmark.getMediaName()); |  | ||||||
|         cv.put(BookmarkPicturesDao.Table.COLUMN_CREATOR, bookmark.getMediaCreator()); |  | ||||||
|         return cv; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     public static class Table { |  | ||||||
|         public static final String TABLE_NAME = "bookmarks"; |  | ||||||
| 
 |  | ||||||
|         public static final String COLUMN_MEDIA_NAME = "media_name"; |  | ||||||
|         public static final String COLUMN_CREATOR = "media_creator"; |  | ||||||
| 
 |  | ||||||
|         // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. |  | ||||||
|         public static final String[] ALL_FIELDS = { |  | ||||||
|                 COLUMN_MEDIA_NAME, |  | ||||||
|                 COLUMN_CREATOR |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; |  | ||||||
| 
 |  | ||||||
|         public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" |  | ||||||
|                 + COLUMN_MEDIA_NAME + " STRING PRIMARY KEY," |  | ||||||
|                 + COLUMN_CREATOR + " STRING" |  | ||||||
|                 + ");"; |  | ||||||
| 
 |  | ||||||
|         public static void onCreate(SQLiteDatabase db) { |  | ||||||
|             db.execSQL(CREATE_TABLE_STATEMENT); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public static void onDelete(SQLiteDatabase db) { |  | ||||||
|             db.execSQL(DROP_TABLE_STATEMENT); |  | ||||||
|             onCreate(db); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public static void onUpdate(SQLiteDatabase db, int from, int to) { |  | ||||||
|             if (from == to) { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             if (from < 7) { |  | ||||||
|                 // doesn't exist yet |  | ||||||
|                 from++; |  | ||||||
|                 onUpdate(db, from, to); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (from == 7) { |  | ||||||
|                 // table added in version 8 |  | ||||||
|                 onCreate(db); |  | ||||||
|                 from++; |  | ||||||
|                 onUpdate(db, from, to); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (from == 8) { |  | ||||||
|                 from++; |  | ||||||
|                 onUpdate(db, from, to); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,144 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.pictures | ||||||
|  | 
 | ||||||
|  | import android.content.ContentProviderClient | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.Cursor | ||||||
|  | import android.os.RemoteException | ||||||
|  | import androidx.core.content.contentValuesOf | ||||||
|  | import fr.free.nrw.commons.bookmarks.models.Bookmark | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.BASE_URI | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.uriForName | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.ALL_FIELDS | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_CREATOR | ||||||
|  | import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME | ||||||
|  | import fr.free.nrw.commons.utils.getString | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Named | ||||||
|  | import javax.inject.Provider | ||||||
|  | import javax.inject.Singleton | ||||||
|  | 
 | ||||||
|  | @Singleton | ||||||
|  | class BookmarkPicturesDao @Inject constructor( | ||||||
|  |     @param:Named("bookmarks") private val clientProvider: Provider<ContentProviderClient> | ||||||
|  | ) { | ||||||
|  |     /** | ||||||
|  |      * Find all persisted pictures bookmarks on database | ||||||
|  |      * | ||||||
|  |      * @return list of bookmarks | ||||||
|  |      */ | ||||||
|  |     fun getAllBookmarks(): List<Bookmark> { | ||||||
|  |         val items: MutableList<Bookmark> = mutableListOf() | ||||||
|  |         var cursor: Cursor? = null | ||||||
|  |         val db = clientProvider.get() | ||||||
|  |         try { | ||||||
|  |             cursor = db.query( | ||||||
|  |                 BASE_URI, ALL_FIELDS, null, arrayOf(), null | ||||||
|  |             ) | ||||||
|  |             while (cursor != null && cursor.moveToNext()) { | ||||||
|  |                 items.add(fromCursor(cursor)) | ||||||
|  |             } | ||||||
|  |         } catch (e: RemoteException) { | ||||||
|  |             throw RuntimeException(e) | ||||||
|  |         } finally { | ||||||
|  |             cursor?.close() | ||||||
|  |             db.release() | ||||||
|  |         } | ||||||
|  |         return items | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Look for a bookmark in database and in order to insert or delete it | ||||||
|  |      * | ||||||
|  |      * @param bookmark : Bookmark object | ||||||
|  |      * @return boolean : is bookmark now fav ? | ||||||
|  |      */ | ||||||
|  |     fun updateBookmark(bookmark: Bookmark): Boolean { | ||||||
|  |         val bookmarkExists = findBookmark(bookmark) | ||||||
|  |         if (bookmarkExists) { | ||||||
|  |             deleteBookmark(bookmark) | ||||||
|  |         } else { | ||||||
|  |             addBookmark(bookmark) | ||||||
|  |         } | ||||||
|  |         return !bookmarkExists | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add a Bookmark to database | ||||||
|  |      * | ||||||
|  |      * @param bookmark : Bookmark to add | ||||||
|  |      */ | ||||||
|  |     private fun addBookmark(bookmark: Bookmark) { | ||||||
|  |         val db = clientProvider.get() | ||||||
|  |         try { | ||||||
|  |             db.insert(BASE_URI, toContentValues(bookmark)) | ||||||
|  |         } catch (e: RemoteException) { | ||||||
|  |             throw RuntimeException(e) | ||||||
|  |         } finally { | ||||||
|  |             db.release() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Delete a bookmark from database | ||||||
|  |      * | ||||||
|  |      * @param bookmark : Bookmark to delete | ||||||
|  |      */ | ||||||
|  |     private fun deleteBookmark(bookmark: Bookmark) { | ||||||
|  |         val db = clientProvider.get() | ||||||
|  |         try { | ||||||
|  |             if (bookmark.contentUri == null) { | ||||||
|  |                 throw RuntimeException("tried to delete item with no content URI") | ||||||
|  |             } else { | ||||||
|  |                 db.delete(bookmark.contentUri!!, null, null) | ||||||
|  |             } | ||||||
|  |         } catch (e: RemoteException) { | ||||||
|  |             throw RuntimeException(e) | ||||||
|  |         } finally { | ||||||
|  |             db.release() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Find a bookmark from database based on its name | ||||||
|  |      * | ||||||
|  |      * @param bookmark : Bookmark to find | ||||||
|  |      * @return boolean : is bookmark in database ? | ||||||
|  |      */ | ||||||
|  |     fun findBookmark(bookmark: Bookmark?): Boolean { | ||||||
|  |         if (bookmark == null) { | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         var cursor: Cursor? = null | ||||||
|  |         val db = clientProvider.get() | ||||||
|  |         try { | ||||||
|  |             cursor = db.query( | ||||||
|  |                 BASE_URI, ALL_FIELDS, "$COLUMN_MEDIA_NAME=?", arrayOf(bookmark.mediaName), null | ||||||
|  |             ) | ||||||
|  |             if (cursor != null && cursor.moveToFirst()) { | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |         } catch (e: RemoteException) { | ||||||
|  |             throw RuntimeException(e) | ||||||
|  |         } finally { | ||||||
|  |             cursor?.close() | ||||||
|  |             db.release() | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun fromCursor(cursor: Cursor): Bookmark { | ||||||
|  |         var fileName = cursor.getString(COLUMN_MEDIA_NAME) | ||||||
|  |         if (fileName == null) { | ||||||
|  |             fileName = "" | ||||||
|  |         } | ||||||
|  |         return Bookmark( | ||||||
|  |             fileName, cursor.getString(COLUMN_CREATOR), uriForName(fileName) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun toContentValues(bookmark: Bookmark): ContentValues = contentValuesOf( | ||||||
|  |         COLUMN_MEDIA_NAME to bookmark.mediaName, | ||||||
|  |         COLUMN_CREATOR to bookmark.mediaCreator | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | @ -1,218 +0,0 @@ | ||||||
| package fr.free.nrw.commons.bookmarks.pictures; |  | ||||||
| 
 |  | ||||||
| import static android.view.View.GONE; |  | ||||||
| import static android.view.View.VISIBLE; |  | ||||||
| 
 |  | ||||||
| import android.annotation.SuppressLint; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.AdapterView; |  | ||||||
| import android.widget.ListAdapter; |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.annotation.Nullable; |  | ||||||
| import dagger.android.support.DaggerFragment; |  | ||||||
| import fr.free.nrw.commons.Media; |  | ||||||
| import fr.free.nrw.commons.R; |  | ||||||
| import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment; |  | ||||||
| import fr.free.nrw.commons.category.GridViewAdapter; |  | ||||||
| import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding; |  | ||||||
| import fr.free.nrw.commons.utils.NetworkUtils; |  | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; |  | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers; |  | ||||||
| import io.reactivex.disposables.CompositeDisposable; |  | ||||||
| import io.reactivex.schedulers.Schedulers; |  | ||||||
| import java.util.List; |  | ||||||
| import javax.inject.Inject; |  | ||||||
| import timber.log.Timber; |  | ||||||
| 
 |  | ||||||
| public class BookmarkPicturesFragment extends DaggerFragment { |  | ||||||
| 
 |  | ||||||
|     private GridViewAdapter gridAdapter; |  | ||||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); |  | ||||||
| 
 |  | ||||||
|     private FragmentBookmarksPicturesBinding binding; |  | ||||||
|     @Inject |  | ||||||
|     BookmarkPicturesController controller; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Create an instance of the fragment with the right bundle parameters |  | ||||||
|      * @return an instance of the fragment |  | ||||||
|      */ |  | ||||||
|     public static BookmarkPicturesFragment newInstance() { |  | ||||||
|         return new BookmarkPicturesFragment(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView( |  | ||||||
|             @NonNull LayoutInflater inflater, |  | ||||||
|             ViewGroup container, |  | ||||||
|             Bundle savedInstanceState |  | ||||||
|     ) { |  | ||||||
|         binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false); |  | ||||||
|         return binding.getRoot(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { |  | ||||||
|         super.onViewCreated(view, savedInstanceState); |  | ||||||
|         binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment()); |  | ||||||
|         initList(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onStop() { |  | ||||||
|         super.onStop(); |  | ||||||
|         controller.stop(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onDestroy() { |  | ||||||
|         super.onDestroy(); |  | ||||||
|         compositeDisposable.clear(); |  | ||||||
|         binding = null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onResume() { |  | ||||||
|         super.onResume(); |  | ||||||
|         if (controller.needRefreshBookmarkedPictures()) { |  | ||||||
|             binding.bookmarkedPicturesList.setVisibility(GONE); |  | ||||||
|             if (gridAdapter != null) { |  | ||||||
|                 gridAdapter.clear(); |  | ||||||
|                 ((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged(); |  | ||||||
|             } |  | ||||||
|             initList(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Checks for internet connection and then initializes |  | ||||||
|      * the recycler view with bookmarked pictures |  | ||||||
|      */ |  | ||||||
|     @SuppressLint("CheckResult") |  | ||||||
|     private void initList() { |  | ||||||
|         if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { |  | ||||||
|             handleNoInternet(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         binding.loadingImagesProgressBar.setVisibility(VISIBLE); |  | ||||||
|         binding.statusMessage.setVisibility(GONE); |  | ||||||
| 
 |  | ||||||
|         compositeDisposable.add(controller.loadBookmarkedPictures() |  | ||||||
|                 .subscribeOn(Schedulers.io()) |  | ||||||
|                 .observeOn(AndroidSchedulers.mainThread()) |  | ||||||
|                 .subscribe(this::handleSuccess, this::handleError)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the UI updates for no internet scenario |  | ||||||
|      */ |  | ||||||
|     private void handleNoInternet() { |  | ||||||
|         binding.loadingImagesProgressBar.setVisibility(GONE); |  | ||||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { |  | ||||||
|             binding.statusMessage.setVisibility(VISIBLE); |  | ||||||
|             binding.statusMessage.setText(getString(R.string.no_internet)); |  | ||||||
|         } else { |  | ||||||
|             ViewUtil.showShortSnackbar(binding.parentLayout, R.string.no_internet); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Logs and handles API error scenario |  | ||||||
|      * @param throwable |  | ||||||
|      */ |  | ||||||
|     private void handleError(Throwable throwable) { |  | ||||||
|         Timber.e(throwable, "Error occurred while loading images inside a category"); |  | ||||||
|         try{ |  | ||||||
|             ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images); |  | ||||||
|             initErrorView(); |  | ||||||
|         }catch (Exception e){ |  | ||||||
|             e.printStackTrace(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the UI updates for a error scenario |  | ||||||
|      */ |  | ||||||
|     private void initErrorView() { |  | ||||||
|         binding.loadingImagesProgressBar.setVisibility(GONE); |  | ||||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { |  | ||||||
|             binding.statusMessage.setVisibility(VISIBLE); |  | ||||||
|             binding.statusMessage.setText(getString(R.string.no_images_found)); |  | ||||||
|         } else { |  | ||||||
|             binding.statusMessage.setVisibility(GONE); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the UI updates when there is no bookmarks |  | ||||||
|      */ |  | ||||||
|     private void initEmptyBookmarkListView() { |  | ||||||
|         binding.loadingImagesProgressBar.setVisibility(GONE); |  | ||||||
|         if (gridAdapter == null || gridAdapter.isEmpty()) { |  | ||||||
|             binding.statusMessage.setVisibility(VISIBLE); |  | ||||||
|             binding.statusMessage.setText(getString(R.string.bookmark_empty)); |  | ||||||
|         } else { |  | ||||||
|             binding.statusMessage.setVisibility(GONE); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Handles the success scenario |  | ||||||
|      * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter |  | ||||||
|      * @param collection List of new Media to be displayed |  | ||||||
|      */ |  | ||||||
|     private void handleSuccess(List<Media> collection) { |  | ||||||
|         if (collection == null) { |  | ||||||
|             initErrorView(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         if (collection.isEmpty()) { |  | ||||||
|             initEmptyBookmarkListView(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (gridAdapter == null) { |  | ||||||
|             setAdapter(collection); |  | ||||||
|         } else { |  | ||||||
|             if (gridAdapter.containsAll(collection)) { |  | ||||||
|                 binding.loadingImagesProgressBar.setVisibility(GONE); |  | ||||||
|                 binding.statusMessage.setVisibility(GONE); |  | ||||||
|                 binding.bookmarkedPicturesList.setVisibility(VISIBLE); |  | ||||||
|                 binding.bookmarkedPicturesList.setAdapter(gridAdapter); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             gridAdapter.addItems(collection); |  | ||||||
|             ((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged(); |  | ||||||
|         } |  | ||||||
|         binding.loadingImagesProgressBar.setVisibility(GONE); |  | ||||||
|         binding.statusMessage.setVisibility(GONE); |  | ||||||
|         binding.bookmarkedPicturesList.setVisibility(VISIBLE); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Initializes the adapter with a list of Media objects |  | ||||||
|      * @param mediaList List of new Media to be displayed |  | ||||||
|      */ |  | ||||||
|     private void setAdapter(List<Media> mediaList) { |  | ||||||
|         gridAdapter = new GridViewAdapter( |  | ||||||
|                 this.getContext(), |  | ||||||
|                 R.layout.layout_category_images, |  | ||||||
|                 mediaList |  | ||||||
|         ); |  | ||||||
|         binding.bookmarkedPicturesList.setAdapter(gridAdapter); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * It return an instance of gridView adapter which helps in extracting media details |  | ||||||
|      * used by the gridView |  | ||||||
|      * @return  GridView Adapter |  | ||||||
|      */ |  | ||||||
|     public ListAdapter getAdapter() { |  | ||||||
|         return binding.bookmarkedPicturesList.getAdapter(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,201 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.pictures | ||||||
|  | 
 | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.AdapterView.OnItemClickListener | ||||||
|  | import android.widget.ListAdapter | ||||||
|  | import dagger.android.support.DaggerFragment | ||||||
|  | import fr.free.nrw.commons.Media | ||||||
|  | import fr.free.nrw.commons.R | ||||||
|  | import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment | ||||||
|  | import fr.free.nrw.commons.category.GridViewAdapter | ||||||
|  | import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding | ||||||
|  | import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished | ||||||
|  | import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar | ||||||
|  | import io.reactivex.android.schedulers.AndroidSchedulers | ||||||
|  | import io.reactivex.disposables.CompositeDisposable | ||||||
|  | import io.reactivex.functions.Consumer | ||||||
|  | import io.reactivex.schedulers.Schedulers | ||||||
|  | import timber.log.Timber | ||||||
|  | import javax.inject.Inject | ||||||
|  | 
 | ||||||
|  | class BookmarkPicturesFragment : DaggerFragment() { | ||||||
|  |     private var gridAdapter: GridViewAdapter? = null | ||||||
|  |     private val compositeDisposable = CompositeDisposable() | ||||||
|  | 
 | ||||||
|  |     private var binding: FragmentBookmarksPicturesBinding? = null | ||||||
|  | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var controller: BookmarkPicturesController? = null | ||||||
|  | 
 | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false) | ||||||
|  |         return binding!!.root | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         binding!!.bookmarkedPicturesList.onItemClickListener = | ||||||
|  |             parentFragment as OnItemClickListener? | ||||||
|  |         initList() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onStop() { | ||||||
|  |         super.onStop() | ||||||
|  |         controller!!.stop() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         compositeDisposable.clear() | ||||||
|  |         binding = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onResume() { | ||||||
|  |         super.onResume() | ||||||
|  |         if (controller!!.needRefreshBookmarkedPictures()) { | ||||||
|  |             binding!!.bookmarkedPicturesList.visibility = View.GONE | ||||||
|  |             gridAdapter?.let { | ||||||
|  |                 it.clear() | ||||||
|  |                 (parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged() | ||||||
|  |             } | ||||||
|  |             initList() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks for internet connection and then initializes | ||||||
|  |      * the recycler view with bookmarked pictures | ||||||
|  |      */ | ||||||
|  |     @SuppressLint("CheckResult") | ||||||
|  |     private fun initList() { | ||||||
|  |         if (!isInternetConnectionEstablished(context)) { | ||||||
|  |             handleNoInternet() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         binding!!.loadingImagesProgressBar.visibility = View.VISIBLE | ||||||
|  |         binding!!.statusMessage.visibility = View.GONE | ||||||
|  | 
 | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             controller!!.loadBookmarkedPictures() | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe(::handleSuccess, ::handleError) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the UI updates for no internet scenario | ||||||
|  |      */ | ||||||
|  |     private fun handleNoInternet() { | ||||||
|  |         binding!!.loadingImagesProgressBar.visibility = View.GONE | ||||||
|  |         if (gridAdapter == null || gridAdapter!!.isEmpty) { | ||||||
|  |             binding!!.statusMessage.visibility = View.VISIBLE | ||||||
|  |             binding!!.statusMessage.text = getString(R.string.no_internet) | ||||||
|  |         } else { | ||||||
|  |             showShortSnackbar(binding!!.parentLayout, R.string.no_internet) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Logs and handles API error scenario | ||||||
|  |      * @param throwable | ||||||
|  |      */ | ||||||
|  |     private fun handleError(throwable: Throwable) { | ||||||
|  |         Timber.e(throwable, "Error occurred while loading images inside a category") | ||||||
|  |         try { | ||||||
|  |             showShortSnackbar(binding!!.root, R.string.error_loading_images) | ||||||
|  |             initErrorView() | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Timber.e(e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the UI updates for a error scenario | ||||||
|  |      */ | ||||||
|  |     private fun initErrorView() { | ||||||
|  |         binding!!.loadingImagesProgressBar.visibility = View.GONE | ||||||
|  |         if (gridAdapter == null || gridAdapter!!.isEmpty) { | ||||||
|  |             binding!!.statusMessage.visibility = View.VISIBLE | ||||||
|  |             binding!!.statusMessage.text = getString(R.string.no_images_found) | ||||||
|  |         } else { | ||||||
|  |             binding!!.statusMessage.visibility = View.GONE | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the UI updates when there is no bookmarks | ||||||
|  |      */ | ||||||
|  |     private fun initEmptyBookmarkListView() { | ||||||
|  |         binding!!.loadingImagesProgressBar.visibility = View.GONE | ||||||
|  |         if (gridAdapter == null || gridAdapter!!.isEmpty) { | ||||||
|  |             binding!!.statusMessage.visibility = View.VISIBLE | ||||||
|  |             binding!!.statusMessage.text = getString(R.string.bookmark_empty) | ||||||
|  |         } else { | ||||||
|  |             binding!!.statusMessage.visibility = View.GONE | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the success scenario | ||||||
|  |      * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter | ||||||
|  |      * @param collection List of new Media to be displayed | ||||||
|  |      */ | ||||||
|  |     private fun handleSuccess(collection: List<Media>?) { | ||||||
|  |         if (collection == null) { | ||||||
|  |             initErrorView() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         if (collection.isEmpty()) { | ||||||
|  |             initEmptyBookmarkListView() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (gridAdapter == null) { | ||||||
|  |             setAdapter(collection) | ||||||
|  |         } else { | ||||||
|  |             if (gridAdapter!!.containsAll(collection)) { | ||||||
|  |                 binding!!.loadingImagesProgressBar.visibility = View.GONE | ||||||
|  |                 binding!!.statusMessage.visibility = View.GONE | ||||||
|  |                 binding!!.bookmarkedPicturesList.visibility = View.VISIBLE | ||||||
|  |                 binding!!.bookmarkedPicturesList.adapter = gridAdapter | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             gridAdapter!!.addItems(collection) | ||||||
|  |             (parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged() | ||||||
|  |         } | ||||||
|  |         binding!!.loadingImagesProgressBar.visibility = View.GONE | ||||||
|  |         binding!!.statusMessage.visibility = View.GONE | ||||||
|  |         binding!!.bookmarkedPicturesList.visibility = View.VISIBLE | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initializes the adapter with a list of Media objects | ||||||
|  |      * @param mediaList List of new Media to be displayed | ||||||
|  |      */ | ||||||
|  |     private fun setAdapter(mediaList: List<Media>) { | ||||||
|  |         gridAdapter = GridViewAdapter( | ||||||
|  |             requireContext(), | ||||||
|  |             R.layout.layout_category_images, | ||||||
|  |             mediaList.toMutableList() | ||||||
|  |         ) | ||||||
|  |         binding?.let {  it.bookmarkedPicturesList.adapter = gridAdapter } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * It return an instance of gridView adapter which helps in extracting media details | ||||||
|  |      * used by the gridView | ||||||
|  |      * @return  GridView Adapter | ||||||
|  |      */ | ||||||
|  |     fun getAdapter(): ListAdapter? = binding?.bookmarkedPicturesList?.adapter | ||||||
|  | } | ||||||
|  | @ -0,0 +1,54 @@ | ||||||
|  | package fr.free.nrw.commons.bookmarks.pictures | ||||||
|  | 
 | ||||||
|  | import android.database.sqlite.SQLiteDatabase | ||||||
|  | 
 | ||||||
|  | object BookmarksTable { | ||||||
|  |     const val TABLE_NAME: String = "bookmarks" | ||||||
|  |     const val COLUMN_MEDIA_NAME: String = "media_name" | ||||||
|  |     const val COLUMN_CREATOR: String = "media_creator" | ||||||
|  | 
 | ||||||
|  |     // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. | ||||||
|  |     val ALL_FIELDS = arrayOf( | ||||||
|  |         COLUMN_MEDIA_NAME, | ||||||
|  |         COLUMN_CREATOR | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME" | ||||||
|  | 
 | ||||||
|  |     const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME (" + | ||||||
|  |             "$COLUMN_MEDIA_NAME STRING PRIMARY KEY, " + | ||||||
|  |             "$COLUMN_CREATOR STRING" + | ||||||
|  |             ");") | ||||||
|  | 
 | ||||||
|  |     fun onCreate(db: SQLiteDatabase) = | ||||||
|  |         db.execSQL(CREATE_TABLE_STATEMENT) | ||||||
|  | 
 | ||||||
|  |     fun onDelete(db: SQLiteDatabase) { | ||||||
|  |         db.execSQL(DROP_TABLE_STATEMENT) | ||||||
|  |         onCreate(db) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { | ||||||
|  |         if (from == to) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (from < 7) { | ||||||
|  |             // doesn't exist yet | ||||||
|  |             onUpdate(db, from+1, to) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (from == 7) { | ||||||
|  |             // table added in version 8 | ||||||
|  |             onCreate(db) | ||||||
|  |             onUpdate(db, from+1, to) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (from == 8) { | ||||||
|  |             onUpdate(db, from+1, to) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -7,8 +7,8 @@ import com.google.gson.annotations.SerializedName | ||||||
|  */ |  */ | ||||||
| class CampaignConfig { | class CampaignConfig { | ||||||
|     @SerializedName("showOnlyLiveCampaigns") |     @SerializedName("showOnlyLiveCampaigns") | ||||||
|     private val showOnlyLiveCampaigns = false |     var showOnlyLiveCampaigns = false | ||||||
| 
 | 
 | ||||||
|     @SerializedName("sortBy") |     @SerializedName("sortBy") | ||||||
|     private val sortBy: String? = null |     var sortBy: String? = null | ||||||
| } | } | ||||||
|  | @ -8,8 +8,8 @@ import fr.free.nrw.commons.campaigns.models.Campaign | ||||||
|  */ |  */ | ||||||
| class CampaignResponseDTO { | class CampaignResponseDTO { | ||||||
|     @SerializedName("config") |     @SerializedName("config") | ||||||
|     val campaignConfig: CampaignConfig? = null |     var campaignConfig: CampaignConfig? = null | ||||||
| 
 | 
 | ||||||
|     @SerializedName("campaigns") |     @SerializedName("campaigns") | ||||||
|     val campaigns: List<Campaign>? = null |     var campaigns: List<Campaign>? = null | ||||||
| } | } | ||||||
|  | @ -7,7 +7,6 @@ import android.view.LayoutInflater | ||||||
| import android.view.View | import android.view.View | ||||||
| import androidx.core.content.ContextCompat | import androidx.core.content.ContextCompat | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.Utils |  | ||||||
| import fr.free.nrw.commons.campaigns.models.Campaign | import fr.free.nrw.commons.campaigns.models.Campaign | ||||||
| import fr.free.nrw.commons.contributions.MainActivity | import fr.free.nrw.commons.contributions.MainActivity | ||||||
| import fr.free.nrw.commons.databinding.LayoutCampaginBinding | import fr.free.nrw.commons.databinding.LayoutCampaginBinding | ||||||
|  | @ -16,6 +15,7 @@ import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort | ||||||
| import fr.free.nrw.commons.utils.DateUtil.getExtraShortDateString | import fr.free.nrw.commons.utils.DateUtil.getExtraShortDateString | ||||||
| import fr.free.nrw.commons.utils.SwipableCardView | import fr.free.nrw.commons.utils.SwipableCardView | ||||||
| import fr.free.nrw.commons.utils.ViewUtil.showLongToast | import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||||
|  | import fr.free.nrw.commons.utils.handleWebUrl | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
| import java.text.ParseException | import java.text.ParseException | ||||||
| 
 | 
 | ||||||
|  | @ -74,7 +74,7 @@ class CampaignView : SwipableCardView { | ||||||
|                 if (it.isWLMCampaign) { |                 if (it.isWLMCampaign) { | ||||||
|                     ((context) as MainActivity).showNearby() |                     ((context) as MainActivity).showNearby() | ||||||
|                 } else { |                 } else { | ||||||
|                     Utils.handleWebUrl(context, Uri.parse(it.link)) |                     handleWebUrl(context, Uri.parse(it.link)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ class CampaignsPresenter @Inject constructor( | ||||||
|     private val okHttpJsonApiClient: OkHttpJsonApiClient?, |     private val okHttpJsonApiClient: OkHttpJsonApiClient?, | ||||||
|     @param:Named(IO_THREAD) private val ioScheduler: Scheduler, |     @param:Named(IO_THREAD) private val ioScheduler: Scheduler, | ||||||
|     @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler |     @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler | ||||||
| ) : BasePresenter<ICampaignsView?> { | ) : BasePresenter<ICampaignsView> { | ||||||
|     private var view: ICampaignsView? = null |     private var view: ICampaignsView? = null | ||||||
|     private var disposable: Disposable? = null |     private var disposable: Disposable? = null | ||||||
|     private var campaign: Campaign? = null |     private var campaign: Campaign? = null | ||||||
|  |  | ||||||
|  | @ -1,11 +1,10 @@ | ||||||
| package fr.free.nrw.commons.campaigns | package fr.free.nrw.commons.campaigns | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.MvpView |  | ||||||
| import fr.free.nrw.commons.campaigns.models.Campaign | import fr.free.nrw.commons.campaigns.models.Campaign | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Interface which defines the view contracts of the campaign view |  * Interface which defines the view contracts of the campaign view | ||||||
|  */ |  */ | ||||||
| interface ICampaignsView : MvpView { | interface ICampaignsView { | ||||||
|     fun showCampaigns(campaign: Campaign?) |     fun showCampaigns(campaign: Campaign?) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,12 +9,9 @@ import android.database.sqlite.SQLiteDatabase | ||||||
| import android.database.sqlite.SQLiteQueryBuilder | import android.database.sqlite.SQLiteQueryBuilder | ||||||
| import android.net.Uri | import android.net.Uri | ||||||
| import android.text.TextUtils | import android.text.TextUtils | ||||||
| import androidx.annotation.NonNull |  | ||||||
| import fr.free.nrw.commons.BuildConfig | import fr.free.nrw.commons.BuildConfig | ||||||
| import fr.free.nrw.commons.data.DBOpenHelper |  | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerContentProvider | import fr.free.nrw.commons.di.CommonsDaggerContentProvider | ||||||
| import timber.log.Timber | import androidx.core.net.toUri | ||||||
| import javax.inject.Inject |  | ||||||
| 
 | 
 | ||||||
| class CategoryContentProvider : CommonsDaggerContentProvider() { | class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
| 
 | 
 | ||||||
|  | @ -23,9 +20,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
|         addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID) |         addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Inject |  | ||||||
|     lateinit var dbOpenHelper: DBOpenHelper |  | ||||||
| 
 |  | ||||||
|     @SuppressWarnings("ConstantConditions") |     @SuppressWarnings("ConstantConditions") | ||||||
|     override fun query(uri: Uri, projection: Array<String>?, selection: String?, |     override fun query(uri: Uri, projection: Array<String>?, selection: String?, | ||||||
|                        selectionArgs: Array<String>?, sortOrder: String?): Cursor? { |                        selectionArgs: Array<String>?, sortOrder: String?): Cursor? { | ||||||
|  | @ -34,7 +28,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         val uriType = uriMatcher.match(uri) |         val uriType = uriMatcher.match(uri) | ||||||
|         val db = dbOpenHelper.readableDatabase |         val db = requireDb() | ||||||
| 
 | 
 | ||||||
|         val cursor: Cursor? = when (uriType) { |         val cursor: Cursor? = when (uriType) { | ||||||
|             CATEGORIES -> queryBuilder.query( |             CATEGORIES -> queryBuilder.query( | ||||||
|  | @ -58,45 +52,37 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
|             else -> throw IllegalArgumentException("Unknown URI $uri") |             else -> throw IllegalArgumentException("Unknown URI $uri") | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         cursor?.setNotificationUri(context?.contentResolver, uri) |         cursor?.setNotificationUri(requireContext().contentResolver, uri) | ||||||
|         return cursor |         return cursor | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun getType(uri: Uri): String? { |     override fun getType(uri: Uri): String? = null | ||||||
|         return null |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     @SuppressWarnings("ConstantConditions") |     @SuppressWarnings("ConstantConditions") | ||||||
|     override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { |     override fun insert(uri: Uri, contentValues: ContentValues?): Uri { | ||||||
|         val uriType = uriMatcher.match(uri) |         val uriType = uriMatcher.match(uri) | ||||||
|         val sqlDB = dbOpenHelper.writableDatabase |  | ||||||
|         val id: Long |         val id: Long | ||||||
|         when (uriType) { |         when (uriType) { | ||||||
|             CATEGORIES -> { |             CATEGORIES -> { | ||||||
|                 id = sqlDB.insert(TABLE_NAME, null, contentValues) |                 id = requireDb().insert(TABLE_NAME, null, contentValues) | ||||||
|             } |             } | ||||||
|             else -> throw IllegalArgumentException("Unknown URI: $uri") |             else -> throw IllegalArgumentException("Unknown URI: $uri") | ||||||
|         } |         } | ||||||
|         context?.contentResolver?.notifyChange(uri, null) |         requireContext().contentResolver?.notifyChange(uri, null) | ||||||
|         return Uri.parse("${Companion.BASE_URI}/$id") |         return "${BASE_URI}/$id".toUri() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @SuppressWarnings("ConstantConditions") |     @SuppressWarnings("ConstantConditions") | ||||||
|     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { |     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0 | ||||||
|         // Not implemented |  | ||||||
|         return 0 |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     @SuppressWarnings("ConstantConditions") |     @SuppressWarnings("ConstantConditions") | ||||||
|     override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int { |     override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int { | ||||||
|         Timber.d("Hello, bulk insert! (CategoryContentProvider)") |  | ||||||
|         val uriType = uriMatcher.match(uri) |         val uriType = uriMatcher.match(uri) | ||||||
|         val sqlDB = dbOpenHelper.writableDatabase |         val sqlDB = requireDb() | ||||||
|         sqlDB.beginTransaction() |         sqlDB.beginTransaction() | ||||||
|         when (uriType) { |         when (uriType) { | ||||||
|             CATEGORIES -> { |             CATEGORIES -> { | ||||||
|                 for (value in values) { |                 for (value in values) { | ||||||
|                     Timber.d("Inserting! %s", value) |  | ||||||
|                     sqlDB.insert(TABLE_NAME, null, value) |                     sqlDB.insert(TABLE_NAME, null, value) | ||||||
|                 } |                 } | ||||||
|                 sqlDB.setTransactionSuccessful() |                 sqlDB.setTransactionSuccessful() | ||||||
|  | @ -104,7 +90,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
|             else -> throw IllegalArgumentException("Unknown URI: $uri") |             else -> throw IllegalArgumentException("Unknown URI: $uri") | ||||||
|         } |         } | ||||||
|         sqlDB.endTransaction() |         sqlDB.endTransaction() | ||||||
|         context?.contentResolver?.notifyChange(uri, null) |         requireContext().contentResolver?.notifyChange(uri, null) | ||||||
|         return values.size |         return values.size | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -112,17 +98,18 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
|     override fun update(uri: Uri, contentValues: ContentValues?, selection: String?, |     override fun update(uri: Uri, contentValues: ContentValues?, selection: String?, | ||||||
|                         selectionArgs: Array<String>?): Int { |                         selectionArgs: Array<String>?): Int { | ||||||
|         val uriType = uriMatcher.match(uri) |         val uriType = uriMatcher.match(uri) | ||||||
|         val sqlDB = dbOpenHelper.writableDatabase |  | ||||||
|         val rowsUpdated: Int |         val rowsUpdated: Int | ||||||
|         when (uriType) { |         when (uriType) { | ||||||
|             CATEGORIES_ID -> { |             CATEGORIES_ID -> { | ||||||
|                 if (TextUtils.isEmpty(selection)) { |                 if (TextUtils.isEmpty(selection)) { | ||||||
|                     val id = uri.lastPathSegment?.toInt() |                     val id = uri.lastPathSegment?.toInt() | ||||||
|                         ?: throw IllegalArgumentException("Invalid ID") |                         ?: throw IllegalArgumentException("Invalid ID") | ||||||
|                     rowsUpdated = sqlDB.update(TABLE_NAME, |                     rowsUpdated = requireDb().update( | ||||||
|  |                         TABLE_NAME, | ||||||
|                         contentValues, |                         contentValues, | ||||||
|                         "$COLUMN_ID = ?", |                         "$COLUMN_ID = ?", | ||||||
|                         arrayOf(id.toString())) |                         arrayOf(id.toString()) | ||||||
|  |                     ) | ||||||
|                 } else { |                 } else { | ||||||
|                     throw IllegalArgumentException( |                     throw IllegalArgumentException( | ||||||
|                         "Parameter `selection` should be empty when updating an ID") |                         "Parameter `selection` should be empty when updating an ID") | ||||||
|  | @ -130,7 +117,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
|             } |             } | ||||||
|             else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType") |             else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType") | ||||||
|         } |         } | ||||||
|         context?.contentResolver?.notifyChange(uri, null) |         requireContext().contentResolver?.notifyChange(uri, null) | ||||||
|         return rowsUpdated |         return rowsUpdated | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -165,13 +152,9 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
|                 "$COLUMN_TIMES_USED INTEGER" + |                 "$COLUMN_TIMES_USED INTEGER" + | ||||||
|                 ");" |                 ");" | ||||||
| 
 | 
 | ||||||
|         fun uriForId(id: Int): Uri { |         fun uriForId(id: Int): Uri = Uri.parse("${BASE_URI}/$id") | ||||||
|             return Uri.parse("${BASE_URI}/$id") |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         fun onCreate(db: SQLiteDatabase) { |         fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT) | ||||||
|             db.execSQL(CREATE_TABLE_STATEMENT) |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         fun onDelete(db: SQLiteDatabase) { |         fun onDelete(db: SQLiteDatabase) { | ||||||
|             db.execSQL(DROP_TABLE_STATEMENT) |             db.execSQL(DROP_TABLE_STATEMENT) | ||||||
|  | @ -200,6 +183,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() { | ||||||
|         private const val CATEGORIES = 1 |         private const val CATEGORIES = 1 | ||||||
|         private const val CATEGORIES_ID = 2 |         private const val CATEGORIES_ID = 2 | ||||||
|         private const val BASE_PATH = "categories" |         private const val BASE_PATH = "categories" | ||||||
|         val  BASE_URI: Uri = Uri.parse("content://${BuildConfig.CATEGORY_AUTHORITY}/${Companion.BASE_PATH}") |         val  BASE_URI: Uri = "content://${BuildConfig.CATEGORY_AUTHORITY}/${BASE_PATH}".toUri() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,21 +8,25 @@ import android.view.Menu | ||||||
| import android.view.MenuItem | import android.view.MenuItem | ||||||
| import android.view.View | import android.view.View | ||||||
| import androidx.activity.viewModels | import androidx.activity.viewModels | ||||||
| import androidx.fragment.app.Fragment |  | ||||||
| import androidx.fragment.app.FragmentManager | import androidx.fragment.app.FragmentManager | ||||||
| import androidx.lifecycle.Lifecycle | import androidx.lifecycle.Lifecycle | ||||||
| import androidx.lifecycle.lifecycleScope | import androidx.lifecycle.lifecycleScope | ||||||
| import androidx.lifecycle.repeatOnLifecycle | import androidx.lifecycle.repeatOnLifecycle | ||||||
|  | import fr.free.nrw.commons.BuildConfig.COMMONS_URL | ||||||
| import fr.free.nrw.commons.Media | import fr.free.nrw.commons.Media | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.Utils |  | ||||||
| import fr.free.nrw.commons.ViewPagerAdapter | import fr.free.nrw.commons.ViewPagerAdapter | ||||||
| import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding | import fr.free.nrw.commons.databinding.ActivityCategoryDetailsBinding | ||||||
| import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment | import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment | ||||||
| import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment | import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment | ||||||
| import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment | import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment | import fr.free.nrw.commons.media.MediaDetailPagerFragment | ||||||
|  | import fr.free.nrw.commons.media.MediaDetailProvider | ||||||
| import fr.free.nrw.commons.theme.BaseActivity | import fr.free.nrw.commons.theme.BaseActivity | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets | ||||||
|  | import fr.free.nrw.commons.utils.handleWebUrl | ||||||
|  | import fr.free.nrw.commons.wikidata.model.WikiSite | ||||||
|  | import fr.free.nrw.commons.wikidata.model.page.PageTitle | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
|  | @ -33,7 +37,7 @@ import javax.inject.Inject | ||||||
|  * a particular category on wikimedia commons. |  * a particular category on wikimedia commons. | ||||||
|  */ |  */ | ||||||
| class CategoryDetailsActivity : BaseActivity(), | class CategoryDetailsActivity : BaseActivity(), | ||||||
|     MediaDetailPagerFragment.MediaDetailProvider, |     MediaDetailProvider, | ||||||
|     CategoryImagesCallback { |     CategoryImagesCallback { | ||||||
| 
 | 
 | ||||||
|     private lateinit var supportFragmentManager: FragmentManager |     private lateinit var supportFragmentManager: FragmentManager | ||||||
|  | @ -54,9 +58,10 @@ class CategoryDetailsActivity : BaseActivity(), | ||||||
| 
 | 
 | ||||||
|         binding = ActivityCategoryDetailsBinding.inflate(layoutInflater) |         binding = ActivityCategoryDetailsBinding.inflate(layoutInflater) | ||||||
|         val view = binding.root |         val view = binding.root | ||||||
|  |         applyEdgeToEdgeAllInsets(view) | ||||||
|         setContentView(view) |         setContentView(view) | ||||||
|         supportFragmentManager = getSupportFragmentManager() |         supportFragmentManager = getSupportFragmentManager() | ||||||
|         viewPagerAdapter = ViewPagerAdapter(supportFragmentManager) |         viewPagerAdapter = ViewPagerAdapter(this, supportFragmentManager) | ||||||
|         binding.viewPager.adapter = viewPagerAdapter |         binding.viewPager.adapter = viewPagerAdapter | ||||||
|         binding.viewPager.offscreenPageLimit = 2 |         binding.viewPager.offscreenPageLimit = 2 | ||||||
|         binding.tabLayout.setupWithViewPager(binding.viewPager) |         binding.tabLayout.setupWithViewPager(binding.viewPager) | ||||||
|  | @ -80,8 +85,6 @@ class CategoryDetailsActivity : BaseActivity(), | ||||||
|      * Set the fragments according to the tab selected in the viewPager. |      * Set the fragments according to the tab selected in the viewPager. | ||||||
|      */ |      */ | ||||||
|     private fun setTabs() { |     private fun setTabs() { | ||||||
|         val fragmentList = mutableListOf<Fragment>() |  | ||||||
|         val titleList = mutableListOf<String>() |  | ||||||
|         categoriesMediaFragment = CategoriesMediaFragment() |         categoriesMediaFragment = CategoriesMediaFragment() | ||||||
|         val subCategoryListFragment = SubCategoriesFragment() |         val subCategoryListFragment = SubCategoriesFragment() | ||||||
|         val parentCategoriesFragment = ParentCategoriesFragment() |         val parentCategoriesFragment = ParentCategoriesFragment() | ||||||
|  | @ -96,13 +99,12 @@ class CategoryDetailsActivity : BaseActivity(), | ||||||
| 
 | 
 | ||||||
|             viewModel.onCheckIfBookmarked(categoryName!!) |             viewModel.onCheckIfBookmarked(categoryName!!) | ||||||
|         } |         } | ||||||
|         fragmentList.add(categoriesMediaFragment) | 
 | ||||||
|         titleList.add("MEDIA") |         viewPagerAdapter.setTabs( | ||||||
|         fragmentList.add(subCategoryListFragment) |             R.string.title_for_media to categoriesMediaFragment, | ||||||
|         titleList.add("SUBCATEGORIES") |             R.string.title_for_subcategories to subCategoryListFragment, | ||||||
|         fragmentList.add(parentCategoriesFragment) |             R.string.title_for_parent_categories to parentCategoriesFragment | ||||||
|         titleList.add("PARENT CATEGORIES") |         ) | ||||||
|         viewPagerAdapter.setTabData(fragmentList, titleList) |  | ||||||
|         viewPagerAdapter.notifyDataSetChanged() |         viewPagerAdapter.notifyDataSetChanged() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -199,8 +201,9 @@ class CategoryDetailsActivity : BaseActivity(), | ||||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { |     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||||
|         return when (item.itemId) { |         return when (item.itemId) { | ||||||
|             R.id.menu_browser_current_category -> { |             R.id.menu_browser_current_category -> { | ||||||
|                 val title = Utils.getPageTitle(CATEGORY_PREFIX + categoryName) |                 val title = PageTitle(CATEGORY_PREFIX + categoryName, WikiSite(COMMONS_URL)) | ||||||
|                 Utils.handleWebUrl(this, Uri.parse(title.canonicalUri)) | 
 | ||||||
|  |                 handleWebUrl(this, Uri.parse(title.canonicalUri)) | ||||||
|                 true |                 true | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -22,9 +22,9 @@ class ExceptionAwareThreadPoolExecutor( | ||||||
|                 if (r.isDone) { |                 if (r.isDone) { | ||||||
|                     r.get() |                     r.get() | ||||||
|                 } |                 } | ||||||
|             } catch (e: CancellationException) { |             } catch (_: CancellationException) { | ||||||
|                 // ignore |                 // ignore | ||||||
|             } catch (e: InterruptedException) { |             } catch (_: InterruptedException) { | ||||||
|                 // ignore |                 // ignore | ||||||
|             } catch (e: ExecutionException) { |             } catch (e: ExecutionException) { | ||||||
|                 throwable = e.cause ?: e |                 throwable = e.cause ?: e | ||||||
|  |  | ||||||
|  | @ -180,8 +180,8 @@ class ContributionController @Inject constructor(@param:Named("default_preferenc | ||||||
|         showAlertDialog( |         showAlertDialog( | ||||||
|             activity, activity.getString(R.string.location_permission_title), |             activity, activity.getString(R.string.location_permission_title), | ||||||
|             activity.getString(R.string.in_app_camera_location_permission_rationale), |             activity.getString(R.string.in_app_camera_location_permission_rationale), | ||||||
|             activity.getString(android.R.string.ok), |             activity.getString(R.string.ok), | ||||||
|             activity.getString(android.R.string.cancel), |             activity.getString(R.string.cancel), | ||||||
|             { |             { | ||||||
|                 createDialogsAndHandleLocationPermissions( |                 createDialogsAndHandleLocationPermissions( | ||||||
|                     activity, |                     activity, | ||||||
|  | @ -253,13 +253,14 @@ class ContributionController @Inject constructor(@param:Named("default_preferenc | ||||||
|      */ |      */ | ||||||
|     fun initiateCustomGalleryPickWithPermission( |     fun initiateCustomGalleryPickWithPermission( | ||||||
|         activity: Activity, |         activity: Activity, | ||||||
|         resultLauncher: ActivityResultLauncher<Intent> |         resultLauncher: ActivityResultLauncher<Intent>, | ||||||
|  |         singleSelection: Boolean = false | ||||||
|     ) { |     ) { | ||||||
|         setPickerConfiguration(activity, true) |         setPickerConfiguration(activity, true) | ||||||
| 
 | 
 | ||||||
|         checkPermissionsAndPerformAction( |         checkPermissionsAndPerformAction( | ||||||
|             activity, |             activity, | ||||||
|             { openCustomSelector(activity, resultLauncher, 0) }, |             { FilePicker.openCustomSelector(activity, resultLauncher, 0, singleSelection) }, | ||||||
|             R.string.storage_permission_title, |             R.string.storage_permission_title, | ||||||
|             R.string.write_storage_permission_rationale, |             R.string.write_storage_permission_rationale, | ||||||
|             *PERMISSIONS_STORAGE |             *PERMISSIONS_STORAGE | ||||||
|  |  | ||||||
|  | @ -8,23 +8,29 @@ import androidx.appcompat.app.AlertDialog | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import com.facebook.imagepipeline.request.ImageRequest | import com.facebook.imagepipeline.request.ImageRequest | ||||||
| import com.facebook.imagepipeline.request.ImageRequestBuilder | import com.facebook.imagepipeline.request.ImageRequestBuilder | ||||||
|  | import fr.free.nrw.commons.Media | ||||||
|  | import fr.free.nrw.commons.utils.MediaAttributionUtil | ||||||
|  | import fr.free.nrw.commons.MediaDataExtractor | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.databinding.LayoutContributionBinding | import fr.free.nrw.commons.databinding.LayoutContributionBinding | ||||||
| import fr.free.nrw.commons.media.MediaClient | import fr.free.nrw.commons.media.MediaClient | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers | import io.reactivex.android.schedulers.AndroidSchedulers | ||||||
| import io.reactivex.disposables.CompositeDisposable | import io.reactivex.disposables.CompositeDisposable | ||||||
| import io.reactivex.schedulers.Schedulers | import io.reactivex.schedulers.Schedulers | ||||||
|  | import timber.log.Timber | ||||||
| import java.io.File | import java.io.File | ||||||
| 
 | 
 | ||||||
| class ContributionViewHolder internal constructor( | class ContributionViewHolder internal constructor( | ||||||
|     private val parent: View, private val callback: ContributionsListAdapter.Callback, |     parent: View, | ||||||
|     private val mediaClient: MediaClient |     private val callback: ContributionsListAdapter.Callback, | ||||||
|  |     private val compositeDisposable: CompositeDisposable, | ||||||
|  |     private val mediaClient: MediaClient, | ||||||
|  |     private val mediaDataExtractor: MediaDataExtractor | ||||||
| ) : RecyclerView.ViewHolder(parent) { | ) : RecyclerView.ViewHolder(parent) { | ||||||
|     var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent) |     var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent) | ||||||
| 
 | 
 | ||||||
|     private var position = 0 |     private var position = 0 | ||||||
|     private var contribution: Contribution? = null |     private var contribution: Contribution? = null | ||||||
|     private val compositeDisposable = CompositeDisposable() |  | ||||||
|     private var isWikipediaButtonDisplayed = false |     private var isWikipediaButtonDisplayed = false | ||||||
|     private val pausingPopUp: AlertDialog |     private val pausingPopUp: AlertDialog | ||||||
|     var imageRequest: ImageRequest? = null |     var imageRequest: ImageRequest? = null | ||||||
|  | @ -54,7 +60,7 @@ an upload might take a dozen seconds. */ | ||||||
|         this.contribution = contribution |         this.contribution = contribution | ||||||
|         this.position = position |         this.position = position | ||||||
|         binding.contributionTitle.text = contribution.media.mostRelevantCaption |         binding.contributionTitle.text = contribution.media.mostRelevantCaption | ||||||
|         binding.authorView.text = contribution.media.getAuthorOrUser() |         setAuthorText(contribution.media) | ||||||
| 
 | 
 | ||||||
|         //Removes flicker of loading image. |         //Removes flicker of loading image. | ||||||
|         binding.contributionImage.hierarchy.fadeDuration = 0 |         binding.contributionImage.hierarchy.fadeDuration = 0 | ||||||
|  | @ -93,6 +99,30 @@ an upload might take a dozen seconds. */ | ||||||
|         checkIfMediaExistsOnWikipediaPage(contribution) |         checkIfMediaExistsOnWikipediaPage(contribution) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fun updateAttribution() { | ||||||
|  |         if (contribution != null) { | ||||||
|  |             val media = contribution!!.media | ||||||
|  |             if (!media.getAttributedAuthor().isNullOrEmpty()) { | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             compositeDisposable.addAll( | ||||||
|  |                 mediaDataExtractor.fetchCreatorIdsAndLabels(media) | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { idAndLabels -> | ||||||
|  |                             media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels) | ||||||
|  |                             setAuthorText(media) | ||||||
|  |                         }, | ||||||
|  |                         { t: Throwable? -> Timber.e(t) }) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setAuthorText(media: Media) { | ||||||
|  |         binding.authorView.text = MediaAttributionUtil.getTagLine(media, itemView.context) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Checks if a media exists on the corresponding Wikipedia article Currently the check is made |      * Checks if a media exists on the corresponding Wikipedia article Currently the check is made | ||||||
|      * for the device's current language Wikipedia |      * for the device's current language Wikipedia | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ import fr.free.nrw.commons.BasePresenter | ||||||
| interface ContributionsContract { | interface ContributionsContract { | ||||||
| 
 | 
 | ||||||
|     interface View { |     interface View { | ||||||
|         fun showMessage(localizedMessage: String) |  | ||||||
|         fun getContext(): Context? |         fun getContext(): Context? | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,7 +30,6 @@ import androidx.work.WorkManager | ||||||
| import fr.free.nrw.commons.MapController.NearbyPlacesInfo | import fr.free.nrw.commons.MapController.NearbyPlacesInfo | ||||||
| import fr.free.nrw.commons.Media | import fr.free.nrw.commons.Media | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.Utils |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager | import fr.free.nrw.commons.auth.SessionManager | ||||||
| import fr.free.nrw.commons.campaigns.CampaignView | import fr.free.nrw.commons.campaigns.CampaignView | ||||||
| import fr.free.nrw.commons.campaigns.CampaignsPresenter | import fr.free.nrw.commons.campaigns.CampaignsPresenter | ||||||
|  | @ -44,7 +43,7 @@ import fr.free.nrw.commons.location.LatLng | ||||||
| import fr.free.nrw.commons.location.LocationServiceManager | import fr.free.nrw.commons.location.LocationServiceManager | ||||||
| import fr.free.nrw.commons.location.LocationUpdateListener | import fr.free.nrw.commons.location.LocationUpdateListener | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment | import fr.free.nrw.commons.media.MediaDetailPagerFragment | ||||||
| import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider | import fr.free.nrw.commons.media.MediaDetailProvider | ||||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient | import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient | ||||||
| import fr.free.nrw.commons.nearby.NearbyController | import fr.free.nrw.commons.nearby.NearbyController | ||||||
| import fr.free.nrw.commons.nearby.NearbyNotificationCardView | import fr.free.nrw.commons.nearby.NearbyNotificationCardView | ||||||
|  | @ -64,13 +63,15 @@ import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween | ||||||
| import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished | import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished | ||||||
| import fr.free.nrw.commons.utils.PermissionUtils.hasPermission | import fr.free.nrw.commons.utils.PermissionUtils.hasPermission | ||||||
| import fr.free.nrw.commons.utils.ViewUtil.showLongToast | import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||||
|  | import fr.free.nrw.commons.utils.isMonumentsEnabled | ||||||
|  | import fr.free.nrw.commons.utils.wLMEndDate | ||||||
|  | import fr.free.nrw.commons.utils.wLMStartDate | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers | import io.reactivex.android.schedulers.AndroidSchedulers | ||||||
| import io.reactivex.disposables.CompositeDisposable | import io.reactivex.disposables.CompositeDisposable | ||||||
| import io.reactivex.schedulers.Schedulers | import io.reactivex.schedulers.Schedulers | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
| import java.util.Calendar | import java.util.Calendar | ||||||
| import java.util.Date |  | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import javax.inject.Named | import javax.inject.Named | ||||||
| 
 | 
 | ||||||
|  | @ -139,7 +140,7 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On | ||||||
| 
 | 
 | ||||||
|     private var wlmCampaign: Campaign? = null |     private var wlmCampaign: Campaign? = null | ||||||
| 
 | 
 | ||||||
|     var userName: String? = null |     private var userName: String? = null | ||||||
|     private var isUserProfile = false |     private var isUserProfile = false | ||||||
| 
 | 
 | ||||||
|     private var mSensorManager: SensorManager? = null |     private var mSensorManager: SensorManager? = null | ||||||
|  | @ -242,8 +243,8 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On | ||||||
|     private fun initWLMCampaign() { |     private fun initWLMCampaign() { | ||||||
|         wlmCampaign = Campaign( |         wlmCampaign = Campaign( | ||||||
|             getString(R.string.wlm_campaign_title), |             getString(R.string.wlm_campaign_title), | ||||||
|             getString(R.string.wlm_campaign_description), Utils.getWLMStartDate().toString(), |             getString(R.string.wlm_campaign_description), wLMStartDate, | ||||||
|             Utils.getWLMEndDate().toString(), NearbyParentFragment.WLM_URL, true |             wLMEndDate, NearbyParentFragment.WLM_URL, true | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -729,7 +730,7 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On | ||||||
|      * of campaigns on the campaigns card |      * of campaigns on the campaigns card | ||||||
|      */ |      */ | ||||||
|     private fun fetchCampaigns() { |     private fun fetchCampaigns() { | ||||||
|         if (Utils.isMonumentsEnabled(Date())) { |         if (isMonumentsEnabled) { | ||||||
|             if (binding != null) { |             if (binding != null) { | ||||||
|                 binding!!.campaignsView.setCampaign(wlmCampaign) |                 binding!!.campaignsView.setCampaign(wlmCampaign) | ||||||
|                 binding!!.campaignsView.visibility = View.VISIBLE |                 binding!!.campaignsView.visibility = View.VISIBLE | ||||||
|  | @ -743,10 +744,6 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun showMessage(message: String) { |  | ||||||
|         Toast.makeText(context, message, Toast.LENGTH_SHORT).show() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun showCampaigns(campaign: Campaign?) { |     override fun showCampaigns(campaign: Campaign?) { | ||||||
|         if (campaign != null && !isUserProfile) { |         if (campaign != null && !isUserProfile) { | ||||||
|             if (binding != null) { |             if (binding != null) { | ||||||
|  | @ -808,10 +805,11 @@ class ContributionsFragment : CommonsDaggerSupportFragment(), FragmentManager.On | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     // /** | ||||||
|      * Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847] |     //  * Temporarily disabled. See issue [#5847](https://github.com/commons-app/apps-android-commons/issues/5847) | ||||||
|      * @param count The number of pending uploads. |     //  * @param count The number of pending uploads. | ||||||
|      */ |     //  */ | ||||||
|  |     // public void updateUploadIcon(int count) { | ||||||
|     //    public void updateUploadIcon(int count) { |     //    public void updateUploadIcon(int count) { | ||||||
|     //        if (pendingUploadsImageView != null) { |     //        if (pendingUploadsImageView != null) { | ||||||
|     //            if (count != 0) { |     //            if (count != 0) { | ||||||
|  |  | ||||||
|  | @ -4,21 +4,26 @@ import android.view.LayoutInflater | ||||||
| import android.view.ViewGroup | import android.view.ViewGroup | ||||||
| import androidx.paging.PagedListAdapter | import androidx.paging.PagedListAdapter | ||||||
| import androidx.recyclerview.widget.DiffUtil | import androidx.recyclerview.widget.DiffUtil | ||||||
|  | import fr.free.nrw.commons.MediaDataExtractor | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.media.MediaClient | import fr.free.nrw.commons.media.MediaClient | ||||||
|  | import io.reactivex.disposables.CompositeDisposable | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Represents The View Adapter for the List of Contributions |  * Represents The View Adapter for the List of Contributions | ||||||
|  */ |  */ | ||||||
| class ContributionsListAdapter internal constructor( | class ContributionsListAdapter internal constructor( | ||||||
|     private val callback: Callback, |     private val callback: Callback, | ||||||
|     private val mediaClient: MediaClient |     private val mediaClient: MediaClient, | ||||||
|  |     private val mediaDataExtractor: MediaDataExtractor, | ||||||
|  |     private val compositeDisposable: CompositeDisposable | ||||||
| ) : PagedListAdapter<Contribution, ContributionViewHolder>(DIFF_CALLBACK) { | ) : PagedListAdapter<Contribution, ContributionViewHolder>(DIFF_CALLBACK) { | ||||||
|     /** |     /** | ||||||
|      * Initializes the view holder with contribution data |      * Initializes the view holder with contribution data | ||||||
|      */ |      */ | ||||||
|     override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) { |     override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) { | ||||||
|         holder.init(position, getItem(position)) |         holder.init(position, getItem(position)) | ||||||
|  |         holder.updateAttribution() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun getContributionForPosition(position: Int): Contribution? { |     fun getContributionForPosition(position: Int): Contribution? { | ||||||
|  | @ -36,7 +41,7 @@ class ContributionsListAdapter internal constructor( | ||||||
|         val viewHolder = ContributionViewHolder( |         val viewHolder = ContributionViewHolder( | ||||||
|             LayoutInflater.from(parent.context) |             LayoutInflater.from(parent.context) | ||||||
|                 .inflate(R.layout.layout_contribution, parent, false), |                 .inflate(R.layout.layout_contribution, parent, false), | ||||||
|             callback, mediaClient |             callback, compositeDisposable, mediaClient, mediaDataExtractor | ||||||
|         ) |         ) | ||||||
|         return viewHolder |         return viewHolder | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ class ContributionsListContract { | ||||||
|         fun showNoContributionsUI(shouldShow: Boolean) |         fun showNoContributionsUI(shouldShow: Boolean) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     interface UserActionListener : BasePresenter<View?> { |     interface UserActionListener : BasePresenter<View> { | ||||||
|         fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?) |         fun refreshList(swipeRefreshLayout: SwipeRefreshLayout?) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,7 +5,6 @@ import android.annotation.SuppressLint | ||||||
| import android.content.Context | import android.content.Context | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
| import android.content.res.Configuration | import android.content.res.Configuration | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.os.Parcelable | import android.os.Parcelable | ||||||
| import android.view.LayoutInflater | import android.view.LayoutInflater | ||||||
|  | @ -20,6 +19,8 @@ import androidx.activity.result.ActivityResultLauncher | ||||||
| import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions | import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions | ||||||
| import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult | import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult | ||||||
| import androidx.annotation.VisibleForTesting | import androidx.annotation.VisibleForTesting | ||||||
|  | import androidx.core.net.toUri | ||||||
|  | import androidx.core.os.BundleCompat | ||||||
| import androidx.paging.PagedList | import androidx.paging.PagedList | ||||||
| import androidx.recyclerview.widget.GridLayoutManager | import androidx.recyclerview.widget.GridLayoutManager | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | @ -27,8 +28,8 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver | ||||||
| import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener | import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener | ||||||
| import androidx.recyclerview.widget.SimpleItemAnimator | import androidx.recyclerview.widget.SimpleItemAnimator | ||||||
| import fr.free.nrw.commons.Media | import fr.free.nrw.commons.Media | ||||||
|  | import fr.free.nrw.commons.MediaDataExtractor | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.Utils |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager | import fr.free.nrw.commons.auth.SessionManager | ||||||
| import fr.free.nrw.commons.contributions.WikipediaInstructionsDialogFragment.Companion.newInstance | import fr.free.nrw.commons.contributions.WikipediaInstructionsDialogFragment.Companion.newInstance | ||||||
| import fr.free.nrw.commons.databinding.FragmentContributionsListBinding | import fr.free.nrw.commons.databinding.FragmentContributionsListBinding | ||||||
|  | @ -38,10 +39,10 @@ import fr.free.nrw.commons.filepicker.FilePicker | ||||||
| import fr.free.nrw.commons.media.MediaClient | import fr.free.nrw.commons.media.MediaClient | ||||||
| import fr.free.nrw.commons.profile.ProfileActivity | import fr.free.nrw.commons.profile.ProfileActivity | ||||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||||
| import fr.free.nrw.commons.utils.SystemThemeUtils |  | ||||||
| import fr.free.nrw.commons.utils.ViewUtil.showShortToast | import fr.free.nrw.commons.utils.ViewUtil.showShortToast | ||||||
|  | import fr.free.nrw.commons.utils.copyToClipboard | ||||||
|  | import fr.free.nrw.commons.utils.handleWebUrl | ||||||
| import fr.free.nrw.commons.wikidata.model.WikiSite | import fr.free.nrw.commons.wikidata.model.WikiSite | ||||||
| import org.apache.commons.lang3.StringUtils |  | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import javax.inject.Named | import javax.inject.Named | ||||||
| 
 | 
 | ||||||
|  | @ -51,10 +52,6 @@ import javax.inject.Named | ||||||
|  */ |  */ | ||||||
| class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsListContract.View, | class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsListContract.View, | ||||||
|     ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback { |     ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback { | ||||||
|     @JvmField |  | ||||||
|     @Inject |  | ||||||
|     var systemThemeUtils: SystemThemeUtils? = null |  | ||||||
| 
 |  | ||||||
|     @JvmField |     @JvmField | ||||||
|     @Inject |     @Inject | ||||||
|     var controller: ContributionController? = null |     var controller: ContributionController? = null | ||||||
|  | @ -63,6 +60,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     @Inject |     @Inject | ||||||
|     var mediaClient: MediaClient? = null |     var mediaClient: MediaClient? = null | ||||||
| 
 | 
 | ||||||
|  |     @JvmField | ||||||
|  |     @Inject | ||||||
|  |     var mediaDataExtractor: MediaDataExtractor? = null | ||||||
|  | 
 | ||||||
|     @JvmField |     @JvmField | ||||||
|     @Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) |     @Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) | ||||||
|     @Inject |     @Inject | ||||||
|  | @ -77,13 +78,14 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     var sessionManager: SessionManager? = null |     var sessionManager: SessionManager? = null | ||||||
| 
 | 
 | ||||||
|     private var binding: FragmentContributionsListBinding? = null |     private var binding: FragmentContributionsListBinding? = null | ||||||
|     private var fab_close: Animation? = null |     private var fabClose: Animation? = null | ||||||
|     private var fab_open: Animation? = null |     private var fabOpen: Animation? = null | ||||||
|     private var rotate_forward: Animation? = null |     private var rotateForward: Animation? = null | ||||||
|     private var rotate_backward: Animation? = null |     private var rotateBackward: Animation? = null | ||||||
|     private var isFabOpen = false |     private var isFabOpen = false | ||||||
| 
 | 
 | ||||||
|     private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>> |     private lateinit var inAppCameraLocationPermissionLauncher: | ||||||
|  |             ActivityResultLauncher<Array<String>> | ||||||
| 
 | 
 | ||||||
|     @VisibleForTesting |     @VisibleForTesting | ||||||
|     var rvContributionsList: RecyclerView? = null |     var rvContributionsList: RecyclerView? = null | ||||||
|  | @ -94,8 +96,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     @VisibleForTesting |     @VisibleForTesting | ||||||
|     var callback: Callback? = null |     var callback: Callback? = null | ||||||
| 
 | 
 | ||||||
|     private val SPAN_COUNT_LANDSCAPE = 3 |     private val spanCountLandscape = 3 | ||||||
|     private val SPAN_COUNT_PORTRAIT = 1 |     private val spanCountPortrait = 1 | ||||||
| 
 | 
 | ||||||
|     private var contributionsSize = 0 |     private var contributionsSize = 0 | ||||||
|     private var userName: String? = null |     private var userName: String? = null | ||||||
|  | @ -144,7 +146,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|             userName = requireArguments().getString(ProfileActivity.KEY_USERNAME) |             userName = requireArguments().getString(ProfileActivity.KEY_USERNAME) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (StringUtils.isEmpty(userName)) { |         if (userName.isNullOrEmpty()) { | ||||||
|             userName = sessionManager!!.userName |             userName = sessionManager!!.userName | ||||||
|         } |         } | ||||||
|         inAppCameraLocationPermissionLauncher = |         inAppCameraLocationPermissionLauncher = | ||||||
|  | @ -155,7 +157,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|                 controller?.locationPermissionCallback?.onLocationPermissionGranted() |                 controller?.locationPermissionCallback?.onLocationPermissionGranted() | ||||||
|             } else { |             } else { | ||||||
|                 activity?.let { currentActivity -> |                 activity?.let { currentActivity -> | ||||||
|                     if (currentActivity.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { |                     if (currentActivity.shouldShowRequestPermissionRationale( | ||||||
|  |                             permission.ACCESS_FINE_LOCATION)) { | ||||||
|                         controller?.handleShowRationaleFlowCameraLocation( |                         controller?.handleShowRationaleFlowCameraLocation( | ||||||
|                             currentActivity, |                             currentActivity, | ||||||
|                             inAppCameraLocationPermissionLauncher, // Pass launcher |                             inAppCameraLocationPermissionLauncher, // Pass launcher | ||||||
|  | @ -163,7 +166,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|                         ) |                         ) | ||||||
|                     } else { |                     } else { | ||||||
|                         controller?.locationPermissionCallback?.onLocationPermissionDenied( |                         controller?.locationPermissionCallback?.onLocationPermissionDenied( | ||||||
|                             currentActivity.getString(R.string.in_app_camera_location_permission_denied) |                             currentActivity.getString( | ||||||
|  |                                 R.string.in_app_camera_location_permission_denied) | ||||||
|                         ) |                         ) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  | @ -183,7 +187,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|         contributionsListPresenter!!.onAttachView(this) |         contributionsListPresenter!!.onAttachView(this) | ||||||
|         binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() } |         binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() } | ||||||
|         binding!!.fabCustomGallery.setOnLongClickListener { view: View? -> |         binding!!.fabCustomGallery.setOnLongClickListener { view: View? -> | ||||||
|             showShortToast(context, fr.free.nrw.commons.R.string.custom_selector_title) |             showShortToast(context, R.string.custom_selector_title) | ||||||
|             true |             true | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -193,7 +197,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|         } else { |         } else { | ||||||
|             binding!!.tvContributionsOfUser.visibility = View.VISIBLE |             binding!!.tvContributionsOfUser.visibility = View.VISIBLE | ||||||
|             binding!!.tvContributionsOfUser.text = |             binding!!.tvContributionsOfUser.text = | ||||||
|                 getString(fr.free.nrw.commons.R.string.contributions_of_user, userName) |                 getString(R.string.contributions_of_user, userName) | ||||||
|             binding!!.fabLayout.visibility = View.GONE |             binding!!.fabLayout.visibility = View.GONE | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -231,7 +235,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun initAdapter() { |     private fun initAdapter() { | ||||||
|         adapter = ContributionsListAdapter(this, mediaClient!!) |         adapter = ContributionsListAdapter(this, | ||||||
|  |             mediaClient!!, | ||||||
|  |             mediaDataExtractor!!, | ||||||
|  |             compositeDisposable) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  | @ -306,7 +313,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|             override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { |             override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { | ||||||
|                 if (e.action == MotionEvent.ACTION_DOWN) { |                 if (e.action == MotionEvent.ACTION_DOWN) { | ||||||
|                     if (isFabOpen) { |                     if (isFabOpen) { | ||||||
|                         animateFAB(isFabOpen) |                         animateFAB(true) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 return false |                 return false | ||||||
|  | @ -338,14 +345,20 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun getSpanCount(orientation: Int): Int { |     private fun getSpanCount(orientation: Int): Int { | ||||||
|         return if (orientation == Configuration.ORIENTATION_LANDSCAPE) SPAN_COUNT_LANDSCAPE else SPAN_COUNT_PORTRAIT |         return if (orientation == Configuration.ORIENTATION_LANDSCAPE) | ||||||
|  |             spanCountLandscape | ||||||
|  |         else | ||||||
|  |             spanCountPortrait | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onConfigurationChanged(newConfig: Configuration) { |     override fun onConfigurationChanged(newConfig: Configuration) { | ||||||
|         super.onConfigurationChanged(newConfig) |         super.onConfigurationChanged(newConfig) | ||||||
|         // check orientation |         // check orientation | ||||||
|         binding!!.fabLayout.orientation = |         binding!!.fabLayout.orientation = | ||||||
|             if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL |             if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) | ||||||
|  |                 LinearLayout.HORIZONTAL | ||||||
|  |             else | ||||||
|  |                 LinearLayout.VERTICAL | ||||||
|         rvContributionsList |         rvContributionsList | ||||||
|             ?.setLayoutManager( |             ?.setLayoutManager( | ||||||
|                 GridLayoutManager(context, getSpanCount(newConfig.orientation)) |                 GridLayoutManager(context, getSpanCount(newConfig.orientation)) | ||||||
|  | @ -353,10 +366,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun initializeAnimations() { |     private fun initializeAnimations() { | ||||||
|         fab_open = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_open) |         fabOpen = AnimationUtils.loadAnimation(activity, R.anim.fab_open) | ||||||
|         fab_close = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.fab_close) |         fabClose = AnimationUtils.loadAnimation(activity, R.anim.fab_close) | ||||||
|         rotate_forward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_forward) |         rotateForward = AnimationUtils.loadAnimation(activity, R.anim.rotate_forward) | ||||||
|         rotate_backward = AnimationUtils.loadAnimation(activity, fr.free.nrw.commons.R.anim.rotate_backward) |         rotateBackward = AnimationUtils.loadAnimation(activity, R.anim.rotate_backward) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun setListeners() { |     private fun setListeners() { | ||||||
|  | @ -372,7 +385,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|         binding!!.fabCamera.setOnLongClickListener { view: View? -> |         binding!!.fabCamera.setOnLongClickListener { view: View? -> | ||||||
|             showShortToast( |             showShortToast( | ||||||
|                 context, |                 context, | ||||||
|                 fr.free.nrw.commons.R.string.add_contribution_from_camera |                 R.string.add_contribution_from_camera | ||||||
|             ) |             ) | ||||||
|             true |             true | ||||||
|         } |         } | ||||||
|  | @ -381,7 +394,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|             animateFAB(isFabOpen) |             animateFAB(isFabOpen) | ||||||
|         } |         } | ||||||
|         binding!!.fabGallery.setOnLongClickListener { view: View? -> |         binding!!.fabGallery.setOnLongClickListener { view: View? -> | ||||||
|             showShortToast(context, fr.free.nrw.commons.R.string.menu_from_gallery) |             showShortToast(context, R.string.menu_from_gallery) | ||||||
|             true |             true | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -389,7 +402,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     /** |     /** | ||||||
|      * Launch Custom Selector. |      * Launch Custom Selector. | ||||||
|      */ |      */ | ||||||
|     protected fun launchCustomSelector() { |     private fun launchCustomSelector() { | ||||||
|         controller!!.initiateCustomGalleryPickWithPermission( |         controller!!.initiateCustomGalleryPickWithPermission( | ||||||
|             requireActivity(), |             requireActivity(), | ||||||
|             customSelectorLauncherForResult |             customSelectorLauncherForResult | ||||||
|  | @ -405,18 +418,18 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|         this.isFabOpen = !isFabOpen |         this.isFabOpen = !isFabOpen | ||||||
|         if (binding!!.fabPlus.isShown) { |         if (binding!!.fabPlus.isShown) { | ||||||
|             if (isFabOpen) { |             if (isFabOpen) { | ||||||
|                 binding!!.fabPlus.startAnimation(rotate_backward) |                 binding!!.fabPlus.startAnimation(rotateBackward) | ||||||
|                 binding!!.fabCamera.startAnimation(fab_close) |                 binding!!.fabCamera.startAnimation(fabClose) | ||||||
|                 binding!!.fabGallery.startAnimation(fab_close) |                 binding!!.fabGallery.startAnimation(fabClose) | ||||||
|                 binding!!.fabCustomGallery.startAnimation(fab_close) |                 binding!!.fabCustomGallery.startAnimation(fabClose) | ||||||
|                 binding!!.fabCamera.hide() |                 binding!!.fabCamera.hide() | ||||||
|                 binding!!.fabGallery.hide() |                 binding!!.fabGallery.hide() | ||||||
|                 binding!!.fabCustomGallery.hide() |                 binding!!.fabCustomGallery.hide() | ||||||
|             } else { |             } else { | ||||||
|                 binding!!.fabPlus.startAnimation(rotate_forward) |                 binding!!.fabPlus.startAnimation(rotateForward) | ||||||
|                 binding!!.fabCamera.startAnimation(fab_open) |                 binding!!.fabCamera.startAnimation(fabOpen) | ||||||
|                 binding!!.fabGallery.startAnimation(fab_open) |                 binding!!.fabGallery.startAnimation(fabOpen) | ||||||
|                 binding!!.fabCustomGallery.startAnimation(fab_open) |                 binding!!.fabCustomGallery.startAnimation(fabOpen) | ||||||
|                 binding!!.fabCamera.show() |                 binding!!.fabCamera.show() | ||||||
|                 binding!!.fabGallery.show() |                 binding!!.fabGallery.show() | ||||||
|                 binding!!.fabCustomGallery.show() |                 binding!!.fabCustomGallery.show() | ||||||
|  | @ -428,9 +441,9 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     /** |     /** | ||||||
|      * Shows welcome message if user has no contributions yet i.e. new user. |      * Shows welcome message if user has no contributions yet i.e. new user. | ||||||
|      */ |      */ | ||||||
|     override fun showWelcomeTip(shouldShow: Boolean) { |     override fun showWelcomeTip(numberOfUploads: Boolean) { | ||||||
|         binding!!.noContributionsYet.visibility = |         binding!!.noContributionsYet.visibility = | ||||||
|             if (shouldShow) View.VISIBLE else View.GONE |             if (numberOfUploads) View.VISIBLE else View.GONE | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -450,22 +463,22 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
| 
 | 
 | ||||||
|     override fun onSaveInstanceState(outState: Bundle) { |     override fun onSaveInstanceState(outState: Bundle) { | ||||||
|         super.onSaveInstanceState(outState) |         super.onSaveInstanceState(outState) | ||||||
|         val layoutManager = rvContributionsList |         val layoutManager = rvContributionsList?.layoutManager as GridLayoutManager? | ||||||
|             ?.getLayoutManager() as GridLayoutManager? |  | ||||||
|         outState.putParcelable(RV_STATE, layoutManager!!.onSaveInstanceState()) |         outState.putParcelable(RV_STATE, layoutManager!!.onSaveInstanceState()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onViewStateRestored(savedInstanceState: Bundle?) { |     override fun onViewStateRestored(savedInstanceState: Bundle?) { | ||||||
|         super.onViewStateRestored(savedInstanceState) |         super.onViewStateRestored(savedInstanceState) | ||||||
|         if (null != savedInstanceState) { |         if (null != savedInstanceState) { | ||||||
|             val savedRecyclerLayoutState = savedInstanceState.getParcelable<Parcelable>(RV_STATE) |             val savedRecyclerLayoutState = | ||||||
|  |                 BundleCompat.getParcelable(savedInstanceState, RV_STATE, Parcelable::class.java) | ||||||
|             rvContributionsList!!.layoutManager!!.onRestoreInstanceState(savedRecyclerLayoutState) |             rvContributionsList!!.layoutManager!!.onRestoreInstanceState(savedRecyclerLayoutState) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun openMediaDetail(position: Int, isWikipediaButtonDisplayed: Boolean) { |     override fun openMediaDetail(contribution: Int, isWikipediaPageExists: Boolean) { | ||||||
|         if (null != callback) { //Just being safe, ideally they won't be called when detached |         if (null != callback) { //Just being safe, ideally they won't be called when detached | ||||||
|             callback!!.showDetail(position, isWikipediaButtonDisplayed) |             callback!!.showDetail(contribution, isWikipediaPageExists) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -477,8 +490,8 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|     override fun addImageToWikipedia(contribution: Contribution?) { |     override fun addImageToWikipedia(contribution: Contribution?) { | ||||||
|         showAlertDialog( |         showAlertDialog( | ||||||
|             requireActivity(), |             requireActivity(), | ||||||
|             getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_title), |             getString(R.string.add_picture_to_wikipedia_article_title), | ||||||
|             getString(fr.free.nrw.commons.R.string.add_picture_to_wikipedia_article_desc), |             getString(R.string.add_picture_to_wikipedia_article_desc), | ||||||
|             { |             { | ||||||
|                 if (contribution != null) { |                 if (contribution != null) { | ||||||
|                     showAddImageToWikipediaInstructions(contribution) |                     showAddImageToWikipediaInstructions(contribution) | ||||||
|  | @ -492,16 +505,18 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|      * @param contribution |      * @param contribution | ||||||
|      */ |      */ | ||||||
|     private fun showAddImageToWikipediaInstructions(contribution: Contribution) { |     private fun showAddImageToWikipediaInstructions(contribution: Contribution) { | ||||||
|         val fragmentManager = fragmentManager |         val fragmentManager = this.parentFragmentManager | ||||||
|         val fragment = newInstance(contribution) |         val fragment = newInstance(contribution) | ||||||
|         fragment.callback = |         fragment.callback = | ||||||
|             WikipediaInstructionsDialogFragment.Callback { contribution: Contribution?, copyWikicode: Boolean -> |             WikipediaInstructionsDialogFragment.Callback { | ||||||
|                 this.onConfirmClicked( |                 contribution: Contribution?, | ||||||
|  |                 copyWikicode: Boolean -> | ||||||
|  |                 onConfirmClicked( | ||||||
|                     contribution, |                     contribution, | ||||||
|                     copyWikicode |                     copyWikicode | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|         fragment.show(fragmentManager!!, "WikimediaFragment") |         fragment.show(fragmentManager, "WikimediaFragment") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -522,14 +537,13 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL | ||||||
|      */ |      */ | ||||||
|     override fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean) { |     override fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean) { | ||||||
|         if (copyWikicode) { |         if (copyWikicode) { | ||||||
|             val wikicode = contribution!!.media.wikiCode |             requireContext().copyToClipboard("wikicode", contribution!!.media.wikiCode) | ||||||
|             Utils.copy("wikicode", wikicode, context) |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         val url = |         val url = | ||||||
|             languageWikipediaSite!!.mobileUrl() + "/wiki/" + (contribution!!.wikidataPlace |             languageWikipediaSite!!.mobileUrl() + "/wiki/" + (contribution!!.wikidataPlace | ||||||
|                 ?.getWikipediaPageTitle()) |                 ?.getWikipediaPageTitle()) | ||||||
|         Utils.handleWebUrl(context, Uri.parse(url)) |         handleWebUrl(requireContext(), url.toUri()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun getContributionStateAt(position: Int): Int { |     fun getContributionStateAt(position: Int): Int { | ||||||
|  |  | ||||||
|  | @ -12,7 +12,6 @@ import androidx.fragment.app.FragmentManager | ||||||
| import androidx.work.ExistingWorkPolicy | import androidx.work.ExistingWorkPolicy | ||||||
| import com.google.android.material.bottomnavigation.BottomNavigationView | import com.google.android.material.bottomnavigation.BottomNavigationView | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.WelcomeActivity |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager | import fr.free.nrw.commons.auth.SessionManager | ||||||
| import fr.free.nrw.commons.bookmarks.BookmarkFragment | import fr.free.nrw.commons.bookmarks.BookmarkFragment | ||||||
| import fr.free.nrw.commons.contributions.ContributionsFragment.Companion.newInstance | import fr.free.nrw.commons.contributions.ContributionsFragment.Companion.newInstance | ||||||
|  | @ -33,7 +32,9 @@ import fr.free.nrw.commons.notification.NotificationActivity.Companion.startYour | ||||||
| import fr.free.nrw.commons.notification.NotificationController | import fr.free.nrw.commons.notification.NotificationController | ||||||
| import fr.free.nrw.commons.quiz.QuizChecker | import fr.free.nrw.commons.quiz.QuizChecker | ||||||
| import fr.free.nrw.commons.settings.SettingsFragment | import fr.free.nrw.commons.settings.SettingsFragment | ||||||
|  | import fr.free.nrw.commons.startWelcome | ||||||
| import fr.free.nrw.commons.theme.BaseActivity | import fr.free.nrw.commons.theme.BaseActivity | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets | ||||||
| import fr.free.nrw.commons.upload.UploadProgressActivity | import fr.free.nrw.commons.upload.UploadProgressActivity | ||||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest | import fr.free.nrw.commons.upload.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest | ||||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper | import fr.free.nrw.commons.utils.ViewUtilWrapper | ||||||
|  | @ -112,6 +113,7 @@ class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener | ||||||
|     public override fun onCreate(savedInstanceState: Bundle?) { |     public override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
|         binding = MainBinding.inflate(layoutInflater) |         binding = MainBinding.inflate(layoutInflater) | ||||||
|  |         applyEdgeToEdgeAllInsets(binding!!.root) | ||||||
|         setContentView(binding!!.root) |         setContentView(binding!!.root) | ||||||
|         setSupportActionBar(binding!!.toolbarBinding.toolbar) |         setSupportActionBar(binding!!.toolbarBinding.toolbar) | ||||||
|         tabLayout = binding!!.fragmentMainNavTabLayout |         tabLayout = binding!!.fragmentMainNavTabLayout | ||||||
|  | @ -151,21 +153,7 @@ after opening the app. | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             setUpPager() |             setUpPager() | ||||||
|             /** | 
 | ||||||
|              * Ask the user for media location access just after login |  | ||||||
|              * so that location in the EXIF metadata of the images shared by the user |  | ||||||
|              * is retained on devices running Android 10 or above |  | ||||||
|              */ |  | ||||||
| //            if (VERSION.SDK_INT >= VERSION_CODES.Q) { |  | ||||||
| //                ActivityCompat.requestPermissions(this, |  | ||||||
| //                    new String[]{Manifest.permission.ACCESS_MEDIA_LOCATION}, 0); |  | ||||||
| //                PermissionUtils.checkPermissionsAndPerformAction( |  | ||||||
| //                    this, |  | ||||||
| //                    () -> {}, |  | ||||||
| //                    R.string.media_location_permission_denied, |  | ||||||
| //                    R.string.add_location_manually, |  | ||||||
| //                    permission.ACCESS_MEDIA_LOCATION); |  | ||||||
| //            } |  | ||||||
|             checkAndResumeStuckUploads() |             checkAndResumeStuckUploads() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -336,7 +324,7 @@ after opening the app. | ||||||
|         ) |         ) | ||||||
|             .subscribeOn(Schedulers.io()) |             .subscribeOn(Schedulers.io()) | ||||||
|             .blockingGet() |             .blockingGet() | ||||||
|         Timber.d("Resuming " + stuckUploads.size + " uploads...") |         Timber.d("Resuming %d uploads...", stuckUploads.size) | ||||||
|         if (!stuckUploads.isEmpty()) { |         if (!stuckUploads.isEmpty()) { | ||||||
|             for (contribution in stuckUploads) { |             for (contribution in stuckUploads) { | ||||||
|                 contribution.state = Contribution.STATE_QUEUED |                 contribution.state = Contribution.STATE_QUEUED | ||||||
|  | @ -517,7 +505,7 @@ after opening the app. | ||||||
|             (!applicationKvStore!!.getBoolean("login_skipped")) |             (!applicationKvStore!!.getBoolean("login_skipped")) | ||||||
|         ) { |         ) { | ||||||
|             defaultKvStore.putBoolean("inAppCameraFirstRun", true) |             defaultKvStore.putBoolean("inAppCameraFirstRun", true) | ||||||
|             WelcomeActivity.startYourself(this) |             startWelcome() | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         retryAllFailedUploads() |         retryAllFailedUploads() | ||||||
|  |  | ||||||
|  | @ -45,10 +45,10 @@ class SetWallpaperWorker(context: Context, params: WorkerParameters) : | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>?) { |             override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>?>) { | ||||||
|                 Timber.d("Error getting bitmap from image url %s", imageUrl.toString()) |                 Timber.d("Error getting bitmap from image url %s", imageUrl.toString()) | ||||||
|                 showNotification(context, "Setting Wallpaper Failed", "Failed to download image.") |                 showNotification(context, "Setting Wallpaper Failed", "Failed to download image.") | ||||||
|                 dataSource?.close() |                 dataSource.close() | ||||||
|             } |             } | ||||||
|         }, CallerThreadExecutor.getInstance()) |         }, CallerThreadExecutor.getInstance()) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -17,8 +17,11 @@ interface ImageSelectListener { | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * onLongPress |      * Called when the user performs a long press on an image. | ||||||
|      * @param imageUri : uri of image |      * | ||||||
|  |      * @param position The index of the pressed image in the list. | ||||||
|  |      * @param images The list of all available images. | ||||||
|  |      * @param selectedImages The currently selected images. | ||||||
|      */ |      */ | ||||||
|     fun onLongPress( |     fun onLongPress( | ||||||
|         position: Int, |         position: Int, | ||||||
|  |  | ||||||
|  | @ -8,15 +8,15 @@ sealed class CallbackStatus { | ||||||
|     /** |     /** | ||||||
|      IDLE : The callback is idle , doing nothing. |      IDLE : The callback is idle , doing nothing. | ||||||
|      */ |      */ | ||||||
|     object IDLE : CallbackStatus() |     data object IDLE : CallbackStatus() | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      FETCHING : Fetching images. |      FETCHING : Fetching images. | ||||||
|      */ |      */ | ||||||
|     object FETCHING : CallbackStatus() |     data object FETCHING : CallbackStatus() | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      SUCCESS : Success fetching images. |      SUCCESS : Success fetching images. | ||||||
|      */ |      */ | ||||||
|     object SUCCESS : CallbackStatus() |     data object SUCCESS : CallbackStatus() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -39,4 +39,11 @@ data class Folder( | ||||||
| 
 | 
 | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     override fun hashCode(): Int { | ||||||
|  |         var result = bucketId.hashCode() | ||||||
|  |         result = 31 * result + name.hashCode() | ||||||
|  |         result = 31 * result + images.hashCode() | ||||||
|  |         return result | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package fr.free.nrw.commons.customselector.model | package fr.free.nrw.commons.customselector.model | ||||||
| 
 | 
 | ||||||
| import android.net.Uri | import android.net.Uri | ||||||
|  | import android.os.Build | ||||||
| import android.os.Parcel | import android.os.Parcel | ||||||
| import android.os.Parcelable | import android.os.Parcelable | ||||||
| 
 | 
 | ||||||
|  | @ -48,7 +49,12 @@ data class Image( | ||||||
|         this( |         this( | ||||||
|             parcel.readLong(), |             parcel.readLong(), | ||||||
|             parcel.readString()!!, |             parcel.readString()!!, | ||||||
|             parcel.readParcelable(Uri::class.java.classLoader)!!, |             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||||||
|  |                 parcel.readParcelable(Uri::class.java.classLoader, Uri::class.java)!! | ||||||
|  |             } else { | ||||||
|  |                 @Suppress("DEPRECATION") | ||||||
|  |                 parcel.readParcelable(Uri::class.java.classLoader)!! | ||||||
|  |             }, | ||||||
|             parcel.readString()!!, |             parcel.readString()!!, | ||||||
|             parcel.readLong(), |             parcel.readLong(), | ||||||
|             parcel.readString()!!, |             parcel.readString()!!, | ||||||
|  | @ -121,4 +127,16 @@ data class Image( | ||||||
| 
 | 
 | ||||||
|         override fun newArray(size: Int): Array<Image?> = arrayOfNulls(size) |         override fun newArray(size: Int): Array<Image?> = arrayOfNulls(size) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     override fun hashCode(): Int { | ||||||
|  |         var result = id.hashCode() | ||||||
|  |         result = 31 * result + bucketId.hashCode() | ||||||
|  |         result = 31 * result + name.hashCode() | ||||||
|  |         result = 31 * result + uri.hashCode() | ||||||
|  |         result = 31 * result + path.hashCode() | ||||||
|  |         result = 31 * result + bucketName.hashCode() | ||||||
|  |         result = 31 * result + sha1.hashCode() | ||||||
|  |         result = 31 * result + date.hashCode() | ||||||
|  |         return result | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ import kotlinx.coroutines.MainScope | ||||||
| import kotlinx.coroutines.cancel | import kotlinx.coroutines.cancel | ||||||
| import kotlinx.coroutines.flow.MutableStateFlow | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
|  | import kotlinx.coroutines.withContext | ||||||
| import java.util.TreeMap | import java.util.TreeMap | ||||||
| import kotlin.collections.ArrayList | import kotlin.collections.ArrayList | ||||||
| 
 | 
 | ||||||
|  | @ -167,8 +168,7 @@ class ImageAdapter( | ||||||
| 
 | 
 | ||||||
|                     // Getting selected index when switch is off |                     // Getting selected index when switch is off | ||||||
|                 } else if (actionableImagesMap.size > position) { |                 } else if (actionableImagesMap.size > position) { | ||||||
|                     ImageHelper |                     ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position]) | ||||||
|                         .getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position]) |  | ||||||
| 
 | 
 | ||||||
|                     // For any other case return -1 |                     // For any other case return -1 | ||||||
|                 } else { |                 } else { | ||||||
|  | @ -326,12 +326,17 @@ class ImageAdapter( | ||||||
| 
 | 
 | ||||||
|         // Getting clicked index from all images index when show_already_actioned_images |         // Getting clicked index from all images index when show_already_actioned_images | ||||||
|         // switch is on |         // switch is on | ||||||
|  |         if (singleSelection) { | ||||||
|  |             // If single selection mode, clear previous selection and select only the new one | ||||||
|  |             if (selectedImages.isNotEmpty() && (selectedImages[0] != images[position])) { | ||||||
|  |                 val prevIndex = images.indexOf(selectedImages[0]) | ||||||
|  |                 selectedImages.clear() | ||||||
|  |                 notifyItemChanged(prevIndex, ImageUnselected()) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|         val clickedIndex: Int = |         val clickedIndex: Int = | ||||||
|             if (showAlreadyActionedImages) { |             if (showAlreadyActionedImages) { | ||||||
|                 ImageHelper.getIndex(selectedImages, images[position]) |                 ImageHelper.getIndex(selectedImages, images[position]) | ||||||
| 
 |  | ||||||
|                 // Getting clicked index from actionable images when show_already_actioned_images |  | ||||||
|                 // switch is off |  | ||||||
|             } else { |             } else { | ||||||
|                 ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position]) |                 ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position]) | ||||||
|             } |             } | ||||||
|  | @ -342,46 +347,42 @@ class ImageAdapter( | ||||||
|                 numberOfSelectedImagesMarkedAsNotForUpload-- |                 numberOfSelectedImagesMarkedAsNotForUpload-- | ||||||
|             } |             } | ||||||
|             notifyItemChanged(position, ImageUnselected()) |             notifyItemChanged(position, ImageUnselected()) | ||||||
| 
 |             // Notify listener of deselection to update UI | ||||||
|             // Getting index from all images index when switch is on |             imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload) | ||||||
|             val indexes = |  | ||||||
|                 if (showAlreadyActionedImages) { |  | ||||||
|                     ImageHelper.getIndexList(selectedImages, images) |  | ||||||
| 
 |  | ||||||
|                     // Getting index from actionable images when switch is off |  | ||||||
|         } else { |         } else { | ||||||
|                     ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values)) |             // Prevent adding the same image multiple times | ||||||
|  |             val image = if (showAlreadyActionedImages) images[position] else ArrayList(actionableImagesMap.values)[position] | ||||||
|  |             if (selectedImages.contains(image)) { | ||||||
|  |                 return // Image already selected, ignore additional clicks | ||||||
|             } |             } | ||||||
|             for (index in indexes) { |             scope.launch(ioDispatcher) { | ||||||
|                 notifyItemChanged(index, ImageSelectedOrUpdated()) |                 val imageSHA1 = imageLoader.getSHA1(image, defaultDispatcher) | ||||||
|             } |                 withContext(Dispatchers.Main) { | ||||||
|         } else { |  | ||||||
|                     if (holder.isItemUploaded()) { |                     if (holder.isItemUploaded()) { | ||||||
|                         Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() |                         Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() | ||||||
|             } else { |                         return@withContext | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     if (imageSHA1.isNotEmpty() && imageLoader.getFromUploaded(imageSHA1) != null) { | ||||||
|  |                         holder.itemUploaded() | ||||||
|  |                         Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() | ||||||
|  |                         return@withContext | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     if (!holder.isItemUploaded() && imageSHA1.isNotEmpty() && imageLoader.getFromUploaded(imageSHA1) != null) { | ||||||
|  |                         Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|                     if (holder.isItemNotForUpload()) { |                     if (holder.isItemNotForUpload()) { | ||||||
|                         numberOfSelectedImagesMarkedAsNotForUpload++ |                         numberOfSelectedImagesMarkedAsNotForUpload++ | ||||||
|                     } |                     } | ||||||
| 
 |                     selectedImages.add(image) | ||||||
|                 // Getting index from all images index when switch is on |                     notifyItemChanged(position, ImageSelectedOrUpdated()) | ||||||
|                 val indexes: ArrayList<Int> = |  | ||||||
|                     if (showAlreadyActionedImages) { |  | ||||||
|                         selectedImages.add(images[position]) |  | ||||||
|                         ImageHelper.getIndexList(selectedImages, images) |  | ||||||
| 
 |  | ||||||
|                         // Getting index from actionable images when switch is off |  | ||||||
|                     } else { |  | ||||||
|                         selectedImages.add(ArrayList(actionableImagesMap.values)[position]) |  | ||||||
|                         ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values)) |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                 for (index in indexes) { |  | ||||||
|                     notifyItemChanged(index, ImageSelectedOrUpdated()) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|                     imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload) |                     imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload) | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Initialize the data set. |      * Initialize the data set. | ||||||
|  | @ -626,4 +627,13 @@ class ImageAdapter( | ||||||
|      * Returns the text for showing inside the bubble during bubble scroll. |      * Returns the text for showing inside the bubble during bubble scroll. | ||||||
|      */ |      */ | ||||||
|     override fun getSectionName(position: Int): String = images[position].date |     override fun getSectionName(position: Int): String = images[position].date | ||||||
|  | 
 | ||||||
|  |     private var singleSelection: Boolean = false | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Set single selection mode | ||||||
|  |      */ | ||||||
|  |     fun setSingleSelection(single: Boolean) { | ||||||
|  |         singleSelection = single | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import androidx.constraintlayout.widget.ConstraintLayout | import androidx.constraintlayout.widget.ConstraintLayout | ||||||
| import androidx.core.content.ContextCompat | import androidx.core.content.ContextCompat | ||||||
|  | import androidx.core.view.ViewGroupCompat | ||||||
| import androidx.lifecycle.ViewModelProvider | import androidx.lifecycle.ViewModelProvider | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.customselector.database.NotForUploadStatus | import fr.free.nrw.commons.customselector.database.NotForUploadStatus | ||||||
|  | @ -56,6 +57,8 @@ import fr.free.nrw.commons.media.ZoomableActivity | ||||||
| import fr.free.nrw.commons.theme.BaseActivity | import fr.free.nrw.commons.theme.BaseActivity | ||||||
| import fr.free.nrw.commons.upload.FileUtilsWrapper | import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||||
| import fr.free.nrw.commons.utils.CustomSelectorUtils | import fr.free.nrw.commons.utils.CustomSelectorUtils | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets | ||||||
| import kotlinx.coroutines.CoroutineDispatcher | import kotlinx.coroutines.CoroutineDispatcher | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
|  | @ -104,7 +107,7 @@ class CustomSelectorActivity : | ||||||
|     /** |     /** | ||||||
|      * Maximum number of images that can be selected. |      * Maximum number of images that can be selected. | ||||||
|      */ |      */ | ||||||
|     private val uploadLimit: Int = 20 |     private var uploadLimit: Int = 20 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Flag that is marked true when the amount |      * Flag that is marked true when the amount | ||||||
|  | @ -198,6 +201,9 @@ class CustomSelectorActivity : | ||||||
|                         .fillMaxWidth(), |                         .fillMaxWidth(), | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  |         ViewGroupCompat.installCompatInsetsDispatch(binding.root) | ||||||
|  |         applyEdgeToEdgeTopInsets(toolbarBinding.toolbarLayout) | ||||||
|  |         bottomSheetBinding.bottomLayout.applyEdgeToEdgeBottomPaddingInsets() | ||||||
|         val view = binding.root |         val view = binding.root | ||||||
|         setContentView(view) |         setContentView(view) | ||||||
| 
 | 
 | ||||||
|  | @ -207,6 +213,9 @@ class CustomSelectorActivity : | ||||||
|                 CustomSelectorViewModel::class.java, |                 CustomSelectorViewModel::class.java, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  |         // Check for single selection extra | ||||||
|  |         uploadLimit = if (intent.getBooleanExtra(EXTRA_SINGLE_SELECTION, false)) 1 else 20 | ||||||
|  | 
 | ||||||
|         setupViews() |         setupViews() | ||||||
| 
 | 
 | ||||||
|         if (prefs.getBoolean("customSelectorFirstLaunch", true)) { |         if (prefs.getBoolean("customSelectorFirstLaunch", true)) { | ||||||
|  | @ -610,8 +619,11 @@ class CustomSelectorActivity : | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * onLongPress |      * Triggered when the user performs a long press on an image. | ||||||
|      * @param imageUri : uri of image |      * | ||||||
|  |      * @param position The index of the selected image. | ||||||
|  |      * @param images The list of all available images. | ||||||
|  |      * @param selectedImages The list of images currently selected. | ||||||
|      */ |      */ | ||||||
|     override fun onLongPress( |     override fun onLongPress( | ||||||
|         position: Int, |         position: Int, | ||||||
|  | @ -638,17 +650,20 @@ class CustomSelectorActivity : | ||||||
|             finishPickImages(arrayListOf()) |             finishPickImages(arrayListOf()) | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         var i = 0 |         scope.launch(ioDispatcher) { | ||||||
|         while (i < selectedImages.size) { |             val uniqueImages = selectedImages.distinctBy { image -> | ||||||
|             val path = selectedImages[i].path |                 CustomSelectorUtils.getImageSHA1( | ||||||
|             val file = File(path) |                     image.uri, | ||||||
|             if (!file.exists()) { |                     ioDispatcher, | ||||||
|                 selectedImages.removeAt(i) |                     fileUtilsWrapper, | ||||||
|                 i-- |                     contentResolver | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             withContext(Dispatchers.Main) { | ||||||
|  |                 finishPickImages(ArrayList(uniqueImages)) | ||||||
|             } |             } | ||||||
|             i++ |  | ||||||
|         } |         } | ||||||
|         finishPickImages(selectedImages) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -722,6 +737,7 @@ class CustomSelectorActivity : | ||||||
|         const val FOLDER_ID: String = "FolderId" |         const val FOLDER_ID: String = "FolderId" | ||||||
|         const val FOLDER_NAME: String = "FolderName" |         const val FOLDER_NAME: String = "FolderName" | ||||||
|         const val ITEM_ID: String = "ItemId" |         const val ITEM_ID: String = "ItemId" | ||||||
|  |         const val EXTRA_SINGLE_SELECTION: String = "EXTRA_SINGLE_SELECTION" | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||||
| import fr.free.nrw.commons.media.MediaClient | import fr.free.nrw.commons.media.MediaClient | ||||||
| import fr.free.nrw.commons.upload.FileProcessor | import fr.free.nrw.commons.upload.FileProcessor | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -99,6 +100,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { | ||||||
|         selectorRV = binding?.selectorRv |         selectorRV = binding?.selectorRv | ||||||
|         loader = binding?.loader |         loader = binding?.loader | ||||||
|         with(binding?.selectorRv) { |         with(binding?.selectorRv) { | ||||||
|  |             this?.applyEdgeToEdgeBottomPaddingInsets() | ||||||
|             this?.layoutManager = gridLayoutManager |             this?.layoutManager = gridLayoutManager | ||||||
|             this?.setHasFixedSize(true) |             this?.setHasFixedSize(true) | ||||||
|             this?.adapter = folderAdapter |             this?.adapter = folderAdapter | ||||||
|  |  | ||||||
|  | @ -104,7 +104,8 @@ class ImageFileLoader( | ||||||
|                 if (file != null && file.exists() && name != null && path != null && bucketName != null) { |                 if (file != null && file.exists() && name != null && path != null && bucketName != null) { | ||||||
|                     val extension = path.substringAfterLast(".", "") |                     val extension = path.substringAfterLast(".", "") | ||||||
|                     // Check if the extension is one of the allowed types |                     // Check if the extension is one of the allowed types | ||||||
|                     if (extension.lowercase(Locale.ROOT) !in arrayOf("jpg", "jpeg", "png", "svg", "gif", "tiff", "webp", "xcf")) { |                     if (extension.lowercase(Locale.ROOT) !in arrayOf("jpg", "jpeg", "png", "svg", | ||||||
|  |                             "gif", "tiff", "webp", "xcf")) { | ||||||
|                         continue |                         continue | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ import android.view.LayoutInflater | ||||||
| import android.view.View | import android.view.View | ||||||
| import android.view.ViewGroup | import android.view.ViewGroup | ||||||
| import android.widget.ProgressBar | import android.widget.ProgressBar | ||||||
| import android.widget.Switch |  | ||||||
| import androidx.appcompat.app.AlertDialog | import androidx.appcompat.app.AlertDialog | ||||||
| import androidx.constraintlayout.widget.ConstraintLayout | import androidx.constraintlayout.widget.ConstraintLayout | ||||||
| import androidx.core.view.isVisible | import androidx.core.view.isVisible | ||||||
|  | @ -20,6 +19,7 @@ import androidx.lifecycle.lifecycleScope | ||||||
| import androidx.lifecycle.repeatOnLifecycle | import androidx.lifecycle.repeatOnLifecycle | ||||||
| import androidx.recyclerview.widget.GridLayoutManager | import androidx.recyclerview.widget.GridLayoutManager | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import com.google.android.material.switchmaterial.SwitchMaterial | ||||||
| import fr.free.nrw.commons.contributions.Contribution | import fr.free.nrw.commons.contributions.Contribution | ||||||
| import fr.free.nrw.commons.contributions.ContributionDao | import fr.free.nrw.commons.contributions.ContributionDao | ||||||
| import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao | import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao | ||||||
|  | @ -41,11 +41,13 @@ import fr.free.nrw.commons.media.MediaClient | ||||||
| import fr.free.nrw.commons.theme.BaseActivity | import fr.free.nrw.commons.theme.BaseActivity | ||||||
| import fr.free.nrw.commons.upload.FileProcessor | import fr.free.nrw.commons.upload.FileProcessor | ||||||
| import fr.free.nrw.commons.upload.FileUtilsWrapper | import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomPaddingInsets | ||||||
| import io.reactivex.schedulers.Schedulers | import io.reactivex.schedulers.Schedulers | ||||||
| import kotlinx.coroutines.flow.MutableStateFlow | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
| import kotlinx.coroutines.flow.asStateFlow | import kotlinx.coroutines.flow.asStateFlow | ||||||
| import kotlinx.coroutines.flow.combine | import kotlinx.coroutines.flow.combine | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
|  | import timber.log.Timber | ||||||
| import java.util.TreeMap | import java.util.TreeMap | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import kotlin.collections.ArrayList | import kotlin.collections.ArrayList | ||||||
|  | @ -80,7 +82,7 @@ class ImageFragment : | ||||||
|      */ |      */ | ||||||
|     private var selectorRV: RecyclerView? = null |     private var selectorRV: RecyclerView? = null | ||||||
|     private var loader: ProgressBar? = null |     private var loader: ProgressBar? = null | ||||||
|     private var switch: Switch? = null |     private var switch: SwitchMaterial? = null | ||||||
|     lateinit var filteredImages: ArrayList<Image> |     lateinit var filteredImages: ArrayList<Image> | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -210,10 +212,18 @@ class ImageFragment : | ||||||
|         savedInstanceState: Bundle?, |         savedInstanceState: Bundle?, | ||||||
|     ): View? { |     ): View? { | ||||||
|         _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) |         _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) | ||||||
|         imageAdapter = | 
 | ||||||
|             ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) |         // ensures imageAdapter is initialized | ||||||
|  |         if (!::imageAdapter.isInitialized) { | ||||||
|  |             imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) | ||||||
|  |             Timber.d("Initialized imageAdapter in onCreateView") | ||||||
|  |         } | ||||||
|  |         // Set single selection mode if needed | ||||||
|  |         val singleSelection = (activity as? CustomSelectorActivity)?.intent?.getBooleanExtra(CustomSelectorActivity.EXTRA_SINGLE_SELECTION, false) == true | ||||||
|  |         imageAdapter.setSingleSelection(singleSelection) | ||||||
|         gridLayoutManager = GridLayoutManager(context, getSpanCount()) |         gridLayoutManager = GridLayoutManager(context, getSpanCount()) | ||||||
|         with(binding?.selectorRv) { |         with(binding?.selectorRv) { | ||||||
|  |             this?.applyEdgeToEdgeBottomPaddingInsets() | ||||||
|             this?.layoutManager = gridLayoutManager |             this?.layoutManager = gridLayoutManager | ||||||
|             this?.setHasFixedSize(true) |             this?.setHasFixedSize(true) | ||||||
|             this?.adapter = imageAdapter |             this?.adapter = imageAdapter | ||||||
|  | @ -365,7 +375,12 @@ class ImageFragment : | ||||||
|      * notifyDataSetChanged, rebuild the holder views to account for deleted images. |      * notifyDataSetChanged, rebuild the holder views to account for deleted images. | ||||||
|      */ |      */ | ||||||
|     override fun onResume() { |     override fun onResume() { | ||||||
|  |         if (::imageAdapter.isInitialized) { | ||||||
|             imageAdapter.notifyDataSetChanged() |             imageAdapter.notifyDataSetChanged() | ||||||
|  |             Timber.d("Notified imageAdapter in onResume") | ||||||
|  |         } else { | ||||||
|  |             Timber.w("imageAdapter not initialized in onResume") | ||||||
|  |         } | ||||||
|         super.onResume() |         super.onResume() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -375,14 +390,19 @@ class ImageFragment : | ||||||
|      * Save the Image Fragment state. |      * Save the Image Fragment state. | ||||||
|      */ |      */ | ||||||
|     override fun onDestroy() { |     override fun onDestroy() { | ||||||
|  |         if (::imageAdapter.isInitialized) { | ||||||
|             imageAdapter.cleanUp() |             imageAdapter.cleanUp() | ||||||
|  |             Timber.d("Cleaned up imageAdapter in onDestroy") | ||||||
|  |         } else { | ||||||
|  |             Timber.w("imageAdapter not initialized in onDestroy, skipping cleanup") | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         val position = |         val position = | ||||||
|             (selectorRV?.layoutManager as GridLayoutManager) |             (selectorRV?.layoutManager as? GridLayoutManager) | ||||||
|                 .findFirstVisibleItemPosition() |                 ?.findFirstVisibleItemPosition() ?: -1 | ||||||
| 
 | 
 | ||||||
|         // Check for empty RecyclerView. |         // check for valid position and non-empty image list | ||||||
|         if (position != -1 && filteredImages.size > 0) { |         if (position != -1 && filteredImages.isNotEmpty() && ::imageAdapter.isInitialized) { | ||||||
|             context?.let { context -> |             context?.let { context -> | ||||||
|                 context |                 context | ||||||
|                     .getSharedPreferences( |                     .getSharedPreferences( | ||||||
|  | @ -391,34 +411,57 @@ class ImageFragment : | ||||||
|                     )?.let { prefs -> |                     )?.let { prefs -> | ||||||
|                         prefs.edit()?.let { editor -> |                         prefs.edit()?.let { editor -> | ||||||
|                             editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply() |                             editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply() | ||||||
|  |                             Timber.d("Saved last visible item ID: %d", imageAdapter.getImageIdAt(position)) | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|             } |             } | ||||||
|  |         } else { | ||||||
|  |             Timber.d("Skipped saving item ID: position=%d, filteredImages.size=%d, imageAdapter initialized=%b", | ||||||
|  |                 position, filteredImages.size, ::imageAdapter.isInitialized) | ||||||
|         } |         } | ||||||
|         super.onDestroy() |         super.onDestroy() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onDestroyView() { |     override fun onDestroyView() { | ||||||
|         _binding = null |         _binding = null | ||||||
|  |         selectorRV = null | ||||||
|  |         loader = null | ||||||
|  |         switch = null | ||||||
|  |         progressLayout = null | ||||||
|         super.onDestroyView() |         super.onDestroyView() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun refresh() { |     override fun refresh() { | ||||||
|  |         if (::imageAdapter.isInitialized) { | ||||||
|             imageAdapter.refresh(filteredImages, allImages, getUploadingContributions()) |             imageAdapter.refresh(filteredImages, allImages, getUploadingContributions()) | ||||||
|  |             Timber.d("Refreshed imageAdapter") | ||||||
|  |         } else { | ||||||
|  |             Timber.w("imageAdapter not initialized in refresh") | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Removes the image from the actionable image map |      * Removes the image from the actionable image map | ||||||
|      */ |      */ | ||||||
|     fun removeImage(image: Image) { |     fun removeImage(image: Image) { | ||||||
|  |         if (::imageAdapter.isInitialized) { | ||||||
|             imageAdapter.removeImageFromActionableImageMap(image) |             imageAdapter.removeImageFromActionableImageMap(image) | ||||||
|  |             Timber.d("Removed image from actionable image map") | ||||||
|  |         } else { | ||||||
|  |             Timber.w("imageAdapter not initialized in removeImage") | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Clears the selected images |      * Clears the selected images | ||||||
|      */ |      */ | ||||||
|     fun clearSelectedImages() { |     fun clearSelectedImages() { | ||||||
|  |         if (::imageAdapter.isInitialized) { | ||||||
|             imageAdapter.clearSelectedImages() |             imageAdapter.clearSelectedImages() | ||||||
|  |             Timber.d("Cleared selected images") | ||||||
|  |         } else { | ||||||
|  |             Timber.w("imageAdapter not initialized in clearSelectedImages") | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -429,6 +472,15 @@ class ImageFragment : | ||||||
|         selectedImages: ArrayList<Image>, |         selectedImages: ArrayList<Image>, | ||||||
|         shouldRefresh: Boolean, |         shouldRefresh: Boolean, | ||||||
|     ) { |     ) { | ||||||
|  |         if (::imageAdapter.isInitialized) { | ||||||
|  |             imageAdapter.setSelectedImages(selectedImages) | ||||||
|  |             if (shouldRefresh) { | ||||||
|  |                 imageAdapter.refresh(filteredImages, allImages, getUploadingContributions()) | ||||||
|  |             } | ||||||
|  |             Timber.d("Passed %d selected images to imageAdapter, shouldRefresh=%b", selectedImages.size, shouldRefresh) | ||||||
|  |         } else { | ||||||
|  |             Timber.w("imageAdapter not initialized in passSelectedImages") | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -438,6 +490,7 @@ class ImageFragment : | ||||||
|         if (!progressDialog.isShowing) { |         if (!progressDialog.isShowing) { | ||||||
|             progressDialogLayout.progressDialogText.text = text |             progressDialogLayout.progressDialogText.text = text | ||||||
|             progressDialog.show() |             progressDialog.show() | ||||||
|  |             Timber.d("Showing mark/unmark progress dialog: %s", text) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -447,6 +500,7 @@ class ImageFragment : | ||||||
|     fun dismissMarkUnmarkProgressDialog() { |     fun dismissMarkUnmarkProgressDialog() { | ||||||
|         if (progressDialog.isShowing) { |         if (progressDialog.isShowing) { | ||||||
|             progressDialog.dismiss() |             progressDialog.dismiss() | ||||||
|  |             Timber.d("Dismissed mark/unmark progress dialog") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,11 +4,10 @@ import android.content.Context | ||||||
| import android.database.sqlite.SQLiteDatabase | import android.database.sqlite.SQLiteDatabase | ||||||
| import android.database.sqlite.SQLiteException | import android.database.sqlite.SQLiteException | ||||||
| import android.database.sqlite.SQLiteOpenHelper | import android.database.sqlite.SQLiteOpenHelper | ||||||
| import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao | import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable | ||||||
| import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao | import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable | ||||||
| import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao |  | ||||||
| import fr.free.nrw.commons.category.CategoryDao | import fr.free.nrw.commons.category.CategoryDao | ||||||
| import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao | import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable | ||||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -30,17 +29,17 @@ class DBOpenHelper( | ||||||
|      */ |      */ | ||||||
|     override fun onCreate(db: SQLiteDatabase) { |     override fun onCreate(db: SQLiteDatabase) { | ||||||
|         CategoryDao.Table.onCreate(db) |         CategoryDao.Table.onCreate(db) | ||||||
|         BookmarkPicturesDao.Table.onCreate(db) |         BookmarksTable.onCreate(db) | ||||||
|         BookmarkItemsDao.Table.onCreate(db) |         BookmarkItemsTable.onCreate(db) | ||||||
|         RecentSearchesDao.Table.onCreate(db) |         RecentSearchesTable.onCreate(db) | ||||||
|         RecentLanguagesDao.Table.onCreate(db) |         RecentLanguagesDao.Table.onCreate(db) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) { |     override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) { | ||||||
|         CategoryDao.Table.onUpdate(db, from, to) |         CategoryDao.Table.onUpdate(db, from, to) | ||||||
|         BookmarkPicturesDao.Table.onUpdate(db, from, to) |         BookmarksTable.onUpdate(db, from, to) | ||||||
|         BookmarkItemsDao.Table.onUpdate(db, from, to) |         BookmarkItemsTable.onUpdate(db, from, to) | ||||||
|         RecentSearchesDao.Table.onUpdate(db, from, to) |         RecentSearchesTable.onUpdate(db, from, to) | ||||||
|         RecentLanguagesDao.Table.onUpdate(db, from, to) |         RecentLanguagesDao.Table.onUpdate(db, from, to) | ||||||
|         deleteTable(db, CONTRIBUTIONS_TABLE) |         deleteTable(db, CONTRIBUTIONS_TABLE) | ||||||
|         deleteTable(db, BOOKMARKS_LOCATIONS) |         deleteTable(db, BOOKMARKS_LOCATIONS) | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao | ||||||
|  */ |  */ | ||||||
| @Database( | @Database( | ||||||
|     entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class, BookmarksLocations::class], |     entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class, BookmarksCategoryModal::class, BookmarksLocations::class], | ||||||
|     version = 20, |     version = 21, | ||||||
|     exportSchema = false, |     exportSchema = false, | ||||||
| ) | ) | ||||||
| @TypeConverters(Converters::class) | @TypeConverters(Converters::class) | ||||||
|  |  | ||||||
|  | @ -53,7 +53,6 @@ class DeleteHelper @Inject constructor( | ||||||
|         media: Media?, |         media: Media?, | ||||||
|         reason: String? |         reason: String? | ||||||
|     ): Single<Boolean>? { |     ): Single<Boolean>? { | ||||||
| 
 |  | ||||||
|         if(context == null && media == null) { |         if(context == null && media == null) { | ||||||
|             return null |             return null | ||||||
|         } |         } | ||||||
|  | @ -86,7 +85,6 @@ class DeleteHelper @Inject constructor( | ||||||
|      * @return |      * @return | ||||||
|      */ |      */ | ||||||
|     private fun delete(media: Media, reason: String): Observable<Boolean> { |     private fun delete(media: Media, reason: String): Observable<Boolean> { | ||||||
|         Timber.d("thread is delete %s", Thread.currentThread().name) |  | ||||||
|         val summary = "Nominating ${media.filename} for deletion." |         val summary = "Nominating ${media.filename} for deletion." | ||||||
|         val calendar = Calendar.getInstance() |         val calendar = Calendar.getInstance() | ||||||
|         val fileDeleteString = """ |         val fileDeleteString = """ | ||||||
|  |  | ||||||
|  | @ -2,21 +2,19 @@ package fr.free.nrw.commons.delete | ||||||
| 
 | 
 | ||||||
| import android.annotation.SuppressLint | import android.annotation.SuppressLint | ||||||
| import android.content.Context | import android.content.Context | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.utils.DateUtil |  | ||||||
| import java.util.Locale |  | ||||||
| 
 |  | ||||||
| import javax.inject.Inject |  | ||||||
| import javax.inject.Singleton |  | ||||||
| 
 |  | ||||||
| import fr.free.nrw.commons.Media | import fr.free.nrw.commons.Media | ||||||
| import fr.free.nrw.commons.R | import fr.free.nrw.commons.R | ||||||
| import fr.free.nrw.commons.profile.achievements.FeedbackResponse |  | ||||||
| import fr.free.nrw.commons.auth.SessionManager | import fr.free.nrw.commons.auth.SessionManager | ||||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient | import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient | ||||||
|  | import fr.free.nrw.commons.utils.DateUtil | ||||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper | import fr.free.nrw.commons.utils.ViewUtilWrapper | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.rx2.rxSingle | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
|  | import java.util.Locale | ||||||
|  | import javax.inject.Inject | ||||||
|  | import javax.inject.Singleton | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This class handles the reason for deleting a Media object |  * This class handles the reason for deleting a Media object | ||||||
|  | @ -29,6 +27,8 @@ class ReasonBuilder @Inject constructor( | ||||||
|     private val viewUtilWrapper: ViewUtilWrapper |     private val viewUtilWrapper: ViewUtilWrapper | ||||||
| ) { | ) { | ||||||
| 
 | 
 | ||||||
|  |     private val defaultFileUsagePageSize = 10 | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * To process the reason and append the media's upload date and uploaded_by_me string |      * To process the reason and append the media's upload date and uploaded_by_me string | ||||||
|      * @param media |      * @param media | ||||||
|  | @ -39,7 +39,7 @@ class ReasonBuilder @Inject constructor( | ||||||
|         if (media == null || reason == null) { |         if (media == null || reason == null) { | ||||||
|             return Single.just("Not known") |             return Single.just("Not known") | ||||||
|         } |         } | ||||||
|         return fetchArticleNumber(media, reason) |         return getAndAppendFileUsage(media, reason) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -54,27 +54,36 @@ class ReasonBuilder @Inject constructor( | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun fetchArticleNumber(media: Media, reason: String): Single<String> { |     private fun getAndAppendFileUsage(media: Media, reason: String): Single<String> { | ||||||
|         return if (checkAccount()) { |         return rxSingle(context = Dispatchers.IO) { | ||||||
|             okHttpJsonApiClient |             if (!checkAccount()) return@rxSingle "" | ||||||
|                 .getAchievements(sessionManager.userName) | 
 | ||||||
|                 .map { feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason) } |             try { | ||||||
|         } else { |                 val globalFileUsage = okHttpJsonApiClient.getGlobalFileUsages( | ||||||
|             Single.just("") |                     fileName = media.filename, | ||||||
|  |                     pageSize = defaultFileUsagePageSize | ||||||
|  |                 ) | ||||||
|  |                 val globalUsages = globalFileUsage?.query?.pages?.sumOf { it.fileUsage.size } ?: 0 | ||||||
|  | 
 | ||||||
|  |                 appendArticlesUsed(globalUsages, media, reason) | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 Timber.e(e, "Error fetching file usage") | ||||||
|  |                 throw e | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Takes the uploaded_by_me string, the upload date, name of articles using images |      * Takes the uploaded_by_me string, the upload date, no. of articles using images | ||||||
|      * and appends it to the received reason |      * and appends it to the received reason | ||||||
|      * @param feedBack object |      * @param fileUsages No. of files/articles using this image | ||||||
|      * @param media whose upload data is to be fetched |      * @param media whose upload data is to be fetched | ||||||
|      * @param reason |      * @param reason string to be appended | ||||||
|      */ |      */ | ||||||
|     @SuppressLint("StringFormatInvalid") |     @SuppressLint("StringFormatInvalid") | ||||||
|     private fun appendArticlesUsed(feedBack: FeedbackResponse, media: Media, reason: String): String { |     private fun appendArticlesUsed(fileUsages: Int, media: Media, reason: String): String { | ||||||
|         val reason1Template = context.getString(R.string.uploaded_by_myself) |         val reason1Template = context.getString(R.string.uploaded_by_myself) | ||||||
|         return reason + String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), feedBack.articlesUsingImages) |         return reason + String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), fileUsages) | ||||||
|             .also { Timber.i("New Reason %s", it) } |             .also { Timber.i("New Reason %s", it) } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import android.speech.RecognizerIntent | ||||||
| import android.view.View | import android.view.View | ||||||
| import androidx.activity.result.ActivityResult | import androidx.activity.result.ActivityResult | ||||||
| import androidx.activity.result.contract.ActivityResultContracts | import androidx.activity.result.contract.ActivityResultContracts | ||||||
|  | import androidx.core.view.WindowCompat | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import fr.free.nrw.commons.CommonsApplication | import fr.free.nrw.commons.CommonsApplication | ||||||
|  | @ -20,9 +21,11 @@ import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT | ||||||
| import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao | ||||||
| import fr.free.nrw.commons.settings.Prefs | import fr.free.nrw.commons.settings.Prefs | ||||||
| import fr.free.nrw.commons.theme.BaseActivity | import fr.free.nrw.commons.theme.BaseActivity | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeBottomInsets | ||||||
| import fr.free.nrw.commons.upload.UploadMediaDetail | import fr.free.nrw.commons.upload.UploadMediaDetail | ||||||
| import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | import fr.free.nrw.commons.upload.UploadMediaDetailAdapter | ||||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||||
|  | import fr.free.nrw.commons.utils.applyEdgeToEdgeTopPaddingInsets | ||||||
| import io.reactivex.android.schedulers.AndroidSchedulers | import io.reactivex.android.schedulers.AndroidSchedulers | ||||||
| import io.reactivex.functions.Consumer | import io.reactivex.functions.Consumer | ||||||
| import io.reactivex.schedulers.Schedulers | import io.reactivex.schedulers.Schedulers | ||||||
|  | @ -87,6 +90,10 @@ class DescriptionEditActivity : | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
| 
 | 
 | ||||||
|         binding = ActivityDescriptionEditBinding.inflate(layoutInflater) |         binding = ActivityDescriptionEditBinding.inflate(layoutInflater) | ||||||
|  |         applyEdgeToEdgeBottomInsets(binding.btnEditSubmit) | ||||||
|  |         WindowCompat.getInsetsController(window, window.decorView) | ||||||
|  |             .isAppearanceLightStatusBars = false | ||||||
|  |         binding.toolbar.applyEdgeToEdgeTopPaddingInsets() | ||||||
|         setContentView(binding.root) |         setContentView(binding.root) | ||||||
| 
 | 
 | ||||||
|         val bundle = intent.extras |         val bundle = intent.extras | ||||||
|  | @ -143,7 +150,7 @@ class DescriptionEditActivity : | ||||||
|             this, |             this, | ||||||
|             getString(titleStringID), |             getString(titleStringID), | ||||||
|             getString(messageStringId), |             getString(messageStringId), | ||||||
|             getString(android.R.string.ok), |             getString(R.string.ok), | ||||||
|             null |             null | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue